From d6461251f925c4be1197a34f7d6be761a1cc014f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:04:19 +1300 Subject: [PATCH 001/609] Bump version to 2026.3.0-dev --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index efc2f464e3..1a9e0b4e10 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.2.0-dev +PROJECT_NUMBER = 2026.3.0-dev # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 00d8013cc6..f72cbc8893 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.2.0-dev" +__version__ = "2026.3.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( 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 002/609] [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 003/609] [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 004/609] [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 005/609] [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 006/609] [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 0e1433329da006a6f58dfc1d5e086d93d743bf07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 11:04:23 -0600 Subject: [PATCH 007/609] [api] Extract cold code from APIServer::loop() hot path (#13902) --- esphome/components/api/api_server.cpp | 130 ++++++++++++++------------ esphome/components/api/api_server.h | 5 + 2 files changed, 74 insertions(+), 61 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 53b41a5c14..5503cf4db8 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -117,37 +117,7 @@ void APIServer::setup() { void APIServer::loop() { // Accept new clients only if the socket exists and has incoming connections if (this->socket_ && this->socket_->ready()) { - while (true) { - struct sockaddr_storage source_addr; - socklen_t addr_len = sizeof(source_addr); - - auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); - if (!sock) - break; - - char peername[socket::SOCKADDR_STR_LEN]; - sock->getpeername_to(peername); - - // Check if we're at the connection limit - if (this->clients_.size() >= this->max_connections_) { - ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername); - // Immediately close - socket destructor will handle cleanup - sock.reset(); - continue; - } - - ESP_LOGD(TAG, "Accept %s", peername); - - auto *conn = new APIConnection(std::move(sock), this); - this->clients_.emplace_back(conn); - conn->start(); - - // First client connected - clear warning and update timestamp - if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { - this->status_clear_warning(); - this->last_connected_ = App.get_loop_component_start_time(); - } - } + this->accept_new_connections_(); } if (this->clients_.empty()) { @@ -178,46 +148,84 @@ void APIServer::loop() { while (client_index < this->clients_.size()) { auto &client = this->clients_[client_index]; - if (!client->flags_.remove) { + if (client->flags_.remove) { + // Rare case: handle disconnection (don't increment - swapped element needs processing) + this->remove_client_(client_index); + } else { // Common case: process active client client->loop(); client_index++; + } + } +} + +void APIServer::remove_client_(size_t client_index) { + auto &client = this->clients_[client_index]; + +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES + this->unregister_active_action_calls_for_connection(client.get()); +#endif + ESP_LOGV(TAG, "Remove connection %s", client->get_name()); + +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER + // Save client info before closing socket and removal for the trigger + char peername_buf[socket::SOCKADDR_STR_LEN]; + std::string client_name(client->get_name()); + std::string client_peername(client->get_peername_to(peername_buf)); +#endif + + // Close socket now (was deferred from on_fatal_error to allow getpeername) + client->helper_->close(); + + // Swap with the last element and pop (avoids expensive vector shifts) + if (client_index < this->clients_.size() - 1) { + std::swap(this->clients_[client_index], this->clients_.back()); + } + this->clients_.pop_back(); + + // Last client disconnected - set warning and start tracking for reboot timeout + if (this->clients_.empty() && this->reboot_timeout_ != 0) { + this->status_set_warning(); + this->last_connected_ = App.get_loop_component_start_time(); + } + +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER + // Fire trigger after client is removed so api.connected reflects the true state + this->client_disconnected_trigger_.trigger(client_name, client_peername); +#endif +} + +void APIServer::accept_new_connections_() { + while (true) { + struct sockaddr_storage source_addr; + socklen_t addr_len = sizeof(source_addr); + + auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); + if (!sock) + break; + + char peername[socket::SOCKADDR_STR_LEN]; + sock->getpeername_to(peername); + + // Check if we're at the connection limit + if (this->clients_.size() >= this->max_connections_) { + ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername); + // Immediately close - socket destructor will handle cleanup + sock.reset(); continue; } - // Rare case: handle disconnection -#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES - this->unregister_active_action_calls_for_connection(client.get()); -#endif - ESP_LOGV(TAG, "Remove connection %s", client->get_name()); + ESP_LOGD(TAG, "Accept %s", peername); -#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - // Save client info before closing socket and removal for the trigger - char peername_buf[socket::SOCKADDR_STR_LEN]; - std::string client_name(client->get_name()); - std::string client_peername(client->get_peername_to(peername_buf)); -#endif + auto *conn = new APIConnection(std::move(sock), this); + this->clients_.emplace_back(conn); + conn->start(); - // Close socket now (was deferred from on_fatal_error to allow getpeername) - client->helper_->close(); - - // Swap with the last element and pop (avoids expensive vector shifts) - if (client_index < this->clients_.size() - 1) { - std::swap(this->clients_[client_index], this->clients_.back()); - } - this->clients_.pop_back(); - - // Last client disconnected - set warning and start tracking for reboot timeout - if (this->clients_.empty() && this->reboot_timeout_ != 0) { - this->status_set_warning(); + // First client connected - clear warning and update timestamp + if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { + this->status_clear_warning(); this->last_connected_ = App.get_loop_component_start_time(); } - -#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - // Fire trigger after client is removed so api.connected reflects the true state - this->client_disconnected_trigger_.trigger(client_name, client_peername); -#endif - // Don't increment client_index since we need to process the swapped element } } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 6ab3cdc576..28f60343e0 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -234,6 +234,11 @@ class APIServer : public Component, #endif protected: + // Accept incoming socket connections. Only called when socket has pending connections. + void __attribute__((noinline)) accept_new_connections_(); + // Remove a disconnected client by index. Swaps with last element and pops. + void __attribute__((noinline)) remove_client_(size_t client_index); + #ifdef USE_API_NOISE bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, const psk_t &active_psk, bool make_active); From cde8b6671932d3b75b0a323ef947f24cd9f2872f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 11:04:41 -0600 Subject: [PATCH 008/609] [web_server] Switch from getParam to arg API to eliminate heap allocations (#13942) --- .../captive_portal/captive_portal.cpp | 8 +- esphome/components/web_server/web_server.cpp | 94 +++++++++---------- esphome/components/web_server/web_server.h | 53 ++++------- esphome/components/web_server_idf/utils.cpp | 41 ++++---- esphome/components/web_server_idf/utils.h | 5 +- .../web_server_idf/web_server_idf.cpp | 52 ++++++++-- .../web_server_idf/web_server_idf.h | 11 +-- 7 files changed, 134 insertions(+), 130 deletions(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 8d88a10b27..5af6ab29a2 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -47,8 +47,8 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) { request->send(stream); } void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { - std::string ssid = request->arg("ssid").c_str(); // NOLINT(readability-redundant-string-cstr) - std::string psk = request->arg("psk").c_str(); // NOLINT(readability-redundant-string-cstr) + const auto &ssid = request->arg("ssid"); + const auto &psk = request->arg("psk"); ESP_LOGI(TAG, "Requested WiFi Settings Change:\n" " SSID='%s'\n" @@ -56,10 +56,10 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ssid.c_str(), psk.c_str()); #ifdef USE_ESP8266 // ESP8266 is single-threaded, call directly - wifi::global_wifi_component->save_wifi_sta(ssid, psk); + wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); #else // Defer save to main loop thread to avoid NVS operations from HTTP thread - this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); }); + this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); }); #endif request->redirect(ESPHOME_F("/?save")); } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 7da8b49c6d..c7a0639382 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -585,8 +585,7 @@ static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const c // Helper to get request detail parameter static JsonDetail get_request_detail(AsyncWebServerRequest *request) { - auto *param = request->getParam(ESPHOME_F("detail")); - return (param && param->value() == "all") ? DETAIL_ALL : DETAIL_STATE; + return request->arg(ESPHOME_F("detail")) == "all" ? DETAIL_ALL : DETAIL_STATE; } #ifdef USE_SENSOR @@ -863,10 +862,10 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } auto call = is_on ? obj->turn_on() : obj->turn_off(); - parse_int_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed); + parse_num_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed); - if (request->hasParam(ESPHOME_F("oscillation"))) { - auto speed = request->getParam(ESPHOME_F("oscillation"))->value(); + if (request->hasArg(ESPHOME_F("oscillation"))) { + auto speed = request->arg(ESPHOME_F("oscillation")); auto val = parse_on_off(speed.c_str()); switch (val) { case PARSE_ON: @@ -1042,14 +1041,14 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa } auto traits = obj->get_traits(); - if ((request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) || - (request->hasParam(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) { + if ((request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) || + (request->hasArg(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) { request->send(409); return; } - parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); - parse_float_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt); + parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); + parse_num_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1108,7 +1107,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM } auto call = obj->make_call(); - parse_float_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value); + parse_num_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1176,12 +1175,13 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat auto call = obj->make_call(); - if (!request->hasParam(ESPHOME_F("value"))) { + const auto &value = request->arg(ESPHOME_F("value")); + // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility + if (value.length() == 0) { // NOLINT(readability-container-size-empty) request->send(409); return; } - - parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_date); + call.set_date(value.c_str(), value.length()); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1236,12 +1236,13 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat auto call = obj->make_call(); - if (!request->hasParam(ESPHOME_F("value"))) { + const auto &value = request->arg(ESPHOME_F("value")); + // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility + if (value.length() == 0) { // NOLINT(readability-container-size-empty) request->send(409); return; } - - parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_time); + call.set_time(value.c_str(), value.length()); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1295,12 +1296,13 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur auto call = obj->make_call(); - if (!request->hasParam(ESPHOME_F("value"))) { + const auto &value = request->arg(ESPHOME_F("value")); + // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility + if (value.length() == 0) { // NOLINT(readability-container-size-empty) request->send(409); return; } - - parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_datetime); + call.set_datetime(value.c_str(), value.length()); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1479,10 +1481,14 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url parse_string_param_(request, ESPHOME_F("swing_mode"), call, &decltype(call)::set_swing_mode); // Parse temperature parameters - parse_float_param_(request, ESPHOME_F("target_temperature_high"), call, - &decltype(call)::set_target_temperature_high); - parse_float_param_(request, ESPHOME_F("target_temperature_low"), call, &decltype(call)::set_target_temperature_low); - parse_float_param_(request, ESPHOME_F("target_temperature"), call, &decltype(call)::set_target_temperature); + // static_cast needed to disambiguate overloaded setters (float vs optional) + using ClimateCall = decltype(call); + parse_num_param_(request, ESPHOME_F("target_temperature_high"), call, + static_cast(&ClimateCall::set_target_temperature_high)); + parse_num_param_(request, ESPHOME_F("target_temperature_low"), call, + static_cast(&ClimateCall::set_target_temperature_low)); + parse_num_param_(request, ESPHOME_F("target_temperature"), call, + static_cast(&ClimateCall::set_target_temperature)); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1723,12 +1729,12 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa } auto traits = obj->get_traits(); - if (request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) { + if (request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) { request->send(409); return; } - parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); + parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1872,12 +1878,12 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons parse_string_param_(request, ESPHOME_F("mode"), base_call, &water_heater::WaterHeaterCall::set_mode); // Parse temperature parameters - parse_float_param_(request, ESPHOME_F("target_temperature"), base_call, - &water_heater::WaterHeaterCall::set_target_temperature); - parse_float_param_(request, ESPHOME_F("target_temperature_low"), base_call, - &water_heater::WaterHeaterCall::set_target_temperature_low); - parse_float_param_(request, ESPHOME_F("target_temperature_high"), base_call, - &water_heater::WaterHeaterCall::set_target_temperature_high); + parse_num_param_(request, ESPHOME_F("target_temperature"), base_call, + &water_heater::WaterHeaterCall::set_target_temperature); + parse_num_param_(request, ESPHOME_F("target_temperature_low"), base_call, + &water_heater::WaterHeaterCall::set_target_temperature_low); + parse_num_param_(request, ESPHOME_F("target_temperature_high"), base_call, + &water_heater::WaterHeaterCall::set_target_temperature_high); // Parse away mode parameter parse_bool_param_(request, ESPHOME_F("away"), base_call, &water_heater::WaterHeaterCall::set_away); @@ -1981,16 +1987,16 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur auto call = obj->make_call(); // Parse carrier frequency (optional) - if (request->hasParam(ESPHOME_F("carrier_frequency"))) { - auto value = parse_number(request->getParam(ESPHOME_F("carrier_frequency"))->value().c_str()); + { + auto value = parse_number(request->arg(ESPHOME_F("carrier_frequency")).c_str()); if (value.has_value()) { call.set_carrier_frequency(*value); } } // Parse repeat count (optional, defaults to 1) - if (request->hasParam(ESPHOME_F("repeat_count"))) { - auto value = parse_number(request->getParam(ESPHOME_F("repeat_count"))->value().c_str()); + { + auto value = parse_number(request->arg(ESPHOME_F("repeat_count")).c_str()); if (value.has_value()) { call.set_repeat_count(*value); } @@ -1998,18 +2004,12 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur // Parse base64url-encoded raw timings (required) // Base64url is URL-safe: uses A-Za-z0-9-_ (no special characters needing escaping) - if (!request->hasParam(ESPHOME_F("data"))) { - request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing 'data' parameter")); - return; - } + const auto &data_arg = request->arg(ESPHOME_F("data")); - // .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string - std::string encoded = - request->getParam(ESPHOME_F("data"))->value().c_str(); // NOLINT(readability-redundant-string-cstr) - - // Validate base64url is not empty - if (encoded.empty()) { - request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Empty 'data' parameter")); + // Validate base64url is not empty (also catches missing parameter since arg() returns empty string) + // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility + if (data_arg.length() == 0) { // NOLINT(readability-container-size-empty) + request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing or empty 'data' parameter")); return; } @@ -2017,7 +2017,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur // it outlives the call - set_raw_timings_base64url stores a pointer, so the string // must remain valid until perform() completes. // ESP8266 also needs this because ESPAsyncWebServer callbacks run in "sys" context. - this->defer([call, encoded = std::move(encoded)]() mutable { + this->defer([call, encoded = std::string(data_arg.c_str(), data_arg.length())]() mutable { call.set_raw_timings_base64url(encoded); call.perform(); }); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index ce09ebf7a9..026da763ea 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -513,11 +513,9 @@ class WebServer : public Controller, template void parse_light_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float), float scale = 1.0f) { - if (request->hasParam(param_name)) { - auto value = parse_number(request->getParam(param_name)->value().c_str()); - if (value.has_value()) { - (call.*setter)(*value / scale); - } + auto value = parse_number(request->arg(param_name).c_str()); + if (value.has_value()) { + (call.*setter)(*value / scale); } } @@ -525,34 +523,19 @@ class WebServer : public Controller, template void parse_light_param_uint_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(uint32_t), uint32_t scale = 1) { - if (request->hasParam(param_name)) { - auto value = parse_number(request->getParam(param_name)->value().c_str()); - if (value.has_value()) { - (call.*setter)(*value * scale); - } + auto value = parse_number(request->arg(param_name).c_str()); + if (value.has_value()) { + (call.*setter)(*value * scale); } } #endif - // Generic helper to parse and apply a float parameter - template - void parse_float_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float)) { - if (request->hasParam(param_name)) { - auto value = parse_number(request->getParam(param_name)->value().c_str()); - if (value.has_value()) { - (call.*setter)(*value); - } - } - } - - // Generic helper to parse and apply an int parameter - template - void parse_int_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(int)) { - if (request->hasParam(param_name)) { - auto value = parse_number(request->getParam(param_name)->value().c_str()); - if (value.has_value()) { - (call.*setter)(*value); - } + // Generic helper to parse and apply a numeric parameter + template + void parse_num_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(NumT)) { + auto value = parse_number(request->arg(param_name).c_str()); + if (value.has_value()) { + (call.*setter)(*value); } } @@ -560,10 +543,9 @@ class WebServer : public Controller, template void parse_string_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(const std::string &)) { - if (request->hasParam(param_name)) { - // .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string - std::string value = request->getParam(param_name)->value().c_str(); // NOLINT(readability-redundant-string-cstr) - (call.*setter)(value); + if (request->hasArg(param_name)) { + const auto &value = request->arg(param_name); + (call.*setter)(std::string(value.c_str(), value.length())); } } @@ -573,8 +555,9 @@ class WebServer : public Controller, // Invalid values are ignored (setter not called) template void parse_bool_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(bool)) { - if (request->hasParam(param_name)) { - auto param_value = request->getParam(param_name)->value(); + const auto ¶m_value = request->arg(param_name); + // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility + if (param_value.length() > 0) { // NOLINT(readability-container-size-empty) // First check on/off (default), then true/false (custom) auto val = parse_on_off(param_value.c_str()); if (val == PARSE_NONE) { diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp index 81ae626277..c58ca2a24f 100644 --- a/esphome/components/web_server_idf/utils.cpp +++ b/esphome/components/web_server_idf/utils.cpp @@ -1,17 +1,13 @@ #ifdef USE_ESP32 -#include #include #include #include "esphome/core/helpers.h" -#include "esphome/core/log.h" #include "http_parser.h" #include "utils.h" namespace esphome::web_server_idf { -static const char *const TAG = "web_server_idf_utils"; - size_t url_decode(char *str) { char *start = str; char *ptr = str, buf; @@ -54,32 +50,15 @@ optional request_get_header(httpd_req_t *req, const char *name) { return {str}; } -optional request_get_url_query(httpd_req_t *req) { - auto len = httpd_req_get_url_query_len(req); - if (len == 0) { - return {}; - } - - std::string str; - str.resize(len); - - auto res = httpd_req_get_url_query_str(req, &str[0], len + 1); - if (res != ESP_OK) { - ESP_LOGW(TAG, "Can't get query for request: %s", esp_err_to_name(res)); - return {}; - } - - return {str}; -} - optional query_key_value(const char *query_url, size_t query_len, const char *key) { if (query_url == nullptr || query_len == 0) { return {}; } - // Use stack buffer for typical query strings, heap fallback for large ones - SmallBufferWithHeapFallback<256, char> val(query_len); - + // Value can't exceed query_len. Use small stack buffer for typical values, + // heap fallback for long ones (e.g. base64 IR data) to limit stack usage + // since callers may also have stack buffers for the query string. + SmallBufferWithHeapFallback<128, char> val(query_len); if (httpd_query_key_value(query_url, key, val.get(), query_len) != ESP_OK) { return {}; } @@ -88,6 +67,18 @@ optional query_key_value(const char *query_url, size_t query_len, c return {val.get()}; } +bool query_has_key(const char *query_url, size_t query_len, const char *key) { + if (query_url == nullptr || query_len == 0) { + return false; + } + // Minimal buffer — we only care if the key exists, not the value + char buf[1]; + // httpd_query_key_value returns ESP_OK if found, ESP_ERR_HTTPD_RESULT_TRUNC if found + // but value truncated (expected with 1-byte buffer), or other errors for invalid input + auto err = httpd_query_key_value(query_url, key, buf, sizeof(buf)); + return err == ESP_OK || err == ESP_ERR_HTTPD_RESULT_TRUNC; +} + // Helper function for case-insensitive string region comparison bool str_ncmp_ci(const char *s1, const char *s2, size_t n) { for (size_t i = 0; i < n; i++) { diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h index 87635c0458..027a2f7b6c 100644 --- a/esphome/components/web_server_idf/utils.h +++ b/esphome/components/web_server_idf/utils.h @@ -13,11 +13,8 @@ size_t url_decode(char *str); bool request_has_header(httpd_req_t *req, const char *name); optional request_get_header(httpd_req_t *req, const char *name); -optional request_get_url_query(httpd_req_t *req); optional query_key_value(const char *query_url, size_t query_len, const char *key); -inline optional query_key_value(const std::string &query_url, const std::string &key) { - return query_key_value(query_url.c_str(), query_url.size(), key.c_str()); -} +bool query_has_key(const char *query_url, size_t query_len, const char *key); // Helper function for case-insensitive character comparison inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b); } diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index f1f89beb49..1798159e7f 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -393,13 +393,7 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) { } // Look up value from query strings - optional val = query_key_value(this->post_query_.c_str(), this->post_query_.size(), name); - if (!val.has_value()) { - auto url_query = request_get_url_query(*this); - if (url_query.has_value()) { - val = query_key_value(url_query.value().c_str(), url_query.value().size(), name); - } - } + auto val = this->find_query_value_(name); // Don't cache misses to avoid wasting memory when handlers check for // optional parameters that don't exist in the request @@ -412,6 +406,50 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) { return param; } +/// Search post_query then URL query with a callback. +/// Returns first truthy result, or value-initialized default. +/// URL query is accessed directly from req->uri (same pattern as url_to()). +template +static auto search_query_sources(httpd_req_t *req, const std::string &post_query, const char *name, Func func) + -> decltype(func(nullptr, size_t{0}, name)) { + if (!post_query.empty()) { + auto result = func(post_query.c_str(), post_query.size(), name); + if (result) { + return result; + } + } + // Use httpd API for query length, then access string directly from URI. + // http_parser identifies components by offset/length without modifying the URI string. + // This is the same pattern used by url_to(). + auto len = httpd_req_get_url_query_len(req); + if (len == 0) { + return {}; + } + const char *query = strchr(req->uri, '?'); + if (query == nullptr) { + return {}; + } + query++; // skip '?' + return func(query, len, name); +} + +optional AsyncWebServerRequest::find_query_value_(const char *name) const { + return search_query_sources(this->req_, this->post_query_, name, + [](const char *q, size_t len, const char *k) { return query_key_value(q, len, k); }); +} + +bool AsyncWebServerRequest::hasArg(const char *name) { + return search_query_sources(this->req_, this->post_query_, name, query_has_key); +} + +std::string AsyncWebServerRequest::arg(const char *name) { + auto val = this->find_query_value_(name); + if (val.has_value()) { + return std::move(val.value()); + } + return {}; +} + void AsyncWebServerResponse::addHeader(const char *name, const char *value) { httpd_resp_set_hdr(*this->req_, name, value); } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 6a409de74e..12df0303de 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -170,14 +170,8 @@ class AsyncWebServerRequest { AsyncWebParameter *getParam(const std::string &name) { return this->getParam(name.c_str()); } // NOLINTNEXTLINE(readability-identifier-naming) - bool hasArg(const char *name) { return this->hasParam(name); } - std::string arg(const char *name) { - auto *param = this->getParam(name); - if (param) { - return param->value(); - } - return {}; - } + bool hasArg(const char *name); + std::string arg(const char *name); std::string arg(const std::string &name) { return this->arg(name.c_str()); } operator httpd_req_t *() const { return this->req_; } @@ -192,6 +186,7 @@ class AsyncWebServerRequest { // is faster than tree/hash overhead. AsyncWebParameter stores both name and value to avoid // duplicate storage. Only successful lookups are cached to prevent cache pollution when // handlers check for optional parameters that don't exist. + optional find_query_value_(const char *name) const; std::vector params_; std::string post_query_; AsyncWebServerRequest(httpd_req_t *req) : req_(req) {} From 0dcff82bb479b131a4dbb33d00940c7a91120dc2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 11:14:36 -0600 Subject: [PATCH 009/609] [wifi] Deprecate wifi_ssid() in favor of wifi_ssid_to() (#13958) --- 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. From e3a457e40268949364f776fd5ec3ff5fd833a443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Ma=C5=88as?= Date: Thu, 12 Feb 2026 18:20:54 +0100 Subject: [PATCH 010/609] [pulse_meter] Fix early edge detection (#12360) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .../pulse_meter/pulse_meter_sensor.cpp | 75 ++++++++++--------- .../pulse_meter/pulse_meter_sensor.h | 11 ++- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index 007deb66e5..433e1f0b7e 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -38,8 +38,7 @@ void PulseMeterSensor::setup() { } void PulseMeterSensor::loop() { - // Reset the count in get before we pass it back to the ISR as set - this->get_->count_ = 0; + State state; { // Lock the interrupt so the interrupt code doesn't interfere with itself @@ -58,31 +57,35 @@ void PulseMeterSensor::loop() { } this->last_pin_val_ = current; - // Swap out set and get to get the latest state from the ISR - std::swap(this->set_, this->get_); + // Get the latest state from the ISR and reset the count in the ISR + state.last_detected_edge_us_ = this->state_.last_detected_edge_us_; + state.last_rising_edge_us_ = this->state_.last_rising_edge_us_; + state.count_ = this->state_.count_; + this->state_.count_ = 0; } const uint32_t now = micros(); // If an edge was peeked, repay the debt - if (this->peeked_edge_ && this->get_->count_ > 0) { + if (this->peeked_edge_ && state.count_ > 0) { this->peeked_edge_ = false; - this->get_->count_--; // NOLINT(clang-diagnostic-deprecated-volatile) + state.count_--; } - // If there is an unprocessed edge, and filter_us_ has passed since, count this edge early - if (this->get_->last_rising_edge_us_ != this->get_->last_detected_edge_us_ && - now - this->get_->last_rising_edge_us_ >= this->filter_us_) { + // If there is an unprocessed edge, and filter_us_ has passed since, count this edge early. + // Wait for the debt to be repaid before counting another unprocessed edge early. + if (!this->peeked_edge_ && state.last_rising_edge_us_ != state.last_detected_edge_us_ && + now - state.last_rising_edge_us_ >= this->filter_us_) { this->peeked_edge_ = true; - this->get_->last_detected_edge_us_ = this->get_->last_rising_edge_us_; - this->get_->count_++; // NOLINT(clang-diagnostic-deprecated-volatile) + state.last_detected_edge_us_ = state.last_rising_edge_us_; + state.count_++; } // Check if we detected a pulse this loop - if (this->get_->count_ > 0) { + if (state.count_ > 0) { // Keep a running total of pulses if a total sensor is configured if (this->total_sensor_ != nullptr) { - this->total_pulses_ += this->get_->count_; + this->total_pulses_ += state.count_; const uint32_t total = this->total_pulses_; this->total_sensor_->publish_state(total); } @@ -94,15 +97,15 @@ void PulseMeterSensor::loop() { this->meter_state_ = MeterState::RUNNING; } break; case MeterState::RUNNING: { - uint32_t delta_us = this->get_->last_detected_edge_us_ - this->last_processed_edge_us_; - float pulse_width_us = delta_us / float(this->get_->count_); - ESP_LOGV(TAG, "New pulse, delta: %" PRIu32 " µs, count: %" PRIu32 ", width: %.5f µs", delta_us, - this->get_->count_, pulse_width_us); + uint32_t delta_us = state.last_detected_edge_us_ - this->last_processed_edge_us_; + float pulse_width_us = delta_us / float(state.count_); + ESP_LOGV(TAG, "New pulse, delta: %" PRIu32 " µs, count: %" PRIu32 ", width: %.5f µs", delta_us, state.count_, + pulse_width_us); this->publish_state((60.0f * 1000000.0f) / pulse_width_us); } break; } - this->last_processed_edge_us_ = this->get_->last_detected_edge_us_; + this->last_processed_edge_us_ = state.last_detected_edge_us_; } // No detected edges this loop else { @@ -141,14 +144,14 @@ void IRAM_ATTR PulseMeterSensor::edge_intr(PulseMeterSensor *sensor) { // This is an interrupt handler - we can't call any virtual method from this method // Get the current time before we do anything else so the measurements are consistent const uint32_t now = micros(); - auto &state = sensor->edge_state_; - auto &set = *sensor->set_; + auto &edge_state = sensor->edge_state_; + auto &state = sensor->state_; - if ((now - state.last_sent_edge_us_) >= sensor->filter_us_) { - state.last_sent_edge_us_ = now; - set.last_detected_edge_us_ = now; - set.last_rising_edge_us_ = now; - set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) + if ((now - edge_state.last_sent_edge_us_) >= sensor->filter_us_) { + edge_state.last_sent_edge_us_ = now; + state.last_detected_edge_us_ = now; + state.last_rising_edge_us_ = now; + state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) } // This ISR is bound to rising edges, so the pin is high @@ -160,26 +163,26 @@ void IRAM_ATTR PulseMeterSensor::pulse_intr(PulseMeterSensor *sensor) { // Get the current time before we do anything else so the measurements are consistent const uint32_t now = micros(); const bool pin_val = sensor->isr_pin_.digital_read(); - auto &state = sensor->pulse_state_; - auto &set = *sensor->set_; + auto &pulse_state = sensor->pulse_state_; + auto &state = sensor->state_; // Filter length has passed since the last interrupt - const bool length = now - state.last_intr_ >= sensor->filter_us_; + const bool length = now - pulse_state.last_intr_ >= sensor->filter_us_; - if (length && state.latched_ && !sensor->last_pin_val_) { // Long enough low edge - state.latched_ = false; - } else if (length && !state.latched_ && sensor->last_pin_val_) { // Long enough high edge - state.latched_ = true; - set.last_detected_edge_us_ = state.last_intr_; - set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) + if (length && pulse_state.latched_ && !sensor->last_pin_val_) { // Long enough low edge + pulse_state.latched_ = false; + } else if (length && !pulse_state.latched_ && sensor->last_pin_val_) { // Long enough high edge + pulse_state.latched_ = true; + state.last_detected_edge_us_ = pulse_state.last_intr_; + state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) } // Due to order of operations this includes // length && latched && rising (just reset from a long low edge) // !latched && (rising || high) (noise on the line resetting the potential rising edge) - set.last_rising_edge_us_ = !state.latched_ && pin_val ? now : set.last_detected_edge_us_; + state.last_rising_edge_us_ = !pulse_state.latched_ && pin_val ? now : state.last_detected_edge_us_; - state.last_intr_ = now; + pulse_state.last_intr_ = now; sensor->last_pin_val_ = pin_val; } diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.h b/esphome/components/pulse_meter/pulse_meter_sensor.h index 5800c4ec42..e46f1e615f 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.h +++ b/esphome/components/pulse_meter/pulse_meter_sensor.h @@ -46,17 +46,16 @@ class PulseMeterSensor : public sensor::Sensor, public Component { uint32_t total_pulses_ = 0; uint32_t last_processed_edge_us_ = 0; - // This struct (and the two pointers) are used to pass data between the ISR and loop. - // These two pointers are exchanged each loop. - // Use these to send data from the ISR to the loop not the other way around (except for resetting the values). + // This struct and variable are used to pass data between the ISR and loop. + // The data from state_ is read and then count_ in state_ is reset in each loop. + // This must be done while guarded by an InterruptLock. Use this variable to send data + // from the ISR to the loop not the other way around (except for resetting count_). struct State { uint32_t last_detected_edge_us_ = 0; uint32_t last_rising_edge_us_ = 0; uint32_t count_ = 0; }; - State state_[2]; - volatile State *set_ = state_; - volatile State *get_ = state_ + 1; + volatile State state_{}; // Only use the following variables in the ISR or while guarded by an InterruptLock ISRInternalGPIOPin isr_pin_; From 7fd535179e4cc793047ebcf802b0fe174b3987cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 11:47:44 -0600 Subject: [PATCH 011/609] [helpers] Add heap warnings to format_hex_pretty, deprecate ethernet/web_server std::string APIs (#13959) --- .../components/ethernet/ethernet_component.h | 2 ++ .../components/web_server_idf/web_server_idf.h | 3 ++- esphome/core/helpers.h | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 5a2869c5a7..b4859c308d 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -110,6 +110,8 @@ class EthernetComponent : public Component { const char *get_use_address() const; void set_use_address(const char *use_address); void get_eth_mac_address_raw(uint8_t *mac); + // Remove before 2026.9.0 + ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0") std::string get_eth_mac_address_pretty(); const char *get_eth_mac_address_pretty_into_buffer(std::span buf); eth_duplex_t get_duplex_mode(); diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 12df0303de..74601ffda8 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -116,7 +116,8 @@ class AsyncWebServerRequest { /// Write URL (without query string) to buffer, returns StringRef pointing to buffer. /// URL is decoded (e.g., %20 -> space). StringRef url_to(std::span buffer) const; - /// Get URL as std::string. Prefer url_to() to avoid heap allocation. + // Remove before 2026.9.0 + ESPDEPRECATED("Use url_to() instead. Removed in 2026.9.0", "2026.3.0") std::string url() const { char buffer[URL_BUF_SIZE]; return std::string(this->url_to(buffer)); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index f7de34b6d5..34c7452484 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1083,6 +1083,9 @@ template std::string format_hex(const std::array &dat * Each byte is displayed as a two-digit uppercase hex value, separated by the specified separator. * Optionally includes the total byte count in parentheses at the end. * + * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. + * Causes heap fragmentation on long-running devices. + * * @param data Pointer to the byte array to format. * @param length Number of bytes in the array. * @param separator Character to use between hex bytes (default: '.'). @@ -1108,6 +1111,9 @@ std::string format_hex_pretty(const uint8_t *data, size_t length, char separator * * Similar to the byte array version, but formats 16-bit words as 4-digit hex values. * + * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. + * Causes heap fragmentation on long-running devices. + * * @param data Pointer to the 16-bit word array to format. * @param length Number of 16-bit words in the array. * @param separator Character to use between hex words (default: '.'). @@ -1131,6 +1137,9 @@ std::string format_hex_pretty(const uint16_t *data, size_t length, char separato * Convenience overload for std::vector. Formats each byte as a two-digit * uppercase hex value with customizable separator. * + * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. + * Causes heap fragmentation on long-running devices. + * * @param data Vector of bytes to format. * @param separator Character to use between hex bytes (default: '.'). * @param show_length Whether to append the byte count in parentheses (default: true). @@ -1154,6 +1163,9 @@ std::string format_hex_pretty(const std::vector &data, char separator = * Convenience overload for std::vector. Each 16-bit word is formatted * as a 4-digit uppercase hex value in big-endian order. * + * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. + * Causes heap fragmentation on long-running devices. + * * @param data Vector of 16-bit words to format. * @param separator Character to use between hex words (default: '.'). * @param show_length Whether to append the word count in parentheses (default: true). @@ -1176,6 +1188,9 @@ std::string format_hex_pretty(const std::vector &data, char separator * Treats each character in the string as a byte and formats it in hex. * Useful for debugging binary data stored in std::string containers. * + * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. + * Causes heap fragmentation on long-running devices. + * * @param data String whose bytes should be formatted as hex. * @param separator Character to use between hex bytes (default: '.'). * @param show_length Whether to append the byte count in parentheses (default: true). @@ -1198,6 +1213,9 @@ std::string format_hex_pretty(const std::string &data, char separator = '.', boo * Converts the integer to big-endian byte order and formats each byte as hex. * The most significant byte appears first in the output string. * + * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. + * Causes heap fragmentation on long-running devices. + * * @tparam T Unsigned integer type (uint8_t, uint16_t, uint32_t, uint64_t, etc.). * @param val The unsigned integer value to format. * @param separator Character to use between hex bytes (default: '.'). From bbc88d92ea0c30efd12d11a27edfe50ea618e299 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:31:43 -0600 Subject: [PATCH 012/609] Bump docker/build-push-action from 6.19.1 to 6.19.2 in /.github/actions/build-image (#13965) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-image/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index 494304eced..38e93c4f17 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -47,7 +47,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false @@ -73,7 +73,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false From db7870ef5fb3f7fce20f897f7c6279f82695e8c9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:04:39 -0500 Subject: [PATCH 013/609] [alarm_control_panel] Fix flaky integration test race condition (#13964) Co-authored-by: Claude Opus 4.6 --- ...t_alarm_control_panel_state_transitions.py | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/integration/test_alarm_control_panel_state_transitions.py b/tests/integration/test_alarm_control_panel_state_transitions.py index 09348f5bea..0b07710961 100644 --- a/tests/integration/test_alarm_control_panel_state_transitions.py +++ b/tests/integration/test_alarm_control_panel_state_transitions.py @@ -270,6 +270,14 @@ async def test_alarm_control_panel_state_transitions( # The chime_sensor has chime: true, so opening it while disarmed # should trigger on_chime callback + # Set up future for the on_ready from opening the chime sensor + # (alarm becomes "not ready" when chime sensor opens). + # We must wait for this BEFORE creating the close future, otherwise + # the open event's log can arrive late and resolve the close future, + # causing the test to proceed before the chime close is processed. + ready_after_chime_open: asyncio.Future[bool] = loop.create_future() + ready_futures.append(ready_after_chime_open) + # We're currently DISARMED - open the chime sensor client.switch_command(chime_switch_info.key, True) @@ -279,11 +287,18 @@ async def test_alarm_control_panel_state_transitions( except TimeoutError: pytest.fail(f"on_chime callback not fired. Log lines: {log_lines[-20:]}") - # Close the chime sensor and wait for alarm to become ready again - # We need to wait for this transition before testing door sensor, - # otherwise there's a race where the door sensor state change could - # arrive before the chime sensor state change, leaving the alarm in - # a continuous "not ready" state with no on_ready callback fired. + # Wait for the on_ready from the chime sensor opening + try: + await asyncio.wait_for(ready_after_chime_open, timeout=2.0) + except TimeoutError: + pytest.fail( + f"on_ready callback not fired when chime sensor opened. " + f"Log lines: {log_lines[-20:]}" + ) + + # Now create the future for the close event and close the sensor. + # Since we waited for the open event above, the close event's + # on_ready log cannot be confused with the open event's. ready_after_chime_close: asyncio.Future[bool] = loop.create_future() ready_futures.append(ready_after_chime_close) From 136d17366f5ab2a08912ce2f6a5459eea358ec24 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:12:17 -0500 Subject: [PATCH 014/609] [docker] Suppress git detached HEAD advice (#13962) Co-authored-by: Claude Opus 4.6 --- docker/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 8ebdd1e49b..540d28be7f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -9,7 +9,8 @@ FROM ghcr.io/esphome/docker-base:${BUILD_OS}-ha-addon-${BUILD_BASE_VERSION} AS b ARG BUILD_TYPE FROM base-source-${BUILD_TYPE} AS base -RUN git config --system --add safe.directory "*" +RUN git config --system --add safe.directory "*" \ + && git config --system advice.detachedHead false # Install build tools for Python packages that require compilation # (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager) From 36aba385af3f502462f1a38fb6c334e580ad7155 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 18:20:21 -0600 Subject: [PATCH 015/609] [web_server] Flatten deq_push_back_with_dedup_ to inline vector realloc (#13968) --- esphome/components/web_server/web_server.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index c7a0639382..3acd2d2119 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -198,7 +198,8 @@ EntityMatchResult UrlMatch::match_entity(EntityBase *entity) const { #if !defined(USE_ESP32) && defined(USE_ARDUINO) // helper for allowing only unique entries in the queue -void DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) { +void __attribute__((flatten)) +DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) { DeferredEvent item(source, message_generator); // Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size From 7dff631dcb133e69a26d841a1f0645cac812202a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 18:20:39 -0600 Subject: [PATCH 016/609] [core] Flatten single-callsite vector realloc functions (#13970) --- esphome/components/api/api_connection.cpp | 6 ++++-- esphome/components/api/api_connection.h | 2 ++ esphome/components/api/api_server.cpp | 2 +- esphome/components/wifi/wifi_component.cpp | 13 +++++++++++++ esphome/components/wifi/wifi_component.h | 13 +------------ 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4bc3c9b307..4d564af9e2 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1864,6 +1864,8 @@ void APIConnection::on_fatal_error() { this->flags_.remove = true; } +void __attribute__((flatten)) APIConnection::DeferredBatch::push_item(const BatchItem &item) { items.push_back(item); } + void APIConnection::DeferredBatch::add_item(EntityBase *entity, uint8_t message_type, uint8_t estimated_size, uint8_t aux_data_index) { // Check if we already have a message of this type for this entity @@ -1880,7 +1882,7 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, uint8_t message_ } } // No existing item found (or event), add new one - items.push_back({entity, message_type, estimated_size, aux_data_index}); + this->push_item({entity, message_type, estimated_size, aux_data_index}); } void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) { @@ -1888,7 +1890,7 @@ void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t me // This avoids expensive vector::insert which shifts all elements // Note: We only ever have one high-priority message at a time (ping OR disconnect) // If we're disconnecting, pings are blocked, so this simple swap is sufficient - items.push_back({entity, message_type, estimated_size, AUX_DATA_UNUSED}); + this->push_item({entity, message_type, estimated_size, AUX_DATA_UNUSED}); if (items.size() > 1) { // Swap the new high-priority item to the front std::swap(items.front(), items.back()); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index d3d09a01c8..e34bed8ada 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -541,6 +541,8 @@ class APIConnection final : public APIServerConnectionBase { uint8_t aux_data_index = AUX_DATA_UNUSED); // Add item to the front of the batch (for high priority messages like ping) void add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size); + // Single push_back site to avoid duplicate _M_realloc_insert instantiation + void push_item(const BatchItem &item); // Clear all items void clear() { diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 5503cf4db8..211bda66de 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -195,7 +195,7 @@ void APIServer::remove_client_(size_t client_index) { #endif } -void APIServer::accept_new_connections_() { +void __attribute__((flatten)) APIServer::accept_new_connections_() { while (true) { struct sockaddr_storage source_addr; socklen_t addr_len = sizeof(source_addr); diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 61d05d7635..a2efac8d26 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -487,6 +487,19 @@ bool WiFiComponent::matches_configured_network_(const char *ssid, const uint8_t return false; } +void __attribute__((flatten)) WiFiComponent::set_sta_priority(bssid_t bssid, int8_t priority) { + for (auto &it : this->sta_priorities_) { + if (it.bssid == bssid) { + it.priority = priority; + return; + } + } + this->sta_priorities_.push_back(WiFiSTAPriority{ + .bssid = bssid, + .priority = priority, + }); +} + void WiFiComponent::log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE // Skip logging during roaming scans to avoid log buffer overflow diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 53ff0d9cad..4a038f602c 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -488,18 +488,7 @@ class WiFiComponent : public Component { } return 0; } - void set_sta_priority(const bssid_t bssid, int8_t priority) { - for (auto &it : this->sta_priorities_) { - if (it.bssid == bssid) { - it.priority = priority; - return; - } - } - this->sta_priorities_.push_back(WiFiSTAPriority{ - .bssid = bssid, - .priority = priority, - }); - } + void set_sta_priority(bssid_t bssid, int8_t priority); network::IPAddresses wifi_sta_ip_addresses(); // Remove before 2026.9.0 From e0c03b2dfa39021bcff1805376ce53dde1e8bbdf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 18:20:58 -0600 Subject: [PATCH 017/609] [api] Fix ESP8266 noise API handshake deadlock and prompt socket cleanup (#13972) --- esphome/components/api/api_frame_helper_noise.cpp | 10 ++++++---- esphome/components/api/api_server.cpp | 8 ++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index c1641b398a..1ae848dead 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -138,10 +138,12 @@ APIError APINoiseFrameHelper::handle_noise_error_(int err, const LogString *func /// Run through handshake messages (if in that phase) APIError APINoiseFrameHelper::loop() { - // During handshake phase, process as many actions as possible until we can't progress - // socket_->ready() stays true until next main loop, but state_action() will return - // WOULD_BLOCK when no more data is available to read - while (state_ != State::DATA && this->socket_->ready()) { + // Cache ready() outside the loop. On ESP8266 LWIP raw TCP, ready() returns false once + // the rx buffer is consumed. Re-checking each iteration would block handshake writes + // that must follow reads, deadlocking the handshake. state_action() will return + // WOULD_BLOCK when no more data is available to read. + bool socket_ready = this->socket_->ready(); + while (state_ != State::DATA && socket_ready) { APIError err = state_action_(); if (err == APIError::WOULD_BLOCK) { break; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 211bda66de..f25a9bc0e2 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -148,12 +148,16 @@ void APIServer::loop() { while (client_index < this->clients_.size()) { auto &client = this->clients_[client_index]; + // Common case: process active client + if (!client->flags_.remove) { + client->loop(); + } + // Handle disconnection promptly - close socket to free LWIP PCB + // resources and prevent retransmit crashes on ESP8266. if (client->flags_.remove) { // Rare case: handle disconnection (don't increment - swapped element needs processing) this->remove_client_(client_index); } else { - // Common case: process active client - client->loop(); client_index++; } } From b04e427f01974947856793b0a53425cccfe180bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 Feb 2026 06:39:00 -0600 Subject: [PATCH 018/609] [usb_host] Extract cold path from loop(), replace std::string with buffer API (#13957) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/usb_host/usb_host.h | 1 + .../components/usb_host/usb_host_client.cpp | 123 ++++++++++-------- 2 files changed, 67 insertions(+), 57 deletions(-) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index d11a148a0f..d1ec356613 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -148,6 +148,7 @@ class USBClient : public Component { EventPool event_pool; protected: + void handle_open_state_(); TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe) virtual void disconnect(); virtual void on_connected() {} diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 09da6e3b73..0612d7a841 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace esphome { namespace usb_host { @@ -142,18 +143,23 @@ static void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc } while (next_desc != NULL); } #endif -static std::string get_descriptor_string(const usb_str_desc_t *desc) { - char buffer[256]; - if (desc == nullptr) +// USB string descriptors: bLength (uint8_t, max 255) includes the 2-byte header (bLength and bDescriptorType). +// Character count = (bLength - 2) / 2, max 126 chars + null terminator. +static constexpr size_t DESC_STRING_BUF_SIZE = 128; + +static const char *get_descriptor_string(const usb_str_desc_t *desc, std::span buffer) { + if (desc == nullptr || desc->bLength < 2) return "(unspecified)"; - char *p = buffer; - for (int i = 0; i != desc->bLength / 2; i++) { + int char_count = (desc->bLength - 2) / 2; + char *p = buffer.data(); + char *end = p + buffer.size() - 1; + for (int i = 0; i != char_count && p < end; i++) { auto c = desc->wData[i]; if (c < 0x100) *p++ = static_cast(c); } *p = '\0'; - return {buffer}; + return buffer.data(); } // CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task) @@ -259,60 +265,63 @@ void USBClient::loop() { ESP_LOGW(TAG, "Dropped %u USB events due to queue overflow", dropped); } - switch (this->state_) { - case USB_CLIENT_OPEN: { - int err; - ESP_LOGD(TAG, "Open device %d", this->device_addr_); - err = usb_host_device_open(this->handle_, this->device_addr_, &this->device_handle_); - if (err != ESP_OK) { - ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err)); - this->state_ = USB_CLIENT_INIT; - break; - } - ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_); - const usb_device_desc_t *desc; - err = usb_host_get_device_descriptor(this->device_handle_, &desc); - if (err != ESP_OK) { - ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err)); - this->disconnect(); - } else { - ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct); - if (desc->idVendor == this->vid_ && desc->idProduct == this->pid_ || this->vid_ == 0 && this->pid_ == 0) { - usb_device_info_t dev_info; - err = usb_host_device_info(this->device_handle_, &dev_info); - if (err != ESP_OK) { - ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err)); - this->disconnect(); - break; - } - this->state_ = USB_CLIENT_CONNECTED; - ESP_LOGD(TAG, "Device connected: Manuf: %s; Prod: %s; Serial: %s", - get_descriptor_string(dev_info.str_desc_manufacturer).c_str(), - get_descriptor_string(dev_info.str_desc_product).c_str(), - get_descriptor_string(dev_info.str_desc_serial_num).c_str()); + if (this->state_ == USB_CLIENT_OPEN) { + this->handle_open_state_(); + } +} + +void USBClient::handle_open_state_() { + int err; + ESP_LOGD(TAG, "Open device %d", this->device_addr_); + err = usb_host_device_open(this->handle_, this->device_addr_, &this->device_handle_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err)); + this->state_ = USB_CLIENT_INIT; + return; + } + ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_); + const usb_device_desc_t *desc; + err = usb_host_get_device_descriptor(this->device_handle_, &desc); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err)); + this->disconnect(); + return; + } + ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct); + if (desc->idVendor != this->vid_ || desc->idProduct != this->pid_) { + if (this->vid_ != 0 || this->pid_ != 0) { + ESP_LOGD(TAG, "Not our device, closing"); + this->disconnect(); + return; + } + } + usb_device_info_t dev_info; + err = usb_host_device_info(this->device_handle_, &dev_info); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err)); + this->disconnect(); + return; + } + this->state_ = USB_CLIENT_CONNECTED; + char buf_manuf[DESC_STRING_BUF_SIZE]; + char buf_product[DESC_STRING_BUF_SIZE]; + char buf_serial[DESC_STRING_BUF_SIZE]; + ESP_LOGD(TAG, "Device connected: Manuf: %s; Prod: %s; Serial: %s", + get_descriptor_string(dev_info.str_desc_manufacturer, buf_manuf), + get_descriptor_string(dev_info.str_desc_product, buf_product), + get_descriptor_string(dev_info.str_desc_serial_num, buf_serial)); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - const usb_device_desc_t *device_desc; - err = usb_host_get_device_descriptor(this->device_handle_, &device_desc); - if (err == ESP_OK) - usb_client_print_device_descriptor(device_desc); - const usb_config_desc_t *config_desc; - err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc); - if (err == ESP_OK) - usb_client_print_config_descriptor(config_desc, nullptr); + const usb_device_desc_t *device_desc; + err = usb_host_get_device_descriptor(this->device_handle_, &device_desc); + if (err == ESP_OK) + usb_client_print_device_descriptor(device_desc); + const usb_config_desc_t *config_desc; + err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc); + if (err == ESP_OK) + usb_client_print_config_descriptor(config_desc, nullptr); #endif - this->on_connected(); - } else { - ESP_LOGD(TAG, "Not our device, closing"); - this->disconnect(); - } - } - break; - } - - default: - break; - } + this->on_connected(); } void USBClient::on_opened(uint8_t addr) { From 903971de12c1b291ca8cd367ce5cdf9af8b304ae Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Fri, 13 Feb 2026 10:25:43 -0600 Subject: [PATCH 019/609] [runtime_image, online_image] Create runtime_image component to decode images (#10212) --- CODEOWNERS | 1 + esphome/components/online_image/__init__.py | 119 +----- ...{image_decoder.cpp => download_buffer.cpp} | 38 +- .../components/online_image/download_buffer.h | 44 ++ .../components/online_image/online_image.cpp | 396 ++++++------------ .../components/online_image/online_image.h | 135 +----- esphome/components/runtime_image/__init__.py | 191 +++++++++ .../bmp_decoder.cpp} | 28 +- .../bmp_decoder.h} | 22 +- .../runtime_image/image_decoder.cpp | 28 ++ .../image_decoder.h | 72 ++-- .../jpeg_decoder.cpp} | 49 ++- .../jpeg_decoder.h} | 17 +- .../png_decoder.cpp} | 25 +- .../png_decoder.h} | 19 +- .../runtime_image/runtime_image.cpp | 300 +++++++++++++ .../components/runtime_image/runtime_image.h | 214 ++++++++++ esphome/core/defines.h | 6 +- 18 files changed, 1075 insertions(+), 629 deletions(-) rename esphome/components/online_image/{image_decoder.cpp => download_buffer.cpp} (52%) create mode 100644 esphome/components/online_image/download_buffer.h create mode 100644 esphome/components/runtime_image/__init__.py rename esphome/components/{online_image/bmp_image.cpp => runtime_image/bmp_decoder.cpp} (82%) rename esphome/components/{online_image/bmp_image.h => runtime_image/bmp_decoder.h} (52%) create mode 100644 esphome/components/runtime_image/image_decoder.cpp rename esphome/components/{online_image => runtime_image}/image_decoder.h (60%) rename esphome/components/{online_image/jpeg_image.cpp => runtime_image/jpeg_decoder.cpp} (69%) rename esphome/components/{online_image/jpeg_image.h => runtime_image/jpeg_decoder.h} (54%) rename esphome/components/{online_image/png_image.cpp => runtime_image/png_decoder.cpp} (82%) rename esphome/components/{online_image/png_image.h => runtime_image/png_decoder.h} (65%) create mode 100644 esphome/components/runtime_image/runtime_image.cpp create mode 100644 esphome/components/runtime_image/runtime_image.h diff --git a/CODEOWNERS b/CODEOWNERS index 25e6dc1b29..2aa0656343 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -411,6 +411,7 @@ esphome/components/rp2040_pwm/* @jesserockz esphome/components/rpi_dpi_rgb/* @clydebarrow esphome/components/rtl87xx/* @kuba2k2 esphome/components/rtttl/* @glmnet +esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt esphome/components/runtime_stats/* @bdraco esphome/components/rx8130/* @beormund esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index 7a6d25bc7d..057244e03d 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -2,97 +2,34 @@ import logging from esphome import automation import esphome.codegen as cg -from esphome.components.const import CONF_BYTE_ORDER, CONF_REQUEST_HEADERS +from esphome.components import runtime_image +from esphome.components.const import CONF_REQUEST_HEADERS from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent -from esphome.components.image import ( - CONF_INVERT_ALPHA, - CONF_TRANSPARENCY, - IMAGE_SCHEMA, - Image_, - get_image_type_enum, - get_transparency_enum, - validate_settings, -) import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, - CONF_DITHER, - CONF_FILE, - CONF_FORMAT, CONF_ID, CONF_ON_ERROR, - CONF_RESIZE, CONF_TRIGGER_ID, - CONF_TYPE, CONF_URL, ) from esphome.core import Lambda -AUTO_LOAD = ["image"] +AUTO_LOAD = ["image", "runtime_image"] DEPENDENCIES = ["display", "http_request"] CODEOWNERS = ["@guillempages", "@clydebarrow"] MULTI_CONF = True CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" -CONF_PLACEHOLDER = "placeholder" CONF_UPDATE = "update" _LOGGER = logging.getLogger(__name__) online_image_ns = cg.esphome_ns.namespace("online_image") -ImageFormat = online_image_ns.enum("ImageFormat") - - -class Format: - def __init__(self, image_type): - self.image_type = image_type - - @property - def enum(self): - return getattr(ImageFormat, self.image_type) - - def actions(self): - pass - - -class BMPFormat(Format): - def __init__(self): - super().__init__("BMP") - - def actions(self): - cg.add_define("USE_ONLINE_IMAGE_BMP_SUPPORT") - - -class JPEGFormat(Format): - def __init__(self): - super().__init__("JPEG") - - def actions(self): - cg.add_define("USE_ONLINE_IMAGE_JPEG_SUPPORT") - cg.add_library("JPEGDEC", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2") - - -class PNGFormat(Format): - def __init__(self): - super().__init__("PNG") - - def actions(self): - cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT") - cg.add_library("pngle", "1.1.0") - - -IMAGE_FORMATS = { - x.image_type: x - for x in ( - BMPFormat(), - JPEGFormat(), - PNGFormat(), - ) -} -IMAGE_FORMATS.update({"JPG": IMAGE_FORMATS["JPEG"]}) - -OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_) +OnlineImage = online_image_ns.class_( + "OnlineImage", cg.PollingComponent, runtime_image.RuntimeImage +) # Actions SetUrlAction = online_image_ns.class_( @@ -111,29 +48,17 @@ DownloadErrorTrigger = online_image_ns.class_( ) -def remove_options(*options): - return { - cv.Optional(option): cv.invalid( - f"{option} is an invalid option for online_image" - ) - for option in options - } - - ONLINE_IMAGE_SCHEMA = ( - IMAGE_SCHEMA.extend(remove_options(CONF_FILE, CONF_INVERT_ALPHA, CONF_DITHER)) + runtime_image.runtime_image_schema(OnlineImage) .extend( { - cv.Required(CONF_ID): cv.declare_id(OnlineImage), - cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), # Online Image specific options + cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), cv.Required(CONF_URL): cv.url, + cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536), cv.Optional(CONF_REQUEST_HEADERS): cv.All( cv.Schema({cv.string: cv.templatable(cv.string)}) ), - cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True), - cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_), - cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536), cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -162,7 +87,7 @@ CONFIG_SCHEMA = cv.Schema( rp2040_arduino=cv.Version(0, 0, 0), host=cv.Version(0, 0, 0), ), - validate_settings, + runtime_image.validate_runtime_image_settings, ) ) @@ -199,23 +124,21 @@ async def online_image_action_to_code(config, action_id, template_arg, args): async def to_code(config): - image_format = IMAGE_FORMATS[config[CONF_FORMAT]] - image_format.actions() + # Use the enhanced helper function to get all runtime image parameters + settings = await runtime_image.process_runtime_image_config(config) url = config[CONF_URL] - width, height = config.get(CONF_RESIZE, (0, 0)) - transparent = get_transparency_enum(config[CONF_TRANSPARENCY]) - var = cg.new_Pvariable( config[CONF_ID], url, - width, - height, - image_format.enum, - get_image_type_enum(config[CONF_TYPE]), - transparent, + settings.width, + settings.height, + settings.format_enum, + settings.image_type_enum, + settings.transparent, + settings.placeholder or cg.nullptr, config[CONF_BUFFER_SIZE], - config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN", + settings.byte_order_big_endian, ) await cg.register_component(var, config) await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) @@ -227,10 +150,6 @@ async def to_code(config): else: cg.add(var.add_request_header(key, value)) - if placeholder_id := config.get(CONF_PLACEHOLDER): - placeholder = await cg.get_variable(placeholder_id) - cg.add(var.set_placeholder(placeholder)) - for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [(bool, "cached")], conf) diff --git a/esphome/components/online_image/image_decoder.cpp b/esphome/components/online_image/download_buffer.cpp similarity index 52% rename from esphome/components/online_image/image_decoder.cpp rename to esphome/components/online_image/download_buffer.cpp index 0ab7dadde3..999005df82 100644 --- a/esphome/components/online_image/image_decoder.cpp +++ b/esphome/components/online_image/download_buffer.cpp @@ -1,29 +1,10 @@ -#include "image_decoder.h" -#include "online_image.h" - +#include "download_buffer.h" #include "esphome/core/log.h" +#include -namespace esphome { -namespace online_image { +namespace esphome::online_image { -static const char *const TAG = "online_image.decoder"; - -bool ImageDecoder::set_size(int width, int height) { - bool success = this->image_->resize_(width, height) > 0; - this->x_scale_ = static_cast(this->image_->buffer_width_) / width; - this->y_scale_ = static_cast(this->image_->buffer_height_) / height; - return success; -} - -void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) { - auto width = std::min(this->image_->buffer_width_, static_cast(std::ceil((x + w) * this->x_scale_))); - auto height = std::min(this->image_->buffer_height_, static_cast(std::ceil((y + h) * this->y_scale_))); - for (int i = x * this->x_scale_; i < width; i++) { - for (int j = y * this->y_scale_; j < height; j++) { - this->image_->draw_pixel_(i, j, color); - } - } -} +static const char *const TAG = "online_image.download_buffer"; DownloadBuffer::DownloadBuffer(size_t size) : size_(size) { this->buffer_ = this->allocator_.allocate(size); @@ -43,10 +24,12 @@ uint8_t *DownloadBuffer::data(size_t offset) { } size_t DownloadBuffer::read(size_t len) { - this->unread_ -= len; - if (this->unread_ > 0) { - memmove(this->data(), this->data(len), this->unread_); + if (len >= this->unread_) { + this->unread_ = 0; + return 0; } + this->unread_ -= len; + memmove(this->data(), this->data(len), this->unread_); return this->unread_; } @@ -69,5 +52,4 @@ size_t DownloadBuffer::resize(size_t size) { } } -} // namespace online_image -} // namespace esphome +} // namespace esphome::online_image diff --git a/esphome/components/online_image/download_buffer.h b/esphome/components/online_image/download_buffer.h new file mode 100644 index 0000000000..110a4b608a --- /dev/null +++ b/esphome/components/online_image/download_buffer.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include +#include + +namespace esphome::online_image { + +/** + * @brief Buffer for managing downloaded data. + * + * This class provides a buffer for downloading data with tracking of + * unread bytes and dynamic resizing capabilities. + */ +class DownloadBuffer { + public: + DownloadBuffer(size_t size); + ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); } + + uint8_t *data(size_t offset = 0); + uint8_t *append() { return this->data(this->unread_); } + + size_t unread() const { return this->unread_; } + size_t size() const { return this->size_; } + size_t free_capacity() const { return this->size_ - this->unread_; } + + size_t read(size_t len); + size_t write(size_t len) { + this->unread_ += len; + return this->unread_; + } + + void reset() { this->unread_ = 0; } + size_t resize(size_t size); + + protected: + RAMAllocator allocator_{}; + uint8_t *buffer_; + size_t size_; + /** Total number of downloaded bytes not yet read. */ + size_t unread_; +}; + +} // namespace esphome::online_image diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 4e2ecc2c77..6f5b82116d 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -1,6 +1,6 @@ #include "online_image.h" - #include "esphome/core/log.h" +#include static const char *const TAG = "online_image"; static const char *const ETAG_HEADER_NAME = "etag"; @@ -8,142 +8,82 @@ static const char *const IF_NONE_MATCH_HEADER_NAME = "if-none-match"; static const char *const LAST_MODIFIED_HEADER_NAME = "last-modified"; static const char *const IF_MODIFIED_SINCE_HEADER_NAME = "if-modified-since"; -#include "image_decoder.h" +namespace esphome::online_image { -#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT -#include "bmp_image.h" -#endif -#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT -#include "jpeg_image.h" -#endif -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT -#include "png_image.h" -#endif - -namespace esphome { -namespace online_image { - -using image::ImageType; - -inline bool is_color_on(const Color &color) { - // This produces the most accurate monochrome conversion, but is slightly slower. - // return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127; - - // Approximation using fast integer computations; produces acceptable results - // Equivalent to 0.25 * R + 0.5 * G + 0.25 * B - return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80; -} - -OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type, - image::Transparency transparency, uint32_t download_buffer_size, bool is_big_endian) - : Image(nullptr, 0, 0, type, transparency), - buffer_(nullptr), - download_buffer_(download_buffer_size), - download_buffer_initial_size_(download_buffer_size), - format_(format), - fixed_width_(width), - fixed_height_(height), - is_big_endian_(is_big_endian) { +OnlineImage::OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format, + image::ImageType type, image::Transparency transparency, image::Image *placeholder, + uint32_t buffer_size, bool is_big_endian) + : RuntimeImage(format, type, transparency, placeholder, is_big_endian, width, height), + download_buffer_(buffer_size), + download_buffer_initial_size_(buffer_size) { this->set_url(url); } -void OnlineImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) { - if (this->data_start_) { - Image::draw(x, y, display, color_on, color_off); - } else if (this->placeholder_) { - this->placeholder_->draw(x, y, display, color_on, color_off); +bool OnlineImage::validate_url_(const std::string &url) { + if (url.empty()) { + ESP_LOGE(TAG, "URL is empty"); + return false; } -} - -void OnlineImage::release() { - if (this->buffer_) { - ESP_LOGV(TAG, "Deallocating old buffer"); - this->allocator_.deallocate(this->buffer_, this->get_buffer_size_()); - this->data_start_ = nullptr; - this->buffer_ = nullptr; - this->width_ = 0; - this->height_ = 0; - this->buffer_width_ = 0; - this->buffer_height_ = 0; - this->last_modified_ = ""; - this->etag_ = ""; - this->end_connection_(); + if (url.length() > 2048) { + ESP_LOGE(TAG, "URL is too long"); + return false; } -} - -size_t OnlineImage::resize_(int width_in, int height_in) { - int width = this->fixed_width_; - int height = this->fixed_height_; - if (this->is_auto_resize_()) { - width = width_in; - height = height_in; - if (this->width_ != width && this->height_ != height) { - this->release(); - } + if (url.compare(0, 7, "http://") != 0 && url.compare(0, 8, "https://") != 0) { + ESP_LOGE(TAG, "URL must start with http:// or https://"); + return false; } - size_t new_size = this->get_buffer_size_(width, height); - if (this->buffer_) { - // Buffer already allocated => no need to resize - return new_size; - } - ESP_LOGD(TAG, "Allocating new buffer of %zu bytes", new_size); - this->buffer_ = this->allocator_.allocate(new_size); - if (this->buffer_ == nullptr) { - ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", new_size, - this->allocator_.get_max_free_block_size()); - this->end_connection_(); - return 0; - } - this->buffer_width_ = width; - this->buffer_height_ = height; - this->width_ = width; - ESP_LOGV(TAG, "New size: (%d, %d)", width, height); - return new_size; + return true; } void OnlineImage::update() { - if (this->decoder_) { + if (this->is_decoding()) { ESP_LOGW(TAG, "Image already being updated."); return; } - ESP_LOGI(TAG, "Updating image %s", this->url_.c_str()); - std::list headers = {}; - - http_request::Header accept_header; - accept_header.name = "Accept"; - std::string accept_mime_type; - switch (this->format_) { -#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT - case ImageFormat::BMP: - accept_mime_type = "image/bmp"; - break; -#endif // USE_ONLINE_IMAGE_BMP_SUPPORT -#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT - case ImageFormat::JPEG: - accept_mime_type = "image/jpeg"; - break; -#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT - case ImageFormat::PNG: - accept_mime_type = "image/png"; - break; -#endif // USE_ONLINE_IMAGE_PNG_SUPPORT - default: - accept_mime_type = "image/*"; + if (!this->validate_url_(this->url_)) { + ESP_LOGE(TAG, "Invalid URL: %s", this->url_.c_str()); + this->download_error_callback_.call(); + return; } - accept_header.value = accept_mime_type + ",*/*;q=0.8"; + ESP_LOGD(TAG, "Updating image from %s", this->url_.c_str()); + + std::list headers; + + // Add caching headers if we have them if (!this->etag_.empty()) { - headers.push_back(http_request::Header{IF_NONE_MATCH_HEADER_NAME, this->etag_}); + headers.push_back({IF_NONE_MATCH_HEADER_NAME, this->etag_}); } - if (!this->last_modified_.empty()) { - headers.push_back(http_request::Header{IF_MODIFIED_SINCE_HEADER_NAME, this->last_modified_}); + headers.push_back({IF_MODIFIED_SINCE_HEADER_NAME, this->last_modified_}); } - headers.push_back(accept_header); + // Add Accept header based on image format + const char *accept_mime_type; + switch (this->get_format()) { +#ifdef USE_RUNTIME_IMAGE_BMP + case runtime_image::BMP: + accept_mime_type = "image/bmp,*/*;q=0.8"; + break; +#endif +#ifdef USE_RUNTIME_IMAGE_JPEG + case runtime_image::JPEG: + accept_mime_type = "image/jpeg,*/*;q=0.8"; + break; +#endif +#ifdef USE_RUNTIME_IMAGE_PNG + case runtime_image::PNG: + accept_mime_type = "image/png,*/*;q=0.8"; + break; +#endif + default: + accept_mime_type = "image/*,*/*;q=0.8"; + break; + } + headers.push_back({"Accept", accept_mime_type}); + // User headers last so they can override any of the above for (auto &header : this->request_headers_) { headers.push_back(http_request::Header{header.first, header.second.value()}); } @@ -175,186 +115,117 @@ void OnlineImage::update() { ESP_LOGD(TAG, "Starting download"); size_t total_size = this->downloader_->content_length; -#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT - if (this->format_ == ImageFormat::BMP) { - ESP_LOGD(TAG, "Allocating BMP decoder"); - this->decoder_ = make_unique(this); - this->enable_loop(); + // Initialize decoder with the known format + if (!this->begin_decode(total_size)) { + ESP_LOGE(TAG, "Failed to initialize decoder for format %d", this->get_format()); + this->end_connection_(); + this->download_error_callback_.call(); + return; } -#endif // USE_ONLINE_IMAGE_BMP_SUPPORT -#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT - if (this->format_ == ImageFormat::JPEG) { - ESP_LOGD(TAG, "Allocating JPEG decoder"); - this->decoder_ = esphome::make_unique(this); - this->enable_loop(); - } -#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT - if (this->format_ == ImageFormat::PNG) { - ESP_LOGD(TAG, "Allocating PNG decoder"); - this->decoder_ = make_unique(this); - this->enable_loop(); - } -#endif // USE_ONLINE_IMAGE_PNG_SUPPORT - if (!this->decoder_) { - ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported: %d", this->format_); - this->end_connection_(); - this->download_error_callback_.call(); - return; - } - auto prepare_result = this->decoder_->prepare(total_size); - if (prepare_result < 0) { - this->end_connection_(); - this->download_error_callback_.call(); - return; + // JPEG requires the complete image in the download buffer before decoding + if (this->get_format() == runtime_image::JPEG && total_size > this->download_buffer_.size()) { + this->download_buffer_.resize(total_size); } + ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size); this->start_time_ = ::time(nullptr); + this->enable_loop(); } void OnlineImage::loop() { - if (!this->decoder_) { + if (!this->is_decoding()) { // Not decoding at the moment => nothing to do. this->disable_loop(); return; } - if (!this->downloader_ || this->decoder_->is_finished()) { - this->data_start_ = buffer_; - this->width_ = buffer_width_; - this->height_ = buffer_height_; - ESP_LOGD(TAG, "Image fully downloaded, read %zu bytes, width/height = %d/%d", this->downloader_->get_bytes_read(), - this->width_, this->height_); - ESP_LOGD(TAG, "Total time: %" PRIu32 "s", (uint32_t) (::time(nullptr) - this->start_time_)); + + if (!this->downloader_) { + ESP_LOGE(TAG, "Downloader not instantiated; cannot download"); + this->end_connection_(); + this->download_error_callback_.call(); + return; + } + + // Check if download is complete — use decoder's format-specific completion check + // to handle both known content-length and chunked transfer encoding + if (this->is_decode_finished() || (this->downloader_->content_length > 0 && + this->downloader_->get_bytes_read() >= this->downloader_->content_length && + this->download_buffer_.unread() == 0)) { + // Finalize decoding + this->end_decode(); + + ESP_LOGD(TAG, "Image fully downloaded, %zu bytes in %" PRIu32 "s", this->downloader_->get_bytes_read(), + (uint32_t) (::time(nullptr) - this->start_time_)); + + // Save caching headers this->etag_ = this->downloader_->get_response_header(ETAG_HEADER_NAME); this->last_modified_ = this->downloader_->get_response_header(LAST_MODIFIED_HEADER_NAME); + this->download_finished_callback_.call(false); this->end_connection_(); return; } - if (this->downloader_ == nullptr) { - ESP_LOGE(TAG, "Downloader not instantiated; cannot download"); - return; - } + + // Download and decode more data size_t available = this->download_buffer_.free_capacity(); - if (available) { - // Some decoders need to fully download the image before downloading. - // In case of huge images, don't wait blocking until the whole image has been downloaded, - // use smaller chunks + if (available > 0) { + // Download in chunks to avoid blocking available = std::min(available, this->download_buffer_initial_size_); auto len = this->downloader_->read(this->download_buffer_.append(), available); + if (len > 0) { this->download_buffer_.write(len); - auto fed = this->decoder_->decode(this->download_buffer_.data(), this->download_buffer_.unread()); - if (fed < 0) { - ESP_LOGE(TAG, "Error when decoding image."); + + // Feed data to decoder + auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread()); + + if (consumed < 0) { + ESP_LOGE(TAG, "Error decoding image: %d", consumed); this->end_connection_(); this->download_error_callback_.call(); return; } - this->download_buffer_.read(fed); - } - } -} -void OnlineImage::map_chroma_key(Color &color) { - if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { - if (color.g == 1 && color.r == 0 && color.b == 0) { - color.g = 0; - } - if (color.w < 0x80) { - color.r = 0; - color.g = this->type_ == ImageType::IMAGE_TYPE_RGB565 ? 4 : 1; - color.b = 0; - } - } -} - -void OnlineImage::draw_pixel_(int x, int y, Color color) { - if (!this->buffer_) { - ESP_LOGE(TAG, "Buffer not allocated!"); - return; - } - if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) { - ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y); - return; - } - uint32_t pos = this->get_position_(x, y); - switch (this->type_) { - case ImageType::IMAGE_TYPE_BINARY: { - const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; - pos = x + y * width_8; - auto bitno = 0x80 >> (pos % 8u); - pos /= 8u; - auto on = is_color_on(color); - if (this->has_transparency() && color.w < 0x80) - on = false; - if (on) { - this->buffer_[pos] |= bitno; - } else { - this->buffer_[pos] &= ~bitno; + if (consumed > 0) { + this->download_buffer_.read(consumed); } - break; + } else if (len < 0) { + ESP_LOGE(TAG, "Error downloading image: %d", len); + this->end_connection_(); + this->download_error_callback_.call(); + return; } - case ImageType::IMAGE_TYPE_GRAYSCALE: { - auto gray = static_cast(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); - if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { - if (gray == 1) { - gray = 0; - } - if (color.w < 0x80) { - gray = 1; - } - } else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { - if (color.w != 0xFF) - gray = color.w; - } - this->buffer_[pos] = gray; - break; - } - case ImageType::IMAGE_TYPE_RGB565: { - this->map_chroma_key(color); - uint16_t col565 = display::ColorUtil::color_to_565(color); - if (this->is_big_endian_) { - this->buffer_[pos + 0] = static_cast((col565 >> 8) & 0xFF); - this->buffer_[pos + 1] = static_cast(col565 & 0xFF); - } else { - this->buffer_[pos + 0] = static_cast(col565 & 0xFF); - this->buffer_[pos + 1] = static_cast((col565 >> 8) & 0xFF); - } - if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { - this->buffer_[pos + 2] = color.w; - } - break; - } - case ImageType::IMAGE_TYPE_RGB: { - this->map_chroma_key(color); - this->buffer_[pos + 0] = color.r; - this->buffer_[pos + 1] = color.g; - this->buffer_[pos + 2] = color.b; - if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { - this->buffer_[pos + 3] = color.w; - } - break; + } else { + // Buffer is full, need to decode some data first + auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread()); + if (consumed > 0) { + this->download_buffer_.read(consumed); + } else if (consumed < 0) { + ESP_LOGE(TAG, "Decode error with full buffer: %d", consumed); + this->end_connection_(); + this->download_error_callback_.call(); + return; + } else { + // Decoder can't process more data, might need complete image + // This is normal for JPEG which needs complete data + ESP_LOGV(TAG, "Decoder waiting for more data"); } } } void OnlineImage::end_connection_() { + // Abort any in-progress decode to free decoder resources. + // Use RuntimeImage::release() directly to avoid recursion with OnlineImage::release(). + if (this->is_decoding()) { + RuntimeImage::release(); + } if (this->downloader_) { this->downloader_->end(); this->downloader_ = nullptr; } - this->decoder_.reset(); this->download_buffer_.reset(); -} - -bool OnlineImage::validate_url_(const std::string &url) { - if ((url.length() < 8) || !url.starts_with("http") || (url.find("://") == std::string::npos)) { - ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'"); - return false; - } - return true; + this->disable_loop(); } void OnlineImage::add_on_finished_callback(std::function &&callback) { @@ -365,5 +236,16 @@ void OnlineImage::add_on_error_callback(std::function &&callback) { this->download_error_callback_.add(std::move(callback)); } -} // namespace online_image -} // namespace esphome +void OnlineImage::release() { + // Clear cache headers + this->etag_ = ""; + this->last_modified_ = ""; + + // End any active connection + this->end_connection_(); + + // Call parent's release to free the image buffer + RuntimeImage::release(); +} + +} // namespace esphome::online_image diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 12d409ca29..c7c80c7c66 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -1,15 +1,14 @@ #pragma once +#include "download_buffer.h" #include "esphome/components/http_request/http_request.h" -#include "esphome/components/image/image.h" +#include "esphome/components/runtime_image/runtime_image.h" +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" -#include "image_decoder.h" - -namespace esphome { -namespace online_image { +namespace esphome::online_image { using t_http_codes = enum { HTTP_CODE_OK = 200, @@ -17,27 +16,13 @@ using t_http_codes = enum { HTTP_CODE_NOT_FOUND = 404, }; -/** - * @brief Format that the image is encoded with. - */ -enum ImageFormat { - /** Automatically detect from MIME type. Not supported yet. */ - AUTO, - /** JPEG format. */ - JPEG, - /** PNG format. */ - PNG, - /** BMP format. */ - BMP, -}; - /** * @brief Download an image from a given URL, and decode it using the specified decoder. * The image will then be stored in a buffer, so that it can be re-displayed without the * need to re-download or re-decode. */ class OnlineImage : public PollingComponent, - public image::Image, + public runtime_image::RuntimeImage, public Parented { public: /** @@ -46,17 +31,19 @@ class OnlineImage : public PollingComponent, * @param url URL to download the image from. * @param width Desired width of the target image area. * @param height Desired height of the target image area. - * @param format Format that the image is encoded in (@see ImageFormat). + * @param format Format that the image is encoded in (@see runtime_image::ImageFormat). + * @param type The pixel format for the image. + * @param transparency The transparency type for the image. + * @param placeholder Optional placeholder image to show while loading. * @param buffer_size Size of the buffer used to download the image. + * @param is_big_endian Whether the image is stored in big-endian format. */ - OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, - image::Transparency transparency, uint32_t buffer_size, bool is_big_endian); - - void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; + OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format, image::ImageType type, + image::Transparency transparency, image::Image *placeholder, uint32_t buffer_size, + bool is_big_endian = false); void update() override; void loop() override; - void map_chroma_key(Color &color); /** Set the URL to download the image from. */ void set_url(const std::string &url) { @@ -69,82 +56,26 @@ class OnlineImage : public PollingComponent, /** Add the request header */ template void add_request_header(const std::string &header, V value) { - this->request_headers_.push_back(std::pair >(header, value)); + this->request_headers_.push_back(std::pair>(header, value)); } - /** - * @brief Set the image that needs to be shown as long as the downloaded image - * is not available. - * - * @param placeholder Pointer to the (@link Image) to show as placeholder. - */ - void set_placeholder(image::Image *placeholder) { this->placeholder_ = placeholder; } - /** * Release the buffer storing the image. The image will need to be downloaded again * to be able to be displayed. */ void release(); - /** - * Resize the download buffer - * - * @param size The new size for the download buffer. - */ - size_t resize_download_buffer(size_t size) { return this->download_buffer_.resize(size); } - void add_on_finished_callback(std::function &&callback); void add_on_error_callback(std::function &&callback); protected: bool validate_url_(const std::string &url); - - RAMAllocator allocator_{}; - - uint32_t get_buffer_size_() const { return get_buffer_size_(this->buffer_width_, this->buffer_height_); } - int get_buffer_size_(int width, int height) const { return (this->get_bpp() * width + 7u) / 8u * height; } - - int get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; } - - ESPHOME_ALWAYS_INLINE bool is_auto_resize_() const { return this->fixed_width_ == 0 || this->fixed_height_ == 0; } - - /** - * @brief Resize the image buffer to the requested dimensions. - * - * The buffer will be allocated if not existing. - * If the dimensions have been fixed in the yaml config, the buffer will be created - * with those dimensions and not resized, even on request. - * Otherwise, the old buffer will be deallocated and a new buffer with the requested - * allocated - * - * @param width - * @param height - * @return 0 if no memory could be allocated, the size of the new buffer otherwise. - */ - size_t resize_(int width, int height); - - /** - * @brief Draw a pixel into the buffer. - * - * This is used by the decoder to fill the buffer that will later be displayed - * by the `draw` method. This will internally convert the supplied 32 bit RGBA - * color into the requested image storage format. - * - * @param x Horizontal pixel position. - * @param y Vertical pixel position. - * @param color 32 bit color to put into the pixel. - */ - void draw_pixel_(int x, int y, Color color); - void end_connection_(); CallbackManager download_finished_callback_{}; CallbackManager download_error_callback_{}; std::shared_ptr downloader_{nullptr}; - std::unique_ptr decoder_{nullptr}; - - uint8_t *buffer_; DownloadBuffer download_buffer_; /** * This is the *initial* size of the download buffer, not the current size. @@ -153,40 +84,10 @@ class OnlineImage : public PollingComponent, */ size_t download_buffer_initial_size_; - const ImageFormat format_; - image::Image *placeholder_{nullptr}; - std::string url_{""}; - std::vector > > request_headers_; + std::vector>> request_headers_; - /** width requested on configuration, or 0 if non specified. */ - const int fixed_width_; - /** height requested on configuration, or 0 if non specified. */ - const int fixed_height_; - /** - * Whether the image is stored in big-endian format. - * This is used to determine how to store 16 bit colors in the buffer. - */ - bool is_big_endian_; - /** - * Actual width of the current image. If fixed_width_ is specified, - * this will be equal to it; otherwise it will be set once the decoding - * starts and the original size is known. - * This needs to be separate from "BaseImage::get_width()" because the latter - * must return 0 until the image has been decoded (to avoid showing partially - * decoded images). - */ - int buffer_width_; - /** - * Actual height of the current image. If fixed_height_ is specified, - * this will be equal to it; otherwise it will be set once the decoding - * starts and the original size is known. - * This needs to be separate from "BaseImage::get_height()" because the latter - * must return 0 until the image has been decoded (to avoid showing partially - * decoded images). - */ - int buffer_height_; /** * The value of the ETag HTTP header provided in the last response. */ @@ -197,9 +98,6 @@ class OnlineImage : public PollingComponent, std::string last_modified_ = ""; time_t start_time_; - - friend bool ImageDecoder::set_size(int width, int height); - friend void ImageDecoder::draw(int x, int y, int w, int h, const Color &color); }; template class OnlineImageSetUrlAction : public Action { @@ -241,5 +139,4 @@ class DownloadErrorTrigger : public Trigger<> { } }; -} // namespace online_image -} // namespace esphome +} // namespace esphome::online_image diff --git a/esphome/components/runtime_image/__init__.py b/esphome/components/runtime_image/__init__.py new file mode 100644 index 0000000000..0773a53d91 --- /dev/null +++ b/esphome/components/runtime_image/__init__.py @@ -0,0 +1,191 @@ +from dataclasses import dataclass + +import esphome.codegen as cg +from esphome.components.const import CONF_BYTE_ORDER +from esphome.components.image import ( + IMAGE_TYPE, + Image_, + validate_settings, + validate_transparency, + validate_type, +) +import esphome.config_validation as cv +from esphome.const import CONF_FORMAT, CONF_ID, CONF_RESIZE, CONF_TYPE + +AUTO_LOAD = ["image"] +CODEOWNERS = ["@guillempages", "@clydebarrow", "@kahrendt"] + +CONF_PLACEHOLDER = "placeholder" +CONF_TRANSPARENCY = "transparency" + +runtime_image_ns = cg.esphome_ns.namespace("runtime_image") + +# Base decoder classes +ImageDecoder = runtime_image_ns.class_("ImageDecoder") +BmpDecoder = runtime_image_ns.class_("BmpDecoder", ImageDecoder) +JpegDecoder = runtime_image_ns.class_("JpegDecoder", ImageDecoder) +PngDecoder = runtime_image_ns.class_("PngDecoder", ImageDecoder) + +# Runtime image class +RuntimeImage = runtime_image_ns.class_( + "RuntimeImage", cg.esphome_ns.namespace("image").class_("Image") +) + +# Image format enum +ImageFormat = runtime_image_ns.enum("ImageFormat") +IMAGE_FORMAT_AUTO = ImageFormat.AUTO +IMAGE_FORMAT_JPEG = ImageFormat.JPEG +IMAGE_FORMAT_PNG = ImageFormat.PNG +IMAGE_FORMAT_BMP = ImageFormat.BMP + +# Export enum for decode errors +DecodeError = runtime_image_ns.enum("DecodeError") +DECODE_ERROR_INVALID_TYPE = DecodeError.DECODE_ERROR_INVALID_TYPE +DECODE_ERROR_UNSUPPORTED_FORMAT = DecodeError.DECODE_ERROR_UNSUPPORTED_FORMAT +DECODE_ERROR_OUT_OF_MEMORY = DecodeError.DECODE_ERROR_OUT_OF_MEMORY + + +class Format: + """Base class for image format definitions.""" + + def __init__(self, name: str, decoder_class: cg.MockObjClass) -> None: + self.name = name + self.decoder_class = decoder_class + + def actions(self) -> None: + """Add defines and libraries needed for this format.""" + + +class BMPFormat(Format): + """BMP format decoder configuration.""" + + def __init__(self): + super().__init__("BMP", BmpDecoder) + + def actions(self) -> None: + cg.add_define("USE_RUNTIME_IMAGE_BMP") + + +class JPEGFormat(Format): + """JPEG format decoder configuration.""" + + def __init__(self): + super().__init__("JPEG", JpegDecoder) + + def actions(self) -> None: + cg.add_define("USE_RUNTIME_IMAGE_JPEG") + cg.add_library("JPEGDEC", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2") + + +class PNGFormat(Format): + """PNG format decoder configuration.""" + + def __init__(self): + super().__init__("PNG", PngDecoder) + + def actions(self) -> None: + cg.add_define("USE_RUNTIME_IMAGE_PNG") + cg.add_library("pngle", "1.1.0") + + +# Registry of available formats +IMAGE_FORMATS = { + "BMP": BMPFormat(), + "JPEG": JPEGFormat(), + "PNG": PNGFormat(), + "JPG": JPEGFormat(), # Alias for JPEG +} + + +def get_format(format_name: str) -> Format | None: + """Get a format instance by name.""" + return IMAGE_FORMATS.get(format_name.upper()) + + +def enable_format(format_name: str) -> Format | None: + """Enable a specific image format by adding its defines and libraries.""" + format_obj = get_format(format_name) + if format_obj: + format_obj.actions() + return format_obj + return None + + +# Runtime image configuration schema base - to be extended by components +def runtime_image_schema(image_class: cg.MockObjClass = RuntimeImage) -> cv.Schema: + """Create a runtime image schema with the specified image class.""" + return cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(image_class), + cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True), + cv.Optional(CONF_RESIZE): cv.dimensions, + cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE), + cv.Optional(CONF_BYTE_ORDER): cv.one_of( + "BIG_ENDIAN", "LITTLE_ENDIAN", upper=True + ), + cv.Optional(CONF_TRANSPARENCY, default="OPAQUE"): validate_transparency(), + cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_), + } + ) + + +def validate_runtime_image_settings(config: dict) -> dict: + """Apply validate_settings from image component to runtime image config.""" + return validate_settings(config) + + +@dataclass +class RuntimeImageSettings: + """Processed runtime image configuration parameters.""" + + width: int + height: int + format_enum: cg.MockObj + image_type_enum: cg.MockObj + transparent: cg.MockObj + byte_order_big_endian: bool + placeholder: cg.MockObj | None + + +async def process_runtime_image_config(config: dict) -> RuntimeImageSettings: + """ + Helper function to process common runtime image configuration parameters. + Handles format enabling and returns all necessary enums and parameters. + """ + from esphome.components.image import get_image_type_enum, get_transparency_enum + + # Get resize dimensions with default (0, 0) + width, height = config.get(CONF_RESIZE, (0, 0)) + + # Handle format (required for runtime images) + format_name = config[CONF_FORMAT] + # Enable the format in the runtime_image component + enable_format(format_name) + # Map format names to enum values (handle JPG as alias for JPEG) + if format_name.upper() == "JPG": + format_name = "JPEG" + format_enum = getattr(ImageFormat, format_name.upper()) + + # Get image type enum + image_type_enum = get_image_type_enum(config[CONF_TYPE]) + + # Get transparency enum + transparent = get_transparency_enum(config.get(CONF_TRANSPARENCY, "OPAQUE")) + + # Get byte order (True for big endian, False for little endian) + byte_order_big_endian = config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN" + + # Get placeholder if specified + placeholder = None + if placeholder_id := config.get(CONF_PLACEHOLDER): + placeholder = await cg.get_variable(placeholder_id) + + return RuntimeImageSettings( + width=width, + height=height, + format_enum=format_enum, + image_type_enum=image_type_enum, + transparent=transparent, + byte_order_big_endian=byte_order_big_endian, + placeholder=placeholder, + ) diff --git a/esphome/components/online_image/bmp_image.cpp b/esphome/components/runtime_image/bmp_decoder.cpp similarity index 82% rename from esphome/components/online_image/bmp_image.cpp rename to esphome/components/runtime_image/bmp_decoder.cpp index 676a2efca9..1a56484c60 100644 --- a/esphome/components/online_image/bmp_image.cpp +++ b/esphome/components/runtime_image/bmp_decoder.cpp @@ -1,15 +1,14 @@ -#include "bmp_image.h" +#include "bmp_decoder.h" -#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT +#ifdef USE_RUNTIME_IMAGE_BMP #include "esphome/components/display/display.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { -static const char *const TAG = "online_image.bmp"; +static const char *const TAG = "image_decoder.bmp"; int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { size_t index = 0; @@ -30,7 +29,11 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { return DECODE_ERROR_INVALID_TYPE; } - this->download_size_ = encode_uint32(buffer[5], buffer[4], buffer[3], buffer[2]); + // BMP file contains its own size in the header + size_t file_size = encode_uint32(buffer[5], buffer[4], buffer[3], buffer[2]); + if (this->expected_size_ == 0) { + this->expected_size_ = file_size; // Use file header size if not provided + } this->data_offset_ = encode_uint32(buffer[13], buffer[12], buffer[11], buffer[10]); this->current_index_ = 14; @@ -90,8 +93,8 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { 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_); + size_t x = (this->paint_index_ % static_cast(this->width_)) + i; + size_t y = static_cast(this->height_ - 1) - (this->paint_index_ / static_cast(this->width_)); Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF; this->draw(x, y, 1, 1, c); } @@ -110,8 +113,8 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { 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_); + size_t x = this->paint_index_ % static_cast(this->width_); + size_t y = static_cast(this->height_ - 1) - (this->paint_index_ / static_cast(this->width_)); Color c = Color(r, g, b); this->draw(x, y, 1, 1, c); this->paint_index_++; @@ -133,7 +136,6 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { return size; }; -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_BMP_SUPPORT +#endif // USE_RUNTIME_IMAGE_BMP diff --git a/esphome/components/online_image/bmp_image.h b/esphome/components/runtime_image/bmp_decoder.h similarity index 52% rename from esphome/components/online_image/bmp_image.h rename to esphome/components/runtime_image/bmp_decoder.h index 916ffea1ad..37db6b4940 100644 --- a/esphome/components/online_image/bmp_image.h +++ b/esphome/components/runtime_image/bmp_decoder.h @@ -1,27 +1,32 @@ #pragma once #include "esphome/core/defines.h" -#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT +#ifdef USE_RUNTIME_IMAGE_BMP #include "image_decoder.h" +#include "runtime_image.h" -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { /** - * @brief Image decoder specialization for PNG images. + * @brief Image decoder specialization for BMP images. */ class BmpDecoder : public ImageDecoder { public: /** * @brief Construct a new BMP Decoder object. * - * @param display The image to decode the stream into. + * @param image The RuntimeImage to decode the stream into. */ - BmpDecoder(OnlineImage *image) : ImageDecoder(image) {} + BmpDecoder(RuntimeImage *image) : ImageDecoder(image) {} int HOT decode(uint8_t *buffer, size_t size) override; + bool is_finished() const override { + // BMP is finished when we've decoded all pixel data + return this->paint_index_ >= static_cast(this->width_ * this->height_); + } + protected: size_t current_index_{0}; size_t paint_index_{0}; @@ -36,7 +41,6 @@ class BmpDecoder : public ImageDecoder { uint8_t padding_bytes_{0}; }; -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_BMP_SUPPORT +#endif // USE_RUNTIME_IMAGE_BMP diff --git a/esphome/components/runtime_image/image_decoder.cpp b/esphome/components/runtime_image/image_decoder.cpp new file mode 100644 index 0000000000..8d3320b5d1 --- /dev/null +++ b/esphome/components/runtime_image/image_decoder.cpp @@ -0,0 +1,28 @@ +#include "image_decoder.h" +#include "runtime_image.h" +#include "esphome/core/log.h" +#include +#include + +namespace esphome::runtime_image { + +static const char *const TAG = "image_decoder"; + +bool ImageDecoder::set_size(int width, int height) { + bool success = this->image_->resize(width, height) > 0; + this->x_scale_ = static_cast(this->image_->get_buffer_width()) / width; + this->y_scale_ = static_cast(this->image_->get_buffer_height()) / height; + return success; +} + +void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) { + auto width = std::min(this->image_->get_buffer_width(), static_cast(std::ceil((x + w) * this->x_scale_))); + auto height = std::min(this->image_->get_buffer_height(), static_cast(std::ceil((y + h) * this->y_scale_))); + for (int i = x * this->x_scale_; i < width; i++) { + for (int j = y * this->y_scale_; j < height; j++) { + this->image_->draw_pixel(i, j, color); + } + } +} + +} // namespace esphome::runtime_image diff --git a/esphome/components/online_image/image_decoder.h b/esphome/components/runtime_image/image_decoder.h similarity index 60% rename from esphome/components/online_image/image_decoder.h rename to esphome/components/runtime_image/image_decoder.h index d11b8b46d3..926108a8a0 100644 --- a/esphome/components/online_image/image_decoder.h +++ b/esphome/components/runtime_image/image_decoder.h @@ -1,8 +1,7 @@ #pragma once #include "esphome/core/color.h" -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { enum DecodeError : int { DECODE_ERROR_INVALID_TYPE = -1, @@ -10,7 +9,7 @@ enum DecodeError : int { DECODE_ERROR_OUT_OF_MEMORY = -3, }; -class OnlineImage; +class RuntimeImage; /** * @brief Class to abstract decoding different image formats. @@ -20,19 +19,19 @@ class ImageDecoder { /** * @brief Construct a new Image Decoder object * - * @param image The image to decode the stream into. + * @param image The RuntimeImage to decode the stream into. */ - ImageDecoder(OnlineImage *image) : image_(image) {} + ImageDecoder(RuntimeImage *image) : image_(image) {} virtual ~ImageDecoder() = default; /** * @brief Initialize the decoder. * - * @param download_size The total number of bytes that need to be downloaded for the image. + * @param expected_size Hint about the expected data size (0 if unknown). * @return int Returns 0 on success, a {@see DecodeError} value in case of an error. */ - virtual int prepare(size_t download_size) { - this->download_size_ = download_size; + virtual int prepare(size_t expected_size) { + this->expected_size_ = expected_size; return 0; } @@ -73,49 +72,26 @@ class ImageDecoder { */ void draw(int x, int y, int w, int h, const Color &color); - bool is_finished() const { return this->decoded_bytes_ == this->download_size_; } + /** + * @brief Check if the decoder has finished processing. + * + * This should be overridden by decoders that can detect completion + * based on format-specific markers rather than byte counts. + */ + virtual bool is_finished() const { + if (this->expected_size_ > 0) { + return this->decoded_bytes_ >= this->expected_size_; + } + // If size is unknown, derived classes should override this + return false; + } protected: - OnlineImage *image_; - // Initializing to 1, to ensure it is distinguishable from initial "decoded_bytes_". - // Will be overwritten anyway once the download size is known. - size_t download_size_ = 1; - size_t decoded_bytes_ = 0; + RuntimeImage *image_; + size_t expected_size_ = 0; // Expected data size (0 if unknown) + size_t decoded_bytes_ = 0; // Bytes processed so far double x_scale_ = 1.0; double y_scale_ = 1.0; }; -class DownloadBuffer { - public: - DownloadBuffer(size_t size); - - virtual ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); } - - uint8_t *data(size_t offset = 0); - - uint8_t *append() { return this->data(this->unread_); } - - size_t unread() const { return this->unread_; } - size_t size() const { return this->size_; } - size_t free_capacity() const { return this->size_ - this->unread_; } - - size_t read(size_t len); - size_t write(size_t len) { - this->unread_ += len; - return this->unread_; - } - - void reset() { this->unread_ = 0; } - - size_t resize(size_t size); - - protected: - RAMAllocator allocator_{}; - uint8_t *buffer_; - size_t size_; - /** Total number of downloaded bytes not yet read. */ - size_t unread_; -}; - -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image diff --git a/esphome/components/online_image/jpeg_image.cpp b/esphome/components/runtime_image/jpeg_decoder.cpp similarity index 69% rename from esphome/components/online_image/jpeg_image.cpp rename to esphome/components/runtime_image/jpeg_decoder.cpp index 10586091d5..dcaa07cd58 100644 --- a/esphome/components/online_image/jpeg_image.cpp +++ b/esphome/components/runtime_image/jpeg_decoder.cpp @@ -1,16 +1,19 @@ -#include "jpeg_image.h" -#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT +#include "jpeg_decoder.h" +#ifdef USE_RUNTIME_IMAGE_JPEG #include "esphome/components/display/display_buffer.h" #include "esphome/core/application.h" +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "online_image.h" -static const char *const TAG = "online_image.jpeg"; +#ifdef USE_ESP_IDF +#include "esp_task_wdt.h" +#endif -namespace esphome { -namespace online_image { +static const char *const TAG = "image_decoder.jpeg"; + +namespace esphome::runtime_image { /** * @brief Callback method that will be called by the JPEGDEC engine when a chunk @@ -22,8 +25,14 @@ static int draw_callback(JPEGDRAW *jpeg) { ImageDecoder *decoder = (ImageDecoder *) jpeg->pUser; // Some very big images take too long to decode, so feed the watchdog on each callback - // to avoid crashing. - App.feed_wdt(); + // to avoid crashing if the executing task has a watchdog enabled. +#ifdef USE_ESP_IDF + if (esp_task_wdt_status(nullptr) == ESP_OK) { +#endif + App.feed_wdt(); +#ifdef USE_ESP_IDF + } +#endif size_t position = 0; size_t height = static_cast(jpeg->iHeight); size_t width = static_cast(jpeg->iWidth); @@ -43,22 +52,23 @@ static int draw_callback(JPEGDRAW *jpeg) { return 1; } -int JpegDecoder::prepare(size_t download_size) { - ImageDecoder::prepare(download_size); - auto size = this->image_->resize_download_buffer(download_size); - if (size < download_size) { - ESP_LOGE(TAG, "Download buffer resize failed!"); - return DECODE_ERROR_OUT_OF_MEMORY; - } +int JpegDecoder::prepare(size_t expected_size) { + ImageDecoder::prepare(expected_size); + // JPEG decoder needs complete data before decoding return 0; } int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) { - if (size < this->download_size_) { - ESP_LOGV(TAG, "Download not complete. Size: %d/%d", size, this->download_size_); + // JPEG decoder requires complete data + // If we know the expected size, wait for it + if (this->expected_size_ > 0 && size < this->expected_size_) { + ESP_LOGV(TAG, "Download not complete. Size: %zu/%zu", size, this->expected_size_); return 0; } + // If size unknown, try to decode and see if it's valid + // The JPEGDEC library will fail gracefully if data is incomplete + if (!this->jpeg_.openRAM(buffer, size, draw_callback)) { ESP_LOGE(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError()); return DECODE_ERROR_INVALID_TYPE; @@ -88,7 +98,6 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) { return size; } -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT +#endif // USE_RUNTIME_IMAGE_JPEG diff --git a/esphome/components/online_image/jpeg_image.h b/esphome/components/runtime_image/jpeg_decoder.h similarity index 54% rename from esphome/components/online_image/jpeg_image.h rename to esphome/components/runtime_image/jpeg_decoder.h index fd488d6138..ed2401e263 100644 --- a/esphome/components/online_image/jpeg_image.h +++ b/esphome/components/runtime_image/jpeg_decoder.h @@ -1,12 +1,12 @@ #pragma once #include "image_decoder.h" +#include "runtime_image.h" #include "esphome/core/defines.h" -#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT +#ifdef USE_RUNTIME_IMAGE_JPEG #include -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { /** * @brief Image decoder specialization for JPEG images. @@ -16,19 +16,18 @@ class JpegDecoder : public ImageDecoder { /** * @brief Construct a new JPEG Decoder object. * - * @param display The image to decode the stream into. + * @param image The RuntimeImage to decode the stream into. */ - JpegDecoder(OnlineImage *image) : ImageDecoder(image) {} + JpegDecoder(RuntimeImage *image) : ImageDecoder(image) {} ~JpegDecoder() override {} - int prepare(size_t download_size) override; + int prepare(size_t expected_size) override; int HOT decode(uint8_t *buffer, size_t size) override; protected: JPEGDEC jpeg_{}; }; -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT +#endif // USE_RUNTIME_IMAGE_JPEG diff --git a/esphome/components/online_image/png_image.cpp b/esphome/components/runtime_image/png_decoder.cpp similarity index 82% rename from esphome/components/online_image/png_image.cpp rename to esphome/components/runtime_image/png_decoder.cpp index ce9d3bdc91..9fe4a9c4ff 100644 --- a/esphome/components/online_image/png_image.cpp +++ b/esphome/components/runtime_image/png_decoder.cpp @@ -1,15 +1,14 @@ -#include "png_image.h" -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT +#include "png_decoder.h" +#ifdef USE_RUNTIME_IMAGE_PNG #include "esphome/components/display/display_buffer.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -static const char *const TAG = "online_image.png"; +static const char *const TAG = "image_decoder.png"; -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { /** * @brief Callback method that will be called by the PNGLE engine when the basic @@ -49,7 +48,7 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui } } -PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) { +PngDecoder::PngDecoder(RuntimeImage *image) : ImageDecoder(image) { { pngle_t *pngle = this->allocator_.allocate(1, PNGLE_T_SIZE); if (!pngle) { @@ -69,8 +68,8 @@ PngDecoder::~PngDecoder() { } } -int PngDecoder::prepare(size_t download_size) { - ImageDecoder::prepare(download_size); +int PngDecoder::prepare(size_t expected_size) { + ImageDecoder::prepare(expected_size); if (!this->pngle_) { ESP_LOGE(TAG, "PNG decoder engine not initialized!"); return DECODE_ERROR_OUT_OF_MEMORY; @@ -86,8 +85,9 @@ int HOT PngDecoder::decode(uint8_t *buffer, size_t size) { ESP_LOGE(TAG, "PNG decoder engine not initialized!"); return DECODE_ERROR_OUT_OF_MEMORY; } - if (size < 256 && size < this->download_size_ - this->decoded_bytes_) { - ESP_LOGD(TAG, "Waiting for data"); + // PNG can be decoded progressively, but wait for a reasonable chunk + if (size < 256 && this->expected_size_ > 0 && size < this->expected_size_ - this->decoded_bytes_) { + ESP_LOGD(TAG, "Waiting for more data"); return 0; } auto fed = pngle_feed(this->pngle_, buffer, size); @@ -99,7 +99,6 @@ int HOT PngDecoder::decode(uint8_t *buffer, size_t size) { return fed; } -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_PNG_SUPPORT +#endif // USE_RUNTIME_IMAGE_PNG diff --git a/esphome/components/online_image/png_image.h b/esphome/components/runtime_image/png_decoder.h similarity index 65% rename from esphome/components/online_image/png_image.h rename to esphome/components/runtime_image/png_decoder.h index 40e85dde33..b5c1e70c2a 100644 --- a/esphome/components/online_image/png_image.h +++ b/esphome/components/runtime_image/png_decoder.h @@ -3,11 +3,11 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "image_decoder.h" -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT +#include "runtime_image.h" +#ifdef USE_RUNTIME_IMAGE_PNG #include -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { /** * @brief Image decoder specialization for PNG images. @@ -17,12 +17,12 @@ class PngDecoder : public ImageDecoder { /** * @brief Construct a new PNG Decoder object. * - * @param display The image to decode the stream into. + * @param image The RuntimeImage to decode the stream into. */ - PngDecoder(OnlineImage *image); + PngDecoder(RuntimeImage *image); ~PngDecoder() override; - int prepare(size_t download_size) override; + int prepare(size_t expected_size) override; int HOT decode(uint8_t *buffer, size_t size) override; void increment_pixels_decoded(uint32_t count) { this->pixels_decoded_ += count; } @@ -30,11 +30,10 @@ class PngDecoder : public ImageDecoder { protected: RAMAllocator allocator_; - pngle_t *pngle_; + pngle_t *pngle_{nullptr}; uint32_t pixels_decoded_{0}; }; -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_PNG_SUPPORT +#endif // USE_RUNTIME_IMAGE_PNG diff --git a/esphome/components/runtime_image/runtime_image.cpp b/esphome/components/runtime_image/runtime_image.cpp new file mode 100644 index 0000000000..1d70f38d6b --- /dev/null +++ b/esphome/components/runtime_image/runtime_image.cpp @@ -0,0 +1,300 @@ +#include "runtime_image.h" +#include "image_decoder.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include + +#ifdef USE_RUNTIME_IMAGE_BMP +#include "bmp_decoder.h" +#endif +#ifdef USE_RUNTIME_IMAGE_JPEG +#include "jpeg_decoder.h" +#endif +#ifdef USE_RUNTIME_IMAGE_PNG +#include "png_decoder.h" +#endif + +namespace esphome::runtime_image { + +static const char *const TAG = "runtime_image"; + +inline bool is_color_on(const Color &color) { + // This produces the most accurate monochrome conversion, but is slightly slower. + // return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127; + + // Approximation using fast integer computations; produces acceptable results + // Equivalent to 0.25 * R + 0.5 * G + 0.25 * B + return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80; +} + +RuntimeImage::RuntimeImage(ImageFormat format, image::ImageType type, image::Transparency transparency, + image::Image *placeholder, bool is_big_endian, int fixed_width, int fixed_height) + : Image(nullptr, 0, 0, type, transparency), + format_(format), + fixed_width_(fixed_width), + fixed_height_(fixed_height), + placeholder_(placeholder), + is_big_endian_(is_big_endian) {} + +RuntimeImage::~RuntimeImage() { this->release(); } + +int RuntimeImage::resize(int width, int height) { + // Use fixed dimensions if specified (0 means auto-resize) + int target_width = this->fixed_width_ ? this->fixed_width_ : width; + int target_height = this->fixed_height_ ? this->fixed_height_ : height; + + size_t result = this->resize_buffer_(target_width, target_height); + if (result > 0 && this->progressive_display_) { + // Update display dimensions for progressive display + this->width_ = this->buffer_width_; + this->height_ = this->buffer_height_; + this->data_start_ = this->buffer_; + } + return result; +} + +void RuntimeImage::draw_pixel(int x, int y, const Color &color) { + if (!this->buffer_) { + ESP_LOGE(TAG, "Buffer not allocated!"); + return; + } + if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) { + ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y); + return; + } + + switch (this->type_) { + case image::IMAGE_TYPE_BINARY: { + const uint32_t width_8 = ((this->buffer_width_ + 7u) / 8u) * 8u; + uint32_t pos = x + y * width_8; + auto bitno = 0x80 >> (pos % 8u); + pos /= 8u; + auto on = is_color_on(color); + if (this->has_transparency() && color.w < 0x80) + on = false; + if (on) { + this->buffer_[pos] |= bitno; + } else { + this->buffer_[pos] &= ~bitno; + } + break; + } + case image::IMAGE_TYPE_GRAYSCALE: { + uint32_t pos = this->get_position_(x, y); + auto gray = static_cast(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); + if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { + if (gray == 1) { + gray = 0; + } + if (color.w < 0x80) { + gray = 1; + } + } else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + if (color.w != 0xFF) + gray = color.w; + } + this->buffer_[pos] = gray; + break; + } + case image::IMAGE_TYPE_RGB565: { + uint32_t pos = this->get_position_(x, y); + Color mapped_color = color; + this->map_chroma_key(mapped_color); + uint16_t rgb565 = display::ColorUtil::color_to_565(mapped_color); + if (this->is_big_endian_) { + this->buffer_[pos + 0] = static_cast((rgb565 >> 8) & 0xFF); + this->buffer_[pos + 1] = static_cast(rgb565 & 0xFF); + } else { + this->buffer_[pos + 0] = static_cast(rgb565 & 0xFF); + this->buffer_[pos + 1] = static_cast((rgb565 >> 8) & 0xFF); + } + if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + this->buffer_[pos + 2] = color.w; + } + break; + } + case image::IMAGE_TYPE_RGB: { + uint32_t pos = this->get_position_(x, y); + Color mapped_color = color; + this->map_chroma_key(mapped_color); + this->buffer_[pos + 0] = mapped_color.r; + this->buffer_[pos + 1] = mapped_color.g; + this->buffer_[pos + 2] = mapped_color.b; + if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + this->buffer_[pos + 3] = color.w; + } + break; + } + } +} + +void RuntimeImage::map_chroma_key(Color &color) { + if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { + if (color.g == 1 && color.r == 0 && color.b == 0) { + color.g = 0; + } + if (color.w < 0x80) { + color.r = 0; + color.g = this->type_ == image::IMAGE_TYPE_RGB565 ? 4 : 1; + color.b = 0; + } + } +} + +void RuntimeImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) { + if (this->data_start_) { + // If we have a complete image, use the base class draw method + Image::draw(x, y, display, color_on, color_off); + } else if (this->placeholder_) { + // Show placeholder while the runtime image is not available + this->placeholder_->draw(x, y, display, color_on, color_off); + } + // If no image is loaded and no placeholder, nothing to draw +} + +bool RuntimeImage::begin_decode(size_t expected_size) { + if (this->decoder_) { + ESP_LOGW(TAG, "Decoding already in progress"); + return false; + } + + this->decoder_ = this->create_decoder_(); + if (!this->decoder_) { + ESP_LOGE(TAG, "Failed to create decoder for format %d", this->format_); + return false; + } + + this->total_size_ = expected_size; + this->decoded_bytes_ = 0; + + // Initialize decoder + int result = this->decoder_->prepare(expected_size); + if (result < 0) { + ESP_LOGE(TAG, "Failed to prepare decoder: %d", result); + this->decoder_ = nullptr; + return false; + } + + return true; +} + +int RuntimeImage::feed_data(uint8_t *data, size_t len) { + if (!this->decoder_) { + ESP_LOGE(TAG, "No decoder initialized"); + return -1; + } + + int consumed = this->decoder_->decode(data, len); + if (consumed > 0) { + this->decoded_bytes_ += consumed; + } + + return consumed; +} + +bool RuntimeImage::end_decode() { + if (!this->decoder_) { + return false; + } + + // Finalize the image for display + if (!this->progressive_display_) { + // Only now make the image visible + this->width_ = this->buffer_width_; + this->height_ = this->buffer_height_; + this->data_start_ = this->buffer_; + } + + // Clean up decoder + this->decoder_ = nullptr; + + ESP_LOGD(TAG, "Decoding complete: %dx%d, %zu bytes", this->width_, this->height_, this->decoded_bytes_); + return true; +} + +bool RuntimeImage::is_decode_finished() const { + if (!this->decoder_) { + return false; + } + return this->decoder_->is_finished(); +} + +void RuntimeImage::release() { + this->release_buffer_(); + // Reset decoder separately — release() can be called from within the decoder + // (via set_size -> resize -> resize_buffer_), so we must not destroy the decoder here. + // The decoder lifecycle is managed by begin_decode()/end_decode(). + this->decoder_ = nullptr; +} + +void RuntimeImage::release_buffer_() { + if (this->buffer_) { + ESP_LOGV(TAG, "Releasing buffer of size %zu", this->get_buffer_size_(this->buffer_width_, this->buffer_height_)); + this->allocator_.deallocate(this->buffer_, this->get_buffer_size_(this->buffer_width_, this->buffer_height_)); + this->buffer_ = nullptr; + this->data_start_ = nullptr; + this->width_ = 0; + this->height_ = 0; + this->buffer_width_ = 0; + this->buffer_height_ = 0; + } +} + +size_t RuntimeImage::resize_buffer_(int width, int height) { + size_t new_size = this->get_buffer_size_(width, height); + + if (this->buffer_ && this->buffer_width_ == width && this->buffer_height_ == height) { + // Buffer already allocated with correct size + return new_size; + } + + // Release old buffer if dimensions changed + if (this->buffer_) { + this->release_buffer_(); + } + + ESP_LOGD(TAG, "Allocating buffer: %dx%d, %zu bytes", width, height, new_size); + this->buffer_ = this->allocator_.allocate(new_size); + + if (!this->buffer_) { + ESP_LOGE(TAG, "Failed to allocate %zu bytes. Largest free block: %zu", new_size, + this->allocator_.get_max_free_block_size()); + return 0; + } + + // Clear buffer + memset(this->buffer_, 0, new_size); + + this->buffer_width_ = width; + this->buffer_height_ = height; + + return new_size; +} + +size_t RuntimeImage::get_buffer_size_(int width, int height) const { + return (this->get_bpp() * width + 7u) / 8u * height; +} + +int RuntimeImage::get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; } + +std::unique_ptr RuntimeImage::create_decoder_() { + switch (this->format_) { +#ifdef USE_RUNTIME_IMAGE_BMP + case BMP: + return make_unique(this); +#endif +#ifdef USE_RUNTIME_IMAGE_JPEG + case JPEG: + return make_unique(this); +#endif +#ifdef USE_RUNTIME_IMAGE_PNG + case PNG: + return make_unique(this); +#endif + default: + ESP_LOGE(TAG, "Unsupported image format: %d", this->format_); + return nullptr; + } +} + +} // namespace esphome::runtime_image diff --git a/esphome/components/runtime_image/runtime_image.h b/esphome/components/runtime_image/runtime_image.h new file mode 100644 index 0000000000..0a5279d86d --- /dev/null +++ b/esphome/components/runtime_image/runtime_image.h @@ -0,0 +1,214 @@ +#pragma once + +#include "esphome/components/image/image.h" +#include "esphome/core/helpers.h" + +namespace esphome::runtime_image { + +// Forward declaration +class ImageDecoder; + +/** + * @brief Image format types that can be decoded dynamically. + */ +enum ImageFormat { + /** Automatically detect from data. Not implemented yet. */ + AUTO, + /** JPEG format. */ + JPEG, + /** PNG format. */ + PNG, + /** BMP format. */ + BMP, +}; + +/** + * @brief A dynamic image that can be loaded and decoded at runtime. + * + * This class provides dynamic buffer allocation and management for images + * that are decoded at runtime, as opposed to static images compiled into + * the firmware. It serves as a base class for components that need to + * load images dynamically from various sources. + */ +class RuntimeImage : public image::Image { + public: + /** + * @brief Construct a new RuntimeImage object. + * + * @param format The image format to decode. + * @param type The pixel format for the image. + * @param transparency The transparency type for the image. + * @param placeholder Optional placeholder image to show while loading. + * @param is_big_endian Whether the image is stored in big-endian format. + * @param fixed_width Fixed width for the image (0 for auto-resize). + * @param fixed_height Fixed height for the image (0 for auto-resize). + */ + RuntimeImage(ImageFormat format, image::ImageType type, image::Transparency transparency, + image::Image *placeholder = nullptr, bool is_big_endian = false, int fixed_width = 0, + int fixed_height = 0); + + ~RuntimeImage(); + + // Decoder interface methods + /** + * @brief Resize the image buffer to the requested dimensions. + * + * The buffer will be allocated if not existing. + * If fixed dimensions have been specified in the constructor, the buffer will be created + * with those dimensions and not resized, even on request. + * Otherwise, the old buffer will be deallocated and a new buffer with the requested + * dimensions allocated. + * + * @param width Requested width (ignored if fixed_width_ is set) + * @param height Requested height (ignored if fixed_height_ is set) + * @return Size of the allocated buffer in bytes, or 0 if allocation failed. + */ + int resize(int width, int height); + void draw_pixel(int x, int y, const Color &color); + void map_chroma_key(Color &color); + int get_buffer_width() const { return this->buffer_width_; } + int get_buffer_height() const { return this->buffer_height_; } + + // Image drawing interface + void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; + + /** + * @brief Begin decoding an image. + * + * @param expected_size Optional hint about the expected data size. + * @return true if decoder was successfully initialized. + */ + bool begin_decode(size_t expected_size = 0); + + /** + * @brief Feed data to the decoder. + * + * @param data Pointer to the data buffer. + * @param len Length of data to process. + * @return Number of bytes consumed by the decoder. + */ + int feed_data(uint8_t *data, size_t len); + + /** + * @brief Complete the decoding process. + * + * @return true if decoding completed successfully. + */ + bool end_decode(); + + /** + * @brief Check if decoding is currently in progress. + */ + bool is_decoding() const { return this->decoder_ != nullptr; } + + /** + * @brief Check if the decoder has finished processing all data. + * + * This delegates to the decoder's format-specific completion check, + * which handles both known-size and chunked transfer cases. + */ + bool is_decode_finished() const; + + /** + * @brief Check if an image is currently loaded. + */ + bool is_loaded() const { return this->buffer_ != nullptr; } + + /** + * @brief Get the image format. + */ + ImageFormat get_format() const { return this->format_; } + + /** + * @brief Release the image buffer and free memory. + */ + void release(); + + /** + * @brief Set whether to allow progressive display during decode. + * + * When enabled, the image can be displayed even while still decoding. + * When disabled, the image is only displayed after decoding completes. + */ + void set_progressive_display(bool progressive) { this->progressive_display_ = progressive; } + + protected: + /** + * @brief Resize the image buffer to the requested dimensions. + * + * @param width New width in pixels. + * @param height New height in pixels. + * @return Size of the allocated buffer, or 0 on failure. + */ + size_t resize_buffer_(int width, int height); + + /** + * @brief Release only the image buffer without resetting the decoder. + * + * This is safe to call from within the decoder (e.g., during resize). + */ + void release_buffer_(); + + /** + * @brief Get the buffer size in bytes for given dimensions. + */ + size_t get_buffer_size_(int width, int height) const; + + /** + * @brief Get the position in the buffer for a pixel. + */ + int get_position_(int x, int y) const; + + /** + * @brief Create decoder instance for the image's format. + */ + std::unique_ptr create_decoder_(); + + // Memory management + RAMAllocator allocator_{}; + uint8_t *buffer_{nullptr}; + + // Decoder management + std::unique_ptr decoder_{nullptr}; + /** The image format this RuntimeImage is configured to decode. */ + const ImageFormat format_; + + /** + * Actual width of the current image. + * This needs to be separate from "Image::get_width()" because the latter + * must return 0 until the image has been decoded (to avoid showing partially + * decoded images). When progressive_display_ is enabled, Image dimensions + * are updated during decoding to allow rendering in progress. + */ + int buffer_width_{0}; + /** + * Actual height of the current image. + * This needs to be separate from "Image::get_height()" because the latter + * must return 0 until the image has been decoded (to avoid showing partially + * decoded images). When progressive_display_ is enabled, Image dimensions + * are updated during decoding to allow rendering in progress. + */ + int buffer_height_{0}; + + // Decoding state + size_t total_size_{0}; + size_t decoded_bytes_{0}; + + /** Fixed width requested on configuration, or 0 if not specified. */ + const int fixed_width_{0}; + /** Fixed height requested on configuration, or 0 if not specified. */ + const int fixed_height_{0}; + + /** Placeholder image to show when the runtime image is not available. */ + image::Image *placeholder_{nullptr}; + + // Configuration + bool progressive_display_{false}; + /** + * Whether the image is stored in big-endian format. + * This is used to determine how to store 16 bit colors in the buffer. + */ + bool is_big_endian_{false}; +}; + +} // namespace esphome::runtime_image diff --git a/esphome/core/defines.h b/esphome/core/defines.h index bfa33e4e59..0d6c1a42e8 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -148,9 +148,9 @@ #define USE_MQTT #define USE_MQTT_COVER_JSON #define USE_NETWORK -#define USE_ONLINE_IMAGE_BMP_SUPPORT -#define USE_ONLINE_IMAGE_PNG_SUPPORT -#define USE_ONLINE_IMAGE_JPEG_SUPPORT +#define USE_RUNTIME_IMAGE_BMP +#define USE_RUNTIME_IMAGE_PNG +#define USE_RUNTIME_IMAGE_JPEG #define USE_OTA #define USE_OTA_PASSWORD #define USE_OTA_STATE_LISTENER From f24e7709aca9e0cb7fc5951ea82141fec1b5dd9e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 Feb 2026 16:21:50 -0600 Subject: [PATCH 020/609] [core] Make LOG_ENTITY_ICON a no-op when icons are compiled out (#13973) --- esphome/core/entity_base.cpp | 2 ++ esphome/core/entity_base.h | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 811b856b5e..f6a7ec1dfd 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -152,11 +152,13 @@ void EntityBase_UnitOfMeasurement::set_unit_of_measurement(const char *unit_of_m this->unit_of_measurement_ = unit_of_measurement; } +#ifdef USE_ENTITY_ICON void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) { if (!obj.get_icon_ref().empty()) { ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj.get_icon_ref().c_str()); } } +#endif void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj) { if (!obj.get_device_class_ref().empty()) { diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 86cb75495b..cbc07cc44c 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -231,8 +231,13 @@ class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming) }; /// Log entity icon if set (for use in dump_config) +#ifdef USE_ENTITY_ICON #define LOG_ENTITY_ICON(tag, prefix, obj) log_entity_icon(tag, prefix, obj) void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj); +#else +#define LOG_ENTITY_ICON(tag, prefix, obj) ((void) 0) +inline void log_entity_icon(const char *, const char *, const EntityBase &) {} +#endif /// Log entity device class if set (for use in dump_config) #define LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj) log_entity_device_class(tag, prefix, obj) void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj); From 79d9fbf64579bee42d14adc9a4e17f8ddac3ac0b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 Feb 2026 16:22:05 -0600 Subject: [PATCH 021/609] [nfc] Replace constant std::vector with static constexpr std::array (#13978) --- esphome/components/pn532/pn532.h | 4 +- .../components/pn532/pn532_mifare_classic.cpp | 68 +++++++++---------- .../pn532/pn532_mifare_ultralight.cpp | 16 ++--- esphome/components/pn7150/pn7150.h | 4 +- .../pn7150/pn7150_mifare_classic.cpp | 66 +++++++++--------- .../pn7150/pn7150_mifare_ultralight.cpp | 13 ++-- esphome/components/pn7160/pn7160.h | 4 +- .../pn7160/pn7160_mifare_classic.cpp | 66 +++++++++--------- .../pn7160/pn7160_mifare_ultralight.cpp | 13 ++-- 9 files changed, 130 insertions(+), 124 deletions(-) diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index 73a6c15164..f98c0f9322 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -76,7 +76,7 @@ class PN532 : public PollingComponent { std::unique_ptr read_mifare_classic_tag_(nfc::NfcTagUid &uid); bool read_mifare_classic_block_(uint8_t block_num, std::vector &data); - bool write_mifare_classic_block_(uint8_t block_num, std::vector &data); + bool write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len); bool auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key); bool format_mifare_classic_mifare_(nfc::NfcTagUid &uid); bool format_mifare_classic_ndef_(nfc::NfcTagUid &uid); @@ -88,7 +88,7 @@ class PN532 : public PollingComponent { uint16_t read_mifare_ultralight_capacity_(); bool find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, uint8_t &message_start_index); - bool write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); + bool write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len); bool write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message); bool clean_mifare_ultralight_(); diff --git a/esphome/components/pn532/pn532_mifare_classic.cpp b/esphome/components/pn532/pn532_mifare_classic.cpp index b762d5d936..cca6acd96d 100644 --- a/esphome/components/pn532/pn532_mifare_classic.cpp +++ b/esphome/components/pn532/pn532_mifare_classic.cpp @@ -1,3 +1,4 @@ +#include #include #include "pn532.h" @@ -106,10 +107,10 @@ bool PN532::auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, u } bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) { - std::vector blank_buffer( - {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector trailer_buffer( - {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + static constexpr std::array BLANK_BUFFER = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array TRAILER_BUFFER = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; bool error = false; @@ -118,20 +119,20 @@ bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) { continue; } if (block != 0) { - if (!this->write_mifare_classic_block_(block, blank_buffer)) { + if (!this->write_mifare_classic_block_(block, BLANK_BUFFER.data(), BLANK_BUFFER.size())) { ESP_LOGE(TAG, "Unable to write block %d", block); error = true; } } - if (!this->write_mifare_classic_block_(block + 1, blank_buffer)) { + if (!this->write_mifare_classic_block_(block + 1, BLANK_BUFFER.data(), BLANK_BUFFER.size())) { ESP_LOGE(TAG, "Unable to write block %d", block + 1); error = true; } - if (!this->write_mifare_classic_block_(block + 2, blank_buffer)) { + if (!this->write_mifare_classic_block_(block + 2, BLANK_BUFFER.data(), BLANK_BUFFER.size())) { ESP_LOGE(TAG, "Unable to write block %d", block + 2); error = true; } - if (!this->write_mifare_classic_block_(block + 3, trailer_buffer)) { + if (!this->write_mifare_classic_block_(block + 3, TRAILER_BUFFER.data(), TRAILER_BUFFER.size())) { ESP_LOGE(TAG, "Unable to write block %d", block + 3); error = true; } @@ -141,28 +142,28 @@ bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) { } bool PN532::format_mifare_classic_ndef_(nfc::NfcTagUid &uid) { - std::vector empty_ndef_message( - {0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector blank_block( - {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector block_1_data( - {0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); - std::vector block_2_data( - {0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); - std::vector block_3_trailer( - {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); - std::vector ndef_trailer( - {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + static constexpr std::array EMPTY_NDEF_MESSAGE = { + 0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLANK_BLOCK = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLOCK_1_DATA = { + 0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}; + static constexpr std::array BLOCK_2_DATA = { + 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}; + static constexpr std::array BLOCK_3_TRAILER = { + 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + static constexpr std::array NDEF_TRAILER = { + 0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; if (!this->auth_mifare_classic_block_(uid, 0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY)) { ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting!"); return false; } - if (!this->write_mifare_classic_block_(1, block_1_data)) + if (!this->write_mifare_classic_block_(1, BLOCK_1_DATA.data(), BLOCK_1_DATA.size())) return false; - if (!this->write_mifare_classic_block_(2, block_2_data)) + if (!this->write_mifare_classic_block_(2, BLOCK_2_DATA.data(), BLOCK_2_DATA.size())) return false; - if (!this->write_mifare_classic_block_(3, block_3_trailer)) + if (!this->write_mifare_classic_block_(3, BLOCK_3_TRAILER.data(), BLOCK_3_TRAILER.size())) return false; ESP_LOGD(TAG, "Sector 0 formatted to NDEF"); @@ -172,36 +173,36 @@ bool PN532::format_mifare_classic_ndef_(nfc::NfcTagUid &uid) { return false; } if (block == 4) { - if (!this->write_mifare_classic_block_(block, empty_ndef_message)) { + if (!this->write_mifare_classic_block_(block, EMPTY_NDEF_MESSAGE.data(), EMPTY_NDEF_MESSAGE.size())) { ESP_LOGE(TAG, "Unable to write block %d", block); } } else { - if (!this->write_mifare_classic_block_(block, blank_block)) { + if (!this->write_mifare_classic_block_(block, BLANK_BLOCK.data(), BLANK_BLOCK.size())) { ESP_LOGE(TAG, "Unable to write block %d", block); } } - if (!this->write_mifare_classic_block_(block + 1, blank_block)) { + if (!this->write_mifare_classic_block_(block + 1, BLANK_BLOCK.data(), BLANK_BLOCK.size())) { ESP_LOGE(TAG, "Unable to write block %d", block + 1); } - if (!this->write_mifare_classic_block_(block + 2, blank_block)) { + if (!this->write_mifare_classic_block_(block + 2, BLANK_BLOCK.data(), BLANK_BLOCK.size())) { ESP_LOGE(TAG, "Unable to write block %d", block + 2); } - if (!this->write_mifare_classic_block_(block + 3, ndef_trailer)) { + if (!this->write_mifare_classic_block_(block + 3, NDEF_TRAILER.data(), NDEF_TRAILER.size())) { ESP_LOGE(TAG, "Unable to write trailer block %d", block + 3); } } return true; } -bool PN532::write_mifare_classic_block_(uint8_t block_num, std::vector &write_data) { - std::vector data({ +bool PN532::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) { + std::vector cmd({ PN532_COMMAND_INDATAEXCHANGE, 0x01, // One card nfc::MIFARE_CMD_WRITE, block_num, }); - data.insert(data.end(), write_data.begin(), write_data.end()); - if (!this->write_command_(data)) { + cmd.insert(cmd.end(), data, data + len); + if (!this->write_command_(cmd)) { ESP_LOGE(TAG, "Error writing block %d", block_num); return false; } @@ -243,8 +244,7 @@ bool PN532::write_mifare_classic_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *mes } } - std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); - if (!this->write_mifare_classic_block_(current_block, data)) { + if (!this->write_mifare_classic_block_(current_block, encoded.data() + index, nfc::MIFARE_CLASSIC_BLOCK_SIZE)) { return false; } index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; diff --git a/esphome/components/pn532/pn532_mifare_ultralight.cpp b/esphome/components/pn532/pn532_mifare_ultralight.cpp index 01e41df5c0..a8a8e2d573 100644 --- a/esphome/components/pn532/pn532_mifare_ultralight.cpp +++ b/esphome/components/pn532/pn532_mifare_ultralight.cpp @@ -1,3 +1,4 @@ +#include #include #include "pn532.h" @@ -143,8 +144,7 @@ bool PN532::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage * uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; while (index < buffer_length) { - std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); - if (!this->write_mifare_ultralight_page_(current_page, data)) { + if (!this->write_mifare_ultralight_page_(current_page, encoded.data() + index, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE)) { return false; } index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; @@ -157,25 +157,25 @@ bool PN532::clean_mifare_ultralight_() { uint32_t capacity = this->read_mifare_ultralight_capacity_(); uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; - std::vector blank_data = {0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLANK_DATA = {0x00, 0x00, 0x00, 0x00}; for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { - if (!this->write_mifare_ultralight_page_(i, blank_data)) { + if (!this->write_mifare_ultralight_page_(i, BLANK_DATA.data(), BLANK_DATA.size())) { return false; } } return true; } -bool PN532::write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data) { - std::vector data({ +bool PN532::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len) { + std::vector cmd({ PN532_COMMAND_INDATAEXCHANGE, 0x01, // One card nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num, }); - data.insert(data.end(), write_data.begin(), write_data.end()); - if (!this->write_command_(data)) { + cmd.insert(cmd.end(), write_data, write_data + len); + if (!this->write_command_(cmd)) { ESP_LOGE(TAG, "Error writing page %u", page_num); return false; } diff --git a/esphome/components/pn7150/pn7150.h b/esphome/components/pn7150/pn7150.h index a5dcef9f99..5feba17d21 100644 --- a/esphome/components/pn7150/pn7150.h +++ b/esphome/components/pn7150/pn7150.h @@ -236,7 +236,7 @@ class PN7150 : public nfc::Nfcc, public Component { uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag); uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector &data); - uint8_t write_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len); uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key); uint8_t sect_to_auth_(uint8_t block_num); uint8_t format_mifare_classic_mifare_(); @@ -250,7 +250,7 @@ class PN7150 : public nfc::Nfcc, public Component { uint16_t read_mifare_ultralight_capacity_(); uint8_t find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, uint8_t &message_start_index); - uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); + uint8_t write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len); uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr &message); uint8_t clean_mifare_ultralight_(); diff --git a/esphome/components/pn7150/pn7150_mifare_classic.cpp b/esphome/components/pn7150/pn7150_mifare_classic.cpp index dee81b610a..61434cdb28 100644 --- a/esphome/components/pn7150/pn7150_mifare_classic.cpp +++ b/esphome/components/pn7150/pn7150_mifare_classic.cpp @@ -1,3 +1,4 @@ +#include #include #include "pn7150.h" @@ -139,10 +140,10 @@ uint8_t PN7150::sect_to_auth_(const uint8_t block_num) { } uint8_t PN7150::format_mifare_classic_mifare_() { - std::vector blank_buffer( - {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector trailer_buffer( - {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + static constexpr std::array BLANK_BUFFER = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array TRAILER_BUFFER = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; auto status = nfc::STATUS_OK; @@ -151,20 +152,20 @@ uint8_t PN7150::format_mifare_classic_mifare_() { continue; } if (block != 0) { - if (this->write_mifare_classic_block_(block, blank_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block); status = nfc::STATUS_FAILED; } } - if (this->write_mifare_classic_block_(block + 1, blank_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 1, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 1); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 2, blank_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 2, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 2); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 3, trailer_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 3, TRAILER_BUFFER.data(), TRAILER_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 3); status = nfc::STATUS_FAILED; } @@ -174,30 +175,30 @@ uint8_t PN7150::format_mifare_classic_mifare_() { } uint8_t PN7150::format_mifare_classic_ndef_() { - std::vector empty_ndef_message( - {0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector blank_block( - {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector block_1_data( - {0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); - std::vector block_2_data( - {0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); - std::vector block_3_trailer( - {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); - std::vector ndef_trailer( - {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + static constexpr std::array EMPTY_NDEF_MESSAGE = { + 0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLANK_BLOCK = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLOCK_1_DATA = { + 0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}; + static constexpr std::array BLOCK_2_DATA = { + 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}; + static constexpr std::array BLOCK_3_TRAILER = { + 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + static constexpr std::array NDEF_TRAILER = { + 0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting"); return nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(1, block_1_data) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(1, BLOCK_1_DATA.data(), BLOCK_1_DATA.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(2, block_2_data) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(2, BLOCK_2_DATA.data(), BLOCK_2_DATA.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(3, block_3_trailer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(3, BLOCK_3_TRAILER.data(), BLOCK_3_TRAILER.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } @@ -210,25 +211,26 @@ uint8_t PN7150::format_mifare_classic_ndef_() { return nfc::STATUS_FAILED; } if (block == 4) { - if (this->write_mifare_classic_block_(block, empty_ndef_message) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block, EMPTY_NDEF_MESSAGE.data(), EMPTY_NDEF_MESSAGE.size()) != + nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block); status = nfc::STATUS_FAILED; } } else { - if (this->write_mifare_classic_block_(block, blank_block) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block); status = nfc::STATUS_FAILED; } } - if (this->write_mifare_classic_block_(block + 1, blank_block) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 1, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 1); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 2, blank_block) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 2, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 2); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 3, ndef_trailer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 3, NDEF_TRAILER.data(), NDEF_TRAILER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3); status = nfc::STATUS_FAILED; } @@ -236,7 +238,7 @@ uint8_t PN7150::format_mifare_classic_ndef_() { return status; } -uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, std::vector &write_data) { +uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) { nfc::NciMessage rx; nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num}); @@ -248,7 +250,7 @@ uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, std::vectortransceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { @@ -294,8 +296,8 @@ uint8_t PN7150::write_mifare_classic_tag_(const std::shared_ptr data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); - if (this->write_mifare_classic_block_(current_block, data) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(current_block, encoded.data() + index, nfc::MIFARE_CLASSIC_BLOCK_SIZE) != + nfc::STATUS_OK) { return nfc::STATUS_FAILED; } index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; diff --git a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp index 166065f6c1..46f5dba2b7 100644 --- a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp +++ b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -144,8 +145,8 @@ uint8_t PN7150::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::sha uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; while (index < buffer_length) { - std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); - if (this->write_mifare_ultralight_page_(current_page, data) != nfc::STATUS_OK) { + if (this->write_mifare_ultralight_page_(current_page, encoded.data() + index, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) != + nfc::STATUS_OK) { return nfc::STATUS_FAILED; } index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; @@ -158,19 +159,19 @@ uint8_t PN7150::clean_mifare_ultralight_() { uint32_t capacity = this->read_mifare_ultralight_capacity_(); uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; - std::vector blank_data = {0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLANK_DATA = {0x00, 0x00, 0x00, 0x00}; for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { - if (this->write_mifare_ultralight_page_(i, blank_data) != nfc::STATUS_OK) { + if (this->write_mifare_ultralight_page_(i, BLANK_DATA.data(), BLANK_DATA.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } } return nfc::STATUS_OK; } -uint8_t PN7150::write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data) { +uint8_t PN7150::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len) { std::vector payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num}; - payload.insert(payload.end(), write_data.begin(), write_data.end()); + payload.insert(payload.end(), write_data, write_data + len); nfc::NciMessage rx; nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload); diff --git a/esphome/components/pn7160/pn7160.h b/esphome/components/pn7160/pn7160.h index 572fab3351..9f2d10c2d5 100644 --- a/esphome/components/pn7160/pn7160.h +++ b/esphome/components/pn7160/pn7160.h @@ -253,7 +253,7 @@ class PN7160 : public nfc::Nfcc, public Component { uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag); uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector &data); - uint8_t write_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len); uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key); uint8_t sect_to_auth_(uint8_t block_num); uint8_t format_mifare_classic_mifare_(); @@ -267,7 +267,7 @@ class PN7160 : public nfc::Nfcc, public Component { uint16_t read_mifare_ultralight_capacity_(); uint8_t find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, uint8_t &message_start_index); - uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); + uint8_t write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len); uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr &message); uint8_t clean_mifare_ultralight_(); diff --git a/esphome/components/pn7160/pn7160_mifare_classic.cpp b/esphome/components/pn7160/pn7160_mifare_classic.cpp index 57d2042eaa..710a7198c6 100644 --- a/esphome/components/pn7160/pn7160_mifare_classic.cpp +++ b/esphome/components/pn7160/pn7160_mifare_classic.cpp @@ -1,3 +1,4 @@ +#include #include #include "pn7160.h" @@ -139,10 +140,10 @@ uint8_t PN7160::sect_to_auth_(const uint8_t block_num) { } uint8_t PN7160::format_mifare_classic_mifare_() { - std::vector blank_buffer( - {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector trailer_buffer( - {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + static constexpr std::array BLANK_BUFFER = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array TRAILER_BUFFER = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; auto status = nfc::STATUS_OK; @@ -151,20 +152,20 @@ uint8_t PN7160::format_mifare_classic_mifare_() { continue; } if (block != 0) { - if (this->write_mifare_classic_block_(block, blank_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block); status = nfc::STATUS_FAILED; } } - if (this->write_mifare_classic_block_(block + 1, blank_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 1, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 1); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 2, blank_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 2, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 2); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 3, trailer_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 3, TRAILER_BUFFER.data(), TRAILER_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 3); status = nfc::STATUS_FAILED; } @@ -174,30 +175,30 @@ uint8_t PN7160::format_mifare_classic_mifare_() { } uint8_t PN7160::format_mifare_classic_ndef_() { - std::vector empty_ndef_message( - {0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector blank_block( - {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector block_1_data( - {0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); - std::vector block_2_data( - {0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); - std::vector block_3_trailer( - {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); - std::vector ndef_trailer( - {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + static constexpr std::array EMPTY_NDEF_MESSAGE = { + 0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLANK_BLOCK = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLOCK_1_DATA = { + 0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}; + static constexpr std::array BLOCK_2_DATA = { + 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}; + static constexpr std::array BLOCK_3_TRAILER = { + 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + static constexpr std::array NDEF_TRAILER = { + 0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting"); return nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(1, block_1_data) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(1, BLOCK_1_DATA.data(), BLOCK_1_DATA.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(2, block_2_data) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(2, BLOCK_2_DATA.data(), BLOCK_2_DATA.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(3, block_3_trailer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(3, BLOCK_3_TRAILER.data(), BLOCK_3_TRAILER.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } @@ -210,25 +211,26 @@ uint8_t PN7160::format_mifare_classic_ndef_() { return nfc::STATUS_FAILED; } if (block == 4) { - if (this->write_mifare_classic_block_(block, empty_ndef_message) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block, EMPTY_NDEF_MESSAGE.data(), EMPTY_NDEF_MESSAGE.size()) != + nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block); status = nfc::STATUS_FAILED; } } else { - if (this->write_mifare_classic_block_(block, blank_block) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block); status = nfc::STATUS_FAILED; } } - if (this->write_mifare_classic_block_(block + 1, blank_block) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 1, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 1); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 2, blank_block) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 2, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 2); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 3, ndef_trailer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 3, NDEF_TRAILER.data(), NDEF_TRAILER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3); status = nfc::STATUS_FAILED; } @@ -236,7 +238,7 @@ uint8_t PN7160::format_mifare_classic_ndef_() { return status; } -uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, std::vector &write_data) { +uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) { nfc::NciMessage rx; nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num}); char buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; @@ -248,7 +250,7 @@ uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, std::vectortransceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { @@ -294,8 +296,8 @@ uint8_t PN7160::write_mifare_classic_tag_(const std::shared_ptr data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); - if (this->write_mifare_classic_block_(current_block, data) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(current_block, encoded.data() + index, nfc::MIFARE_CLASSIC_BLOCK_SIZE) != + nfc::STATUS_OK) { return nfc::STATUS_FAILED; } index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; diff --git a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp index c473ff48d9..9dc8d3dd2d 100644 --- a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp +++ b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -144,8 +145,8 @@ uint8_t PN7160::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::sha uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; while (index < buffer_length) { - std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); - if (this->write_mifare_ultralight_page_(current_page, data) != nfc::STATUS_OK) { + if (this->write_mifare_ultralight_page_(current_page, encoded.data() + index, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) != + nfc::STATUS_OK) { return nfc::STATUS_FAILED; } index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; @@ -158,19 +159,19 @@ uint8_t PN7160::clean_mifare_ultralight_() { uint32_t capacity = this->read_mifare_ultralight_capacity_(); uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; - std::vector blank_data = {0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLANK_DATA = {0x00, 0x00, 0x00, 0x00}; for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { - if (this->write_mifare_ultralight_page_(i, blank_data) != nfc::STATUS_OK) { + if (this->write_mifare_ultralight_page_(i, BLANK_DATA.data(), BLANK_DATA.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } } return nfc::STATUS_OK; } -uint8_t PN7160::write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data) { +uint8_t PN7160::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len) { std::vector payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num}; - payload.insert(payload.end(), write_data.begin(), write_data.end()); + payload.insert(payload.end(), write_data, write_data + len); nfc::NciMessage rx; nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload); From 931b47673c49e3f29324290b52d591aacab5faed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 16:22:26 -0600 Subject: [PATCH 022/609] Bump github/codeql-action from 4.32.2 to 4.32.3 (#13981) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 51ea4085e0..376825bad6 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 + uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 + uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 with: category: "/language:${{matrix.language}}" From 36776b40c2010b6635c9bad297e4985c9db4cbcb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Feb 2026 08:21:04 -0700 Subject: [PATCH 023/609] [wifi] Fix ESP8266 DHCP state corruption from premature dhcp_renew() (#13983) Co-authored-by: Claude Opus 4.6 --- .../components/wifi/wifi_component_esp8266.cpp | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index c87345f0bf..cbf7d7d80f 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -216,23 +216,16 @@ bool WiFiComponent::wifi_apply_hostname_() { ESP_LOGV(TAG, "Set hostname failed"); } - // inform dhcp server of hostname change using dhcp_renew() + // Update hostname on all lwIP interfaces so DHCP packets include it. + // lwIP includes the hostname in DHCP DISCOVER/REQUEST automatically + // via LWIP_NETIF_HOSTNAME — no dhcp_renew() needed. The hostname is + // fixed at compile time and never changes at runtime. for (netif *intf = netif_list; intf; intf = intf->next) { - // unconditionally update all known interfaces #if LWIP_VERSION_MAJOR == 1 intf->hostname = (char *) wifi_station_get_hostname(); #else intf->hostname = wifi_station_get_hostname(); #endif - if (netif_dhcp_data(intf) != nullptr) { - // renew already started DHCP leases - err_t lwipret = dhcp_renew(intf); - if (lwipret != ERR_OK) { - ESP_LOGW(TAG, "wifi_apply_hostname_(%s): lwIP error %d on interface %c%c (index %d)", intf->hostname, - (int) lwipret, intf->name[0], intf->name[1], intf->num); - ret = false; - } - } } return ret; From 5a6d64814a3a5f51f6e238620938416523947e94 Mon Sep 17 00:00:00 2001 From: AndreKR Date: Sat, 14 Feb 2026 18:08:26 +0100 Subject: [PATCH 024/609] [http_request] Improve TLS logging on ESP8266 (#13985) --- .../http_request/http_request_arduino.cpp | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index aee1f651bf..e5b919e380 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -9,9 +9,20 @@ #include "esphome/core/defines.h" #include "esphome/core/log.h" +// Include BearSSL error constants for TLS failure diagnostics +#ifdef USE_ESP8266 +#include +#endif + namespace esphome::http_request { static const char *const TAG = "http_request.arduino"; +#ifdef USE_ESP8266 +static constexpr int RX_BUFFER_SIZE = 512; +static constexpr int TX_BUFFER_SIZE = 512; +// ESP8266 Arduino core (WiFiClientSecureBearSSL.cpp) returns -1000 on OOM +static constexpr int ESP8266_SSL_ERR_OOM = -1000; +#endif std::shared_ptr HttpRequestArduino::perform(const std::string &url, const std::string &method, const std::string &body, @@ -47,7 +58,7 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur ESP_LOGV(TAG, "ESP8266 HTTPS connection with WiFiClientSecure"); stream_ptr = std::make_unique(); WiFiClientSecure *secure_client = static_cast(stream_ptr.get()); - secure_client->setBufferSizes(512, 512); + secure_client->setBufferSizes(RX_BUFFER_SIZE, TX_BUFFER_SIZE); secure_client->setInsecure(); } else { stream_ptr = std::make_unique(); @@ -107,13 +118,42 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur container->status_code = container->client_.sendRequest(method.c_str(), body.c_str()); App.feed_wdt(); if (container->status_code < 0) { +#if defined(USE_ESP8266) && defined(USE_HTTP_REQUEST_ESP8266_HTTPS) + if (secure) { + WiFiClientSecure *secure_client = static_cast(stream_ptr.get()); + int last_error = secure_client->getLastSSLError(); + + if (last_error != 0) { + const LogString *error_msg; + switch (last_error) { + case ESP8266_SSL_ERR_OOM: + error_msg = LOG_STR("Unable to allocate buffer memory"); + break; + case BR_ERR_TOO_LARGE: + error_msg = LOG_STR("Incoming TLS record does not fit in receive buffer (BR_ERR_TOO_LARGE)"); + break; + default: + error_msg = LOG_STR("Unknown SSL error"); + break; + } + ESP_LOGW(TAG, "SSL failure: %s (Code: %d)", LOG_STR_ARG(error_msg), last_error); + if (last_error == ESP8266_SSL_ERR_OOM) { + ESP_LOGW(TAG, "Heap free: %u bytes, configured buffer sizes: %u bytes", ESP.getFreeHeap(), + static_cast(RX_BUFFER_SIZE + TX_BUFFER_SIZE)); + } + } else { + ESP_LOGW(TAG, "Connection failure with no error code"); + } + } +#endif + ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url.c_str(), HTTPClient::errorToString(container->status_code).c_str()); + this->status_momentary_error("failed", 1000); container->end(); return nullptr; } - if (!is_success(container->status_code)) { ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code); this->status_momentary_error("failed", 1000); From 38404b2013f031c921ac43cbec4f3d4b898deccf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 15:11:17 -0700 Subject: [PATCH 025/609] Bump ruff from 0.15.0 to 0.15.1 (#13980) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 991e053d5a..6d89060b0d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.0 + rev: v0.15.1 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 2cf6f6456e..9e99855f6f 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.4 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.0 # also change in .pre-commit-config.yaml when updating +ruff==0.15.1 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From f48c8a64443e56e06f90271cbfd8d0e67116093d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:11:36 -0500 Subject: [PATCH 026/609] [combination] Fix 'coeffecient' typo with backward-compatible deprecation (#14004) Co-authored-by: Claude Opus 4.6 --- .../components/combination/combination.cpp | 2 +- esphome/components/combination/sensor.py | 46 +++++++++++++++---- tests/components/combination/common.yaml | 4 +- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/esphome/components/combination/combination.cpp b/esphome/components/combination/combination.cpp index 716d270390..ece7cca482 100644 --- a/esphome/components/combination/combination.cpp +++ b/esphome/components/combination/combination.cpp @@ -126,7 +126,7 @@ void LinearCombinationComponent::setup() { } void LinearCombinationComponent::handle_new_value(float value) { - // Multiplies each sensor state by a configured coeffecient and then sums + // Multiplies each sensor state by a configured coefficient and then sums if (!std::isfinite(value)) return; diff --git a/esphome/components/combination/sensor.py b/esphome/components/combination/sensor.py index f5255fec03..0204162e8d 100644 --- a/esphome/components/combination/sensor.py +++ b/esphome/components/combination/sensor.py @@ -1,3 +1,5 @@ +import logging + import esphome.codegen as cg from esphome.components import sensor import esphome.config_validation as cv @@ -15,6 +17,8 @@ from esphome.const import ( ) from esphome.core.entity_helpers import inherit_property_from +_LOGGER = logging.getLogger(__name__) + CODEOWNERS = ["@Cat-Ion", "@kahrendt"] combination_ns = cg.esphome_ns.namespace("combination") @@ -47,7 +51,8 @@ SumCombinationComponent = combination_ns.class_( "SumCombinationComponent", cg.Component, sensor.Sensor ) -CONF_COEFFECIENT = "coeffecient" +CONF_COEFFICIENT = "coefficient" +CONF_COEFFECIENT = "coeffecient" # Deprecated, remove before 2026.12.0 CONF_ERROR = "error" CONF_KALMAN = "kalman" CONF_LINEAR = "linear" @@ -68,11 +73,34 @@ KALMAN_SOURCE_SCHEMA = cv.Schema( } ) -LINEAR_SOURCE_SCHEMA = cv.Schema( - { - cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), - cv.Required(CONF_COEFFECIENT): cv.templatable(cv.float_), - } + +def _migrate_coeffecient(config): + """Migrate deprecated 'coeffecient' spelling to 'coefficient'.""" + if CONF_COEFFECIENT in config: + if CONF_COEFFICIENT in config: + raise cv.Invalid( + f"Cannot specify both '{CONF_COEFFICIENT}' and '{CONF_COEFFECIENT}'" + ) + _LOGGER.warning( + "'%s' is deprecated, use '%s' instead. Will be removed in 2026.12.0", + CONF_COEFFECIENT, + CONF_COEFFICIENT, + ) + config[CONF_COEFFICIENT] = config.pop(CONF_COEFFECIENT) + elif CONF_COEFFICIENT not in config: + raise cv.Invalid(f"'{CONF_COEFFICIENT}' is a required option") + return config + + +LINEAR_SOURCE_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), + cv.Optional(CONF_COEFFICIENT): cv.templatable(cv.float_), + cv.Optional(CONF_COEFFECIENT): cv.templatable(cv.float_), + } + ), + _migrate_coeffecient, ) SENSOR_ONLY_SOURCE_SCHEMA = cv.Schema( @@ -162,12 +190,12 @@ async def to_code(config): ) cg.add(var.add_source(source, error)) elif config[CONF_TYPE] == CONF_LINEAR: - coeffecient = await cg.templatable( - source_conf[CONF_COEFFECIENT], + coefficient = await cg.templatable( + source_conf[CONF_COEFFICIENT], [(float, "x")], cg.float_, ) - cg.add(var.add_source(source, coeffecient)) + cg.add(var.add_source(source, coefficient)) else: cg.add(var.add_source(source)) diff --git a/tests/components/combination/common.yaml b/tests/components/combination/common.yaml index 0e5d512d08..5d46419399 100644 --- a/tests/components/combination/common.yaml +++ b/tests/components/combination/common.yaml @@ -27,9 +27,9 @@ sensor: name: Linearly combined temperatures sources: - source: template_temperature1 - coeffecient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" + coefficient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" - source: template_temperature2 - coeffecient: 1.5 + coefficient: 1.5 - platform: combination type: max name: Max of combined temperatures From 0f4dc6702df0d9646cfd28d85ae86a34bc2f6b6d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:11:50 -0500 Subject: [PATCH 027/609] [fan] Fix preset_mode not restored on boot (#14002) Co-authored-by: Claude Opus 4.6 --- esphome/components/fan/fan.cpp | 8 +++++++- esphome/components/fan/fan.h | 4 +++- esphome/components/hbridge/fan/hbridge_fan.cpp | 8 ++++---- esphome/components/speed/fan/speed_fan.cpp | 8 ++++---- esphome/components/template/fan/template_fan.cpp | 10 +++++----- 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index d70a2940bc..c1e0a3dc2e 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -221,12 +221,17 @@ void Fan::publish_state() { } // Random 32-bit value, change this every time the layout of the FanRestoreState struct changes. -constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA; +constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABB; optional Fan::restore_state_() { FanRestoreState recovered{}; this->rtc_ = this->make_entity_preference(RESTORE_STATE_VERSION); bool restored = this->rtc_.load(&recovered); + if (!restored) { + // No valid saved data; ensure preset_mode sentinel is set + recovered.preset_mode = FanRestoreState::NO_PRESET; + } + switch (this->restore_mode_) { case FanRestoreMode::NO_RESTORE: return {}; @@ -264,6 +269,7 @@ void Fan::save_state_() { state.oscillating = this->oscillating; state.speed = this->speed; state.direction = this->direction; + state.preset_mode = FanRestoreState::NO_PRESET; if (this->has_preset_mode()) { const auto &preset_modes = traits.supported_preset_modes(); diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 55d4ba8825..2caf3a712a 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -91,11 +91,13 @@ class FanCall { }; struct FanRestoreState { + static constexpr uint8_t NO_PRESET = UINT8_MAX; + bool state; int speed; bool oscillating; FanDirection direction; - uint8_t preset_mode; + uint8_t preset_mode{NO_PRESET}; /// Convert this struct to a fan call that can be performed. FanCall to_call(Fan &fan); diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 9bf58f9d1e..38e4129e66 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -28,15 +28,15 @@ fan::FanCall HBridgeFan::brake() { } void HBridgeFan::setup() { + // Construct traits before restore so preset modes can be looked up by index + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); + auto restore = this->restore_state_(); if (restore.has_value()) { restore->apply(*this); this->write_state_(); } - - // Construct traits - this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); } void HBridgeFan::dump_config() { diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index af98e3a51f..55f7fd162c 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -7,15 +7,15 @@ namespace speed { static const char *const TAG = "speed.fan"; void SpeedFan::setup() { + // Construct traits before restore so preset modes can be looked up by index + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); + auto restore = this->restore_state_(); if (restore.has_value()) { restore->apply(*this); this->write_state_(); } - - // Construct traits - this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); } void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); } diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp index 0e1920a984..cd267bd552 100644 --- a/esphome/components/template/fan/template_fan.cpp +++ b/esphome/components/template/fan/template_fan.cpp @@ -6,15 +6,15 @@ namespace esphome::template_ { static const char *const TAG = "template.fan"; void TemplateFan::setup() { + // Construct traits before restore so preset modes can be looked up by index + this->traits_ = + fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); + auto restore = this->restore_state_(); if (restore.has_value()) { restore->apply(*this); } - - // Construct traits - this->traits_ = - fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); } void TemplateFan::dump_config() { LOG_FAN("", "Template Fan", this); } From 6303bc3e355bd73c22658a2745d8d5e19e195755 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 15 Feb 2026 13:23:06 -0500 Subject: [PATCH 028/609] [esp32_rmt] Handle ESP32 variants without RMT hardware (#14001) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32_rmt/__init__.py | 22 +++++ .../components/esp32_rmt_led_strip/light.py | 6 +- esphome/components/remote_base/remote_base.h | 5 +- .../components/remote_receiver/__init__.py | 41 +++++++- .../remote_receiver/remote_receiver.cpp | 2 +- .../remote_receiver/remote_receiver.h | 17 ++-- .../remote_receiver/remote_receiver_esp32.cpp | 5 +- .../components/remote_transmitter/__init__.py | 93 ++++++++++++------- .../remote_transmitter/automation.h | 6 +- .../remote_transmitter/remote_transmitter.cpp | 8 +- .../remote_transmitter/remote_transmitter.h | 21 +++-- .../remote_transmitter_esp32.cpp | 11 ++- .../remote_receiver/test.esp32-c2-idf.yaml | 12 +++ .../remote_transmitter/test.esp32-c2-idf.yaml | 7 ++ 14 files changed, 181 insertions(+), 75 deletions(-) create mode 100644 tests/components/remote_receiver/test.esp32-c2-idf.yaml create mode 100644 tests/components/remote_transmitter/test.esp32-c2-idf.yaml diff --git a/esphome/components/esp32_rmt/__init__.py b/esphome/components/esp32_rmt/__init__.py index 272c7c81ba..1076bcabdc 100644 --- a/esphome/components/esp32_rmt/__init__.py +++ b/esphome/components/esp32_rmt/__init__.py @@ -1,8 +1,30 @@ from esphome.components import esp32 import esphome.config_validation as cv +from esphome.core import CORE CODEOWNERS = ["@jesserockz"] +VARIANTS_NO_RMT = {esp32.VARIANT_ESP32C2, esp32.VARIANT_ESP32C61} + + +def validate_rmt_not_supported(rmt_only_keys): + """Validate that RMT-only config keys are not used on variants without RMT hardware.""" + rmt_only_keys = set(rmt_only_keys) + + def _validator(config): + if CORE.is_esp32: + variant = esp32.get_esp32_variant() + if variant in VARIANTS_NO_RMT: + unsupported = rmt_only_keys.intersection(config) + if unsupported: + keys = ", ".join(sorted(f"'{k}'" for k in unsupported)) + raise cv.Invalid( + f"{keys} not available on {variant} (no RMT hardware)" + ) + return config + + return _validator + def validate_clock_resolution(): def _validator(value): diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 6d41f6b5b8..1c6943b003 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -3,7 +3,7 @@ import logging from esphome import pins import esphome.codegen as cg -from esphome.components import esp32, light +from esphome.components import esp32, esp32_rmt, light from esphome.components.const import CONF_USE_PSRAM from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv @@ -71,6 +71,10 @@ CONF_RESET_LOW = "reset_low" CONFIG_SCHEMA = cv.All( + esp32.only_on_variant( + unsupported=list(esp32_rmt.VARIANTS_NO_RMT), + msg_prefix="ESP32 RMT LED strip", + ), light.ADDRESSABLE_LIGHT_SCHEMA.extend( { cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput), diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 0cac28506f..d73fff2b0a 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -119,6 +119,8 @@ class RemoteComponentBase { }; #ifdef USE_ESP32 +#include +#if SOC_RMT_SUPPORTED class RemoteRMTChannel { public: void set_clock_resolution(uint32_t clock_resolution) { this->clock_resolution_ = clock_resolution; } @@ -137,7 +139,8 @@ class RemoteRMTChannel { uint32_t clock_resolution_{1000000}; uint32_t rmt_symbols_; }; -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 class RemoteTransmitterBase : public RemoteComponentBase { public: diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index b3dc213c5f..362f6e99db 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -65,6 +65,8 @@ RemoteReceiverComponent = remote_receiver_ns.class_( def validate_config(config): if CORE.is_esp32: variant = esp32.get_esp32_variant() + if variant in esp32_rmt.VARIANTS_NO_RMT: + return config if variant in (esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S2): max_idle = 65535 else: @@ -110,6 +112,8 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.SplitDefault( CONF_BUFFER_SIZE, esp32="10000b", + esp32_c2="1000b", + esp32_c61="1000b", esp8266="1000b", bk72xx="1000b", ln882x="1000b", @@ -131,9 +135,11 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.SplitDefault( CONF_RMT_SYMBOLS, esp32=192, + esp32_c2=cv.UNDEFINED, esp32_c3=96, esp32_c5=96, esp32_c6=96, + esp32_c61=cv.UNDEFINED, esp32_h2=96, esp32_p4=192, esp32_s2=192, @@ -145,6 +151,8 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.SplitDefault( CONF_RECEIVE_SYMBOLS, esp32=192, + esp32_c2=cv.UNDEFINED, + esp32_c61=cv.UNDEFINED, ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), cv.Optional(CONF_USE_DMA): cv.All( esp32.only_on_variant( @@ -152,24 +160,45 @@ CONFIG_SCHEMA = remote_base.validate_triggers( ), cv.boolean, ), - cv.SplitDefault(CONF_CARRIER_DUTY_PERCENT, esp32=100): cv.All( + cv.SplitDefault( + CONF_CARRIER_DUTY_PERCENT, + esp32=100, + esp32_c2=cv.UNDEFINED, + esp32_c61=cv.UNDEFINED, + ): cv.All( cv.only_on_esp32, cv.percentage_int, cv.Range(min=1, max=100), ), - cv.SplitDefault(CONF_CARRIER_FREQUENCY, esp32="0Hz"): cv.All( - cv.only_on_esp32, cv.frequency, cv.int_ - ), + cv.SplitDefault( + CONF_CARRIER_FREQUENCY, + esp32="0Hz", + esp32_c2=cv.UNDEFINED, + esp32_c61=cv.UNDEFINED, + ): cv.All(cv.only_on_esp32, cv.frequency, cv.int_), } ) .extend(cv.COMPONENT_SCHEMA) + .add_extra( + esp32_rmt.validate_rmt_not_supported( + [ + CONF_CLOCK_RESOLUTION, + CONF_USE_DMA, + CONF_RMT_SYMBOLS, + CONF_FILTER_SYMBOLS, + CONF_RECEIVE_SYMBOLS, + CONF_CARRIER_DUTY_PERCENT, + CONF_CARRIER_FREQUENCY, + ] + ) + ) .add_extra(validate_config) ) async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) - if CORE.is_esp32: + if CORE.is_esp32 and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT: # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) esp32.include_builtin_idf_component("esp_driver_rmt") @@ -213,6 +242,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_IDF, }, "remote_receiver.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, diff --git a/esphome/components/remote_receiver/remote_receiver.cpp b/esphome/components/remote_receiver/remote_receiver.cpp index de47457dac..d59ee63695 100644 --- a/esphome/components/remote_receiver/remote_receiver.cpp +++ b/esphome/components/remote_receiver/remote_receiver.cpp @@ -3,7 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) namespace esphome::remote_receiver { diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 3d9199a904..5da9283a6e 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -6,12 +6,15 @@ #include #if defined(USE_ESP32) +#include +#if SOC_RMT_SUPPORTED #include -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 namespace esphome::remote_receiver { -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) struct RemoteReceiverComponentStore { static void gpio_intr(RemoteReceiverComponentStore *arg); @@ -35,7 +38,7 @@ struct RemoteReceiverComponentStore { volatile bool prev_level{false}; volatile bool overflow{false}; }; -#elif defined(USE_ESP32) +#elif defined(USE_ESP32) && SOC_RMT_SUPPORTED struct RemoteReceiverComponentStore { /// Stores RMT symbols and rx done event data volatile uint8_t *buffer{nullptr}; @@ -54,7 +57,7 @@ struct RemoteReceiverComponentStore { class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, public Component -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED , public remote_base::RemoteRMTChannel #endif @@ -66,7 +69,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, void dump_config() override; void loop() override; -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void set_filter_symbols(uint32_t filter_symbols) { this->filter_symbols_ = filter_symbols; } void set_receive_symbols(uint32_t receive_symbols) { this->receive_symbols_ = receive_symbols; } void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } @@ -78,7 +81,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, void set_idle_us(uint32_t idle_us) { this->idle_us_ = idle_us; } protected: -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void decode_rmt_(rmt_symbol_word_t *item, size_t item_count); rmt_channel_handle_t channel_{NULL}; uint32_t filter_symbols_{0}; @@ -94,7 +97,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, RemoteReceiverComponentStore store_; #endif -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) HighFrequencyLoopRequester high_freq_; #endif diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index f95244ea45..357a36d052 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -2,6 +2,8 @@ #include "esphome/core/log.h" #ifdef USE_ESP32 +#include +#if SOC_RMT_SUPPORTED #include #include @@ -248,4 +250,5 @@ void RemoteReceiverComponent::decode_rmt_(rmt_symbol_word_t *item, size_t item_c } // namespace esphome::remote_receiver -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index 8383b9dd75..fc772f88b2 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -40,45 +40,66 @@ DigitalWriteAction = remote_transmitter_ns.class_( cg.Parented.template(RemoteTransmitterComponent), ) + MULTI_CONF = True -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(RemoteTransmitterComponent), - cv.Required(CONF_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_CARRIER_DUTY_PERCENT): cv.All( - cv.percentage_int, cv.Range(min=1, max=100) - ), - cv.Optional(CONF_CLOCK_RESOLUTION): cv.All( - cv.only_on_esp32, - esp32_rmt.validate_clock_resolution(), - ), - cv.Optional(CONF_EOT_LEVEL): cv.All(cv.only_on_esp32, cv.boolean), - cv.Optional(CONF_USE_DMA): cv.All( - esp32.only_on_variant( - supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3] +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(RemoteTransmitterComponent), + cv.Required(CONF_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CARRIER_DUTY_PERCENT): cv.All( + cv.percentage_int, cv.Range(min=1, max=100) ), - cv.boolean, - ), - cv.SplitDefault( - CONF_RMT_SYMBOLS, - esp32=64, - esp32_c3=48, - esp32_c5=48, - esp32_c6=48, - esp32_h2=48, - esp32_p4=48, - esp32_s2=64, - esp32_s3=48, - ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), - cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean), - cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), - cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), - } -).extend(cv.COMPONENT_SCHEMA) + cv.Optional(CONF_CLOCK_RESOLUTION): cv.All( + cv.only_on_esp32, + esp32_rmt.validate_clock_resolution(), + ), + cv.Optional(CONF_EOT_LEVEL): cv.All(cv.only_on_esp32, cv.boolean), + cv.Optional(CONF_USE_DMA): cv.All( + esp32.only_on_variant( + supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3] + ), + cv.boolean, + ), + cv.SplitDefault( + CONF_RMT_SYMBOLS, + esp32=64, + esp32_c2=cv.UNDEFINED, + esp32_c3=48, + esp32_c5=48, + esp32_c6=48, + esp32_c61=cv.UNDEFINED, + esp32_h2=48, + esp32_p4=48, + esp32_s2=64, + esp32_s3=48, + ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), + cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean), + cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), + cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .add_extra( + esp32_rmt.validate_rmt_not_supported( + [ + CONF_CLOCK_RESOLUTION, + CONF_EOT_LEVEL, + CONF_USE_DMA, + CONF_RMT_SYMBOLS, + CONF_NON_BLOCKING, + ] + ) + ) +) def _validate_non_blocking(config): - if CORE.is_esp32 and CONF_NON_BLOCKING not in config: + if ( + CORE.is_esp32 + and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT + and CONF_NON_BLOCKING not in config + ): _LOGGER.warning( "'non_blocking' is not set for 'remote_transmitter' and will default to 'true'.\n" "The default behavior changed in 2025.11.0; previously blocking mode was used.\n" @@ -111,7 +132,7 @@ async def digital_write_action_to_code(config, action_id, template_arg, args): async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) - if CORE.is_esp32: + if CORE.is_esp32 and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT: # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) esp32.include_builtin_idf_component("esp_driver_rmt") @@ -155,6 +176,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_IDF, }, "remote_transmitter.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, diff --git a/esphome/components/remote_transmitter/automation.h b/esphome/components/remote_transmitter/automation.h index bee1d0be8a..8da4cfd95d 100644 --- a/esphome/components/remote_transmitter/automation.h +++ b/esphome/components/remote_transmitter/automation.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { template class DigitalWriteAction : public Action, public Parented { public: @@ -14,5 +13,4 @@ template class DigitalWriteAction : public Action, public void play(const Ts &...x) override { this->parent_->digital_write(this->value_.value(x...)); } }; -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter.cpp b/esphome/components/remote_transmitter/remote_transmitter.cpp index d35541e2e1..51a3c0b1d4 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter.cpp @@ -2,10 +2,9 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { static const char *const TAG = "remote_transmitter"; @@ -105,7 +104,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen this->complete_trigger_.trigger(); } -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter #endif diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index 65bd2ac8b2..aee52ea170 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -6,13 +6,15 @@ #include #if defined(USE_ESP32) +#include +#if SOC_RMT_SUPPORTED #include -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) // IDF version 5.5.1 and above is required because of a bug in // the RMT encoder: https://github.com/espressif/esp-idf/issues/17244 @@ -33,7 +35,7 @@ struct RemoteTransmitterComponentStore { class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, public Component -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED , public remote_base::RemoteRMTChannel #endif @@ -51,7 +53,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, void digital_write(bool value); -#if defined(USE_ESP32) +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; } void set_non_blocking(bool non_blocking) { this->non_blocking_ = non_blocking; } @@ -62,7 +64,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, protected: void send_internal(uint32_t send_times, uint32_t send_wait) override; -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) void calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, uint32_t *off_time_period); void mark_(uint32_t on_time, uint32_t off_time, uint32_t usec); @@ -73,7 +75,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, uint32_t target_time_; #endif -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void configure_rmt_(); void wait_for_rmt_(); @@ -100,5 +102,4 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, Trigger<> complete_trigger_; }; -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index 89d97895b2..71773e3ddf 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -3,10 +3,11 @@ #include "esphome/core/application.h" #ifdef USE_ESP32 +#include +#if SOC_RMT_SUPPORTED #include -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { static const char *const TAG = "remote_transmitter"; @@ -358,7 +359,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen } #endif -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 diff --git a/tests/components/remote_receiver/test.esp32-c2-idf.yaml b/tests/components/remote_receiver/test.esp32-c2-idf.yaml new file mode 100644 index 0000000000..87154e19fc --- /dev/null +++ b/tests/components/remote_receiver/test.esp32-c2-idf.yaml @@ -0,0 +1,12 @@ +remote_receiver: + id: rcvr + pin: GPIO2 + dump: all + <<: !include common-actions.yaml + +binary_sensor: + - platform: remote_receiver + name: Panasonic Remote Input + panasonic: + address: 0x4004 + command: 0x100BCBD diff --git a/tests/components/remote_transmitter/test.esp32-c2-idf.yaml b/tests/components/remote_transmitter/test.esp32-c2-idf.yaml new file mode 100644 index 0000000000..424cd8d249 --- /dev/null +++ b/tests/components/remote_transmitter/test.esp32-c2-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + id: xmitr + pin: GPIO2 + carrier_duty_percent: 50% + +packages: + buttons: !include common-buttons.yaml From 15da6d0a0b3a7ec14462e49c00f329af3808ce7b Mon Sep 17 00:00:00 2001 From: Pawelo <81100874+pgolawsk@users.noreply.github.com> Date: Sun, 15 Feb 2026 21:58:51 +0100 Subject: [PATCH 029/609] [epaper_spi] Add WeAct 3-color e-paper display support (#13894) --- esphome/components/epaper_spi/display.py | 4 - .../components/epaper_spi/epaper_weact_3c.cpp | 231 ++++++++++++++++++ .../components/epaper_spi/epaper_weact_3c.h | 39 +++ .../components/epaper_spi/models/weact_bwr.py | 75 ++++++ .../epaper_spi/test.esp32-s3-idf.yaml | 63 +++++ 5 files changed, 408 insertions(+), 4 deletions(-) create mode 100644 esphome/components/epaper_spi/epaper_weact_3c.cpp create mode 100644 esphome/components/epaper_spi/epaper_weact_3c.h create mode 100644 esphome/components/epaper_spi/models/weact_bwr.py diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 13f66691b2..2657071f45 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -49,10 +49,6 @@ EPaperBase = epaper_spi_ns.class_( ) Transform = epaper_spi_ns.enum("Transform") -EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase) -EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6) - - # Import all models dynamically from the models package for module_info in pkgutil.iter_modules(models.__path__): importlib.import_module(f".models.{module_info.name}", package=__package__) diff --git a/esphome/components/epaper_spi/epaper_weact_3c.cpp b/esphome/components/epaper_spi/epaper_weact_3c.cpp new file mode 100644 index 0000000000..bd83105dd7 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_weact_3c.cpp @@ -0,0 +1,231 @@ +#include "epaper_weact_3c.h" +#include "esphome/core/log.h" + +namespace esphome::epaper_spi { + +static constexpr const char *const TAG = "epaper_weact_3c"; + +// SSD1680 3-color display notes: +// - Buffer uses 1 bit per pixel, 8 pixels per byte +// - Buffer first half (black_offset): Black/White plane (1=black, 0=white) +// - Buffer second half (red_offset): Red plane (1=red, 0=no red) +// - Total buffer: width * height / 4 bytes = 2 * (width * height / 8) +// - For 128x296: 128*296/4 = 9472 bytes total (4736 per color) + +void EPaperWeAct3C::draw_pixel_at(int x, int y, Color color) { + if (!this->rotate_coordinates_(x, y)) + return; + + // Calculate position in the 1-bit buffer + const uint32_t pos = (x / 8) + (y * this->row_width_); + const uint8_t bit = 0x80 >> (x & 0x07); + const uint32_t red_offset = this->buffer_length_ / 2u; + + // Use luminance threshold for B/W mapping + // Split at halfway point (382 = (255*3)/2) + bool is_white = (static_cast(color.r) + color.g + color.b) > 382; + + // Update black/white plane (first half of buffer) + if (is_white) { + // White pixel - clear bit in black plane + this->buffer_[pos] &= ~bit; + } else { + // Black pixel - set bit in black plane + this->buffer_[pos] |= bit; + } + + // Update red plane (second half of buffer) + // Red if red component is dominant (r > g+b) + if (color.r > color.g + color.b) { + // Red pixel - set bit in red plane + this->buffer_[red_offset + pos] |= bit; + } else { + // Not red - clear bit in red plane + this->buffer_[red_offset + pos] &= ~bit; + } +} + +void EPaperWeAct3C::fill(Color color) { + // For 3-color e-paper with 1-bit buffer format: + // - Black buffer: 1=black, 0=white + // - Red buffer: 1=red, 0=no red + // The buffer is stored as two halves: [black plane][red plane] + const size_t half_buffer = this->buffer_length_ / 2u; + + // Use luminance threshold for B/W mapping + bool is_white = (static_cast(color.r) + color.g + color.b) > 382; + bool is_red = color.r > color.g + color.b; + + // Fill both planes + if (is_white) { + // White - both planes = 0x00 + this->buffer_.fill(0x00); + } else if (is_red) { + // Red - black plane = 0x00, red plane = 0xFF + for (size_t i = 0; i < half_buffer; i++) + this->buffer_[i] = 0x00; + for (size_t i = 0; i < half_buffer; i++) + this->buffer_[half_buffer + i] = 0xFF; + } else { + // Black - black plane = 0xFF, red plane = 0x00 + for (size_t i = 0; i < half_buffer; i++) + this->buffer_[i] = 0xFF; + for (size_t i = 0; i < half_buffer; i++) + this->buffer_[half_buffer + i] = 0x00; + } +} + +void EPaperWeAct3C::clear() { + // Clear buffer to white, just like real paper. + this->fill(COLOR_ON); +} + +void EPaperWeAct3C::set_window_() { + // For full screen refresh, we always start from (0,0) + // The y_low_/y_high_ values track the dirty region for optimization, + // but for display refresh we need to write from the beginning + uint16_t x_start = 0; + uint16_t x_end = this->width_ - 1; + uint16_t y_start = 0; + uint16_t y_end = this->height_ - 1; // height = 296 for 2.9" display + + // Set RAM X address boundaries (0x44) + // X coordinates are byte-aligned (divided by 8) + this->cmd_data(0x44, {(uint8_t) (x_start / 8), (uint8_t) (x_end / 8)}); + + // Set RAM Y address boundaries (0x45) + // Format: Y start (LSB, MSB), Y end (LSB, MSB) + this->cmd_data(0x45, {(uint8_t) y_start, (uint8_t) (y_start >> 8), (uint8_t) (y_end & 0xFF), (uint8_t) (y_end >> 8)}); + + // Reset RAM X counter to start (0x4E) - 1 byte + this->cmd_data(0x4E, {(uint8_t) (x_start / 8)}); + + // Reset RAM Y counter to start (0x4F) - 2 bytes (LSB, MSB) + this->cmd_data(0x4F, {(uint8_t) y_start, (uint8_t) (y_start >> 8)}); +} + +bool HOT EPaperWeAct3C::transfer_data() { + const uint32_t start_time = millis(); + const size_t buffer_length = this->buffer_length_; + const size_t half_buffer = buffer_length / 2u; + + ESP_LOGV(TAG, "transfer_data: buffer_length=%u, half_buffer=%u", buffer_length, half_buffer); + + // Use a local buffer for SPI transfers + static constexpr size_t MAX_TRANSFER_SIZE = 128; + uint8_t bytes_to_send[MAX_TRANSFER_SIZE]; + + // First, send the RED buffer (0x26 = WRITE_COLOR) + // The red plane is in the second half of our buffer + // NOTE: Must set RAM window first to reset address counters! + if (this->current_data_index_ < half_buffer) { + if (this->current_data_index_ == 0) { + ESP_LOGV(TAG, "transfer_data: sending RED buffer (0x26)"); + this->set_window_(); // Reset RAM X/Y counters to start position + this->command(0x26); + } + + this->start_data_(); + size_t red_offset = half_buffer; + while (this->current_data_index_ < half_buffer) { + size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, half_buffer - this->current_data_index_); + + for (size_t i = 0; i < bytes_to_copy; i++) { + bytes_to_send[i] = this->buffer_[red_offset + this->current_data_index_ + i]; + } + + this->write_array(bytes_to_send, bytes_to_copy); + + this->current_data_index_ += bytes_to_copy; + + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + this->disable(); + return false; + } + } + this->disable(); + } + + // Finished the red buffer, now send the BLACK buffer (0x24 = WRITE_BLACK) + // The black plane is in the first half of our buffer + if (this->current_data_index_ < buffer_length) { + if (this->current_data_index_ == half_buffer) { + ESP_LOGV(TAG, "transfer_data: finished red buffer, sending BLACK buffer (0x24)"); + + // Do NOT reset RAM counters here for WeAct displays (Reference implementation behavior) + // this->set_window(); + this->command(0x24); + // Continue using current_data_index_, but we need to map it to the start of the buffer + } + + this->start_data_(); + while (this->current_data_index_ < buffer_length) { + size_t remaining = buffer_length - this->current_data_index_; + size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, remaining); + + // Calculate offset into the BLACK buffer (which is at the start of this->buffer_) + // current_data_index_ goes from half_buffer to buffer_length + size_t buffer_offset = this->current_data_index_ - half_buffer; + + for (size_t i = 0; i < bytes_to_copy; i++) { + bytes_to_send[i] = this->buffer_[buffer_offset + i]; + } + + this->write_array(bytes_to_send, bytes_to_copy); + + this->current_data_index_ += bytes_to_copy; + + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + this->disable(); + return false; + } + } + this->disable(); + } + + this->current_data_index_ = 0; + ESP_LOGV(TAG, "transfer_data: completed (red=%u, black=%u bytes)", half_buffer, half_buffer); + return true; +} + +void EPaperWeAct3C::refresh_screen(bool partial) { + // SSD1680 refresh sequence: + // Reset RAM X/Y address counters to 0,0 so display reads from start + // 0x4E: RAM X counter - 1 byte (X / 8) + // 0x4F: RAM Y counter - 2 bytes (Y LSB, Y MSB) + this->cmd_data(0x4E, {0x00}); // RAM X counter = 0 (1 byte) + this->cmd_data(0x4F, {0x00, 0x00}); // RAM Y counter = 0 (2 bytes) + + // Send UPDATE_FULL command (0x22) with display update control parameter + // Both WeAct and waveshare reference use 0xF7: {0x22, 0xF7} + // 0xF7 = Display update: Load temperature, Load LUT, Enable RAM content + this->cmd_data(0x22, {0xF7}); // Command 0x22 with parameter 0xF7 + this->command(0x20); // Activate display update + + // COMMAND TERMINATE FRAME READ WRITE (required by SSD1680) + // Removed 0xFF based on working reference implementation + // this->command(0xFF); +} + +void EPaperWeAct3C::power_on() { + // Power on sequence - send command to turn on power + // According to SSD1680 spec: 0x22, 0xF8 powers on the display + this->cmd_data(0x22, {0xF8}); // Power on + this->command(0x20); // Activate +} + +void EPaperWeAct3C::power_off() { + // Power off sequence - send command to turn off power + // According to SSD1680 spec: 0x22, 0x83 powers off the display + this->cmd_data(0x22, {0x83}); // Power off + this->command(0x20); // Activate +} + +void EPaperWeAct3C::deep_sleep() { + // Deep sleep sequence + this->cmd_data(0x10, {0x01}); // Deep sleep mode +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_weact_3c.h b/esphome/components/epaper_spi/epaper_weact_3c.h new file mode 100644 index 0000000000..2df6f1ba09 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_weact_3c.h @@ -0,0 +1,39 @@ +#pragma once + +#include "epaper_spi.h" + +namespace esphome::epaper_spi { + +/** + * WeAct 3-color e-paper displays (SSD1683 controller). + * Supports multiple sizes: 2.9" (128x296), 4.2" (400x300), etc. + * + * Color scheme: Black, White, Red (BWR) + * Buffer layout: 1 bit per pixel, separate planes + * - Buffer first half: Black/White plane (1=black, 0=white) + * - Buffer second half: Red plane (1=red, 0=no red) + * - Total buffer: width * height / 4 bytes (2 * width * height / 8) + */ +class EPaperWeAct3C : public EPaperBase { + public: + EPaperWeAct3C(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length) + : EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) { + this->buffer_length_ = this->row_width_ * height * 2; + } + + void fill(Color color) override; + void clear() override; + + protected: + void set_window_(); + void refresh_screen(bool partial) override; + void power_on() override; + void power_off() override; + void deep_sleep() override; + void draw_pixel_at(int x, int y, Color color) override; + + bool transfer_data() override; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/models/weact_bwr.py b/esphome/components/epaper_spi/models/weact_bwr.py new file mode 100644 index 0000000000..21a015b08d --- /dev/null +++ b/esphome/components/epaper_spi/models/weact_bwr.py @@ -0,0 +1,75 @@ +"""WeAct Black/White/Red e-paper displays using SSD1683 controller. + +Supported models: +- weact-2.13in-3c: 122x250 pixels (2.13" display) +- weact-2.9in-3c: 128x296 pixels (2.9" display) +- weact-4.2in-3c: 400x300 pixels (4.2" display) + +These displays use SSD1680 or SSD1683 controller and require a specific initialization +sequence. The DRV_OUT_CTL command is calculated from the display height. +""" + +from . import EpaperModel + + +class WeActBWR(EpaperModel): + """Base EpaperModel class for WeAct Black/White/Red displays using SSD1683 controller.""" + + def __init__(self, name, **defaults): + super().__init__(name, "EPaperWeAct3C", **defaults) + + def get_init_sequence(self, config): + """Generate initialization sequence for WeAct BWR displays. + + The initialization sequence is based on SSD1680 and SSD1683 controller datasheet + and the WeAct display specifications. + """ + _, height = self.get_dimensions(config) + # DRV_OUT_CTL: MSB of (height-1), LSB of (height-1), gate setting (0x00) + height_minus_1 = height - 1 + msb = height_minus_1 >> 8 + lsb = height_minus_1 & 0xFF + return ( + # Step 1: Software Reset (0x12) - REQUIRED per SSD1680, but works without it as well, so it's commented out for now + # (0x12,), + # Step 2: Wait 10ms after SWRESET (?) not sure how to implement wht waiting for 10ms after SWRESET, so it's commented out for now + # Step 3: DRV_OUT_CTL - driver output control (height-dependent) + # Format: (command, LSB, MSB, gate setting) + (0x01, lsb, msb, 0x00), + # Step 4: DATA_ENTRY - data entry mode (0x03 = decrement Y, increment X) + (0x11, 0x03), + # Step 5: BORDER_FULL - border waveform control + (0x3C, 0x05), + # Step 6: TEMP_SENS - internal temperature sensor + (0x18, 0x80), + # Step 7: DISPLAY_UPDATE - display update control + (0x21, 0x00, 0x80), + ) + + +# Model: WeAct 2.9" 3C - 128x296 pixels, SSD1680 controller +weact_2p9in3c = WeActBWR( + "weact-2.9in-3c", + width=128, + height=296, + data_rate="10MHz", + minimum_update_interval="1s", +) + +# Model: WeAct 2.13" 3C - 122x250 pixels, SSD1680 controller +weact_2p13in3c = WeActBWR( + "weact-2.13in-3c", + width=122, + height=250, + data_rate="10MHz", + minimum_update_interval="1s", +) + +# Model: WeAct 4.2" 3C - 400x300 pixels, SSD1683 controller +weact_4p2in3c = WeActBWR( + "weact-4.2in-3c", + width=400, + height=300, + data_rate="10MHz", + minimum_update_interval="10s", +) diff --git a/tests/components/epaper_spi/test.esp32-s3-idf.yaml b/tests/components/epaper_spi/test.esp32-s3-idf.yaml index 333ab567cd..aa454c73fa 100644 --- a/tests/components/epaper_spi/test.esp32-s3-idf.yaml +++ b/tests/components/epaper_spi/test.esp32-s3-idf.yaml @@ -64,3 +64,66 @@ display: # Override pins to avoid conflict with other display configs busy_pin: 43 dc_pin: 42 + + # WeAct 2.13" 3-color e-paper (122x250, SSD1680) + - platform: epaper_spi + spi_id: spi_bus + model: weact-2.13in-3c + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO17 + reset_pin: + allow_other_uses: true + number: GPIO16 + busy_pin: + allow_other_uses: true + number: GPIO4 + lambda: |- + it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE); + it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK); + it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0)); + + # WeAct 2.9" 3-color e-paper (128x296, SSD1683) + - platform: epaper_spi + spi_id: spi_bus + model: weact-2.9in-3c + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO17 + reset_pin: + allow_other_uses: true + number: GPIO16 + busy_pin: + allow_other_uses: true + number: GPIO4 + lambda: |- + it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE); + it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK); + it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0)); + + # WeAct 4.2" 3-color e-paper (400x300, SSD1683) + - platform: epaper_spi + spi_id: spi_bus + model: weact-4.2in-3c + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO17 + reset_pin: + allow_other_uses: true + number: GPIO16 + busy_pin: + allow_other_uses: true + number: GPIO4 + lambda: |- + it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE); + it.circle(it.get_width() / 2, it.get_height() / 2, 30, Color::BLACK); + it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color(255, 0, 0)); From 066419019fb9997522788e940adbbf27243d5e6e Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Sun, 15 Feb 2026 15:09:35 -0600 Subject: [PATCH 030/609] [audio] Support reallocating non-empty AudioTransferBuffer (#13979) --- .../audio/audio_transfer_buffer.cpp | 34 +++++++++++++++---- .../components/audio/audio_transfer_buffer.h | 3 ++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/esphome/components/audio/audio_transfer_buffer.cpp b/esphome/components/audio/audio_transfer_buffer.cpp index 790cd62db0..ddb669e0eb 100644 --- a/esphome/components/audio/audio_transfer_buffer.cpp +++ b/esphome/components/audio/audio_transfer_buffer.cpp @@ -2,6 +2,8 @@ #ifdef USE_ESP32 +#include + #include "esphome/core/helpers.h" namespace esphome { @@ -75,12 +77,32 @@ bool AudioTransferBuffer::has_buffered_data() const { } bool AudioTransferBuffer::reallocate(size_t new_buffer_size) { - if (this->buffer_length_ > 0) { - // Buffer currently has data, so reallocation is impossible + if (this->buffer_ == nullptr) { + return this->allocate_buffer_(new_buffer_size); + } + + if (new_buffer_size < this->buffer_length_) { + // New size is too small to hold existing data return false; } - this->deallocate_buffer_(); - return this->allocate_buffer_(new_buffer_size); + + // Shift existing data to the start of the buffer so realloc preserves it + if ((this->buffer_length_ > 0) && (this->data_start_ != this->buffer_)) { + std::memmove(this->buffer_, this->data_start_, this->buffer_length_); + this->data_start_ = this->buffer_; + } + + RAMAllocator allocator; + uint8_t *new_buffer = allocator.reallocate(this->buffer_, new_buffer_size); + if (new_buffer == nullptr) { + // Reallocation failed, but the original buffer is still valid + return false; + } + + this->buffer_ = new_buffer; + this->data_start_ = this->buffer_; + this->buffer_size_ = new_buffer_size; + return true; } bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) { @@ -115,7 +137,7 @@ size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_ if (pre_shift) { // Shift data in buffer to start if (this->buffer_length_ > 0) { - memmove(this->buffer_, this->data_start_, this->buffer_length_); + std::memmove(this->buffer_, this->data_start_, this->buffer_length_); } this->data_start_ = this->buffer_; } @@ -150,7 +172,7 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait, if (post_shift) { // Shift unwritten data to the start of the buffer - memmove(this->buffer_, this->data_start_, this->buffer_length_); + std::memmove(this->buffer_, this->data_start_, this->buffer_length_); this->data_start_ = this->buffer_; } diff --git a/esphome/components/audio/audio_transfer_buffer.h b/esphome/components/audio/audio_transfer_buffer.h index edb484e7d2..24c0670d1a 100644 --- a/esphome/components/audio/audio_transfer_buffer.h +++ b/esphome/components/audio/audio_transfer_buffer.h @@ -56,6 +56,9 @@ class AudioTransferBuffer { /// @return True if there is data, false otherwise. virtual bool has_buffered_data() const; + /// @brief Reallocates the transfer buffer, preserving any existing data. + /// @param new_buffer_size The new size in bytes. Must be at least as large as available(). + /// @return True if successful, false otherwise. On failure, the original buffer remains valid. bool reallocate(size_t new_buffer_size); protected: From f2cb5db9e0dd543d33dd45e561412adf3c616f0e Mon Sep 17 00:00:00 2001 From: "Cornelius A. Ludmann" Date: Mon, 16 Feb 2026 03:44:30 +0100 Subject: [PATCH 031/609] [epaper_spi] Add Waveshare 7.5in e-Paper (H) (#13991) --- .../components/epaper_spi/models/jd79660.py | 32 +++++++++++++++++++ .../epaper_spi/test.esp32-s3-idf.yaml | 17 ++++++++++ 2 files changed, 49 insertions(+) diff --git a/esphome/components/epaper_spi/models/jd79660.py b/esphome/components/epaper_spi/models/jd79660.py index 2d8830ebd2..a0457c5812 100644 --- a/esphome/components/epaper_spi/models/jd79660.py +++ b/esphome/components/epaper_spi/models/jd79660.py @@ -84,3 +84,35 @@ jd79660.extend( (0xA5, 0x00,), ), ) + +# Waveshare 7.5-H +# +# Vendor init derived from vendor sample code +# +# Compatible MIT license, see esphome/LICENSE file. +# +# Note: busy pin uses LOW=busy, HIGH=idle. Configure with inverted: true in YAML. +# +# fmt: off +jd79660.extend( + "Waveshare-7.5in-H", + width=800, + height=480, + + initsequence=( + (0x00, 0x0F, 0x29,), + (0x06, 0x0F, 0x8B, 0x93, 0xA1,), + (0x41, 0x00,), + (0x50, 0x37,), + (0x60, 0x02, 0x02,), + (0x61, 800 // 256, 800 % 256, 480 // 256, 480 % 256,), # RES: 800x480 + (0x62, 0x98, 0x98, 0x98, 0x75, 0xCA, 0xB2, 0x98, 0x7E,), + (0x65, 0x00, 0x00, 0x00, 0x00,), + (0xE7, 0x1C,), + (0xE3, 0x00,), + (0xE9, 0x01,), + (0x30, 0x08,), + # Power On (0x04): Must be early part of init seq = Disabled later! + (0x04,), + ), +) diff --git a/tests/components/epaper_spi/test.esp32-s3-idf.yaml b/tests/components/epaper_spi/test.esp32-s3-idf.yaml index aa454c73fa..9593d0f6f0 100644 --- a/tests/components/epaper_spi/test.esp32-s3-idf.yaml +++ b/tests/components/epaper_spi/test.esp32-s3-idf.yaml @@ -57,6 +57,23 @@ display: allow_other_uses: true number: GPIO4 + - platform: epaper_spi + spi_id: spi_bus + model: waveshare-7.5in-H + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO17 + reset_pin: + allow_other_uses: true + number: GPIO16 + busy_pin: + allow_other_uses: true + number: GPIO4 + inverted: true + - platform: epaper_spi model: seeed-reterminal-e1002 - platform: epaper_spi From f2c827f9a2395ac34144bddbb3f9a3edd4c43bd0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Feb 2026 08:08:43 -0600 Subject: [PATCH 032/609] [runtime_image] Remove stored RAMAllocator member (#13998) --- esphome/components/runtime_image/png_decoder.cpp | 6 ++++-- esphome/components/runtime_image/png_decoder.h | 1 - esphome/components/runtime_image/runtime_image.cpp | 8 +++++--- esphome/components/runtime_image/runtime_image.h | 1 - 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/runtime_image/png_decoder.cpp b/esphome/components/runtime_image/png_decoder.cpp index 9fe4a9c4ff..591504328d 100644 --- a/esphome/components/runtime_image/png_decoder.cpp +++ b/esphome/components/runtime_image/png_decoder.cpp @@ -50,7 +50,8 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui PngDecoder::PngDecoder(RuntimeImage *image) : ImageDecoder(image) { { - pngle_t *pngle = this->allocator_.allocate(1, PNGLE_T_SIZE); + RAMAllocator allocator; + pngle_t *pngle = allocator.allocate(1, PNGLE_T_SIZE); if (!pngle) { ESP_LOGE(TAG, "Failed to allocate memory for PNGLE engine!"); return; @@ -64,7 +65,8 @@ PngDecoder::PngDecoder(RuntimeImage *image) : ImageDecoder(image) { PngDecoder::~PngDecoder() { if (this->pngle_) { pngle_reset(this->pngle_); - this->allocator_.deallocate(this->pngle_, PNGLE_T_SIZE); + RAMAllocator allocator; + allocator.deallocate(this->pngle_, PNGLE_T_SIZE); } } diff --git a/esphome/components/runtime_image/png_decoder.h b/esphome/components/runtime_image/png_decoder.h index b5c1e70c2a..24521d33a8 100644 --- a/esphome/components/runtime_image/png_decoder.h +++ b/esphome/components/runtime_image/png_decoder.h @@ -29,7 +29,6 @@ class PngDecoder : public ImageDecoder { uint32_t get_pixels_decoded() const { return this->pixels_decoded_; } protected: - RAMAllocator allocator_; pngle_t *pngle_{nullptr}; uint32_t pixels_decoded_{0}; }; diff --git a/esphome/components/runtime_image/runtime_image.cpp b/esphome/components/runtime_image/runtime_image.cpp index 1d70f38d6b..603ea76f01 100644 --- a/esphome/components/runtime_image/runtime_image.cpp +++ b/esphome/components/runtime_image/runtime_image.cpp @@ -230,7 +230,8 @@ void RuntimeImage::release() { void RuntimeImage::release_buffer_() { if (this->buffer_) { ESP_LOGV(TAG, "Releasing buffer of size %zu", this->get_buffer_size_(this->buffer_width_, this->buffer_height_)); - this->allocator_.deallocate(this->buffer_, this->get_buffer_size_(this->buffer_width_, this->buffer_height_)); + RAMAllocator allocator; + allocator.deallocate(this->buffer_, this->get_buffer_size_(this->buffer_width_, this->buffer_height_)); this->buffer_ = nullptr; this->data_start_ = nullptr; this->width_ = 0; @@ -254,11 +255,12 @@ size_t RuntimeImage::resize_buffer_(int width, int height) { } ESP_LOGD(TAG, "Allocating buffer: %dx%d, %zu bytes", width, height, new_size); - this->buffer_ = this->allocator_.allocate(new_size); + RAMAllocator allocator; + this->buffer_ = allocator.allocate(new_size); if (!this->buffer_) { ESP_LOGE(TAG, "Failed to allocate %zu bytes. Largest free block: %zu", new_size, - this->allocator_.get_max_free_block_size()); + allocator.get_max_free_block_size()); return 0; } diff --git a/esphome/components/runtime_image/runtime_image.h b/esphome/components/runtime_image/runtime_image.h index 0a5279d86d..4bdcdcac9e 100644 --- a/esphome/components/runtime_image/runtime_image.h +++ b/esphome/components/runtime_image/runtime_image.h @@ -165,7 +165,6 @@ class RuntimeImage : public image::Image { std::unique_ptr create_decoder_(); // Memory management - RAMAllocator allocator_{}; uint8_t *buffer_{nullptr}; // Decoder management From ffb9a00e2683d6ad0aad47fc4c6a783ca615e101 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Feb 2026 08:09:13 -0600 Subject: [PATCH 033/609] [online_image] Remove stored RAMAllocator member from DownloadBuffer (#13999) --- esphome/components/online_image/download_buffer.cpp | 10 ++++++---- esphome/components/online_image/download_buffer.h | 6 ++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/esphome/components/online_image/download_buffer.cpp b/esphome/components/online_image/download_buffer.cpp index 999005df82..2ec6365aa1 100644 --- a/esphome/components/online_image/download_buffer.cpp +++ b/esphome/components/online_image/download_buffer.cpp @@ -7,7 +7,8 @@ namespace esphome::online_image { static const char *const TAG = "online_image.download_buffer"; DownloadBuffer::DownloadBuffer(size_t size) : size_(size) { - this->buffer_ = this->allocator_.allocate(size); + RAMAllocator allocator; + this->buffer_ = allocator.allocate(size); this->reset(); if (!this->buffer_) { ESP_LOGE(TAG, "Initial allocation of download buffer failed!"); @@ -38,15 +39,16 @@ size_t DownloadBuffer::resize(size_t size) { // Avoid useless reallocations; if the buffer is big enough, don't reallocate. return this->size_; } - this->allocator_.deallocate(this->buffer_, this->size_); - this->buffer_ = this->allocator_.allocate(size); + RAMAllocator allocator; + allocator.deallocate(this->buffer_, this->size_); + this->buffer_ = allocator.allocate(size); this->reset(); if (this->buffer_) { this->size_ = size; return size; } else { ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", size, - this->allocator_.get_max_free_block_size()); + allocator.get_max_free_block_size()); this->size_ = 0; return 0; } diff --git a/esphome/components/online_image/download_buffer.h b/esphome/components/online_image/download_buffer.h index 110a4b608a..73061b23b5 100644 --- a/esphome/components/online_image/download_buffer.h +++ b/esphome/components/online_image/download_buffer.h @@ -15,7 +15,10 @@ namespace esphome::online_image { class DownloadBuffer { public: DownloadBuffer(size_t size); - ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); } + ~DownloadBuffer() { + RAMAllocator allocator; + allocator.deallocate(this->buffer_, this->size_); + } uint8_t *data(size_t offset = 0); uint8_t *append() { return this->data(this->unread_); } @@ -34,7 +37,6 @@ class DownloadBuffer { size_t resize(size_t size); protected: - RAMAllocator allocator_{}; uint8_t *buffer_; size_t size_; /** Total number of downloaded bytes not yet read. */ From 81872d9822d1930e2408110acfcdb8e8e3dfd95a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Feb 2026 08:09:26 -0600 Subject: [PATCH 034/609] [camera, camera_encoder] Remove stored RAMAllocator member (#13997) --- esphome/components/camera/buffer_impl.cpp | 12 ++++++++---- esphome/components/camera/buffer_impl.h | 1 - .../camera_encoder/encoder_buffer_impl.cpp | 9 ++++++--- .../components/camera_encoder/encoder_buffer_impl.h | 1 - 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/esphome/components/camera/buffer_impl.cpp b/esphome/components/camera/buffer_impl.cpp index d17a4e2707..97cdd2e104 100644 --- a/esphome/components/camera/buffer_impl.cpp +++ b/esphome/components/camera/buffer_impl.cpp @@ -3,18 +3,22 @@ namespace esphome::camera { BufferImpl::BufferImpl(size_t size) { - this->data_ = this->allocator_.allocate(size); + RAMAllocator allocator; + this->data_ = allocator.allocate(size); this->size_ = size; } BufferImpl::BufferImpl(CameraImageSpec *spec) { - this->data_ = this->allocator_.allocate(spec->bytes_per_image()); + RAMAllocator allocator; + this->data_ = allocator.allocate(spec->bytes_per_image()); this->size_ = spec->bytes_per_image(); } BufferImpl::~BufferImpl() { - if (this->data_ != nullptr) - this->allocator_.deallocate(this->data_, this->size_); + if (this->data_ != nullptr) { + RAMAllocator allocator; + allocator.deallocate(this->data_, this->size_); + } } } // namespace esphome::camera diff --git a/esphome/components/camera/buffer_impl.h b/esphome/components/camera/buffer_impl.h index 46398295fa..5e42df7957 100644 --- a/esphome/components/camera/buffer_impl.h +++ b/esphome/components/camera/buffer_impl.h @@ -18,7 +18,6 @@ class BufferImpl : public Buffer { ~BufferImpl() override; protected: - RAMAllocator allocator_; size_t size_{}; uint8_t *data_{}; }; diff --git a/esphome/components/camera_encoder/encoder_buffer_impl.cpp b/esphome/components/camera_encoder/encoder_buffer_impl.cpp index db84026496..f12c66f203 100644 --- a/esphome/components/camera_encoder/encoder_buffer_impl.cpp +++ b/esphome/components/camera_encoder/encoder_buffer_impl.cpp @@ -4,7 +4,8 @@ namespace esphome::camera_encoder { bool EncoderBufferImpl::set_buffer_size(size_t size) { if (size > this->capacity_) { - uint8_t *p = this->allocator_.reallocate(this->data_, size); + RAMAllocator allocator; + uint8_t *p = allocator.reallocate(this->data_, size); if (p == nullptr) return false; @@ -16,8 +17,10 @@ bool EncoderBufferImpl::set_buffer_size(size_t size) { } EncoderBufferImpl::~EncoderBufferImpl() { - if (this->data_ != nullptr) - this->allocator_.deallocate(this->data_, this->capacity_); + if (this->data_ != nullptr) { + RAMAllocator allocator; + allocator.deallocate(this->data_, this->capacity_); + } } } // namespace esphome::camera_encoder diff --git a/esphome/components/camera_encoder/encoder_buffer_impl.h b/esphome/components/camera_encoder/encoder_buffer_impl.h index 13eccb7d56..d394daff14 100644 --- a/esphome/components/camera_encoder/encoder_buffer_impl.h +++ b/esphome/components/camera_encoder/encoder_buffer_impl.h @@ -16,7 +16,6 @@ class EncoderBufferImpl : public camera::EncoderBuffer { ~EncoderBufferImpl() override; protected: - RAMAllocator allocator_; size_t capacity_{}; size_t size_{}; uint8_t *data_{}; From 0c4827d348e4f55807fa910490e59144711ef392 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Feb 2026 08:09:53 -0600 Subject: [PATCH 035/609] [json, core] Remove stored RAMAllocator, make constructors constexpr (#14000) --- esphome/components/json/json_util.h | 11 ++++++----- esphome/core/helpers.h | 11 ++++------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/esphome/components/json/json_util.h b/esphome/components/json/json_util.h index ca074926bd..c472b9a9ec 100644 --- a/esphome/components/json/json_util.h +++ b/esphome/components/json/json_util.h @@ -18,7 +18,10 @@ namespace json { // Build an allocator for the JSON Library using the RAMAllocator class // This is only compiled when PSRAM is enabled struct SpiRamAllocator : ArduinoJson::Allocator { - void *allocate(size_t size) override { return allocator_.allocate(size); } + void *allocate(size_t size) override { + RAMAllocator allocator; + return allocator.allocate(size); + } void deallocate(void *ptr) override { // ArduinoJson's Allocator interface doesn't provide the size parameter in deallocate. @@ -31,11 +34,9 @@ struct SpiRamAllocator : ArduinoJson::Allocator { } void *reallocate(void *ptr, size_t new_size) override { - return allocator_.reallocate(static_cast(ptr), new_size); + RAMAllocator allocator; + return allocator.reallocate(static_cast(ptr), new_size); } - - protected: - RAMAllocator allocator_{RAMAllocator::NONE}; }; #endif diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 34c7452484..298b93fbc4 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1673,13 +1673,10 @@ template class RAMAllocator { ALLOW_FAILURE = 1 << 2, // Does nothing. Kept for compatibility. }; - RAMAllocator() = default; - RAMAllocator(uint8_t flags) { - // default is both external and internal - flags &= ALLOC_INTERNAL | ALLOC_EXTERNAL; - if (flags != 0) - this->flags_ = flags; - } + constexpr RAMAllocator() = default; + constexpr RAMAllocator(uint8_t flags) + : flags_((flags & (ALLOC_INTERNAL | ALLOC_EXTERNAL)) != 0 ? (flags & (ALLOC_INTERNAL | ALLOC_EXTERNAL)) + : (ALLOC_INTERNAL | ALLOC_EXTERNAL)) {} template constexpr RAMAllocator(const RAMAllocator &other) : flags_{other.flags_} {} T *allocate(size_t n) { return this->allocate(n, sizeof(T)); } From 4cd3f6c36a5485d6efa570e71cd861d705e7ac29 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Feb 2026 21:30:57 -0600 Subject: [PATCH 036/609] [api] Remove unused reserve from APIServer constructor (#14017) --- esphome/components/api/api_server.cpp | 6 +----- esphome/components/api/api_server.h | 6 +++++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index f25a9bc0e2..67a117e68f 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -28,11 +28,7 @@ static const char *const TAG = "api"; // APIServer APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -APIServer::APIServer() { - global_api_server = this; - // Pre-allocate shared write buffer - shared_write_buffer_.reserve(64); -} +APIServer::APIServer() { global_api_server = this; } void APIServer::setup() { ControllerRegistry::register_controller(this); diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 28f60343e0..323acc2efb 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -268,7 +268,11 @@ class APIServer : public Component, // Vectors and strings (12 bytes each on 32-bit) std::vector> clients_; - std::vector shared_write_buffer_; // Shared proto write buffer for all connections + // Shared proto write buffer for all connections. + // Not pre-allocated: all send paths call prepare_first_message_buffer() which + // reserves the exact needed size. Pre-allocating here would cause heap fragmentation + // since the buffer would almost always reallocate on first use. + std::vector shared_write_buffer_; #ifdef USE_API_HOMEASSISTANT_STATES std::vector state_subs_; #endif From e826d71bd85b7a91b683c0502846fbcc9b9d3f02 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:16:57 +0100 Subject: [PATCH 037/609] [openthread] Fix compiler format warning (#14030) --- esphome/components/openthread/openthread_esp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 79cd809809..ec212d1f68 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -115,7 +115,7 @@ void OpenThreadComponent::ot_main() { ESP_LOGE(TAG, "Failed to set OpenThread pollperiod."); } uint32_t link_polling_period = otLinkGetPollPeriod(esp_openthread_get_instance()); - ESP_LOGD(TAG, "Link Polling Period: %d", link_polling_period); + ESP_LOGD(TAG, "Link Polling Period: %" PRIu32, link_polling_period); } link_mode_config.mRxOnWhenIdle = this->poll_period == 0; link_mode_config.mDeviceType = false; From 81ed70325c164612878457033c5f08b27b19f331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Tue, 17 Feb 2026 18:45:21 +0100 Subject: [PATCH 038/609] [esp32_ble_server] fix infinitely large characteristic value (#14011) --- .../components/esp32_ble_server/__init__.py | 2 +- .../esp32_ble_server/ble_characteristic.cpp | 28 +++++++++++++++---- .../esp32_ble_server/ble_characteristic.h | 1 - 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index a7e2522fac..cb494ed1bc 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -527,7 +527,7 @@ async def to_code_characteristic(service_var, char_conf): action_conf, char_conf[CONF_CHAR_VALUE_ACTION_ID_], cg.TemplateArguments(), - {}, + [], ) cg.add(value_action.play()) else: diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index 0482848ea0..a1b1ff94bb 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -246,9 +246,27 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt if (this->handle_ != param->write.handle) break; + esp_gatt_status_t status = ESP_GATT_OK; + if (param->write.is_prep) { - this->value_.insert(this->value_.end(), param->write.value, param->write.value + param->write.len); - this->write_event_ = true; + const size_t offset = param->write.offset; + const size_t write_len = param->write.len; + const size_t new_size = offset + write_len; + // Clean the buffer on the first prepared write event + if (offset == 0) { + this->value_.clear(); + } + + if (offset != this->value_.size()) { + status = ESP_GATT_INVALID_OFFSET; + } else if (new_size > ESP_GATT_MAX_ATTR_LEN) { + status = ESP_GATT_INVALID_ATTR_LEN; + } else { + if (this->value_.size() < new_size) { + this->value_.resize(new_size); + } + memcpy(this->value_.data() + offset, param->write.value, write_len); + } } else { this->set_value(ByteBuffer::wrap(param->write.value, param->write.len)); } @@ -263,7 +281,7 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt memcpy(response.attr_value.value, param->write.value, param->write.len); esp_err_t err = - esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, &response); + esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, &response); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err); @@ -280,9 +298,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt } case ESP_GATTS_EXEC_WRITE_EVT: { - if (!this->write_event_) + // BLE stack will guarantee that ESP_GATTS_EXEC_WRITE_EVT is only received after prepared writes + if (this->value_.empty()) break; - this->write_event_ = false; if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { if (this->on_write_callback_) { (*this->on_write_callback_)(this->value_, param->exec_write.conn_id); diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index b913915789..c2cdb1660c 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -77,7 +77,6 @@ class BLECharacteristic { } protected: - bool write_event_{false}; BLEService *service_{}; ESPBTUUID uuid_; esp_gatt_char_prop_t properties_; From 5bb863f7dadadedf3c8620d1e275d7d354fedae8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:24:39 -0600 Subject: [PATCH 039/609] Bump actions/stale from 10.1.1 to 10.2.0 (#14036) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 7e03e2a5f9..ba5c32e016 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Stale - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 + uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 with: debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch remove-stale-when-updated: true From fb35ddebb9915f6bda82e4050943ee5de1ba0360 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 08:22:07 -0600 Subject: [PATCH 040/609] [display] Make COLOR_OFF and COLOR_ON inline constexpr (#14044) --- esphome/components/display/display.cpp | 3 +-- esphome/components/display/display.h | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index ebc3c0a9f6..53a087803c 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -9,8 +9,7 @@ namespace esphome { namespace display { static const char *const TAG = "display"; -const Color COLOR_OFF(0, 0, 0, 0); -const Color COLOR_ON(255, 255, 255, 255); +// COLOR_OFF and COLOR_ON are now inline constexpr in display.h void Display::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } void Display::clear() { this->fill(COLOR_OFF); } diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 47d40915aa..e40f6ec963 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -298,9 +298,9 @@ using display_writer_t = DisplayWriter; } /// Turn the pixel OFF. -extern const Color COLOR_OFF; +inline constexpr Color COLOR_OFF(0, 0, 0, 0); /// Turn the pixel ON. -extern const Color COLOR_ON; +inline constexpr Color COLOR_ON(255, 255, 255, 255); class BaseImage { public: From fb89900c64f2a88d6f86feaa43e453af8d83b53c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 08:22:36 -0600 Subject: [PATCH 041/609] [core] Make setup_priority and component state constants constexpr (#14041) --- esphome/core/component.cpp | 32 ++--------------------- esphome/core/component.h | 52 +++++++++++++++++++------------------- 2 files changed, 28 insertions(+), 56 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 90aa36f4db..a452f4d400 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -76,36 +76,8 @@ void store_component_error_message(const Component *component, const char *messa } } // namespace -namespace setup_priority { - -const float BUS = 1000.0f; -const float IO = 900.0f; -const float HARDWARE = 800.0f; -const float DATA = 600.0f; -const float PROCESSOR = 400.0; -const float BLUETOOTH = 350.0f; -const float AFTER_BLUETOOTH = 300.0f; -const float WIFI = 250.0f; -const float ETHERNET = 250.0f; -const float BEFORE_CONNECTION = 220.0f; -const float AFTER_WIFI = 200.0f; -const float AFTER_CONNECTION = 100.0f; -const float LATE = -100.0f; - -} // namespace setup_priority - -// Component state uses bits 0-2 (8 states, 5 used) -const uint8_t COMPONENT_STATE_MASK = 0x07; -const uint8_t COMPONENT_STATE_CONSTRUCTION = 0x00; -const uint8_t COMPONENT_STATE_SETUP = 0x01; -const uint8_t COMPONENT_STATE_LOOP = 0x02; -const uint8_t COMPONENT_STATE_FAILED = 0x03; -const uint8_t COMPONENT_STATE_LOOP_DONE = 0x04; -// Status LED uses bits 3-4 -const uint8_t STATUS_LED_MASK = 0x18; -const uint8_t STATUS_LED_OK = 0x00; -const uint8_t STATUS_LED_WARNING = 0x08; // Bit 3 -const uint8_t STATUS_LED_ERROR = 0x10; // Bit 4 +// setup_priority, component state, and status LED constants are now +// constexpr in component.h const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again diff --git a/esphome/core/component.h b/esphome/core/component.h index 9ab77cc2f9..848bc0ba35 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -21,33 +21,31 @@ struct LogString; namespace setup_priority { /// For communication buses like i2c/spi -extern const float BUS; +inline constexpr float BUS = 1000.0f; /// For components that represent GPIO pins like PCF8573 -extern const float IO; +inline constexpr float IO = 900.0f; /// For components that deal with hardware and are very important like GPIO switch -extern const float HARDWARE; +inline constexpr float HARDWARE = 800.0f; /// For components that import data from directly connected sensors like DHT. -extern const float DATA; -/// Alias for DATA (here for compatibility reasons) -extern const float HARDWARE_LATE; +inline constexpr float DATA = 600.0f; /// For components that use data from sensors like displays -extern const float PROCESSOR; -extern const float BLUETOOTH; -extern const float AFTER_BLUETOOTH; -extern const float WIFI; -extern const float ETHERNET; +inline constexpr float PROCESSOR = 400.0f; +inline constexpr float BLUETOOTH = 350.0f; +inline constexpr float AFTER_BLUETOOTH = 300.0f; +inline constexpr float WIFI = 250.0f; +inline constexpr float ETHERNET = 250.0f; /// For components that should be initialized after WiFi and before API is connected. -extern const float BEFORE_CONNECTION; +inline constexpr float BEFORE_CONNECTION = 220.0f; /// For components that should be initialized after WiFi is connected. -extern const float AFTER_WIFI; +inline constexpr float AFTER_WIFI = 200.0f; /// For components that should be initialized after a data connection (API/MQTT) is connected. -extern const float AFTER_CONNECTION; +inline constexpr float AFTER_CONNECTION = 100.0f; /// For components that should be initialized at the very end of the setup process. -extern const float LATE; +inline constexpr float LATE = -100.0f; } // namespace setup_priority -static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL; +inline constexpr uint32_t SCHEDULER_DONT_RUN = 4294967295UL; /// Type-safe scheduler IDs for core base classes. /// Uses a separate NameType (NUMERIC_ID_INTERNAL) so IDs can never collide @@ -65,16 +63,18 @@ void log_update_interval(const char *tag, PollingComponent *component); #define LOG_UPDATE_INTERVAL(this) log_update_interval(TAG, this) -extern const uint8_t COMPONENT_STATE_MASK; -extern const uint8_t COMPONENT_STATE_CONSTRUCTION; -extern const uint8_t COMPONENT_STATE_SETUP; -extern const uint8_t COMPONENT_STATE_LOOP; -extern const uint8_t COMPONENT_STATE_FAILED; -extern const uint8_t COMPONENT_STATE_LOOP_DONE; -extern const uint8_t STATUS_LED_MASK; -extern const uint8_t STATUS_LED_OK; -extern const uint8_t STATUS_LED_WARNING; -extern const uint8_t STATUS_LED_ERROR; +// Component state uses bits 0-2 (8 states, 5 used) +inline constexpr uint8_t COMPONENT_STATE_MASK = 0x07; +inline constexpr uint8_t COMPONENT_STATE_CONSTRUCTION = 0x00; +inline constexpr uint8_t COMPONENT_STATE_SETUP = 0x01; +inline constexpr uint8_t COMPONENT_STATE_LOOP = 0x02; +inline constexpr uint8_t COMPONENT_STATE_FAILED = 0x03; +inline constexpr uint8_t COMPONENT_STATE_LOOP_DONE = 0x04; +// Status LED uses bits 3-4 +inline constexpr uint8_t STATUS_LED_MASK = 0x18; +inline constexpr uint8_t STATUS_LED_OK = 0x00; +inline constexpr uint8_t STATUS_LED_WARNING = 0x08; +inline constexpr uint8_t STATUS_LED_ERROR = 0x10; // Remove before 2026.8.0 enum class RetryResult { DONE, RETRY }; From 652c669777ecaa125f575aa7eac72bf7f5cc5a1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:08:02 -0600 Subject: [PATCH 042/609] Bump pillow from 11.3.0 to 12.1.1 (#14048) Signed-off-by: dependabot[bot] 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 a0a29ad30a..77d30ecd3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import ruamel.yaml.clib==0.2.15 # dashboard_import esphome-glyphsets==0.2.0 -pillow==11.3.0 +pillow==12.1.1 resvg-py==0.2.6 freetype-py==2.5.1 jinja2==3.1.6 From f73bcc0e7bebba965a76859f680ac16681833a4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:08:12 -0600 Subject: [PATCH 043/609] Bump cryptography from 45.0.1 to 46.0.5 (#14049) Signed-off-by: dependabot[bot] 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 77d30ecd3d..2e6268284a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -cryptography==45.0.1 +cryptography==46.0.5 voluptuous==0.16.0 PyYAML==6.0.3 paho-mqtt==1.6.1 From 9cd7b0b32b1a4c27a3b0673b1d10187e69e55109 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Feb 2026 13:09:33 -0500 Subject: [PATCH 044/609] [external_components] Clean up incomplete clone on failed ref fetch (#14051) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32/__init__.py | 4 +- esphome/git.py | 47 ++++++----- esphome/helpers.py | 20 ++++- esphome/writer.py | 27 +------ tests/unit_tests/test_git.py | 113 ++++++++++++++++++++++++++- 5 files changed, 162 insertions(+), 49 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b78b945a24..8b3e1afea6 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -44,9 +44,9 @@ from esphome.const import ( from esphome.core import CORE, HexInt, TimePeriod from esphome.coroutine import CoroPriority, coroutine_with_priority import esphome.final_validate as fv -from esphome.helpers import copy_file_if_changed, write_file_if_changed +from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed from esphome.types import ConfigType -from esphome.writer import clean_cmake_cache, rmtree +from esphome.writer import clean_cmake_cache from .boards import BOARDS, STANDARD_BOARDS from .const import ( # noqa diff --git a/esphome/git.py b/esphome/git.py index 4ff07ffe75..a45768b5cd 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -5,12 +5,12 @@ import hashlib import logging from pathlib import Path import re -import shutil import subprocess import urllib.parse import esphome.config_validation as cv from esphome.core import CORE, TimePeriodSeconds +from esphome.helpers import rmtree _LOGGER = logging.getLogger(__name__) @@ -115,24 +115,35 @@ def clone_or_update( if not repo_dir.is_dir(): _LOGGER.info("Cloning %s", key) _LOGGER.debug("Location: %s", repo_dir) - cmd = ["git", "clone", "--depth=1"] - cmd += ["--", url, str(repo_dir)] - run_git_command(cmd) + try: + cmd = ["git", "clone", "--depth=1"] + cmd += ["--", url, str(repo_dir)] + run_git_command(cmd) - if ref is not None: - # We need to fetch the PR branch first, otherwise git will complain - # about missing objects - _LOGGER.info("Fetching %s", ref) - run_git_command(["git", "fetch", "--", "origin", ref], git_dir=repo_dir) - run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], git_dir=repo_dir) + if ref is not None: + # We need to fetch the PR branch first, otherwise git will complain + # about missing objects + _LOGGER.info("Fetching %s", ref) + run_git_command(["git", "fetch", "--", "origin", ref], git_dir=repo_dir) + run_git_command( + ["git", "reset", "--hard", "FETCH_HEAD"], git_dir=repo_dir + ) - if submodules is not None: - _LOGGER.info( - "Initializing submodules (%s) for %s", ", ".join(submodules), key - ) - run_git_command( - ["git", "submodule", "update", "--init"] + submodules, git_dir=repo_dir - ) + if submodules is not None: + _LOGGER.info( + "Initializing submodules (%s) for %s", ", ".join(submodules), key + ) + run_git_command( + ["git", "submodule", "update", "--init"] + submodules, + git_dir=repo_dir, + ) + except GitException: + # Remove incomplete clone to prevent stale state. Without this, + # a failed ref fetch leaves a clone on the default branch, and + # subsequent calls skip the update due to the refresh window. + if repo_dir.is_dir(): + rmtree(repo_dir) + raise else: # Check refresh needed @@ -193,7 +204,7 @@ def clone_or_update( err, ) _LOGGER.info("Removing broken repository at %s", repo_dir) - shutil.rmtree(repo_dir) + rmtree(repo_dir) _LOGGER.info("Successfully removed broken repository, re-cloning...") # Recursively call clone_or_update to re-clone diff --git a/esphome/helpers.py b/esphome/helpers.py index ae142b7f8b..145ebd4096 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -8,6 +8,7 @@ from pathlib import Path import platform import re import shutil +import stat import tempfile from typing import TYPE_CHECKING from urllib.parse import urlparse @@ -354,6 +355,23 @@ def is_ha_addon(): return get_bool_env("ESPHOME_IS_HA_ADDON") +def rmtree(path: Path | str) -> None: + """Remove a directory tree, handling read-only files on Windows. + + On Windows, git pack files and other files may be marked read-only, + causing shutil.rmtree to fail. This handles that by removing the + read-only flag and retrying. + """ + + def _onerror(func, path, exc_info): + if os.access(path, os.W_OK): + raise exc_info[1].with_traceback(exc_info[2]) + os.chmod(path, stat.S_IWUSR | stat.S_IRUSR) + func(path) + + shutil.rmtree(path, onerror=_onerror) + + def walk_files(path: Path): for root, _, files in os.walk(path): for name in files: @@ -481,8 +499,6 @@ def list_starts_with(list_, sub): def file_compare(path1: Path, path2: Path) -> bool: """Return True if the files path1 and path2 have the same contents.""" - import stat - try: stat1, stat2 = path1.stat(), path2.stat() except OSError: diff --git a/esphome/writer.py b/esphome/writer.py index 661118e518..fd4c811fb3 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -1,14 +1,10 @@ -from collections.abc import Callable import importlib import json import logging import os from pathlib import Path import re -import shutil -import stat import time -from types import TracebackType from esphome import loader from esphome.config import iter_component_configs, iter_components @@ -25,6 +21,7 @@ from esphome.helpers import ( get_str_env, is_ha_addon, read_file, + rmtree, walk_files, write_file, write_file_if_changed, @@ -404,28 +401,6 @@ def clean_cmake_cache(): pioenvs_cmake_path.unlink() -def _rmtree_error_handler( - func: Callable[[str], object], - path: str, - exc_info: tuple[type[BaseException], BaseException, TracebackType | None], -) -> None: - """Error handler for shutil.rmtree to handle read-only files on Windows. - - On Windows, git pack files and other files may be marked read-only, - causing shutil.rmtree to fail with "Access is denied". This handler - removes the read-only flag and retries the deletion. - """ - if os.access(path, os.W_OK): - raise exc_info[1].with_traceback(exc_info[2]) - os.chmod(path, stat.S_IWUSR | stat.S_IRUSR) - func(path) - - -def rmtree(path: Path | str) -> None: - """Remove a directory tree, handling read-only files on Windows.""" - shutil.rmtree(path, onerror=_rmtree_error_handler) - - def clean_build(clear_pio_cache: bool = True): # Allow skipping cache cleaning for integration tests if os.environ.get("ESPHOME_SKIP_CLEAN_BUILD"): diff --git a/tests/unit_tests/test_git.py b/tests/unit_tests/test_git.py index 0411fe5e43..745dfad487 100644 --- a/tests/unit_tests/test_git.py +++ b/tests/unit_tests/test_git.py @@ -656,7 +656,7 @@ def test_clone_or_update_recover_broken_flag_prevents_infinite_loop( # Should raise on the second attempt when _recover_broken=False # This hits the "if not _recover_broken: raise" path with ( - unittest.mock.patch("esphome.git.shutil.rmtree", side_effect=mock_rmtree), + unittest.mock.patch("esphome.git.rmtree", side_effect=mock_rmtree), pytest.raises(GitCommandError, match="fatal: unable to write new index file"), ): git.clone_or_update( @@ -671,3 +671,114 @@ def test_clone_or_update_recover_broken_flag_prevents_infinite_loop( stash_calls = [c for c in call_list if "stash" in c[0][0]] # Should have exactly two stash calls assert len(stash_calls) == 2 + + +def test_clone_or_update_cleans_up_on_failed_ref_fetch( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that a failed ref fetch removes the incomplete clone directory. + + When cloning with a specific ref, if `git clone` succeeds but the + subsequent `git fetch ` fails, the clone directory should be + removed so the next attempt starts fresh instead of finding a stale + clone on the default branch. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "pull/123/head" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + cmd_type = _get_git_command_type(cmd) + if cmd_type == "clone": + # Simulate successful clone by creating the directory + repo_dir.mkdir(parents=True, exist_ok=True) + (repo_dir / ".git").mkdir(exist_ok=True) + return "" + if cmd_type == "fetch": + raise GitCommandError("fatal: couldn't find remote ref pull/123/head") + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + refresh = TimePeriodSeconds(days=1) + + with pytest.raises(GitCommandError, match="couldn't find remote ref"): + git.clone_or_update( + url=url, + ref=ref, + refresh=refresh, + domain=domain, + ) + + # The incomplete clone directory should have been removed + assert not repo_dir.exists() + + # Verify clone was attempted then fetch failed + call_list = mock_run_git_command.call_args_list + clone_calls = [c for c in call_list if "clone" in c[0][0]] + assert len(clone_calls) == 1 + fetch_calls = [c for c in call_list if "fetch" in c[0][0]] + assert len(fetch_calls) == 1 + + +def test_clone_or_update_stale_clone_is_retried_after_cleanup( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that after cleanup, a subsequent call does a fresh clone. + + This is the full scenario: first call fails at fetch (directory cleaned up), + second call sees no directory and clones fresh. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "pull/123/head" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + call_count = {"clone": 0, "fetch": 0} + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + cmd_type = _get_git_command_type(cmd) + if cmd_type == "clone": + call_count["clone"] += 1 + repo_dir.mkdir(parents=True, exist_ok=True) + (repo_dir / ".git").mkdir(exist_ok=True) + return "" + if cmd_type == "fetch": + call_count["fetch"] += 1 + if call_count["fetch"] == 1: + # First fetch fails + raise GitCommandError("fatal: couldn't find remote ref pull/123/head") + # Second fetch succeeds + return "" + if cmd_type == "reset": + return "" + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + refresh = TimePeriodSeconds(days=1) + + # First call: clone succeeds, fetch fails, directory cleaned up + with pytest.raises(GitCommandError, match="couldn't find remote ref"): + git.clone_or_update(url=url, ref=ref, refresh=refresh, domain=domain) + + assert not repo_dir.exists() + + # Second call: fresh clone + fetch succeeds + result_dir, _ = git.clone_or_update( + url=url, ref=ref, refresh=refresh, domain=domain + ) + + assert result_dir == repo_dir + assert repo_dir.exists() + assert call_count["clone"] == 2 + assert call_count["fetch"] == 2 From 4a038978d27b4ed193de8fb934ff32221ae9a31f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 15:04:14 -0600 Subject: [PATCH 045/609] [pca9685] Make mode constants inline constexpr (#14042) --- esphome/components/pca9685/pca9685_output.cpp | 6 +----- esphome/components/pca9685/pca9685_output.h | 10 +++++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/esphome/components/pca9685/pca9685_output.cpp b/esphome/components/pca9685/pca9685_output.cpp index 77e3d5a6c6..89a6bcdcc0 100644 --- a/esphome/components/pca9685/pca9685_output.cpp +++ b/esphome/components/pca9685/pca9685_output.cpp @@ -8,11 +8,7 @@ namespace pca9685 { static const char *const TAG = "pca9685"; -const uint8_t PCA9685_MODE_INVERTED = 0x10; -const uint8_t PCA9685_MODE_OUTPUT_ONACK = 0x08; -const uint8_t PCA9685_MODE_OUTPUT_TOTEM_POLE = 0x04; -const uint8_t PCA9685_MODE_OUTNE_HIGHZ = 0x02; -const uint8_t PCA9685_MODE_OUTNE_LOW = 0x01; +// PCA9685 mode constants are now inline constexpr in pca9685_output.h static const uint8_t PCA9685_REGISTER_SOFTWARE_RESET = 0x06; static const uint8_t PCA9685_REGISTER_MODE1 = 0x00; diff --git a/esphome/components/pca9685/pca9685_output.h b/esphome/components/pca9685/pca9685_output.h index 288c923d4c..785cc974da 100644 --- a/esphome/components/pca9685/pca9685_output.h +++ b/esphome/components/pca9685/pca9685_output.h @@ -13,15 +13,15 @@ enum class PhaseBalancer { }; /// Inverts polarity of channel output signal -extern const uint8_t PCA9685_MODE_INVERTED; +inline constexpr uint8_t PCA9685_MODE_INVERTED = 0x10; /// Channel update happens upon ACK (post-set) rather than on STOP (endTransmission) -extern const uint8_t PCA9685_MODE_OUTPUT_ONACK; +inline constexpr uint8_t PCA9685_MODE_OUTPUT_ONACK = 0x08; /// Use a totem-pole (push-pull) style output rather than an open-drain structure. -extern const uint8_t PCA9685_MODE_OUTPUT_TOTEM_POLE; +inline constexpr uint8_t PCA9685_MODE_OUTPUT_TOTEM_POLE = 0x04; /// For active low output enable, sets channel output to high-impedance state -extern const uint8_t PCA9685_MODE_OUTNE_HIGHZ; +inline constexpr uint8_t PCA9685_MODE_OUTNE_HIGHZ = 0x02; /// Similarly, sets channel output to high if in totem-pole mode, otherwise -extern const uint8_t PCA9685_MODE_OUTNE_LOW; +inline constexpr uint8_t PCA9685_MODE_OUTNE_LOW = 0x01; class PCA9685Output; From 82cfa00a97a8b6c9d0361c2bec66c3723a2be6e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 15:04:30 -0600 Subject: [PATCH 046/609] [tlc59208f] Make mode constants inline constexpr (#14043) --- esphome/components/tlc59208f/tlc59208f_output.cpp | 12 +----------- esphome/components/tlc59208f/tlc59208f_output.h | 14 +++++++------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/esphome/components/tlc59208f/tlc59208f_output.cpp b/esphome/components/tlc59208f/tlc59208f_output.cpp index 85311a877c..d35585fe5f 100644 --- a/esphome/components/tlc59208f/tlc59208f_output.cpp +++ b/esphome/components/tlc59208f/tlc59208f_output.cpp @@ -26,17 +26,7 @@ const uint8_t TLC59208F_MODE1_SUB3 = (1 << 1); // 0: device doesn't respond to i2c all-call 3, 1*: responds to all-call const uint8_t TLC59208F_MODE1_ALLCALL = (1 << 0); -// 0*: Group dimming, 1: Group blinking -const uint8_t TLC59208F_MODE2_DMBLNK = (1 << 5); -// 0*: Output change on Stop command, 1: Output change on ACK -const uint8_t TLC59208F_MODE2_OCH = (1 << 3); -// 0*: WDT disabled, 1: WDT enabled -const uint8_t TLC59208F_MODE2_WDTEN = (1 << 2); -// WDT timeouts -const uint8_t TLC59208F_MODE2_WDT_5MS = (0 << 0); -const uint8_t TLC59208F_MODE2_WDT_15MS = (1 << 0); -const uint8_t TLC59208F_MODE2_WDT_25MS = (2 << 0); -const uint8_t TLC59208F_MODE2_WDT_35MS = (3 << 0); +// TLC59208F MODE2 constants are now inline constexpr in tlc59208f_output.h // --- Special function --- // Call address to perform software reset, no devices will ACK diff --git a/esphome/components/tlc59208f/tlc59208f_output.h b/esphome/components/tlc59208f/tlc59208f_output.h index 68ca8061d7..34663cd364 100644 --- a/esphome/components/tlc59208f/tlc59208f_output.h +++ b/esphome/components/tlc59208f/tlc59208f_output.h @@ -9,16 +9,16 @@ namespace esphome { namespace tlc59208f { // 0*: Group dimming, 1: Group blinking -extern const uint8_t TLC59208F_MODE2_DMBLNK; +inline constexpr uint8_t TLC59208F_MODE2_DMBLNK = (1 << 5); // 0*: Output change on Stop command, 1: Output change on ACK -extern const uint8_t TLC59208F_MODE2_OCH; +inline constexpr uint8_t TLC59208F_MODE2_OCH = (1 << 3); // 0*: WDT disabled, 1: WDT enabled -extern const uint8_t TLC59208F_MODE2_WDTEN; +inline constexpr uint8_t TLC59208F_MODE2_WDTEN = (1 << 2); // WDT timeouts -extern const uint8_t TLC59208F_MODE2_WDT_5MS; -extern const uint8_t TLC59208F_MODE2_WDT_15MS; -extern const uint8_t TLC59208F_MODE2_WDT_25MS; -extern const uint8_t TLC59208F_MODE2_WDT_35MS; +inline constexpr uint8_t TLC59208F_MODE2_WDT_5MS = (0 << 0); +inline constexpr uint8_t TLC59208F_MODE2_WDT_15MS = (1 << 0); +inline constexpr uint8_t TLC59208F_MODE2_WDT_25MS = (2 << 0); +inline constexpr uint8_t TLC59208F_MODE2_WDT_35MS = (3 << 0); class TLC59208FOutput; From 09fc0288953d13a09bafef3e9038b36d3ff4c1ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 15:16:26 -0600 Subject: [PATCH 047/609] [core] Remove dead global_state variable (#14060) --- esphome/core/component.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index a452f4d400..47c4a70c0f 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -82,8 +82,6 @@ void store_component_error_message(const Component *component, const char *messa const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again -uint32_t global_state = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - float Component::get_loop_priority() const { return 0.0f; } float Component::get_setup_priority() const { return setup_priority::DATA; } From 02e310f2c9549349ae12615f3fec627d9189479a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 16:48:13 -0600 Subject: [PATCH 048/609] [core] Remove unnecessary IRAM_ATTR from yield(), delay(), feed_wdt(), and arch_feed_wdt() (#14063) --- esphome/components/esp32/core.cpp | 6 +++--- esphome/components/esp8266/core.cpp | 6 +++--- esphome/components/host/core.cpp | 6 +++--- esphome/components/libretiny/core.cpp | 6 +++--- esphome/components/rp2040/core.cpp | 6 +++--- esphome/core/application.cpp | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 09a45c14a6..202d929ab9 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -21,9 +21,9 @@ extern "C" __attribute__((weak)) void initArduino() {} namespace esphome { -void IRAM_ATTR HOT yield() { vPortYield(); } +void HOT yield() { vPortYield(); } uint32_t IRAM_ATTR HOT millis() { return (uint32_t) (esp_timer_get_time() / 1000ULL); } -void IRAM_ATTR HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } +void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { @@ -44,7 +44,7 @@ void arch_init() { esp_ota_mark_app_valid_cancel_rollback(); #endif } -void IRAM_ATTR HOT arch_feed_wdt() { esp_task_wdt_reset(); } +void HOT arch_feed_wdt() { esp_task_wdt_reset(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 784b87916b..236d3022be 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -14,9 +14,9 @@ extern "C" { namespace esphome { -void IRAM_ATTR HOT yield() { ::yield(); } +void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); } +void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { @@ -27,7 +27,7 @@ void arch_restart() { } } void arch_init() {} -void IRAM_ATTR HOT arch_feed_wdt() { system_soft_wdt_feed(); } +void HOT arch_feed_wdt() { system_soft_wdt_feed(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index 164d622dd4..c20a33fa37 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -11,7 +11,7 @@ namespace esphome { -void IRAM_ATTR HOT yield() { ::sched_yield(); } +void HOT yield() { ::sched_yield(); } uint32_t IRAM_ATTR HOT millis() { struct timespec spec; clock_gettime(CLOCK_MONOTONIC, &spec); @@ -19,7 +19,7 @@ uint32_t IRAM_ATTR HOT millis() { uint32_t ms = round(spec.tv_nsec / 1e6); return ((uint32_t) seconds) * 1000U + ms; } -void IRAM_ATTR HOT delay(uint32_t ms) { +void HOT delay(uint32_t ms) { struct timespec ts; ts.tv_sec = ms / 1000; ts.tv_nsec = (ms % 1000) * 1000000; @@ -48,7 +48,7 @@ void arch_restart() { exit(0); } void arch_init() { // pass } -void IRAM_ATTR HOT arch_feed_wdt() { +void HOT arch_feed_wdt() { // pass } diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index b22740f02a..4dda7c3856 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -11,10 +11,10 @@ void loop(); namespace esphome { -void IRAM_ATTR HOT yield() { ::yield(); } +void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } -void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); } +void HOT delay(uint32_t ms) { ::delay(ms); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } void arch_init() { @@ -30,7 +30,7 @@ void arch_restart() { while (1) { } } -void IRAM_ATTR HOT arch_feed_wdt() { lt_wdt_feed(); } +void HOT arch_feed_wdt() { lt_wdt_feed(); } uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index d88e9f54b7..37378d88bb 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -9,9 +9,9 @@ namespace esphome { -void IRAM_ATTR HOT yield() { ::yield(); } +void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); } +void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { @@ -27,7 +27,7 @@ void arch_init() { #endif } -void IRAM_ATTR HOT arch_feed_wdt() { watchdog_update(); } +void HOT arch_feed_wdt() { watchdog_update(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 449acc64cf..406885fd81 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -240,7 +240,7 @@ void Application::process_dump_config_() { this->dump_config_at_++; } -void IRAM_ATTR HOT Application::feed_wdt(uint32_t time) { +void HOT Application::feed_wdt(uint32_t time) { static uint32_t last_feed = 0; // Use provided time if available, otherwise get current time uint32_t now = time ? time : millis(); From 387f615dae037ad8a19dd6f0fad7e04741eee1d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 16:48:30 -0600 Subject: [PATCH 049/609] [api] Add handshake timeout to prevent connection slot exhaustion (#14050) --- esphome/components/api/api_connection.cpp | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4d564af9e2..5a7994a322 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -60,6 +60,11 @@ static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5; static constexpr uint8_t MAX_PING_RETRIES = 60; static constexpr uint16_t PING_RETRY_INTERVAL = 1000; static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2; +// Timeout for completing the handshake (Noise transport + HelloRequest). +// A stalled handshake from a buggy client or network glitch holds a connection +// slot, which can prevent legitimate clients from reconnecting. Also hardens +// against the less likely case of intentional connection slot exhaustion. +static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 15000; static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION); @@ -205,7 +210,12 @@ void APIConnection::loop() { this->fatal_error_with_log_(LOG_STR("Reading failed"), err); return; } else { - this->last_traffic_ = now; + // Only update last_traffic_ after authentication to ensure the + // handshake timeout is an absolute deadline from connection start. + // Pre-auth messages (e.g. PingRequest) must not reset the timer. + if (this->is_authenticated()) { + this->last_traffic_ = now; + } // read a packet this->read_message(buffer.data_len, buffer.type, buffer.data); if (this->flags_.remove) @@ -223,6 +233,15 @@ void APIConnection::loop() { this->process_active_iterator_(); } + // Disconnect clients that haven't completed the handshake in time. + // Stale half-open connections from buggy clients or network issues can + // accumulate and block legitimate clients from reconnecting. + if (!this->is_authenticated() && now - this->last_traffic_ > HANDSHAKE_TIMEOUT_MS) { + this->on_fatal_error(); + this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("handshake timeout; disconnecting")); + return; + } + if (this->flags_.sent_ping) { // Disconnect if not responded within 2.5*keepalive if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { @@ -1484,6 +1503,8 @@ void APIConnection::complete_authentication_() { } this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); + // Reset traffic timer so keepalive starts from authentication, not connection start + this->last_traffic_ = App.get_loop_component_start_time(); this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected")); #ifdef USE_API_CLIENT_CONNECTED_TRIGGER { From d90754dc0a182785575f2232f3faa29c4d787c84 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 16:49:19 -0600 Subject: [PATCH 050/609] [http_request] Replace heavy STL containers with std::vector for headers (#14024) --- esphome/components/http_request/__init__.py | 2 +- .../components/http_request/http_request.cpp | 22 +++---- .../components/http_request/http_request.h | 58 ++++++++++++------- .../http_request/http_request_arduino.cpp | 12 ++-- .../http_request/http_request_arduino.h | 2 +- .../http_request/http_request_host.cpp | 6 +- .../http_request/http_request_host.h | 2 +- .../http_request/http_request_idf.cpp | 16 ++--- .../http_request/http_request_idf.h | 7 +-- 9 files changed, 65 insertions(+), 62 deletions(-) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 64d74323d6..5faffccbe4 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -310,7 +310,7 @@ async def http_request_action_to_code(config, action_id, template_arg, args): cg.add(var.add_request_header(key, template_)) for value in config.get(CONF_COLLECT_HEADERS, []): - cg.add(var.add_collect_header(value)) + cg.add(var.add_collect_header(value.lower())) if response_conf := config.get(CONF_ON_RESPONSE): if capture_response: diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 11dde4715a..6590d2018e 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -22,23 +22,15 @@ void HttpRequestComponent::dump_config() { } std::string HttpContainer::get_response_header(const std::string &header_name) { - auto response_headers = this->get_response_headers(); - auto header_name_lower_case = str_lower_case(header_name); - if (response_headers.count(header_name_lower_case) == 0) { - ESP_LOGW(TAG, "No header with name %s found", header_name_lower_case.c_str()); - return ""; - } else { - auto values = response_headers[header_name_lower_case]; - if (values.empty()) { - ESP_LOGE(TAG, "header with name %s returned an empty list, this shouldn't happen", - header_name_lower_case.c_str()); - return ""; - } else { - auto header_value = values.front(); - ESP_LOGD(TAG, "Header with name %s found with value %s", header_name_lower_case.c_str(), header_value.c_str()); - return header_value; + auto lower = str_lower_case(header_name); + for (const auto &entry : this->response_headers_) { + if (entry.name == lower) { + ESP_LOGD(TAG, "Header with name %s found with value %s", lower.c_str(), entry.value.c_str()); + return entry.value; } } + ESP_LOGW(TAG, "No header with name %s found", lower.c_str()); + return ""; } } // namespace esphome::http_request diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index a427cc4a05..458ffe94a8 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -80,6 +80,16 @@ inline bool is_redirect(int const status) { */ inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && status < HTTP_STATUS_MULTIPLE_CHOICES; } +/// Check if a header name should be collected (linear scan, fine for small lists) +inline bool should_collect_header(const std::vector &lower_case_collect_headers, + const std::string &lower_header_name) { + for (const auto &h : lower_case_collect_headers) { + if (h == lower_header_name) + return true; + } + return false; +} + /* * HTTP Container Read Semantics * ============================= @@ -258,20 +268,13 @@ class HttpContainer : public Parented { return !this->is_chunked_ && this->bytes_read_ >= this->content_length; } - /** - * @brief Get response headers. - * - * @return The key is the lower case response header name, the value is the header value. - */ - std::map> get_response_headers() { return this->response_headers_; } - std::string get_response_header(const std::string &header_name); protected: size_t bytes_read_{0}; bool secure_{false}; bool is_chunked_{false}; ///< True if response uses chunked transfer encoding - std::map> response_headers_{}; + std::vector
response_headers_{}; }; /// Read data from HTTP container into buffer with timeout handling @@ -333,8 +336,8 @@ class HttpRequestComponent : public Component { return this->start(url, "GET", "", request_headers); } std::shared_ptr get(const std::string &url, const std::list
&request_headers, - const std::set &collect_headers) { - return this->start(url, "GET", "", request_headers, collect_headers); + const std::vector &lower_case_collect_headers) { + return this->start(url, "GET", "", request_headers, lower_case_collect_headers); } std::shared_ptr post(const std::string &url, const std::string &body) { return this->start(url, "POST", body, {}); @@ -345,29 +348,40 @@ class HttpRequestComponent : public Component { } std::shared_ptr post(const std::string &url, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) { - return this->start(url, "POST", body, request_headers, collect_headers); + const std::vector &lower_case_collect_headers) { + return this->start(url, "POST", body, request_headers, lower_case_collect_headers); } std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers) { - return this->start(url, method, body, request_headers, {}); + // Call perform() directly to avoid ambiguity with the std::set overload + return this->perform(url, method, body, request_headers, {}); + } + + // Remove before 2027.1.0 + ESPDEPRECATED("Pass collect_headers as std::vector instead of std::set. Removed in 2027.1.0.", + "2026.7.0") + std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, + const std::list
&request_headers, + const std::set &collect_headers) { + std::vector lower; + lower.reserve(collect_headers.size()); + for (const auto &h : collect_headers) { + lower.push_back(str_lower_case(h)); + } + return this->perform(url, method, body, request_headers, lower); } std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) { - std::set lower_case_collect_headers; - for (const std::string &collect_header : collect_headers) { - lower_case_collect_headers.insert(str_lower_case(collect_header)); - } + const std::vector &lower_case_collect_headers) { return this->perform(url, method, body, request_headers, lower_case_collect_headers); } protected: virtual std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) = 0; + const std::vector &lower_case_collect_headers) = 0; const char *useragent_{nullptr}; bool follow_redirects_{}; uint16_t redirect_limit_{}; @@ -389,7 +403,7 @@ template class HttpRequestSendAction : public Action { this->request_headers_.insert({key, value}); } - void add_collect_header(const char *value) { this->collect_headers_.insert(value); } + void add_collect_header(const char *value) { this->lower_case_collect_headers_.push_back(value); } void add_json(const char *key, TemplatableValue value) { this->json_.insert({key, value}); } @@ -431,7 +445,7 @@ template class HttpRequestSendAction : public Action { } auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers, - this->collect_headers_); + this->lower_case_collect_headers_); auto captured_args = std::make_tuple(x...); @@ -494,7 +508,7 @@ template class HttpRequestSendAction : public Action { void encode_json_func_(Ts... x, JsonObject root) { this->json_func_(x..., root); } HttpRequestComponent *parent_; std::map> request_headers_{}; - std::set collect_headers_{"content-type", "content-length"}; + std::vector lower_case_collect_headers_{"content-type", "content-length"}; std::map> json_{}; std::function json_func_{nullptr}; #ifdef USE_HTTP_REQUEST_RESPONSE diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index e5b919e380..3f60b76b58 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -27,7 +27,7 @@ static constexpr int ESP8266_SSL_ERR_OOM = -1000; std::shared_ptr HttpRequestArduino::perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) { + const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); @@ -107,9 +107,9 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur } // returned needed headers must be collected before the requests - const char *header_keys[collect_headers.size()]; + const char *header_keys[lower_case_collect_headers.size()]; int index = 0; - for (auto const &header_name : collect_headers) { + for (auto const &header_name : lower_case_collect_headers) { header_keys[index++] = header_name.c_str(); } container->client_.collectHeaders(header_keys, index); @@ -160,14 +160,14 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur // Still return the container, so it can be used to get the status code and error message } - container->response_headers_ = {}; + container->response_headers_.clear(); auto header_count = container->client_.headers(); for (int i = 0; i < header_count; i++) { const std::string header_name = str_lower_case(container->client_.headerName(i).c_str()); - if (collect_headers.count(header_name) > 0) { + if (should_collect_header(lower_case_collect_headers, header_name)) { std::string header_value = container->client_.header(i).c_str(); ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str()); - container->response_headers_[header_name].push_back(header_value); + container->response_headers_.push_back({header_name, header_value}); } } diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index a1084b12d5..dbd61de364 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -50,7 +50,7 @@ class HttpRequestArduino : public HttpRequestComponent { protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) override; + const std::vector &lower_case_collect_headers) override; }; } // namespace esphome::http_request diff --git a/esphome/components/http_request/http_request_host.cpp b/esphome/components/http_request/http_request_host.cpp index b94570be12..714a73fc31 100644 --- a/esphome/components/http_request/http_request_host.cpp +++ b/esphome/components/http_request/http_request_host.cpp @@ -19,7 +19,7 @@ static const char *const TAG = "http_request.host"; std::shared_ptr HttpRequestHost::perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &response_headers) { + const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); @@ -116,8 +116,8 @@ std::shared_ptr HttpRequestHost::perform(const std::string &url, for (auto header : response.headers) { ESP_LOGD(TAG, "Header: %s: %s", header.first.c_str(), header.second.c_str()); auto lower_name = str_lower_case(header.first); - if (response_headers.find(lower_name) != response_headers.end()) { - container->response_headers_[lower_name].emplace_back(header.second); + if (should_collect_header(lower_case_collect_headers, lower_name)) { + container->response_headers_.push_back({lower_name, header.second}); } } container->duration_ms = millis() - start; diff --git a/esphome/components/http_request/http_request_host.h b/esphome/components/http_request/http_request_host.h index 32e149e6a3..79f5b7e817 100644 --- a/esphome/components/http_request/http_request_host.h +++ b/esphome/components/http_request/http_request_host.h @@ -20,7 +20,7 @@ class HttpRequestHost : public HttpRequestComponent { public: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &response_headers) override; + const std::vector &lower_case_collect_headers) override; void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; } protected: diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 486984a694..0921c50b9f 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -19,8 +19,8 @@ namespace esphome::http_request { static const char *const TAG = "http_request.idf"; struct UserData { - const std::set &collect_headers; - std::map> response_headers; + const std::vector &lower_case_collect_headers; + std::vector
&response_headers; }; void HttpRequestIDF::dump_config() { @@ -38,10 +38,10 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { switch (evt->event_id) { case HTTP_EVENT_ON_HEADER: { const std::string header_name = str_lower_case(evt->header_key); - if (user_data->collect_headers.count(header_name)) { + if (should_collect_header(user_data->lower_case_collect_headers, header_name)) { const std::string header_value = evt->header_value; ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str()); - user_data->response_headers[header_name].push_back(header_value); + user_data->response_headers.push_back({header_name, header_value}); } break; } @@ -55,7 +55,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { std::shared_ptr HttpRequestIDF::perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) { + const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGE(TAG, "HTTP Request failed; Not connected to network"); @@ -110,8 +110,6 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c watchdog::WatchdogManager wdm(this->get_watchdog_timeout()); config.event_handler = http_event_handler; - auto user_data = UserData{collect_headers, {}}; - config.user_data = static_cast(&user_data); esp_http_client_handle_t client = esp_http_client_init(&config); @@ -120,6 +118,9 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c container->set_secure(secure); + auto user_data = UserData{lower_case_collect_headers, container->response_headers_}; + esp_http_client_set_user_data(client, static_cast(&user_data)); + for (const auto &header : request_headers) { esp_http_client_set_header(client, header.name.c_str(), header.value.c_str()); } @@ -164,7 +165,6 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); - container->set_response_headers(user_data.response_headers); container->duration_ms = millis() - start; if (is_success(container->status_code)) { return container; diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index 2a130eae58..9206ba6f5d 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -21,11 +21,8 @@ class HttpContainerIDF : public HttpContainer { /// @brief Feeds the watchdog timer if the executing task has one attached void feed_wdt(); - void set_response_headers(std::map> &response_headers) { - this->response_headers_ = std::move(response_headers); - } - protected: + friend class HttpRequestIDF; esp_http_client_handle_t client_; }; @@ -41,7 +38,7 @@ class HttpRequestIDF : public HttpRequestComponent { protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) override; + const std::vector &lower_case_collect_headers) override; // if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE uint16_t buffer_size_rx_{}; uint16_t buffer_size_tx_{}; From bd055e75b9c2def6d9e12170e771920115c9751f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 16:49:37 -0600 Subject: [PATCH 051/609] [core] Shrink Application::dump_config_at_ from size_t to uint16_t (#14053) Co-authored-by: Claude Opus 4.6 --- esphome/core/application.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 30611227a2..e0299f3db3 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -582,9 +582,6 @@ class Application { std::string name_; std::string friendly_name_; - // size_t members - size_t dump_config_at_{SIZE_MAX}; - // 4-byte members uint32_t last_loop_{0}; uint32_t loop_component_start_time_{0}; @@ -594,7 +591,8 @@ class Application { #endif // 2-byte members (grouped together for alignment) - uint16_t loop_interval_{16}; // Loop interval in ms (max 65535ms = 65.5 seconds) + uint16_t dump_config_at_{std::numeric_limits::max()}; // Index into components_ for dump_config progress + uint16_t loop_interval_{16}; // Loop interval in ms (max 65535ms = 65.5 seconds) uint16_t looping_components_active_end_{0}; // Index marking end of active components in looping_components_ uint16_t current_loop_index_{0}; // For safe reentrant modifications during iteration From 5f82017a310bd60123eadee17a5f2f3d36106694 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 19:01:00 -0600 Subject: [PATCH 052/609] [udp] Register socket consumption for CONFIG_LWIP_MAX_SOCKETS (#14068) --- esphome/components/udp/__init__.py | 63 ++++++++++++++++++------------ 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index bfaa5f2516..c9586d0b95 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -14,6 +14,7 @@ import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID from esphome.core import ID from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["network"] @@ -65,33 +66,47 @@ RELOCATED = { ) } -CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(UDPComponent), - cv.Optional(CONF_PORT, default=18511): cv.Any( - cv.port, - cv.Schema( + +def _consume_udp_sockets(config: ConfigType) -> ConfigType: + """Register socket needs for UDP component.""" + from esphome.components import socket + + # UDP uses up to 2 sockets: 1 broadcast + 1 listen + # Whether each is used depends on code generation, so register worst case + socket.consume_sockets(2, "udp")(config) + return config + + +CONFIG_SCHEMA = cv.All( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(UDPComponent), + cv.Optional(CONF_PORT, default=18511): cv.Any( + cv.port, + cv.Schema( + { + cv.Required(CONF_LISTEN_PORT): cv.port, + cv.Required(CONF_BROADCAST_PORT): cv.port, + } + ), + ), + cv.Optional( + CONF_LISTEN_ADDRESS, default="255.255.255.255" + ): cv.ipv4address_multi_broadcast, + cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( + cv.ipv4address, + ), + cv.Optional(CONF_ON_RECEIVE): automation.validate_automation( { - cv.Required(CONF_LISTEN_PORT): cv.port, - cv.Required(CONF_BROADCAST_PORT): cv.port, + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + Trigger.template(trigger_args) + ), } ), - ), - cv.Optional( - CONF_LISTEN_ADDRESS, default="255.255.255.255" - ): cv.ipv4address_multi_broadcast, - cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( - cv.ipv4address, - ), - cv.Optional(CONF_ON_RECEIVE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Trigger.template(trigger_args) - ), - } - ), - } -).extend(RELOCATED) + } + ).extend(RELOCATED), + _consume_udp_sockets, +) async def register_udp_client(var, config): From 3b869f172064dc994f2ff001e4c73c8da858b01e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 19:01:37 -0600 Subject: [PATCH 053/609] [web_server] Double socket allocation to prevent connection exhaustion (#14067) --- esphome/components/web_server/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 8b02a6baee..294a5e0a15 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -144,9 +144,10 @@ def _consume_web_server_sockets(config: ConfigType) -> ConfigType: """Register socket needs for web_server component.""" from esphome.components import socket - # Web server needs 1 listening socket + typically 2 concurrent client connections - # (browser makes 2 connections for page + event stream) - sockets_needed = 3 + # Web server needs 1 listening socket + typically 5 concurrent client connections + # (browser opens connections for page resources, SSE event stream, and POST + # requests for entity control which may linger before closing) + sockets_needed = 6 socket.consume_sockets(sockets_needed, "web_server")(config) return config From 565443b710eb8b56bd7bc2d53fef765d195e4303 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 19:08:53 -0600 Subject: [PATCH 054/609] [pulse_counter] Fix compilation on ESP32-C6/C5/H2/P4 (#14070) --- esphome/components/pulse_counter/pulse_counter_sensor.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index 8ac5a28d8f..ef4cc980f6 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -2,7 +2,7 @@ #include "esphome/core/log.h" #ifdef HAS_PCNT -#include +#include #include #endif @@ -117,9 +117,7 @@ bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { } if (this->filter_us != 0) { - uint32_t apb_freq; - esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_APB, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &apb_freq); - uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / apb_freq; + uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / (uint32_t) esp_clk_apb_freq(); pcnt_glitch_filter_config_t filter_config = { .max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns), }; From be853afc2465a291d3a4806122884c52d0bd905e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 20:57:56 -0600 Subject: [PATCH 055/609] [core] Conditionally compile setup_priority override infrastructure (#14057) --- esphome/core/application.cpp | 2 ++ esphome/core/component.cpp | 19 +++++++++++++------ esphome/core/component.h | 1 + esphome/core/defines.h | 1 + esphome/cpp_helpers.py | 3 ++- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 406885fd81..b216233f9b 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -137,8 +137,10 @@ void Application::setup() { ESP_LOGI(TAG, "setup() finished successfully!"); +#ifdef USE_SETUP_PRIORITY_OVERRIDE // Clear setup priority overrides to free memory clear_setup_priority_overrides(); +#endif #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) // Set up wake socket for waking main loop from tasks diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 47c4a70c0f..ba0f1663b9 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -41,20 +41,23 @@ struct ComponentErrorMessage { bool is_flash_ptr; }; +#ifdef USE_SETUP_PRIORITY_OVERRIDE struct ComponentPriorityOverride { const Component *component; float priority; }; +// Setup priority overrides - freed after setup completes +// Using raw pointer instead of unique_ptr to avoid global constructor/destructor overhead +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +std::vector *setup_priority_overrides = nullptr; +#endif + // Error messages for failed components // Using raw pointer instead of unique_ptr to avoid global constructor/destructor overhead // This is never freed as error messages persist for the lifetime of the device // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) std::vector *component_error_messages = nullptr; -// Setup priority overrides - freed after setup completes -// Using raw pointer instead of unique_ptr to avoid global constructor/destructor overhead -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -std::vector *setup_priority_overrides = nullptr; // Helper to store error messages - reduces duplication between deprecated and new API // Remove before 2026.6.0 when deprecated const char* API is removed @@ -459,6 +462,7 @@ void log_update_interval(const char *tag, PollingComponent *component) { } } float Component::get_actual_setup_priority() const { +#ifdef USE_SETUP_PRIORITY_OVERRIDE // Check if there's an override in the global vector if (setup_priority_overrides) { // Linear search is fine for small n (typically < 5 overrides) @@ -468,14 +472,14 @@ float Component::get_actual_setup_priority() const { } } } +#endif return this->get_setup_priority(); } +#ifdef USE_SETUP_PRIORITY_OVERRIDE void Component::set_setup_priority(float priority) { // Lazy allocate the vector if needed if (!setup_priority_overrides) { setup_priority_overrides = new std::vector(); - // Reserve some space to avoid reallocations (most configs have < 10 overrides) - setup_priority_overrides->reserve(10); } // Check if this component already has an override @@ -489,6 +493,7 @@ void Component::set_setup_priority(float priority) { // Add new override setup_priority_overrides->emplace_back(ComponentPriorityOverride{this, priority}); } +#endif bool Component::has_overridden_loop() const { #if defined(USE_HOST) || defined(CLANG_TIDY) @@ -557,10 +562,12 @@ uint32_t WarnIfComponentBlockingGuard::finish() { WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {} +#ifdef USE_SETUP_PRIORITY_OVERRIDE void clear_setup_priority_overrides() { // Free the setup priority map completely delete setup_priority_overrides; setup_priority_overrides = nullptr; } +#endif } // namespace esphome diff --git a/esphome/core/component.h b/esphome/core/component.h index 848bc0ba35..6f7f77dbc1 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -563,6 +563,7 @@ class WarnIfComponentBlockingGuard { }; // Function to clear setup priority overrides after all components are set up +// Only has an implementation when USE_SETUP_PRIORITY_OVERRIDE is defined void clear_setup_priority_overrides(); } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 0d6c1a42e8..bcafcb4c60 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -109,6 +109,7 @@ #define USE_SAFE_MODE_CALLBACK #define USE_SELECT #define USE_SENSOR +#define USE_SETUP_PRIORITY_OVERRIDE #define USE_STATUS_LED #define USE_STATUS_SENSOR #define USE_SWITCH diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 2698b9b3d5..954a28d3d1 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -9,7 +9,7 @@ from esphome.const import ( ) from esphome.core import CORE, ID, coroutine from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import LogStringLiteral, add, get_variable +from esphome.cpp_generator import LogStringLiteral, add, add_define, get_variable from esphome.cpp_types import App from esphome.types import ConfigFragmentType, ConfigType from esphome.util import Registry, RegistryEntry @@ -49,6 +49,7 @@ async def register_component(var, config): ) CORE.component_ids.remove(id_) if CONF_SETUP_PRIORITY in config: + add_define("USE_SETUP_PRIORITY_OVERRIDE") add(var.set_setup_priority(config[CONF_SETUP_PRIORITY])) if CONF_UPDATE_INTERVAL in config: add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) From e4c233b6ceef1d1a306e5879752ffc9815d1190b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 20:59:31 -0600 Subject: [PATCH 056/609] [mqtt] Use constexpr for compile-time constants (#14075) --- esphome/components/mqtt/mqtt_backend_esp32.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index adba0cf004..ccc4c4026c 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -114,11 +114,11 @@ struct QueueElement { class MQTTBackendESP32 final : public MQTTBackend { public: - static const size_t MQTT_BUFFER_SIZE = 4096; - static const size_t TASK_STACK_SIZE = 3072; - static const size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations - static const ssize_t TASK_PRIORITY = 5; - static const uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360 + static constexpr size_t MQTT_BUFFER_SIZE = 4096; + static constexpr size_t TASK_STACK_SIZE = 3072; + static constexpr size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations + static constexpr ssize_t TASK_PRIORITY = 5; + static constexpr uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360 void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; } void set_client_id(const char *client_id) final { this->client_id_ = client_id; } From 66d2ac8cb93a5b7380210ee7933d28620dabd3ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:00:09 -0600 Subject: [PATCH 057/609] [web_server] Move climate static traits to DETAIL_ALL only (#14066) --- esphome/components/web_server/web_server.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 3acd2d2119..a796c1426b 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1546,16 +1546,16 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con for (auto const &custom_preset : traits.get_supported_custom_presets()) opt.add(custom_preset); } + root[ESPHOME_F("max_temp")] = + (value_accuracy_to_buf(temp_buf, traits.get_visual_max_temperature(), target_accuracy), temp_buf); + root[ESPHOME_F("min_temp")] = + (value_accuracy_to_buf(temp_buf, traits.get_visual_min_temperature(), target_accuracy), temp_buf); + root[ESPHOME_F("step")] = traits.get_visual_target_temperature_step(); this->add_sorting_info_(root, obj); } bool has_state = false; root[ESPHOME_F("mode")] = PSTR_LOCAL(climate_mode_to_string(obj->mode)); - root[ESPHOME_F("max_temp")] = - (value_accuracy_to_buf(temp_buf, traits.get_visual_max_temperature(), target_accuracy), temp_buf); - root[ESPHOME_F("min_temp")] = - (value_accuracy_to_buf(temp_buf, traits.get_visual_min_temperature(), target_accuracy), temp_buf); - root[ESPHOME_F("step")] = traits.get_visual_target_temperature_step(); if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { root[ESPHOME_F("action")] = PSTR_LOCAL(climate_action_to_string(obj->action)); root[ESPHOME_F("state")] = root[ESPHOME_F("action")]; From 7e118178b3d5e208eab87e0f882b38dcb18e5ed6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:00:24 -0600 Subject: [PATCH 058/609] [web_server] Fix water_heater JSON key names and move traits to DETAIL_ALL (#14064) --- esphome/components/web_server/web_server.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index a796c1426b..e3d131f58e 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1922,6 +1922,9 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe JsonArray modes = root[ESPHOME_F("modes")].to(); for (auto m : traits.get_supported_modes()) modes.add(PSTR_LOCAL(water_heater::water_heater_mode_to_string(m))); + root[ESPHOME_F("min_temp")] = traits.get_min_temperature(); + root[ESPHOME_F("max_temp")] = traits.get_max_temperature(); + root[ESPHOME_F("step")] = traits.get_target_temperature_step(); this->add_sorting_info_(root, obj); } @@ -1944,10 +1947,6 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe root[ESPHOME_F("target_temperature")] = target; } - root[ESPHOME_F("min_temperature")] = traits.get_min_temperature(); - root[ESPHOME_F("max_temperature")] = traits.get_max_temperature(); - root[ESPHOME_F("step")] = traits.get_target_temperature_step(); - if (traits.get_supports_away_mode()) { root[ESPHOME_F("away")] = obj->is_away(); } From 9c9365c146064177dbc371d425307f8e2cb728d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:07:06 -0600 Subject: [PATCH 059/609] [bluetooth_proxy][esp32_ble_client][esp32_ble_server] Use constexpr for compile-time constants (#14073) --- esphome/components/bluetooth_proxy/bluetooth_proxy.h | 4 ++-- .../components/esp32_ble_client/ble_client_base.cpp | 12 ++++++------ .../components/esp32_ble_server/ble_characteristic.h | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index ab9aee2d81..62a035e79f 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -35,8 +35,8 @@ using namespace esp32_ble_client; // Version 3: New connection API // Version 4: Pairing support // Version 5: Cache clear support -static const uint32_t LEGACY_ACTIVE_CONNECTIONS_VERSION = 5; -static const uint32_t LEGACY_PASSIVE_ONLY_VERSION = 1; +static constexpr uint32_t LEGACY_ACTIVE_CONNECTIONS_VERSION = 5; +static constexpr uint32_t LEGACY_PASSIVE_ONLY_VERSION = 1; enum BluetoothProxyFeature : uint32_t { FEATURE_PASSIVE_SCAN = 1 << 0, diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index c464c89390..3f0eeeab4a 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -16,17 +16,17 @@ static const char *const TAG = "esp32_ble_client"; // Intermediate connection parameters for standard operation // ESP-IDF defaults (12.5-15ms) are too slow for stable connections through WiFi-based BLE proxies, // causing disconnections. These medium parameters balance responsiveness with bandwidth usage. -static const uint16_t MEDIUM_MIN_CONN_INTERVAL = 0x07; // 7 * 1.25ms = 8.75ms -static const uint16_t MEDIUM_MAX_CONN_INTERVAL = 0x09; // 9 * 1.25ms = 11.25ms +static constexpr uint16_t MEDIUM_MIN_CONN_INTERVAL = 0x07; // 7 * 1.25ms = 8.75ms +static constexpr uint16_t MEDIUM_MAX_CONN_INTERVAL = 0x09; // 9 * 1.25ms = 11.25ms // The timeout value was increased from 6s to 8s to address stability issues observed // in certain BLE devices when operating through WiFi-based BLE proxies. The longer // timeout reduces the likelihood of disconnections during periods of high latency. -static const uint16_t MEDIUM_CONN_TIMEOUT = 800; // 800 * 10ms = 8s +static constexpr uint16_t MEDIUM_CONN_TIMEOUT = 800; // 800 * 10ms = 8s // Fastest connection parameters for devices with short discovery timeouts -static const uint16_t FAST_MIN_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms (BLE minimum) -static const uint16_t FAST_MAX_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms -static const uint16_t FAST_CONN_TIMEOUT = 1000; // 1000 * 10ms = 10s +static constexpr uint16_t FAST_MIN_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms (BLE minimum) +static constexpr uint16_t FAST_MAX_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms +static constexpr uint16_t FAST_CONN_TIMEOUT = 1000; // 1000 * 10ms = 10s static const esp_bt_uuid_t NOTIFY_DESC_UUID = { .len = ESP_UUID_LEN_16, .uuid = diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index c2cdb1660c..72897d1dfb 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -57,12 +57,12 @@ class BLECharacteristic { ESPBTUUID get_uuid() { return this->uuid_; } std::vector &get_value() { return this->value_; } - static const uint32_t PROPERTY_READ = 1 << 0; - static const uint32_t PROPERTY_WRITE = 1 << 1; - static const uint32_t PROPERTY_NOTIFY = 1 << 2; - static const uint32_t PROPERTY_BROADCAST = 1 << 3; - static const uint32_t PROPERTY_INDICATE = 1 << 4; - static const uint32_t PROPERTY_WRITE_NR = 1 << 5; + static constexpr uint32_t PROPERTY_READ = 1 << 0; + static constexpr uint32_t PROPERTY_WRITE = 1 << 1; + static constexpr uint32_t PROPERTY_NOTIFY = 1 << 2; + static constexpr uint32_t PROPERTY_BROADCAST = 1 << 3; + static constexpr uint32_t PROPERTY_INDICATE = 1 << 4; + static constexpr uint32_t PROPERTY_WRITE_NR = 1 << 5; bool is_created(); bool is_failed(); From 76c151c6e6948fbe2f5c62356ef299797cc83480 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:07:38 -0600 Subject: [PATCH 060/609] [api] Use constexpr for compile-time constant (#14072) --- esphome/components/api/api_frame_helper_noise.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 1ae848dead..492988128a 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -474,7 +474,7 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s // buf_start[1], buf_start[2] to be set after encryption // Write message header (to be encrypted) - const uint8_t msg_offset = 3; + constexpr uint8_t msg_offset = 3; buf_start[msg_offset] = static_cast(msg.message_type >> 8); // type high byte buf_start[msg_offset + 1] = static_cast(msg.message_type); // type low byte buf_start[msg_offset + 2] = static_cast(msg.payload_size >> 8); // data_len high byte From ee7d63f73a3587cc86c6d6fe71e5fcd3df4e31b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:09:49 -0600 Subject: [PATCH 061/609] [packet_transport] Use constexpr for compile-time constants (#14074) --- esphome/components/packet_transport/packet_transport.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 365a5f2ec7..7b7a852398 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -58,9 +58,9 @@ union FuData { float f32; }; -static const uint16_t MAGIC_NUMBER = 0x4553; -static const uint16_t MAGIC_PING = 0x5048; -static const uint32_t PREF_HASH = 0x45535043; +static constexpr uint16_t MAGIC_NUMBER = 0x4553; +static constexpr uint16_t MAGIC_PING = 0x5048; +static constexpr uint32_t PREF_HASH = 0x45535043; enum DataKey { ZERO_FILL_KEY, DATA_KEY, From 20239d1bb304ceea6f7056c0466071178ad8af50 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:16:09 -0600 Subject: [PATCH 062/609] [remote_base] Use constexpr for compile-time constants (#14076) --- .../remote_base/abbwelcome_protocol.cpp | 8 +++---- .../remote_base/abbwelcome_protocol.h | 4 ++-- .../components/remote_base/aeha_protocol.cpp | 14 +++++------ .../remote_base/byronsx_protocol.cpp | 10 ++++---- .../remote_base/canalsat_protocol.cpp | 8 +++---- .../components/remote_base/dish_protocol.cpp | 10 ++++---- .../components/remote_base/dooya_protocol.cpp | 12 +++++----- .../remote_base/drayton_protocol.cpp | 22 ++++++++--------- .../components/remote_base/jvc_protocol.cpp | 12 +++++----- .../remote_base/keeloq_protocol.cpp | 22 ++++++++--------- .../components/remote_base/lg_protocol.cpp | 10 ++++---- .../remote_base/magiquest_protocol.cpp | 10 ++++---- .../components/remote_base/midea_protocol.h | 2 +- .../components/remote_base/nec_protocol.cpp | 10 ++++---- .../components/remote_base/nexa_protocol.cpp | 22 ++++++++--------- .../remote_base/panasonic_protocol.cpp | 10 ++++---- .../remote_base/pioneer_protocol.cpp | 12 +++++----- .../remote_base/pronto_protocol.cpp | 24 +++++++++---------- .../components/remote_base/rc5_protocol.cpp | 4 ++-- .../components/remote_base/rc6_protocol.cpp | 10 ++++---- .../remote_base/roomba_protocol.cpp | 10 ++++---- .../remote_base/samsung36_protocol.cpp | 20 ++++++++-------- .../remote_base/samsung_protocol.cpp | 14 +++++------ .../components/remote_base/sony_protocol.cpp | 10 ++++---- .../remote_base/symphony_protocol.cpp | 14 +++++------ .../remote_base/toshiba_ac_protocol.cpp | 16 ++++++------- .../components/remote_base/toto_protocol.cpp | 12 +++++----- 27 files changed, 166 insertions(+), 166 deletions(-) diff --git a/esphome/components/remote_base/abbwelcome_protocol.cpp b/esphome/components/remote_base/abbwelcome_protocol.cpp index 352ae10ed7..a67ca48dbe 100644 --- a/esphome/components/remote_base/abbwelcome_protocol.cpp +++ b/esphome/components/remote_base/abbwelcome_protocol.cpp @@ -6,10 +6,10 @@ namespace remote_base { static const char *const TAG = "remote.abbwelcome"; -static const uint32_t BIT_ONE_SPACE_US = 102; -static const uint32_t BIT_ZERO_MARK_US = 32; // 18-44 -static const uint32_t BIT_ZERO_SPACE_US = BIT_ONE_SPACE_US - BIT_ZERO_MARK_US; -static const uint16_t BYTE_SPACE_US = 210; +static constexpr uint32_t BIT_ONE_SPACE_US = 102; +static constexpr uint32_t BIT_ZERO_MARK_US = 32; // 18-44 +static constexpr uint32_t BIT_ZERO_SPACE_US = BIT_ONE_SPACE_US - BIT_ZERO_MARK_US; +static constexpr uint16_t BYTE_SPACE_US = 210; uint8_t ABBWelcomeData::calc_cs_() const { uint8_t checksum = 0; diff --git a/esphome/components/remote_base/abbwelcome_protocol.h b/esphome/components/remote_base/abbwelcome_protocol.h index 1dddedf8ce..66664a89f3 100644 --- a/esphome/components/remote_base/abbwelcome_protocol.h +++ b/esphome/components/remote_base/abbwelcome_protocol.h @@ -11,8 +11,8 @@ namespace esphome { namespace remote_base { -static const uint8_t MAX_DATA_LENGTH = 15; -static const uint8_t DATA_LENGTH_MASK = 0x3f; +static constexpr uint8_t MAX_DATA_LENGTH = 15; +static constexpr uint8_t DATA_LENGTH_MASK = 0x3f; /* Message Format: diff --git a/esphome/components/remote_base/aeha_protocol.cpp b/esphome/components/remote_base/aeha_protocol.cpp index 3b926e7981..f40cff7623 100644 --- a/esphome/components/remote_base/aeha_protocol.cpp +++ b/esphome/components/remote_base/aeha_protocol.cpp @@ -7,13 +7,13 @@ namespace remote_base { static const char *const TAG = "remote.aeha"; -static const uint16_t BITWISE = 425; -static const uint16_t HEADER_HIGH_US = BITWISE * 8; -static const uint16_t HEADER_LOW_US = BITWISE * 4; -static const uint16_t BIT_HIGH_US = BITWISE; -static const uint16_t BIT_ONE_LOW_US = BITWISE * 3; -static const uint16_t BIT_ZERO_LOW_US = BITWISE; -static const uint16_t TRAILER = BITWISE; +static constexpr uint16_t BITWISE = 425; +static constexpr uint16_t HEADER_HIGH_US = BITWISE * 8; +static constexpr uint16_t HEADER_LOW_US = BITWISE * 4; +static constexpr uint16_t BIT_HIGH_US = BITWISE; +static constexpr uint16_t BIT_ONE_LOW_US = BITWISE * 3; +static constexpr uint16_t BIT_ZERO_LOW_US = BITWISE; +static constexpr uint16_t TRAILER = BITWISE; void AEHAProtocol::encode(RemoteTransmitData *dst, const AEHAData &data) { dst->reserve(2 + 32 + (data.data.size() * 2) + 1); diff --git a/esphome/components/remote_base/byronsx_protocol.cpp b/esphome/components/remote_base/byronsx_protocol.cpp index 6bfa4b7ff9..f243b5bdfd 100644 --- a/esphome/components/remote_base/byronsx_protocol.cpp +++ b/esphome/components/remote_base/byronsx_protocol.cpp @@ -8,11 +8,11 @@ namespace remote_base { static const char *const TAG = "remote.byronsx"; -static const uint32_t BIT_TIME_US = 333; -static const uint8_t NBITS_ADDRESS = 8; -static const uint8_t NBITS_COMMAND = 4; -static const uint8_t NBITS_START_BIT = 1; -static const uint8_t NBITS_DATA = NBITS_ADDRESS + NBITS_COMMAND /*+ NBITS_COMMAND*/; +static constexpr uint32_t BIT_TIME_US = 333; +static constexpr uint8_t NBITS_ADDRESS = 8; +static constexpr uint8_t NBITS_COMMAND = 4; +static constexpr uint8_t NBITS_START_BIT = 1; +static constexpr uint8_t NBITS_DATA = NBITS_ADDRESS + NBITS_COMMAND /*+ NBITS_COMMAND*/; /* ByronSX Protocol diff --git a/esphome/components/remote_base/canalsat_protocol.cpp b/esphome/components/remote_base/canalsat_protocol.cpp index bee3d57fd8..1468b66939 100644 --- a/esphome/components/remote_base/canalsat_protocol.cpp +++ b/esphome/components/remote_base/canalsat_protocol.cpp @@ -7,10 +7,10 @@ namespace remote_base { static const char *const CANALSAT_TAG = "remote.canalsat"; static const char *const CANALSATLD_TAG = "remote.canalsatld"; -static const uint16_t CANALSAT_FREQ = 55500; -static const uint16_t CANALSATLD_FREQ = 56000; -static const uint16_t CANALSAT_UNIT = 250; -static const uint16_t CANALSATLD_UNIT = 320; +static constexpr uint16_t CANALSAT_FREQ = 55500; +static constexpr uint16_t CANALSATLD_FREQ = 56000; +static constexpr uint16_t CANALSAT_UNIT = 250; +static constexpr uint16_t CANALSATLD_UNIT = 320; CanalSatProtocol::CanalSatProtocol() { this->frequency_ = CANALSAT_FREQ; diff --git a/esphome/components/remote_base/dish_protocol.cpp b/esphome/components/remote_base/dish_protocol.cpp index 754b6c3b12..69226101bf 100644 --- a/esphome/components/remote_base/dish_protocol.cpp +++ b/esphome/components/remote_base/dish_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.dish"; -static const uint32_t HEADER_HIGH_US = 400; -static const uint32_t HEADER_LOW_US = 6100; -static const uint32_t BIT_HIGH_US = 400; -static const uint32_t BIT_ONE_LOW_US = 1700; -static const uint32_t BIT_ZERO_LOW_US = 2800; +static constexpr uint32_t HEADER_HIGH_US = 400; +static constexpr uint32_t HEADER_LOW_US = 6100; +static constexpr uint32_t BIT_HIGH_US = 400; +static constexpr uint32_t BIT_ONE_LOW_US = 1700; +static constexpr uint32_t BIT_ZERO_LOW_US = 2800; void DishProtocol::encode(RemoteTransmitData *dst, const DishData &data) { dst->reserve(138); diff --git a/esphome/components/remote_base/dooya_protocol.cpp b/esphome/components/remote_base/dooya_protocol.cpp index 04c5fef8f3..84bdbf3e08 100644 --- a/esphome/components/remote_base/dooya_protocol.cpp +++ b/esphome/components/remote_base/dooya_protocol.cpp @@ -6,12 +6,12 @@ namespace remote_base { static const char *const TAG = "remote.dooya"; -static const uint32_t HEADER_HIGH_US = 5000; -static const uint32_t HEADER_LOW_US = 1500; -static const uint32_t BIT_ZERO_HIGH_US = 350; -static const uint32_t BIT_ZERO_LOW_US = 750; -static const uint32_t BIT_ONE_HIGH_US = 750; -static const uint32_t BIT_ONE_LOW_US = 350; +static constexpr uint32_t HEADER_HIGH_US = 5000; +static constexpr uint32_t HEADER_LOW_US = 1500; +static constexpr uint32_t BIT_ZERO_HIGH_US = 350; +static constexpr uint32_t BIT_ZERO_LOW_US = 750; +static constexpr uint32_t BIT_ONE_HIGH_US = 750; +static constexpr uint32_t BIT_ONE_LOW_US = 350; void DooyaProtocol::encode(RemoteTransmitData *dst, const DooyaData &data) { dst->set_carrier_frequency(0); diff --git a/esphome/components/remote_base/drayton_protocol.cpp b/esphome/components/remote_base/drayton_protocol.cpp index da2e985af0..946bd9cacb 100644 --- a/esphome/components/remote_base/drayton_protocol.cpp +++ b/esphome/components/remote_base/drayton_protocol.cpp @@ -8,18 +8,18 @@ namespace remote_base { static const char *const TAG = "remote.drayton"; -static const uint32_t BIT_TIME_US = 500; -static const uint8_t CARRIER_KHZ = 2; -static const uint8_t NBITS_PREAMBLE = 12; -static const uint8_t NBITS_SYNC = 4; -static const uint8_t NBITS_ADDRESS = 16; -static const uint8_t NBITS_CHANNEL = 5; -static const uint8_t NBITS_COMMAND = 7; -static const uint8_t NDATABITS = NBITS_ADDRESS + NBITS_CHANNEL + NBITS_COMMAND; -static const uint8_t MIN_RX_SRC = (NDATABITS + NBITS_SYNC / 2); +static constexpr uint32_t BIT_TIME_US = 500; +static constexpr uint8_t CARRIER_KHZ = 2; +static constexpr uint8_t NBITS_PREAMBLE = 12; +static constexpr uint8_t NBITS_SYNC = 4; +static constexpr uint8_t NBITS_ADDRESS = 16; +static constexpr uint8_t NBITS_CHANNEL = 5; +static constexpr uint8_t NBITS_COMMAND = 7; +static constexpr uint8_t NDATABITS = NBITS_ADDRESS + NBITS_CHANNEL + NBITS_COMMAND; +static constexpr uint8_t MIN_RX_SRC = (NDATABITS + NBITS_SYNC / 2); -static const uint8_t CMD_ON = 0x41; -static const uint8_t CMD_OFF = 0x02; +static constexpr uint8_t CMD_ON = 0x41; +static constexpr uint8_t CMD_OFF = 0x02; /* Drayton Protocol diff --git a/esphome/components/remote_base/jvc_protocol.cpp b/esphome/components/remote_base/jvc_protocol.cpp index ca423b61e6..c33cae7a48 100644 --- a/esphome/components/remote_base/jvc_protocol.cpp +++ b/esphome/components/remote_base/jvc_protocol.cpp @@ -6,12 +6,12 @@ namespace remote_base { static const char *const TAG = "remote.jvc"; -static const uint8_t NBITS = 16; -static const uint32_t HEADER_HIGH_US = 8400; -static const uint32_t HEADER_LOW_US = 4200; -static const uint32_t BIT_ONE_LOW_US = 1725; -static const uint32_t BIT_ZERO_LOW_US = 525; -static const uint32_t BIT_HIGH_US = 525; +static constexpr uint8_t NBITS = 16; +static constexpr uint32_t HEADER_HIGH_US = 8400; +static constexpr uint32_t HEADER_LOW_US = 4200; +static constexpr uint32_t BIT_ONE_LOW_US = 1725; +static constexpr uint32_t BIT_ZERO_LOW_US = 525; +static constexpr uint32_t BIT_HIGH_US = 525; void JVCProtocol::encode(RemoteTransmitData *dst, const JVCData &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/keeloq_protocol.cpp b/esphome/components/remote_base/keeloq_protocol.cpp index 72540c37f1..e95c79ef25 100644 --- a/esphome/components/remote_base/keeloq_protocol.cpp +++ b/esphome/components/remote_base/keeloq_protocol.cpp @@ -8,18 +8,18 @@ namespace remote_base { static const char *const TAG = "remote.keeloq"; -static const uint32_t BIT_TIME_US = 380; -static const uint8_t NBITS_PREAMBLE = 12; -static const uint8_t NBITS_REPEAT = 1; -static const uint8_t NBITS_VLOW = 1; -static const uint8_t NBITS_SERIAL = 28; -static const uint8_t NBITS_BUTTONS = 4; -static const uint8_t NBITS_DISC = 12; -static const uint8_t NBITS_SYNC_CNT = 16; +static constexpr uint32_t BIT_TIME_US = 380; +static constexpr uint8_t NBITS_PREAMBLE = 12; +static constexpr uint8_t NBITS_REPEAT = 1; +static constexpr uint8_t NBITS_VLOW = 1; +static constexpr uint8_t NBITS_SERIAL = 28; +static constexpr uint8_t NBITS_BUTTONS = 4; +static constexpr uint8_t NBITS_DISC = 12; +static constexpr uint8_t NBITS_SYNC_CNT = 16; -static const uint8_t NBITS_FIXED_DATA = NBITS_REPEAT + NBITS_VLOW + NBITS_BUTTONS + NBITS_SERIAL; -static const uint8_t NBITS_ENCRYPTED_DATA = NBITS_BUTTONS + NBITS_DISC + NBITS_SYNC_CNT; -static const uint8_t NBITS_DATA = NBITS_FIXED_DATA + NBITS_ENCRYPTED_DATA; +static constexpr uint8_t NBITS_FIXED_DATA = NBITS_REPEAT + NBITS_VLOW + NBITS_BUTTONS + NBITS_SERIAL; +static constexpr uint8_t NBITS_ENCRYPTED_DATA = NBITS_BUTTONS + NBITS_DISC + NBITS_SYNC_CNT; +static constexpr uint8_t NBITS_DATA = NBITS_FIXED_DATA + NBITS_ENCRYPTED_DATA; /* KeeLoq Protocol diff --git a/esphome/components/remote_base/lg_protocol.cpp b/esphome/components/remote_base/lg_protocol.cpp index d25c59d2b1..4c54ff00bd 100644 --- a/esphome/components/remote_base/lg_protocol.cpp +++ b/esphome/components/remote_base/lg_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.lg"; -static const uint32_t HEADER_HIGH_US = 8000; -static const uint32_t HEADER_LOW_US = 4000; -static const uint32_t BIT_HIGH_US = 600; -static const uint32_t BIT_ONE_LOW_US = 1600; -static const uint32_t BIT_ZERO_LOW_US = 550; +static constexpr uint32_t HEADER_HIGH_US = 8000; +static constexpr uint32_t HEADER_LOW_US = 4000; +static constexpr uint32_t BIT_HIGH_US = 600; +static constexpr uint32_t BIT_ONE_LOW_US = 1600; +static constexpr uint32_t BIT_ZERO_LOW_US = 550; void LGProtocol::encode(RemoteTransmitData *dst, const LGData &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/magiquest_protocol.cpp b/esphome/components/remote_base/magiquest_protocol.cpp index 1cec58a55f..f25a982fdc 100644 --- a/esphome/components/remote_base/magiquest_protocol.cpp +++ b/esphome/components/remote_base/magiquest_protocol.cpp @@ -10,11 +10,11 @@ namespace remote_base { static const char *const TAG = "remote.magiquest"; -static const uint32_t MAGIQUEST_UNIT = 288; // us -static const uint32_t MAGIQUEST_ONE_MARK = 2 * MAGIQUEST_UNIT; -static const uint32_t MAGIQUEST_ONE_SPACE = 2 * MAGIQUEST_UNIT; -static const uint32_t MAGIQUEST_ZERO_MARK = MAGIQUEST_UNIT; -static const uint32_t MAGIQUEST_ZERO_SPACE = 3 * MAGIQUEST_UNIT; +static constexpr uint32_t MAGIQUEST_UNIT = 288; // us +static constexpr uint32_t MAGIQUEST_ONE_MARK = 2 * MAGIQUEST_UNIT; +static constexpr uint32_t MAGIQUEST_ONE_SPACE = 2 * MAGIQUEST_UNIT; +static constexpr uint32_t MAGIQUEST_ZERO_MARK = MAGIQUEST_UNIT; +static constexpr uint32_t MAGIQUEST_ZERO_SPACE = 3 * MAGIQUEST_UNIT; void MagiQuestProtocol::encode(RemoteTransmitData *dst, const MagiQuestData &data) { dst->reserve(101); // 2 start bits, 48 data bits, 1 stop bit diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h index ddefff867a..334e8a7cb3 100644 --- a/esphome/components/remote_base/midea_protocol.h +++ b/esphome/components/remote_base/midea_protocol.h @@ -62,7 +62,7 @@ class MideaData { this->data_[idx] |= (value << shift); } void set_mask_(uint8_t idx, bool state, uint8_t mask = 255) { this->set_value_(idx, state ? mask : 0, mask); } - static const uint8_t OFFSET_CS = 5; + static constexpr uint8_t OFFSET_CS = 5; // 48-bits data std::array data_; // Calculate checksum diff --git a/esphome/components/remote_base/nec_protocol.cpp b/esphome/components/remote_base/nec_protocol.cpp index 6ea9a8583c..062f81b4d6 100644 --- a/esphome/components/remote_base/nec_protocol.cpp +++ b/esphome/components/remote_base/nec_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.nec"; -static const uint32_t HEADER_HIGH_US = 9000; -static const uint32_t HEADER_LOW_US = 4500; -static const uint32_t BIT_HIGH_US = 560; -static const uint32_t BIT_ONE_LOW_US = 1690; -static const uint32_t BIT_ZERO_LOW_US = 560; +static constexpr uint32_t HEADER_HIGH_US = 9000; +static constexpr uint32_t HEADER_LOW_US = 4500; +static constexpr uint32_t BIT_HIGH_US = 560; +static constexpr uint32_t BIT_ONE_LOW_US = 1690; +static constexpr uint32_t BIT_ZERO_LOW_US = 560; void NECProtocol::encode(RemoteTransmitData *dst, const NECData &data) { ESP_LOGD(TAG, "Sending NEC: address=0x%04X, command=0x%04X command_repeats=%d", data.address, data.command, diff --git a/esphome/components/remote_base/nexa_protocol.cpp b/esphome/components/remote_base/nexa_protocol.cpp index 862165714e..28b415d4d6 100644 --- a/esphome/components/remote_base/nexa_protocol.cpp +++ b/esphome/components/remote_base/nexa_protocol.cpp @@ -6,18 +6,18 @@ namespace remote_base { static const char *const TAG = "remote.nexa"; -static const uint8_t NBITS = 32; -static const uint32_t HEADER_HIGH_US = 319; -static const uint32_t HEADER_LOW_US = 2610; -static const uint32_t BIT_HIGH_US = 319; -static const uint32_t BIT_ONE_LOW_US = 1000; -static const uint32_t BIT_ZERO_LOW_US = 140; +static constexpr uint8_t NBITS = 32; +static constexpr uint32_t HEADER_HIGH_US = 319; +static constexpr uint32_t HEADER_LOW_US = 2610; +static constexpr uint32_t BIT_HIGH_US = 319; +static constexpr uint32_t BIT_ONE_LOW_US = 1000; +static constexpr uint32_t BIT_ZERO_LOW_US = 140; -static const uint32_t TX_HEADER_HIGH_US = 250; -static const uint32_t TX_HEADER_LOW_US = TX_HEADER_HIGH_US * 10; -static const uint32_t TX_BIT_HIGH_US = 250; -static const uint32_t TX_BIT_ONE_LOW_US = TX_BIT_HIGH_US * 5; -static const uint32_t TX_BIT_ZERO_LOW_US = TX_BIT_HIGH_US * 1; +static constexpr uint32_t TX_HEADER_HIGH_US = 250; +static constexpr uint32_t TX_HEADER_LOW_US = TX_HEADER_HIGH_US * 10; +static constexpr uint32_t TX_BIT_HIGH_US = 250; +static constexpr uint32_t TX_BIT_ONE_LOW_US = TX_BIT_HIGH_US * 5; +static constexpr uint32_t TX_BIT_ZERO_LOW_US = TX_BIT_HIGH_US * 1; void NexaProtocol::one(RemoteTransmitData *dst) const { // '1' => '10' diff --git a/esphome/components/remote_base/panasonic_protocol.cpp b/esphome/components/remote_base/panasonic_protocol.cpp index d6cf1a160d..e0acc42692 100644 --- a/esphome/components/remote_base/panasonic_protocol.cpp +++ b/esphome/components/remote_base/panasonic_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.panasonic"; -static const uint32_t HEADER_HIGH_US = 3502; -static const uint32_t HEADER_LOW_US = 1750; -static const uint32_t BIT_HIGH_US = 502; -static const uint32_t BIT_ZERO_LOW_US = 400; -static const uint32_t BIT_ONE_LOW_US = 1244; +static constexpr uint32_t HEADER_HIGH_US = 3502; +static constexpr uint32_t HEADER_LOW_US = 1750; +static constexpr uint32_t BIT_HIGH_US = 502; +static constexpr uint32_t BIT_ZERO_LOW_US = 400; +static constexpr uint32_t BIT_ONE_LOW_US = 1244; void PanasonicProtocol::encode(RemoteTransmitData *dst, const PanasonicData &data) { dst->reserve(100); diff --git a/esphome/components/remote_base/pioneer_protocol.cpp b/esphome/components/remote_base/pioneer_protocol.cpp index 043565282d..f350ef66ae 100644 --- a/esphome/components/remote_base/pioneer_protocol.cpp +++ b/esphome/components/remote_base/pioneer_protocol.cpp @@ -6,12 +6,12 @@ namespace remote_base { static const char *const TAG = "remote.pioneer"; -static const uint32_t HEADER_HIGH_US = 9000; -static const uint32_t HEADER_LOW_US = 4500; -static const uint32_t BIT_HIGH_US = 560; -static const uint32_t BIT_ONE_LOW_US = 1690; -static const uint32_t BIT_ZERO_LOW_US = 560; -static const uint32_t TRAILER_SPACE_US = 25500; +static constexpr uint32_t HEADER_HIGH_US = 9000; +static constexpr uint32_t HEADER_LOW_US = 4500; +static constexpr uint32_t BIT_HIGH_US = 560; +static constexpr uint32_t BIT_ONE_LOW_US = 1690; +static constexpr uint32_t BIT_ZERO_LOW_US = 560; +static constexpr uint32_t TRAILER_SPACE_US = 25500; void PioneerProtocol::encode(RemoteTransmitData *dst, const PioneerData &data) { uint32_t address1 = ((data.rc_code_1 & 0xff00) | (~(data.rc_code_1 >> 8) & 0xff)); diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index 401a0976b2..cff3145199 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -59,18 +59,18 @@ bool ProntoData::operator==(const ProntoData &rhs) const { } // DO NOT EXPORT from this file -static const uint16_t MICROSECONDS_T_MAX = 0xFFFFU; -static const uint16_t LEARNED_TOKEN = 0x0000U; -static const uint16_t LEARNED_NON_MODULATED_TOKEN = 0x0100U; -static const uint16_t BITS_IN_HEXADECIMAL = 4U; -static const uint16_t DIGITS_IN_PRONTO_NUMBER = 4U; -static const uint16_t NUMBERS_IN_PREAMBLE = 4U; -static const uint16_t HEX_MASK = 0xFU; -static const uint32_t REFERENCE_FREQUENCY = 4145146UL; -static const uint16_t FALLBACK_FREQUENCY = 64767U; // To use with frequency = 0; -static const uint32_t MICROSECONDS_IN_SECONDS = 1000000UL; -static const uint16_t PRONTO_DEFAULT_GAP = 45000; -static const uint16_t MARK_EXCESS_MICROS = 20; +static constexpr uint16_t MICROSECONDS_T_MAX = 0xFFFFU; +static constexpr uint16_t LEARNED_TOKEN = 0x0000U; +static constexpr uint16_t LEARNED_NON_MODULATED_TOKEN = 0x0100U; +static constexpr uint16_t BITS_IN_HEXADECIMAL = 4U; +static constexpr uint16_t DIGITS_IN_PRONTO_NUMBER = 4U; +static constexpr uint16_t NUMBERS_IN_PREAMBLE = 4U; +static constexpr uint16_t HEX_MASK = 0xFU; +static constexpr uint32_t REFERENCE_FREQUENCY = 4145146UL; +static constexpr uint16_t FALLBACK_FREQUENCY = 64767U; // To use with frequency = 0; +static constexpr uint32_t MICROSECONDS_IN_SECONDS = 1000000UL; +static constexpr uint16_t PRONTO_DEFAULT_GAP = 45000; +static constexpr uint16_t MARK_EXCESS_MICROS = 20; static constexpr size_t PRONTO_LOG_CHUNK_SIZE = 230; static uint16_t to_frequency_k_hz(uint16_t code) { diff --git a/esphome/components/remote_base/rc5_protocol.cpp b/esphome/components/remote_base/rc5_protocol.cpp index 08f2f2eaa3..bb6d382d80 100644 --- a/esphome/components/remote_base/rc5_protocol.cpp +++ b/esphome/components/remote_base/rc5_protocol.cpp @@ -6,8 +6,8 @@ namespace remote_base { static const char *const TAG = "remote.rc5"; -static const uint32_t BIT_TIME_US = 889; -static const uint8_t NBITS = 14; +static constexpr uint32_t BIT_TIME_US = 889; +static constexpr uint8_t NBITS = 14; void RC5Protocol::encode(RemoteTransmitData *dst, const RC5Data &data) { static bool toggle = false; diff --git a/esphome/components/remote_base/rc6_protocol.cpp b/esphome/components/remote_base/rc6_protocol.cpp index fcb4da11a4..b442bb4c27 100644 --- a/esphome/components/remote_base/rc6_protocol.cpp +++ b/esphome/components/remote_base/rc6_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const RC6_TAG = "remote.rc6"; -static const uint16_t RC6_FREQ = 36000; -static const uint16_t RC6_UNIT = 444; -static const uint16_t RC6_HEADER_MARK = (6 * RC6_UNIT); -static const uint16_t RC6_HEADER_SPACE = (2 * RC6_UNIT); -static const uint16_t RC6_MODE_MASK = 0x07; +static constexpr uint16_t RC6_FREQ = 36000; +static constexpr uint16_t RC6_UNIT = 444; +static constexpr uint16_t RC6_HEADER_MARK = (6 * RC6_UNIT); +static constexpr uint16_t RC6_HEADER_SPACE = (2 * RC6_UNIT); +static constexpr uint16_t RC6_MODE_MASK = 0x07; void RC6Protocol::encode(RemoteTransmitData *dst, const RC6Data &data) { dst->reserve(44); diff --git a/esphome/components/remote_base/roomba_protocol.cpp b/esphome/components/remote_base/roomba_protocol.cpp index 2d2dde114a..6b7d216374 100644 --- a/esphome/components/remote_base/roomba_protocol.cpp +++ b/esphome/components/remote_base/roomba_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.roomba"; -static const uint8_t NBITS = 8; -static const uint32_t BIT_ONE_HIGH_US = 3000; -static const uint32_t BIT_ONE_LOW_US = 1000; -static const uint32_t BIT_ZERO_HIGH_US = BIT_ONE_LOW_US; -static const uint32_t BIT_ZERO_LOW_US = BIT_ONE_HIGH_US; +static constexpr uint8_t NBITS = 8; +static constexpr uint32_t BIT_ONE_HIGH_US = 3000; +static constexpr uint32_t BIT_ONE_LOW_US = 1000; +static constexpr uint32_t BIT_ZERO_HIGH_US = BIT_ONE_LOW_US; +static constexpr uint32_t BIT_ZERO_LOW_US = BIT_ONE_HIGH_US; void RoombaProtocol::encode(RemoteTransmitData *dst, const RoombaData &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/samsung36_protocol.cpp b/esphome/components/remote_base/samsung36_protocol.cpp index bd3eee5849..10e8bd2d01 100644 --- a/esphome/components/remote_base/samsung36_protocol.cpp +++ b/esphome/components/remote_base/samsung36_protocol.cpp @@ -6,17 +6,17 @@ namespace remote_base { static const char *const TAG = "remote.samsung36"; -static const uint8_t NBITS = 78; +static constexpr uint8_t NBITS = 78; -static const uint32_t HEADER_HIGH_US = 4500; -static const uint32_t HEADER_LOW_US = 4500; -static const uint32_t BIT_HIGH_US = 500; -static const uint32_t BIT_ONE_LOW_US = 1500; -static const uint32_t BIT_ZERO_LOW_US = 500; -static const uint32_t MIDDLE_HIGH_US = 500; -static const uint32_t MIDDLE_LOW_US = 4500; -static const uint32_t FOOTER_HIGH_US = 500; -static const uint32_t FOOTER_LOW_US = 59000; +static constexpr uint32_t HEADER_HIGH_US = 4500; +static constexpr uint32_t HEADER_LOW_US = 4500; +static constexpr uint32_t BIT_HIGH_US = 500; +static constexpr uint32_t BIT_ONE_LOW_US = 1500; +static constexpr uint32_t BIT_ZERO_LOW_US = 500; +static constexpr uint32_t MIDDLE_HIGH_US = 500; +static constexpr uint32_t MIDDLE_LOW_US = 4500; +static constexpr uint32_t FOOTER_HIGH_US = 500; +static constexpr uint32_t FOOTER_LOW_US = 59000; void Samsung36Protocol::encode(RemoteTransmitData *dst, const Samsung36Data &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/samsung_protocol.cpp b/esphome/components/remote_base/samsung_protocol.cpp index 2d6d5531e5..2a48cbb918 100644 --- a/esphome/components/remote_base/samsung_protocol.cpp +++ b/esphome/components/remote_base/samsung_protocol.cpp @@ -7,13 +7,13 @@ namespace remote_base { static const char *const TAG = "remote.samsung"; -static const uint32_t HEADER_HIGH_US = 4500; -static const uint32_t HEADER_LOW_US = 4500; -static const uint32_t BIT_HIGH_US = 560; -static const uint32_t BIT_ONE_LOW_US = 1690; -static const uint32_t BIT_ZERO_LOW_US = 560; -static const uint32_t FOOTER_HIGH_US = 560; -static const uint32_t FOOTER_LOW_US = 560; +static constexpr uint32_t HEADER_HIGH_US = 4500; +static constexpr uint32_t HEADER_LOW_US = 4500; +static constexpr uint32_t BIT_HIGH_US = 560; +static constexpr uint32_t BIT_ONE_LOW_US = 1690; +static constexpr uint32_t BIT_ZERO_LOW_US = 560; +static constexpr uint32_t FOOTER_HIGH_US = 560; +static constexpr uint32_t FOOTER_LOW_US = 560; void SamsungProtocol::encode(RemoteTransmitData *dst, const SamsungData &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/sony_protocol.cpp b/esphome/components/remote_base/sony_protocol.cpp index 69f2b49c42..504b346925 100644 --- a/esphome/components/remote_base/sony_protocol.cpp +++ b/esphome/components/remote_base/sony_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.sony"; -static const uint32_t HEADER_HIGH_US = 2400; -static const uint32_t HEADER_LOW_US = 600; -static const uint32_t BIT_ONE_HIGH_US = 1200; -static const uint32_t BIT_ZERO_HIGH_US = 600; -static const uint32_t BIT_LOW_US = 600; +static constexpr uint32_t HEADER_HIGH_US = 2400; +static constexpr uint32_t HEADER_LOW_US = 600; +static constexpr uint32_t BIT_ONE_HIGH_US = 1200; +static constexpr uint32_t BIT_ZERO_HIGH_US = 600; +static constexpr uint32_t BIT_LOW_US = 600; void SonyProtocol::encode(RemoteTransmitData *dst, const SonyData &data) { dst->set_carrier_frequency(40000); diff --git a/esphome/components/remote_base/symphony_protocol.cpp b/esphome/components/remote_base/symphony_protocol.cpp index 34b5dba07f..f30a980d91 100644 --- a/esphome/components/remote_base/symphony_protocol.cpp +++ b/esphome/components/remote_base/symphony_protocol.cpp @@ -13,16 +13,16 @@ static const char *const TAG = "remote.symphony"; // footer-gap handling used there. // Symphony protocol timing specifications (tuned to handset captures) -static const uint32_t BIT_ZERO_HIGH_US = 460; // short -static const uint32_t BIT_ZERO_LOW_US = 1260; // long -static const uint32_t BIT_ONE_HIGH_US = 1260; // long -static const uint32_t BIT_ONE_LOW_US = 460; // short -static const uint32_t CARRIER_FREQUENCY = 38000; +static constexpr uint32_t BIT_ZERO_HIGH_US = 460; // short +static constexpr uint32_t BIT_ZERO_LOW_US = 1260; // long +static constexpr uint32_t BIT_ONE_HIGH_US = 1260; // long +static constexpr uint32_t BIT_ONE_LOW_US = 460; // short +static constexpr uint32_t CARRIER_FREQUENCY = 38000; // IRremoteESP8266 reference: kSymphonyFooterGap = 4 * (mark + space) -static const uint32_t FOOTER_GAP_US = 4 * (BIT_ZERO_HIGH_US + BIT_ZERO_LOW_US); +static constexpr uint32_t FOOTER_GAP_US = 4 * (BIT_ZERO_HIGH_US + BIT_ZERO_LOW_US); // Typical inter-frame gap (~34.8 ms observed) -static const uint32_t INTER_FRAME_GAP_US = 34760; +static constexpr uint32_t INTER_FRAME_GAP_US = 34760; void SymphonyProtocol::encode(RemoteTransmitData *dst, const SymphonyData &data) { dst->set_carrier_frequency(CARRIER_FREQUENCY); diff --git a/esphome/components/remote_base/toshiba_ac_protocol.cpp b/esphome/components/remote_base/toshiba_ac_protocol.cpp index 42241eea8c..a20a29b84a 100644 --- a/esphome/components/remote_base/toshiba_ac_protocol.cpp +++ b/esphome/components/remote_base/toshiba_ac_protocol.cpp @@ -7,14 +7,14 @@ namespace remote_base { static const char *const TAG = "remote.toshibaac"; -static const uint32_t HEADER_HIGH_US = 4500; -static const uint32_t HEADER_LOW_US = 4500; -static const uint32_t BIT_HIGH_US = 560; -static const uint32_t BIT_ONE_LOW_US = 1690; -static const uint32_t BIT_ZERO_LOW_US = 560; -static const uint32_t FOOTER_HIGH_US = 560; -static const uint32_t FOOTER_LOW_US = 4500; -static const uint16_t PACKET_SPACE = 5500; +static constexpr uint32_t HEADER_HIGH_US = 4500; +static constexpr uint32_t HEADER_LOW_US = 4500; +static constexpr uint32_t BIT_HIGH_US = 560; +static constexpr uint32_t BIT_ONE_LOW_US = 1690; +static constexpr uint32_t BIT_ZERO_LOW_US = 560; +static constexpr uint32_t FOOTER_HIGH_US = 560; +static constexpr uint32_t FOOTER_LOW_US = 4500; +static constexpr uint16_t PACKET_SPACE = 5500; void ToshibaAcProtocol::encode(RemoteTransmitData *dst, const ToshibaAcData &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/toto_protocol.cpp b/esphome/components/remote_base/toto_protocol.cpp index ba21263bc8..f08258c4a3 100644 --- a/esphome/components/remote_base/toto_protocol.cpp +++ b/esphome/components/remote_base/toto_protocol.cpp @@ -6,12 +6,12 @@ namespace remote_base { static const char *const TAG = "remote.toto"; -static const uint32_t PREAMBLE_HIGH_US = 6200; -static const uint32_t PREAMBLE_LOW_US = 2800; -static const uint32_t BIT_HIGH_US = 550; -static const uint32_t BIT_ONE_LOW_US = 1700; -static const uint32_t BIT_ZERO_LOW_US = 550; -static const uint32_t TOTO_HEADER = 0x2008; +static constexpr uint32_t PREAMBLE_HIGH_US = 6200; +static constexpr uint32_t PREAMBLE_LOW_US = 2800; +static constexpr uint32_t BIT_HIGH_US = 550; +static constexpr uint32_t BIT_ONE_LOW_US = 1700; +static constexpr uint32_t BIT_ZERO_LOW_US = 550; +static constexpr uint32_t TOTO_HEADER = 0x2008; void TotoProtocol::encode(RemoteTransmitData *dst, const TotoData &data) { uint32_t payload = 0; From dff9780d3af9f6ce41bf9b3db8e6138dc197c08f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:19:48 -0600 Subject: [PATCH 063/609] [core] Use constexpr for compile-time constants (#14071) --- esphome/core/application.h | 2 +- esphome/core/helpers.cpp | 6 +++--- esphome/core/scheduler.cpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index e0299f3db3..5b3e3dfed6 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -111,7 +111,7 @@ namespace esphome { // For reboots, it's more important to shut down quickly than disconnect cleanly // since we're not entering deep sleep. The only consequence of not shutting down // cleanly is a warning in the log. -static const uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick reboot +static constexpr uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick reboot class Application { public: diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index c2f7f67d9a..09e755ca71 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -846,9 +846,9 @@ void IRAM_ATTR HOT delay_microseconds_safe(uint32_t us) { // avoids CPU locks that could trigger WDT or affect WiFi/BT stability uint32_t start = micros(); - const uint32_t lag = 5000; // microseconds, specifies the maximum time for a CPU busy-loop. - // it must be larger than the worst-case duration of a delay(1) call (hardware tasks) - // 5ms is conservative, it could be reduced when exact BT/WiFi stack delays are known + constexpr uint32_t lag = 5000; // microseconds, specifies the maximum time for a CPU busy-loop. + // it must be larger than the worst-case duration of a delay(1) call (hardware tasks) + // 5ms is conservative, it could be reduced when exact BT/WiFi stack delays are known if (us > lag) { delay((us - lag) / 1000UL); // note: in disabled-interrupt contexts delay() won't actually sleep while (micros() - start < us - lag) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 4194c3aa9e..3294f689e8 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -728,7 +728,7 @@ uint64_t Scheduler::millis_64_(uint32_t now) { // Define a safe window around the rollover point (10 seconds) // This covers any reasonable scheduler delays or thread preemption - static const uint32_t ROLLOVER_WINDOW = 10000; // 10 seconds in milliseconds + static constexpr uint32_t ROLLOVER_WINDOW = 10000; // 10 seconds in milliseconds // Check if we're near the rollover boundary (close to std::numeric_limits::max() or just past 0) bool near_rollover = (last > (std::numeric_limits::max() - ROLLOVER_WINDOW)) || (now < ROLLOVER_WINDOW); From e7f202186450ad2f865ecbc865ca87b10b243dc3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:32:24 -0600 Subject: [PATCH 064/609] [http_request] Replace std::map with std::vector in action template (#14026) --- esphome/components/http_request/__init__.py | 6 +++++- esphome/components/http_request/http_request.h | 11 ++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 5faffccbe4..2d6ecae0bc 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -302,10 +302,14 @@ async def http_request_action_to_code(config, action_id, template_arg, args): lambda_ = await cg.process_lambda(json_, args_, return_type=cg.void) cg.add(var.set_json(lambda_)) else: + cg.add(var.init_json(len(json_))) for key in json_: template_ = await cg.templatable(json_[key], args, cg.std_string) cg.add(var.add_json(key, template_)) - for key, value in config.get(CONF_REQUEST_HEADERS, {}).items(): + request_headers = config.get(CONF_REQUEST_HEADERS, {}) + if request_headers: + cg.add(var.init_request_headers(len(request_headers))) + for key, value in request_headers.items(): template_ = await cg.templatable(value, args, cg.const_char_ptr) cg.add(var.add_request_header(key, template_)) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 458ffe94a8..2b2d05c63f 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include #include @@ -399,13 +398,15 @@ template class HttpRequestSendAction : public Action { TEMPLATABLE_VALUE(bool, capture_response) #endif + void init_request_headers(size_t count) { this->request_headers_.init(count); } void add_request_header(const char *key, TemplatableValue value) { - this->request_headers_.insert({key, value}); + this->request_headers_.push_back({key, value}); } void add_collect_header(const char *value) { this->lower_case_collect_headers_.push_back(value); } - void add_json(const char *key, TemplatableValue value) { this->json_.insert({key, value}); } + void init_json(size_t count) { this->json_.init(count); } + void add_json(const char *key, TemplatableValue value) { this->json_.push_back({key, value}); } void set_json(std::function json_func) { this->json_func_ = json_func; } @@ -507,9 +508,9 @@ template class HttpRequestSendAction : public Action { } void encode_json_func_(Ts... x, JsonObject root) { this->json_func_(x..., root); } HttpRequestComponent *parent_; - std::map> request_headers_{}; + FixedVector>> request_headers_{}; std::vector lower_case_collect_headers_{"content-type", "content-length"}; - std::map> json_{}; + FixedVector>> json_{}; std::function json_func_{nullptr}; #ifdef USE_HTTP_REQUEST_RESPONSE Trigger, std::string &, Ts...> success_trigger_with_response_; From eaf0d03a37a85ead5f33c5b3512d80944f2bfa09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:32:37 -0600 Subject: [PATCH 065/609] [ld2420] Use constexpr for compile-time constants (#14079) --- esphome/components/ld2420/ld2420.cpp | 118 +++++++++++++-------------- esphome/components/ld2420/ld2420.h | 6 +- 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index 69b69f4a61..cf78a1a460 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -63,73 +63,73 @@ namespace esphome::ld2420 { static const char *const TAG = "ld2420"; // Local const's -static const uint16_t REFRESH_RATE_MS = 1000; +static constexpr uint16_t REFRESH_RATE_MS = 1000; // Command sets -static const uint16_t CMD_DISABLE_CONF = 0x00FE; -static const uint16_t CMD_ENABLE_CONF = 0x00FF; -static const uint16_t CMD_PARM_HIGH_TRESH = 0x0012; -static const uint16_t CMD_PARM_LOW_TRESH = 0x0021; -static const uint16_t CMD_PROTOCOL_VER = 0x0002; -static const uint16_t CMD_READ_ABD_PARAM = 0x0008; -static const uint16_t CMD_READ_REG_ADDR = 0x0020; -static const uint16_t CMD_READ_REGISTER = 0x0002; -static const uint16_t CMD_READ_SERIAL_NUM = 0x0011; -static const uint16_t CMD_READ_SYS_PARAM = 0x0013; -static const uint16_t CMD_READ_VERSION = 0x0000; -static const uint16_t CMD_RESTART = 0x0068; -static const uint16_t CMD_SYSTEM_MODE = 0x0000; -static const uint16_t CMD_SYSTEM_MODE_GR = 0x0003; -static const uint16_t CMD_SYSTEM_MODE_MTT = 0x0001; -static const uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064; -static const uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000; -static const uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004; -static const uint16_t CMD_SYSTEM_MODE_VS = 0x0002; -static const uint16_t CMD_WRITE_ABD_PARAM = 0x0007; -static const uint16_t CMD_WRITE_REGISTER = 0x0001; -static const uint16_t CMD_WRITE_SYS_PARAM = 0x0012; +static constexpr uint16_t CMD_DISABLE_CONF = 0x00FE; +static constexpr uint16_t CMD_ENABLE_CONF = 0x00FF; +static constexpr uint16_t CMD_PARM_HIGH_TRESH = 0x0012; +static constexpr uint16_t CMD_PARM_LOW_TRESH = 0x0021; +static constexpr uint16_t CMD_PROTOCOL_VER = 0x0002; +static constexpr uint16_t CMD_READ_ABD_PARAM = 0x0008; +static constexpr uint16_t CMD_READ_REG_ADDR = 0x0020; +static constexpr uint16_t CMD_READ_REGISTER = 0x0002; +static constexpr uint16_t CMD_READ_SERIAL_NUM = 0x0011; +static constexpr uint16_t CMD_READ_SYS_PARAM = 0x0013; +static constexpr uint16_t CMD_READ_VERSION = 0x0000; +static constexpr uint16_t CMD_RESTART = 0x0068; +static constexpr uint16_t CMD_SYSTEM_MODE = 0x0000; +static constexpr uint16_t CMD_SYSTEM_MODE_GR = 0x0003; +static constexpr uint16_t CMD_SYSTEM_MODE_MTT = 0x0001; +static constexpr uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064; +static constexpr uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000; +static constexpr uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004; +static constexpr uint16_t CMD_SYSTEM_MODE_VS = 0x0002; +static constexpr uint16_t CMD_WRITE_ABD_PARAM = 0x0007; +static constexpr uint16_t CMD_WRITE_REGISTER = 0x0001; +static constexpr uint16_t CMD_WRITE_SYS_PARAM = 0x0012; -static const uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04; -static const uint8_t CMD_ABD_DATA_REPLY_START = 0x0A; -static const uint8_t CMD_MAX_BYTES = 0x64; -static const uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02; +static constexpr uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04; +static constexpr uint8_t CMD_ABD_DATA_REPLY_START = 0x0A; +static constexpr uint8_t CMD_MAX_BYTES = 0x64; +static constexpr uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02; -static const uint8_t LD2420_ERROR_NONE = 0x00; -static const uint8_t LD2420_ERROR_TIMEOUT = 0x02; -static const uint8_t LD2420_ERROR_UNKNOWN = 0x01; +static constexpr uint8_t LD2420_ERROR_NONE = 0x00; +static constexpr uint8_t LD2420_ERROR_TIMEOUT = 0x02; +static constexpr uint8_t LD2420_ERROR_UNKNOWN = 0x01; // Register address values -static const uint16_t CMD_MIN_GATE_REG = 0x0000; -static const uint16_t CMD_MAX_GATE_REG = 0x0001; -static const uint16_t CMD_TIMEOUT_REG = 0x0004; -static const uint16_t CMD_GATE_MOVE_THRESH[TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, - 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, - 0x001C, 0x001D, 0x001E, 0x001F}; -static const uint16_t CMD_GATE_STILL_THRESH[TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, - 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, - 0x002C, 0x002D, 0x002E, 0x002F}; -static const uint32_t FACTORY_MOVE_THRESH[TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250, - 250, 250, 250, 250, 250, 250, 250, 250}; -static const uint32_t FACTORY_STILL_THRESH[TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150, - 150, 100, 100, 100, 100, 100, 100, 100}; -static const uint16_t FACTORY_TIMEOUT = 120; -static const uint16_t FACTORY_MIN_GATE = 1; -static const uint16_t FACTORY_MAX_GATE = 12; +static constexpr uint16_t CMD_MIN_GATE_REG = 0x0000; +static constexpr uint16_t CMD_MAX_GATE_REG = 0x0001; +static constexpr uint16_t CMD_TIMEOUT_REG = 0x0004; +static constexpr uint16_t CMD_GATE_MOVE_THRESH[TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, + 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, + 0x001C, 0x001D, 0x001E, 0x001F}; +static constexpr uint16_t CMD_GATE_STILL_THRESH[TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, + 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, + 0x002C, 0x002D, 0x002E, 0x002F}; +static constexpr uint32_t FACTORY_MOVE_THRESH[TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250, + 250, 250, 250, 250, 250, 250, 250, 250}; +static constexpr uint32_t FACTORY_STILL_THRESH[TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150, + 150, 100, 100, 100, 100, 100, 100, 100}; +static constexpr uint16_t FACTORY_TIMEOUT = 120; +static constexpr uint16_t FACTORY_MIN_GATE = 1; +static constexpr uint16_t FACTORY_MAX_GATE = 12; // COMMAND_BYTE Header & Footer -static const uint32_t CMD_FRAME_FOOTER = 0x01020304; -static const uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD; -static const uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD; -static const uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA; -static const uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8; -static const uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4; -static const int CALIBRATE_VERSION_MIN = 154; -static const uint8_t CMD_FRAME_COMMAND = 6; -static const uint8_t CMD_FRAME_DATA_LENGTH = 4; -static const uint8_t CMD_FRAME_STATUS = 7; -static const uint8_t CMD_ERROR_WORD = 8; -static const uint8_t ENERGY_SENSOR_START = 9; -static const uint8_t CALIBRATE_REPORT_INTERVAL = 4; +static constexpr uint32_t CMD_FRAME_FOOTER = 0x01020304; +static constexpr uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD; +static constexpr uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD; +static constexpr uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA; +static constexpr uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8; +static constexpr uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4; +static constexpr int CALIBRATE_VERSION_MIN = 154; +static constexpr uint8_t CMD_FRAME_COMMAND = 6; +static constexpr uint8_t CMD_FRAME_DATA_LENGTH = 4; +static constexpr uint8_t CMD_FRAME_STATUS = 7; +static constexpr uint8_t CMD_ERROR_WORD = 8; +static constexpr uint8_t ENERGY_SENSOR_START = 9; +static constexpr uint8_t CALIBRATE_REPORT_INTERVAL = 4; static const char *const OP_NORMAL_MODE_STRING = "Normal"; static const char *const OP_SIMPLE_MODE_STRING = "Simple"; diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 6d81f86497..02250c5911 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -20,9 +20,9 @@ namespace esphome::ld2420 { -static const uint8_t CALIBRATE_SAMPLES = 64; -static const uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer -static const uint8_t TOTAL_GATES = 16; +static constexpr uint8_t CALIBRATE_SAMPLES = 64; +static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer +static constexpr uint8_t TOTAL_GATES = 16; enum OpMode : uint8_t { OP_NORMAL_MODE = 1, From 9a8b00a42835b20db98081f1801c5cbafafc723a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:33:23 -0600 Subject: [PATCH 066/609] [nfc] Use constexpr for compile-time constants (#14077) --- esphome/components/nfc/nci_core.h | 228 +++++++++++------------ esphome/components/nfc/ndef_message.h | 2 +- esphome/components/nfc/ndef_record.h | 16 +- esphome/components/nfc/ndef_record_uri.h | 2 +- esphome/components/nfc/nfc.h | 60 +++--- 5 files changed, 154 insertions(+), 154 deletions(-) diff --git a/esphome/components/nfc/nci_core.h b/esphome/components/nfc/nci_core.h index fdaf6d0cc5..6b42070ed0 100644 --- a/esphome/components/nfc/nci_core.h +++ b/esphome/components/nfc/nci_core.h @@ -8,137 +8,137 @@ namespace esphome { namespace nfc { // Header info -static const uint8_t NCI_PKT_HEADER_SIZE = 3; // NCI packet (pkt) headers are always three bytes -static const uint8_t NCI_PKT_MT_GID_OFFSET = 0; // NCI packet (pkt) MT and GID offsets -static const uint8_t NCI_PKT_OID_OFFSET = 1; // NCI packet (pkt) OID offset -static const uint8_t NCI_PKT_LENGTH_OFFSET = 2; // NCI packet (pkt) message length (size) offset -static const uint8_t NCI_PKT_PAYLOAD_OFFSET = 3; // NCI packet (pkt) payload offset +static constexpr uint8_t NCI_PKT_HEADER_SIZE = 3; // NCI packet (pkt) headers are always three bytes +static constexpr uint8_t NCI_PKT_MT_GID_OFFSET = 0; // NCI packet (pkt) MT and GID offsets +static constexpr uint8_t NCI_PKT_OID_OFFSET = 1; // NCI packet (pkt) OID offset +static constexpr uint8_t NCI_PKT_LENGTH_OFFSET = 2; // NCI packet (pkt) message length (size) offset +static constexpr uint8_t NCI_PKT_PAYLOAD_OFFSET = 3; // NCI packet (pkt) payload offset // Important masks -static const uint8_t NCI_PKT_MT_MASK = 0xE0; // NCI packet (pkt) message type mask -static const uint8_t NCI_PKT_PBF_MASK = 0x10; // packet boundary flag bit -static const uint8_t NCI_PKT_GID_MASK = 0x0F; -static const uint8_t NCI_PKT_OID_MASK = 0x3F; +static constexpr uint8_t NCI_PKT_MT_MASK = 0xE0; // NCI packet (pkt) message type mask +static constexpr uint8_t NCI_PKT_PBF_MASK = 0x10; // packet boundary flag bit +static constexpr uint8_t NCI_PKT_GID_MASK = 0x0F; +static constexpr uint8_t NCI_PKT_OID_MASK = 0x3F; // Message types -static const uint8_t NCI_PKT_MT_DATA = 0x00; // For sending commands to NFC endpoint (card/tag) -static const uint8_t NCI_PKT_MT_CTRL_COMMAND = 0x20; // For sending commands to NFCC -static const uint8_t NCI_PKT_MT_CTRL_RESPONSE = 0x40; // Response from NFCC to commands -static const uint8_t NCI_PKT_MT_CTRL_NOTIFICATION = 0x60; // Notification from NFCC +static constexpr uint8_t NCI_PKT_MT_DATA = 0x00; // For sending commands to NFC endpoint (card/tag) +static constexpr uint8_t NCI_PKT_MT_CTRL_COMMAND = 0x20; // For sending commands to NFCC +static constexpr uint8_t NCI_PKT_MT_CTRL_RESPONSE = 0x40; // Response from NFCC to commands +static constexpr uint8_t NCI_PKT_MT_CTRL_NOTIFICATION = 0x60; // Notification from NFCC // GIDs -static const uint8_t NCI_CORE_GID = 0x0; -static const uint8_t RF_GID = 0x1; -static const uint8_t NFCEE_GID = 0x1; -static const uint8_t NCI_PROPRIETARY_GID = 0xF; +static constexpr uint8_t NCI_CORE_GID = 0x0; +static constexpr uint8_t RF_GID = 0x1; +static constexpr uint8_t NFCEE_GID = 0x1; +static constexpr uint8_t NCI_PROPRIETARY_GID = 0xF; // OIDs -static const uint8_t NCI_CORE_RESET_OID = 0x00; -static const uint8_t NCI_CORE_INIT_OID = 0x01; -static const uint8_t NCI_CORE_SET_CONFIG_OID = 0x02; -static const uint8_t NCI_CORE_GET_CONFIG_OID = 0x03; -static const uint8_t NCI_CORE_CONN_CREATE_OID = 0x04; -static const uint8_t NCI_CORE_CONN_CLOSE_OID = 0x05; -static const uint8_t NCI_CORE_CONN_CREDITS_OID = 0x06; -static const uint8_t NCI_CORE_GENERIC_ERROR_OID = 0x07; -static const uint8_t NCI_CORE_INTERFACE_ERROR_OID = 0x08; +static constexpr uint8_t NCI_CORE_RESET_OID = 0x00; +static constexpr uint8_t NCI_CORE_INIT_OID = 0x01; +static constexpr uint8_t NCI_CORE_SET_CONFIG_OID = 0x02; +static constexpr uint8_t NCI_CORE_GET_CONFIG_OID = 0x03; +static constexpr uint8_t NCI_CORE_CONN_CREATE_OID = 0x04; +static constexpr uint8_t NCI_CORE_CONN_CLOSE_OID = 0x05; +static constexpr uint8_t NCI_CORE_CONN_CREDITS_OID = 0x06; +static constexpr uint8_t NCI_CORE_GENERIC_ERROR_OID = 0x07; +static constexpr uint8_t NCI_CORE_INTERFACE_ERROR_OID = 0x08; -static const uint8_t RF_DISCOVER_MAP_OID = 0x00; -static const uint8_t RF_SET_LISTEN_MODE_ROUTING_OID = 0x01; -static const uint8_t RF_GET_LISTEN_MODE_ROUTING_OID = 0x02; -static const uint8_t RF_DISCOVER_OID = 0x03; -static const uint8_t RF_DISCOVER_SELECT_OID = 0x04; -static const uint8_t RF_INTF_ACTIVATED_OID = 0x05; -static const uint8_t RF_DEACTIVATE_OID = 0x06; -static const uint8_t RF_FIELD_INFO_OID = 0x07; -static const uint8_t RF_T3T_POLLING_OID = 0x08; -static const uint8_t RF_NFCEE_ACTION_OID = 0x09; -static const uint8_t RF_NFCEE_DISCOVERY_REQ_OID = 0x0A; -static const uint8_t RF_PARAMETER_UPDATE_OID = 0x0B; +static constexpr uint8_t RF_DISCOVER_MAP_OID = 0x00; +static constexpr uint8_t RF_SET_LISTEN_MODE_ROUTING_OID = 0x01; +static constexpr uint8_t RF_GET_LISTEN_MODE_ROUTING_OID = 0x02; +static constexpr uint8_t RF_DISCOVER_OID = 0x03; +static constexpr uint8_t RF_DISCOVER_SELECT_OID = 0x04; +static constexpr uint8_t RF_INTF_ACTIVATED_OID = 0x05; +static constexpr uint8_t RF_DEACTIVATE_OID = 0x06; +static constexpr uint8_t RF_FIELD_INFO_OID = 0x07; +static constexpr uint8_t RF_T3T_POLLING_OID = 0x08; +static constexpr uint8_t RF_NFCEE_ACTION_OID = 0x09; +static constexpr uint8_t RF_NFCEE_DISCOVERY_REQ_OID = 0x0A; +static constexpr uint8_t RF_PARAMETER_UPDATE_OID = 0x0B; -static const uint8_t NFCEE_DISCOVER_OID = 0x00; -static const uint8_t NFCEE_MODE_SET_OID = 0x01; +static constexpr uint8_t NFCEE_DISCOVER_OID = 0x00; +static constexpr uint8_t NFCEE_MODE_SET_OID = 0x01; // Interfaces -static const uint8_t INTF_NFCEE_DIRECT = 0x00; -static const uint8_t INTF_FRAME = 0x01; -static const uint8_t INTF_ISODEP = 0x02; -static const uint8_t INTF_NFCDEP = 0x03; -static const uint8_t INTF_TAGCMD = 0x80; // NXP proprietary +static constexpr uint8_t INTF_NFCEE_DIRECT = 0x00; +static constexpr uint8_t INTF_FRAME = 0x01; +static constexpr uint8_t INTF_ISODEP = 0x02; +static constexpr uint8_t INTF_NFCDEP = 0x03; +static constexpr uint8_t INTF_TAGCMD = 0x80; // NXP proprietary // Bit rates -static const uint8_t NFC_BIT_RATE_106 = 0x00; -static const uint8_t NFC_BIT_RATE_212 = 0x01; -static const uint8_t NFC_BIT_RATE_424 = 0x02; -static const uint8_t NFC_BIT_RATE_848 = 0x03; -static const uint8_t NFC_BIT_RATE_1695 = 0x04; -static const uint8_t NFC_BIT_RATE_3390 = 0x05; -static const uint8_t NFC_BIT_RATE_6780 = 0x06; +static constexpr uint8_t NFC_BIT_RATE_106 = 0x00; +static constexpr uint8_t NFC_BIT_RATE_212 = 0x01; +static constexpr uint8_t NFC_BIT_RATE_424 = 0x02; +static constexpr uint8_t NFC_BIT_RATE_848 = 0x03; +static constexpr uint8_t NFC_BIT_RATE_1695 = 0x04; +static constexpr uint8_t NFC_BIT_RATE_3390 = 0x05; +static constexpr uint8_t NFC_BIT_RATE_6780 = 0x06; // Protocols -static const uint8_t PROT_UNDETERMINED = 0x00; -static const uint8_t PROT_T1T = 0x01; -static const uint8_t PROT_T2T = 0x02; -static const uint8_t PROT_T3T = 0x03; -static const uint8_t PROT_ISODEP = 0x04; -static const uint8_t PROT_NFCDEP = 0x05; -static const uint8_t PROT_T5T = 0x06; -static const uint8_t PROT_MIFARE = 0x80; +static constexpr uint8_t PROT_UNDETERMINED = 0x00; +static constexpr uint8_t PROT_T1T = 0x01; +static constexpr uint8_t PROT_T2T = 0x02; +static constexpr uint8_t PROT_T3T = 0x03; +static constexpr uint8_t PROT_ISODEP = 0x04; +static constexpr uint8_t PROT_NFCDEP = 0x05; +static constexpr uint8_t PROT_T5T = 0x06; +static constexpr uint8_t PROT_MIFARE = 0x80; // RF Technologies -static const uint8_t NFC_RF_TECH_A = 0x00; -static const uint8_t NFC_RF_TECH_B = 0x01; -static const uint8_t NFC_RF_TECH_F = 0x02; -static const uint8_t NFC_RF_TECH_15693 = 0x03; +static constexpr uint8_t NFC_RF_TECH_A = 0x00; +static constexpr uint8_t NFC_RF_TECH_B = 0x01; +static constexpr uint8_t NFC_RF_TECH_F = 0x02; +static constexpr uint8_t NFC_RF_TECH_15693 = 0x03; // RF Technology & Modes -static const uint8_t MODE_MASK = 0xF0; -static const uint8_t MODE_LISTEN_MASK = 0x80; -static const uint8_t MODE_POLL = 0x00; +static constexpr uint8_t MODE_MASK = 0xF0; +static constexpr uint8_t MODE_LISTEN_MASK = 0x80; +static constexpr uint8_t MODE_POLL = 0x00; -static const uint8_t TECH_PASSIVE_NFCA = 0x00; -static const uint8_t TECH_PASSIVE_NFCB = 0x01; -static const uint8_t TECH_PASSIVE_NFCF = 0x02; -static const uint8_t TECH_ACTIVE_NFCA = 0x03; -static const uint8_t TECH_ACTIVE_NFCF = 0x05; -static const uint8_t TECH_PASSIVE_15693 = 0x06; +static constexpr uint8_t TECH_PASSIVE_NFCA = 0x00; +static constexpr uint8_t TECH_PASSIVE_NFCB = 0x01; +static constexpr uint8_t TECH_PASSIVE_NFCF = 0x02; +static constexpr uint8_t TECH_ACTIVE_NFCA = 0x03; +static constexpr uint8_t TECH_ACTIVE_NFCF = 0x05; +static constexpr uint8_t TECH_PASSIVE_15693 = 0x06; // Status codes -static const uint8_t STATUS_OK = 0x00; -static const uint8_t STATUS_REJECTED = 0x01; -static const uint8_t STATUS_RF_FRAME_CORRUPTED = 0x02; -static const uint8_t STATUS_FAILED = 0x03; -static const uint8_t STATUS_NOT_INITIALIZED = 0x04; -static const uint8_t STATUS_SYNTAX_ERROR = 0x05; -static const uint8_t STATUS_SEMANTIC_ERROR = 0x06; -static const uint8_t STATUS_INVALID_PARAM = 0x09; -static const uint8_t STATUS_MESSAGE_SIZE_EXCEEDED = 0x0A; -static const uint8_t DISCOVERY_ALREADY_STARTED = 0xA0; -static const uint8_t DISCOVERY_TARGET_ACTIVATION_FAILED = 0xA1; -static const uint8_t DISCOVERY_TEAR_DOWN = 0xA2; -static const uint8_t RF_TRANSMISSION_ERROR = 0xB0; -static const uint8_t RF_PROTOCOL_ERROR = 0xB1; -static const uint8_t RF_TIMEOUT_ERROR = 0xB2; -static const uint8_t NFCEE_INTERFACE_ACTIVATION_FAILED = 0xC0; -static const uint8_t NFCEE_TRANSMISSION_ERROR = 0xC1; -static const uint8_t NFCEE_PROTOCOL_ERROR = 0xC2; -static const uint8_t NFCEE_TIMEOUT_ERROR = 0xC3; +static constexpr uint8_t STATUS_OK = 0x00; +static constexpr uint8_t STATUS_REJECTED = 0x01; +static constexpr uint8_t STATUS_RF_FRAME_CORRUPTED = 0x02; +static constexpr uint8_t STATUS_FAILED = 0x03; +static constexpr uint8_t STATUS_NOT_INITIALIZED = 0x04; +static constexpr uint8_t STATUS_SYNTAX_ERROR = 0x05; +static constexpr uint8_t STATUS_SEMANTIC_ERROR = 0x06; +static constexpr uint8_t STATUS_INVALID_PARAM = 0x09; +static constexpr uint8_t STATUS_MESSAGE_SIZE_EXCEEDED = 0x0A; +static constexpr uint8_t DISCOVERY_ALREADY_STARTED = 0xA0; +static constexpr uint8_t DISCOVERY_TARGET_ACTIVATION_FAILED = 0xA1; +static constexpr uint8_t DISCOVERY_TEAR_DOWN = 0xA2; +static constexpr uint8_t RF_TRANSMISSION_ERROR = 0xB0; +static constexpr uint8_t RF_PROTOCOL_ERROR = 0xB1; +static constexpr uint8_t RF_TIMEOUT_ERROR = 0xB2; +static constexpr uint8_t NFCEE_INTERFACE_ACTIVATION_FAILED = 0xC0; +static constexpr uint8_t NFCEE_TRANSMISSION_ERROR = 0xC1; +static constexpr uint8_t NFCEE_PROTOCOL_ERROR = 0xC2; +static constexpr uint8_t NFCEE_TIMEOUT_ERROR = 0xC3; // Deactivation types/reasons -static const uint8_t DEACTIVATION_TYPE_IDLE = 0x00; -static const uint8_t DEACTIVATION_TYPE_SLEEP = 0x01; -static const uint8_t DEACTIVATION_TYPE_SLEEP_AF = 0x02; -static const uint8_t DEACTIVATION_TYPE_DISCOVERY = 0x03; +static constexpr uint8_t DEACTIVATION_TYPE_IDLE = 0x00; +static constexpr uint8_t DEACTIVATION_TYPE_SLEEP = 0x01; +static constexpr uint8_t DEACTIVATION_TYPE_SLEEP_AF = 0x02; +static constexpr uint8_t DEACTIVATION_TYPE_DISCOVERY = 0x03; // RF discover map modes -static const uint8_t RF_DISCOVER_MAP_MODE_POLL = 0x1; -static const uint8_t RF_DISCOVER_MAP_MODE_LISTEN = 0x2; +static constexpr uint8_t RF_DISCOVER_MAP_MODE_POLL = 0x1; +static constexpr uint8_t RF_DISCOVER_MAP_MODE_LISTEN = 0x2; // RF discover notification types -static const uint8_t RF_DISCOVER_NTF_NT_LAST = 0x00; -static const uint8_t RF_DISCOVER_NTF_NT_LAST_RL = 0x01; -static const uint8_t RF_DISCOVER_NTF_NT_MORE = 0x02; +static constexpr uint8_t RF_DISCOVER_NTF_NT_LAST = 0x00; +static constexpr uint8_t RF_DISCOVER_NTF_NT_LAST_RL = 0x01; +static constexpr uint8_t RF_DISCOVER_NTF_NT_MORE = 0x02; // Important message offsets -static const uint8_t RF_DISCOVER_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_DISCOVER_NTF_PROTOCOL = 1 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_DISCOVER_NTF_MODE_TECH = 2 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_DISCOVER_NTF_RF_TECH_LENGTH = 3 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_DISCOVER_NTF_RF_TECH_PARAMS = 4 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_INTERFACE = 1 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_PROTOCOL = 2 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_MODE_TECH = 3 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_MAX_SIZE = 4 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_INIT_CRED = 5 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_LENGTH = 6 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_PARAMS = 7 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_DISCOVER_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_DISCOVER_NTF_PROTOCOL = 1 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_DISCOVER_NTF_MODE_TECH = 2 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_DISCOVER_NTF_RF_TECH_LENGTH = 3 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_DISCOVER_NTF_RF_TECH_PARAMS = 4 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_INTERFACE = 1 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_PROTOCOL = 2 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_MODE_TECH = 3 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_MAX_SIZE = 4 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_INIT_CRED = 5 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_LENGTH = 6 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_PARAMS = 7 + NCI_PKT_HEADER_SIZE; } // namespace nfc } // namespace esphome diff --git a/esphome/components/nfc/ndef_message.h b/esphome/components/nfc/ndef_message.h index 2e81fb216c..48f79b8854 100644 --- a/esphome/components/nfc/ndef_message.h +++ b/esphome/components/nfc/ndef_message.h @@ -12,7 +12,7 @@ namespace esphome { namespace nfc { -static const uint8_t MAX_NDEF_RECORDS = 4; +static constexpr uint8_t MAX_NDEF_RECORDS = 4; class NdefMessage { public: diff --git a/esphome/components/nfc/ndef_record.h b/esphome/components/nfc/ndef_record.h index 76d8b6a50a..1a7c24aee9 100644 --- a/esphome/components/nfc/ndef_record.h +++ b/esphome/components/nfc/ndef_record.h @@ -8,14 +8,14 @@ namespace esphome { namespace nfc { -static const uint8_t TNF_EMPTY = 0x00; -static const uint8_t TNF_WELL_KNOWN = 0x01; -static const uint8_t TNF_MIME_MEDIA = 0x02; -static const uint8_t TNF_ABSOLUTE_URI = 0x03; -static const uint8_t TNF_EXTERNAL_TYPE = 0x04; -static const uint8_t TNF_UNKNOWN = 0x05; -static const uint8_t TNF_UNCHANGED = 0x06; -static const uint8_t TNF_RESERVED = 0x07; +static constexpr uint8_t TNF_EMPTY = 0x00; +static constexpr uint8_t TNF_WELL_KNOWN = 0x01; +static constexpr uint8_t TNF_MIME_MEDIA = 0x02; +static constexpr uint8_t TNF_ABSOLUTE_URI = 0x03; +static constexpr uint8_t TNF_EXTERNAL_TYPE = 0x04; +static constexpr uint8_t TNF_UNKNOWN = 0x05; +static constexpr uint8_t TNF_UNCHANGED = 0x06; +static constexpr uint8_t TNF_RESERVED = 0x07; class NdefRecord { public: diff --git a/esphome/components/nfc/ndef_record_uri.h b/esphome/components/nfc/ndef_record_uri.h index 1eadda1b4f..2f7790a9a9 100644 --- a/esphome/components/nfc/ndef_record_uri.h +++ b/esphome/components/nfc/ndef_record_uri.h @@ -9,7 +9,7 @@ namespace esphome { namespace nfc { -static const uint8_t PAYLOAD_IDENTIFIERS_COUNT = 0x23; +static constexpr uint8_t PAYLOAD_IDENTIFIERS_COUNT = 0x23; static const char *const PAYLOAD_IDENTIFIERS[] = {"", "http://www.", "https://www.", diff --git a/esphome/components/nfc/nfc.h b/esphome/components/nfc/nfc.h index 5191904833..cdaea82af6 100644 --- a/esphome/components/nfc/nfc.h +++ b/esphome/components/nfc/nfc.h @@ -12,47 +12,47 @@ namespace esphome { namespace nfc { -static const uint8_t MIFARE_CLASSIC_BLOCK_SIZE = 16; -static const uint8_t MIFARE_CLASSIC_LONG_TLV_SIZE = 4; -static const uint8_t MIFARE_CLASSIC_SHORT_TLV_SIZE = 2; -static const uint8_t MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW = 4; -static const uint8_t MIFARE_CLASSIC_BLOCKS_PER_SECT_HIGH = 16; -static const uint8_t MIFARE_CLASSIC_16BLOCK_SECT_START = 32; +static constexpr uint8_t MIFARE_CLASSIC_BLOCK_SIZE = 16; +static constexpr uint8_t MIFARE_CLASSIC_LONG_TLV_SIZE = 4; +static constexpr uint8_t MIFARE_CLASSIC_SHORT_TLV_SIZE = 2; +static constexpr uint8_t MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW = 4; +static constexpr uint8_t MIFARE_CLASSIC_BLOCKS_PER_SECT_HIGH = 16; +static constexpr uint8_t MIFARE_CLASSIC_16BLOCK_SECT_START = 32; -static const uint8_t MIFARE_ULTRALIGHT_PAGE_SIZE = 4; -static const uint8_t MIFARE_ULTRALIGHT_READ_SIZE = 4; -static const uint8_t MIFARE_ULTRALIGHT_DATA_START_PAGE = 4; -static const uint8_t MIFARE_ULTRALIGHT_MAX_PAGE = 63; +static constexpr uint8_t MIFARE_ULTRALIGHT_PAGE_SIZE = 4; +static constexpr uint8_t MIFARE_ULTRALIGHT_READ_SIZE = 4; +static constexpr uint8_t MIFARE_ULTRALIGHT_DATA_START_PAGE = 4; +static constexpr uint8_t MIFARE_ULTRALIGHT_MAX_PAGE = 63; -static const uint8_t TAG_TYPE_MIFARE_CLASSIC = 0; -static const uint8_t TAG_TYPE_1 = 1; -static const uint8_t TAG_TYPE_2 = 2; -static const uint8_t TAG_TYPE_3 = 3; -static const uint8_t TAG_TYPE_4 = 4; -static const uint8_t TAG_TYPE_UNKNOWN = 99; +static constexpr uint8_t TAG_TYPE_MIFARE_CLASSIC = 0; +static constexpr uint8_t TAG_TYPE_1 = 1; +static constexpr uint8_t TAG_TYPE_2 = 2; +static constexpr uint8_t TAG_TYPE_3 = 3; +static constexpr uint8_t TAG_TYPE_4 = 4; +static constexpr uint8_t TAG_TYPE_UNKNOWN = 99; // Mifare Commands -static const uint8_t MIFARE_CMD_AUTH_A = 0x60; -static const uint8_t MIFARE_CMD_AUTH_B = 0x61; -static const uint8_t MIFARE_CMD_HALT = 0x50; -static const uint8_t MIFARE_CMD_READ = 0x30; -static const uint8_t MIFARE_CMD_WRITE = 0xA0; -static const uint8_t MIFARE_CMD_WRITE_ULTRALIGHT = 0xA2; +static constexpr uint8_t MIFARE_CMD_AUTH_A = 0x60; +static constexpr uint8_t MIFARE_CMD_AUTH_B = 0x61; +static constexpr uint8_t MIFARE_CMD_HALT = 0x50; +static constexpr uint8_t MIFARE_CMD_READ = 0x30; +static constexpr uint8_t MIFARE_CMD_WRITE = 0xA0; +static constexpr uint8_t MIFARE_CMD_WRITE_ULTRALIGHT = 0xA2; // Mifare Ack/Nak -static const uint8_t MIFARE_CMD_ACK = 0x0A; -static const uint8_t MIFARE_CMD_NAK_INVALID_XFER_BUFF_VALID = 0x00; -static const uint8_t MIFARE_CMD_NAK_CRC_ERROR_XFER_BUFF_VALID = 0x01; -static const uint8_t MIFARE_CMD_NAK_INVALID_XFER_BUFF_INVALID = 0x04; -static const uint8_t MIFARE_CMD_NAK_CRC_ERROR_XFER_BUFF_INVALID = 0x05; +static constexpr uint8_t MIFARE_CMD_ACK = 0x0A; +static constexpr uint8_t MIFARE_CMD_NAK_INVALID_XFER_BUFF_VALID = 0x00; +static constexpr uint8_t MIFARE_CMD_NAK_CRC_ERROR_XFER_BUFF_VALID = 0x01; +static constexpr uint8_t MIFARE_CMD_NAK_INVALID_XFER_BUFF_INVALID = 0x04; +static constexpr uint8_t MIFARE_CMD_NAK_CRC_ERROR_XFER_BUFF_INVALID = 0x05; static const char *const MIFARE_CLASSIC = "Mifare Classic"; static const char *const NFC_FORUM_TYPE_2 = "NFC Forum Type 2"; static const char *const ERROR = "Error"; -static const uint8_t DEFAULT_KEY[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; -static const uint8_t NDEF_KEY[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7}; -static const uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}; +static constexpr uint8_t DEFAULT_KEY[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static constexpr uint8_t NDEF_KEY[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7}; +static constexpr uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}; /// Max UID size is 10 bytes, formatted as "XX-XX-XX-XX-XX-XX-XX-XX-XX-XX\0" = 30 chars static constexpr size_t FORMAT_UID_BUFFER_SIZE = 30; From 2f9b76f129c2b278b0c45ba5c66430d638631ee2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:33:39 -0600 Subject: [PATCH 067/609] [pn7160] Use constexpr for compile-time constants (#14078) --- esphome/components/pn7150/pn7150.h | 111 +++++++++---------- esphome/components/pn7160/pn7160.h | 121 +++++++++++---------- esphome/components/pn7160_spi/pn7160_spi.h | 4 +- 3 files changed, 119 insertions(+), 117 deletions(-) diff --git a/esphome/components/pn7150/pn7150.h b/esphome/components/pn7150/pn7150.h index 5feba17d21..c5dd283832 100644 --- a/esphome/components/pn7150/pn7150.h +++ b/esphome/components/pn7150/pn7150.h @@ -14,48 +14,48 @@ namespace esphome { namespace pn7150 { -static const uint16_t NFCC_DEFAULT_TIMEOUT = 10; -static const uint16_t NFCC_INIT_TIMEOUT = 50; -static const uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; +static constexpr uint16_t NFCC_DEFAULT_TIMEOUT = 10; +static constexpr uint16_t NFCC_INIT_TIMEOUT = 50; +static constexpr uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; -static const uint8_t NFCC_MAX_COMM_FAILS = 3; -static const uint8_t NFCC_MAX_ERROR_COUNT = 10; +static constexpr uint8_t NFCC_MAX_COMM_FAILS = 3; +static constexpr uint8_t NFCC_MAX_ERROR_COUNT = 10; -static const uint8_t XCHG_DATA_OID = 0x10; -static const uint8_t MF_SECTORSEL_OID = 0x32; -static const uint8_t MFC_AUTHENTICATE_OID = 0x40; -static const uint8_t TEST_PRBS_OID = 0x30; -static const uint8_t TEST_ANTENNA_OID = 0x3D; -static const uint8_t TEST_GET_REGISTER_OID = 0x33; +static constexpr uint8_t XCHG_DATA_OID = 0x10; +static constexpr uint8_t MF_SECTORSEL_OID = 0x32; +static constexpr uint8_t MFC_AUTHENTICATE_OID = 0x40; +static constexpr uint8_t TEST_PRBS_OID = 0x30; +static constexpr uint8_t TEST_ANTENNA_OID = 0x3D; +static constexpr uint8_t TEST_GET_REGISTER_OID = 0x33; -static const uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A -static const uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B -static const uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; -static const uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, - 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; -static const uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, - 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; -static const uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; -static const uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; -static const uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; -static const uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; -static const uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; -static const uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; +static constexpr uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, + 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, + 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; +static constexpr uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; +static constexpr uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; +static constexpr uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; +static constexpr uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; -static const uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields - 0x00, // config param identifier (TOTAL_DURATION) - 0x02, // length of value - 0x01, // TOTAL_DURATION (low)... - 0x00}; // TOTAL_DURATION (high): 1 ms +static constexpr uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0x01, // TOTAL_DURATION (low)... + 0x00}; // TOTAL_DURATION (high): 1 ms -static const uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields - 0x00, // config param identifier (TOTAL_DURATION) - 0x02, // length of value - 0xF8, // TOTAL_DURATION (low)... - 0x02}; // TOTAL_DURATION (high): 760 ms +static constexpr uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0xF8, // TOTAL_DURATION (low)... + 0x02}; // TOTAL_DURATION (high): 760 ms -static const uint8_t PMU_CFG[] = { +static constexpr uint8_t PMU_CFG[] = { 0x01, // Number of parameters 0xA0, 0x0E, // ext. tag 3, // length @@ -64,7 +64,7 @@ static const uint8_t PMU_CFG[] = { 0x01, // RFU; must be 0x00 for CFG1 and 0x01 for CFG2 }; -static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes +static constexpr uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes nfc::PROT_T1T, nfc::RF_DISCOVER_MAP_MODE_POLL, nfc::INTF_FRAME, // poll mode nfc::PROT_T2T, nfc::RF_DISCOVER_MAP_MODE_POLL, @@ -76,28 +76,29 @@ static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes nfc::PROT_MIFARE, nfc::RF_DISCOVER_MAP_MODE_POLL, nfc::INTF_TAGCMD}; // poll mode -static const uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = {nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode +static constexpr uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = { + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode -static const uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode +static constexpr uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode -static const uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode +static constexpr uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode -static const uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) - 1, // number of table entries - 0x01, // type = protocol-based - 3, // length - 0, // DH NFCEE ID, a static ID representing the DH-NFCEE - 0x01, // power state - nfc::PROT_ISODEP}; // protocol +static constexpr uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) + 1, // number of table entries + 0x01, // type = protocol-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x01, // power state + nfc::PROT_ISODEP}; // protocol enum class CardEmulationState : uint8_t { CARD_EMU_IDLE, diff --git a/esphome/components/pn7160/pn7160.h b/esphome/components/pn7160/pn7160.h index 9f2d10c2d5..77ab49399c 100644 --- a/esphome/components/pn7160/pn7160.h +++ b/esphome/components/pn7160/pn7160.h @@ -14,48 +14,48 @@ namespace esphome { namespace pn7160 { -static const uint16_t NFCC_DEFAULT_TIMEOUT = 10; -static const uint16_t NFCC_INIT_TIMEOUT = 50; -static const uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; +static constexpr uint16_t NFCC_DEFAULT_TIMEOUT = 10; +static constexpr uint16_t NFCC_INIT_TIMEOUT = 50; +static constexpr uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; -static const uint8_t NFCC_MAX_COMM_FAILS = 3; -static const uint8_t NFCC_MAX_ERROR_COUNT = 10; +static constexpr uint8_t NFCC_MAX_COMM_FAILS = 3; +static constexpr uint8_t NFCC_MAX_ERROR_COUNT = 10; -static const uint8_t XCHG_DATA_OID = 0x10; -static const uint8_t MF_SECTORSEL_OID = 0x32; -static const uint8_t MFC_AUTHENTICATE_OID = 0x40; -static const uint8_t TEST_PRBS_OID = 0x30; -static const uint8_t TEST_ANTENNA_OID = 0x3D; -static const uint8_t TEST_GET_REGISTER_OID = 0x33; +static constexpr uint8_t XCHG_DATA_OID = 0x10; +static constexpr uint8_t MF_SECTORSEL_OID = 0x32; +static constexpr uint8_t MFC_AUTHENTICATE_OID = 0x40; +static constexpr uint8_t TEST_PRBS_OID = 0x30; +static constexpr uint8_t TEST_ANTENNA_OID = 0x3D; +static constexpr uint8_t TEST_GET_REGISTER_OID = 0x33; -static const uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A -static const uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B -static const uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; -static const uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, - 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; -static const uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, - 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; -static const uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; -static const uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; -static const uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; -static const uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; -static const uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; -static const uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; +static constexpr uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, + 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, + 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; +static constexpr uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; +static constexpr uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; +static constexpr uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; +static constexpr uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; -static const uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields - 0x00, // config param identifier (TOTAL_DURATION) - 0x02, // length of value - 0x01, // TOTAL_DURATION (low)... - 0x00}; // TOTAL_DURATION (high): 1 ms +static constexpr uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0x01, // TOTAL_DURATION (low)... + 0x00}; // TOTAL_DURATION (high): 1 ms -static const uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields - 0x00, // config param identifier (TOTAL_DURATION) - 0x02, // length of value - 0xF8, // TOTAL_DURATION (low)... - 0x02}; // TOTAL_DURATION (high): 760 ms +static constexpr uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0xF8, // TOTAL_DURATION (low)... + 0x02}; // TOTAL_DURATION (high): 760 ms -static const uint8_t PMU_CFG[] = { +static constexpr uint8_t PMU_CFG[] = { 0x01, // Number of parameters 0xA0, 0x0E, // ext. tag 11, // length @@ -74,7 +74,7 @@ static const uint8_t PMU_CFG[] = { 0x0C, // RFU }; -static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes +static constexpr uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes nfc::PROT_T1T, nfc::RF_DISCOVER_MAP_MODE_POLL, nfc::INTF_FRAME, // poll mode nfc::PROT_T2T, nfc::RF_DISCOVER_MAP_MODE_POLL, @@ -86,33 +86,34 @@ static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes nfc::PROT_MIFARE, nfc::RF_DISCOVER_MAP_MODE_POLL, nfc::INTF_TAGCMD}; // poll mode -static const uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = {nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode +static constexpr uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = { + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode -static const uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode +static constexpr uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode -static const uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode +static constexpr uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode -static const uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) - 2, // number of table entries - 0x01, // type = protocol-based - 3, // length - 0, // DH NFCEE ID, a static ID representing the DH-NFCEE - 0x07, // power state - nfc::PROT_ISODEP, // protocol - 0x00, // type = technology-based - 3, // length - 0, // DH NFCEE ID, a static ID representing the DH-NFCEE - 0x07, // power state - nfc::TECH_PASSIVE_NFCA}; // technology +static constexpr uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) + 2, // number of table entries + 0x01, // type = protocol-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x07, // power state + nfc::PROT_ISODEP, // protocol + 0x00, // type = technology-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x07, // power state + nfc::TECH_PASSIVE_NFCA}; // technology enum class CardEmulationState : uint8_t { CARD_EMU_IDLE, diff --git a/esphome/components/pn7160_spi/pn7160_spi.h b/esphome/components/pn7160_spi/pn7160_spi.h index 7d4460a76d..9b6e21fa2a 100644 --- a/esphome/components/pn7160_spi/pn7160_spi.h +++ b/esphome/components/pn7160_spi/pn7160_spi.h @@ -10,8 +10,8 @@ namespace esphome { namespace pn7160_spi { -static const uint8_t TDD_SPI_READ = 0xFF; -static const uint8_t TDD_SPI_WRITE = 0x0A; +static constexpr uint8_t TDD_SPI_READ = 0xFF; +static constexpr uint8_t TDD_SPI_WRITE = 0x0A; class PN7160Spi : public pn7160::PN7160, public spi::SPIDevice Date: Wed, 18 Feb 2026 21:34:25 -0600 Subject: [PATCH 068/609] [bluetooth_proxy] Use constexpr for remaining compile-time constants (#14080) --- esphome/components/bluetooth_proxy/bluetooth_proxy.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 62a035e79f..85461755aa 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -23,9 +23,9 @@ namespace esphome::bluetooth_proxy { -static const esp_err_t ESP_GATT_NOT_CONNECTED = -1; -static const int DONE_SENDING_SERVICES = -2; -static const int INIT_SENDING_SERVICES = -3; +static constexpr esp_err_t ESP_GATT_NOT_CONNECTED = -1; +static constexpr int DONE_SENDING_SERVICES = -2; +static constexpr int INIT_SENDING_SERVICES = -3; using namespace esp32_ble_client; From 3c227eeca46d5ca6e99e5e59a9585d0f168642fb Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 18 Feb 2026 21:50:39 -0600 Subject: [PATCH 069/609] [audio] Add support for sinking via an arbitrary callback (#14035) --- esphome/components/audio/audio_transfer_buffer.cpp | 2 ++ esphome/components/audio/audio_transfer_buffer.h | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/esphome/components/audio/audio_transfer_buffer.cpp b/esphome/components/audio/audio_transfer_buffer.cpp index ddb669e0eb..a8be55d62f 100644 --- a/esphome/components/audio/audio_transfer_buffer.cpp +++ b/esphome/components/audio/audio_transfer_buffer.cpp @@ -165,6 +165,8 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait, if (this->ring_buffer_.use_count() > 0) { bytes_written = this->ring_buffer_->write_without_replacement((void *) this->data_start_, this->available(), ticks_to_wait); + } else if (this->sink_callback_ != nullptr) { + bytes_written = this->sink_callback_->audio_sink_write(this->data_start_, this->available(), ticks_to_wait); } this->decrease_buffer_length(bytes_written); diff --git a/esphome/components/audio/audio_transfer_buffer.h b/esphome/components/audio/audio_transfer_buffer.h index 24c0670d1a..22c22cc9ae 100644 --- a/esphome/components/audio/audio_transfer_buffer.h +++ b/esphome/components/audio/audio_transfer_buffer.h @@ -15,6 +15,12 @@ namespace esphome { namespace audio { +/// @brief Abstract interface for writing decoded audio data to a sink. +class AudioSinkCallback { + public: + virtual size_t audio_sink_write(uint8_t *data, size_t length, TickType_t ticks_to_wait) = 0; +}; + class AudioTransferBuffer { /* * @brief Class that facilitates tranferring data between a buffer and an audio source or sink. @@ -108,6 +114,10 @@ class AudioSinkTransferBuffer : public AudioTransferBuffer { void set_sink(speaker::Speaker *speaker) { this->speaker_ = speaker; } #endif + /// @brief Adds a callback as the transfer buffer's sink. + /// @param callback Pointer to the AudioSinkCallback implementation + void set_sink(AudioSinkCallback *callback) { this->sink_callback_ = callback; } + void clear_buffered_data() override; bool has_buffered_data() const override; @@ -116,6 +126,7 @@ class AudioSinkTransferBuffer : public AudioTransferBuffer { #ifdef USE_SPEAKER speaker::Speaker *speaker_{nullptr}; #endif + AudioSinkCallback *sink_callback_{nullptr}; }; class AudioSourceTransferBuffer : public AudioTransferBuffer { From 264c8faedd0ab956b0437ec2ecc0bacae1708923 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 18 Feb 2026 21:51:01 -0600 Subject: [PATCH 070/609] [media_player] Add more commands to support Sendspin (#12258) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/media_player/__init__.py | 250 +++++++----------- esphome/components/media_player/automation.h | 27 ++ .../components/media_player/media_player.cpp | 52 ++++ .../components/media_player/media_player.h | 46 ++-- tests/components/media_player/common.yaml | 25 ++ 5 files changed, 232 insertions(+), 168 deletions(-) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index c6ffe50d79..b2afbe5e58 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -35,86 +35,73 @@ MEDIA_PLAYER_FORMAT_PURPOSE_ENUM = { "announcement": MediaPlayerFormatPurpose.PURPOSE_ANNOUNCEMENT, } - -PlayAction = media_player_ns.class_( - "PlayAction", automation.Action, cg.Parented.template(MediaPlayer) -) -PlayMediaAction = media_player_ns.class_( - "PlayMediaAction", automation.Action, cg.Parented.template(MediaPlayer) -) -ToggleAction = media_player_ns.class_( - "ToggleAction", automation.Action, cg.Parented.template(MediaPlayer) -) -PauseAction = media_player_ns.class_( - "PauseAction", automation.Action, cg.Parented.template(MediaPlayer) -) -StopAction = media_player_ns.class_( - "StopAction", automation.Action, cg.Parented.template(MediaPlayer) -) -VolumeUpAction = media_player_ns.class_( - "VolumeUpAction", automation.Action, cg.Parented.template(MediaPlayer) -) -VolumeDownAction = media_player_ns.class_( - "VolumeDownAction", automation.Action, cg.Parented.template(MediaPlayer) -) -VolumeSetAction = media_player_ns.class_( - "VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer) -) -TurnOnAction = media_player_ns.class_( - "TurnOnAction", automation.Action, cg.Parented.template(MediaPlayer) -) -TurnOffAction = media_player_ns.class_( - "TurnOffAction", automation.Action, cg.Parented.template(MediaPlayer) -) - +# Local config key constants CONF_ANNOUNCEMENT = "announcement" CONF_ON_PLAY = "on_play" CONF_ON_PAUSE = "on_pause" CONF_ON_ANNOUNCEMENT = "on_announcement" CONF_MEDIA_URL = "media_url" -StateTrigger = media_player_ns.class_("StateTrigger", automation.Trigger.template()) -IdleTrigger = media_player_ns.class_("IdleTrigger", automation.Trigger.template()) -PlayTrigger = media_player_ns.class_("PlayTrigger", automation.Trigger.template()) -PauseTrigger = media_player_ns.class_("PauseTrigger", automation.Trigger.template()) -AnnoucementTrigger = media_player_ns.class_( - "AnnouncementTrigger", automation.Trigger.template() +# Command actions that all share the same schema and codegen handler +_COMMAND_ACTIONS = [ + "play", + "pause", + "stop", + "toggle", + "volume_up", + "volume_down", + "turn_on", + "turn_off", + "next", + "previous", + "mute", + "unmute", + "repeat_off", + "repeat_one", + "repeat_all", + "shuffle", + "unshuffle", + "group_join", + "clear_playlist", +] + +# State triggers: (config_key, C++ class name) +_STATE_TRIGGERS = [ + (CONF_ON_STATE, "StateTrigger"), + (CONF_ON_IDLE, "IdleTrigger"), + (CONF_ON_PLAY, "PlayTrigger"), + (CONF_ON_PAUSE, "PauseTrigger"), + (CONF_ON_ANNOUNCEMENT, "AnnouncementTrigger"), + (CONF_ON_TURN_ON, "OnTrigger"), + (CONF_ON_TURN_OFF, "OffTrigger"), +] + +# State conditions that all share the same schema and codegen handler +_STATE_CONDITIONS = [ + "idle", + "paused", + "playing", + "announcing", + "on", + "off", + "muted", +] + +# Special action classes with custom schemas/handlers +PlayMediaAction = media_player_ns.class_( + "PlayMediaAction", automation.Action, cg.Parented.template(MediaPlayer) ) -OnTrigger = media_player_ns.class_("OnTrigger", automation.Trigger.template()) -OffTrigger = media_player_ns.class_("OffTrigger", automation.Trigger.template()) -IsIdleCondition = media_player_ns.class_("IsIdleCondition", automation.Condition) -IsPausedCondition = media_player_ns.class_("IsPausedCondition", automation.Condition) -IsPlayingCondition = media_player_ns.class_("IsPlayingCondition", automation.Condition) -IsAnnouncingCondition = media_player_ns.class_( - "IsAnnouncingCondition", automation.Condition +VolumeSetAction = media_player_ns.class_( + "VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer) ) -IsOnCondition = media_player_ns.class_("IsOnCondition", automation.Condition) -IsOffCondition = media_player_ns.class_("IsOffCondition", automation.Condition) async def setup_media_player_core_(var, config): await setup_entity(var, config, "media_player") - for conf in config.get(CONF_ON_STATE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_IDLE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_PLAY, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_PAUSE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_ANNOUNCEMENT, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_TURN_ON, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_TURN_OFF, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + for conf_key, _ in _STATE_TRIGGERS: + for conf in config.get(conf_key, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) async def register_media_player(var, config): @@ -133,41 +120,14 @@ async def new_media_player(config, *args): _MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( { - cv.Optional(CONF_ON_STATE): automation.validate_automation( + cv.Optional(conf_key): automation.validate_automation( { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + media_player_ns.class_(class_name, automation.Trigger.template()) + ), } - ), - cv.Optional(CONF_ON_IDLE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger), - } - ), - cv.Optional(CONF_ON_PLAY): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PlayTrigger), - } - ), - cv.Optional(CONF_ON_PAUSE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger), - } - ), - cv.Optional(CONF_ON_ANNOUNCEMENT): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(AnnoucementTrigger), - } - ), - cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnTrigger), - } - ), - cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OffTrigger), - } - ), + ) + for conf_key, class_name in _STATE_TRIGGERS } ) @@ -228,56 +188,48 @@ async def media_player_play_media_action(config, action_id, template_arg, args): return var -@automation.register_action("media_player.play", PlayAction, MEDIA_PLAYER_ACTION_SCHEMA) -@automation.register_action( - "media_player.toggle", ToggleAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action( - "media_player.pause", PauseAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action("media_player.stop", StopAction, MEDIA_PLAYER_ACTION_SCHEMA) -@automation.register_action( - "media_player.volume_up", VolumeUpAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action( - "media_player.volume_down", VolumeDownAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action( - "media_player.turn_on", TurnOnAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action( - "media_player.turn_off", TurnOffAction, MEDIA_PLAYER_ACTION_SCHEMA -) -async def media_player_action(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_) - cg.add(var.set_announcement(announcement)) - return var +def _snake_to_camel(name): + return "".join(word.capitalize() for word in name.split("_")) -@automation.register_condition( - "media_player.is_idle", IsIdleCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -@automation.register_condition( - "media_player.is_paused", IsPausedCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -@automation.register_condition( - "media_player.is_playing", IsPlayingCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -@automation.register_condition( - "media_player.is_announcing", IsAnnouncingCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -@automation.register_condition( - "media_player.is_on", IsOnCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -@automation.register_condition( - "media_player.is_off", IsOffCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -async def media_player_condition(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var +def _register_command_actions(): + async def handler(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_) + cg.add(var.set_announcement(announcement)) + return var + + for action_name in _COMMAND_ACTIONS: + class_name = f"{_snake_to_camel(action_name)}Action" + action_class = media_player_ns.class_( + class_name, automation.Action, cg.Parented.template(MediaPlayer) + ) + automation.register_action( + f"media_player.{action_name}", action_class, MEDIA_PLAYER_ACTION_SCHEMA + )(handler) + + +_register_command_actions() + + +def _register_state_conditions(): + async def handler(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + for condition_name in _STATE_CONDITIONS: + class_name = f"Is{_snake_to_camel(condition_name)}Condition" + condition_class = media_player_ns.class_(class_name, automation.Condition) + automation.register_condition( + f"media_player.is_{condition_name}", + condition_class, + MEDIA_PLAYER_CONDITION_SCHEMA, + )(handler) + + +_register_state_conditions() @automation.register_action( diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index 50e7693cb5..90e7bf75b5 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -32,6 +32,28 @@ template using TurnOnAction = MediaPlayerCommandAction; template using TurnOffAction = MediaPlayerCommandAction; +template +using NextAction = MediaPlayerCommandAction; +template +using PreviousAction = MediaPlayerCommandAction; +template +using MuteAction = MediaPlayerCommandAction; +template +using UnmuteAction = MediaPlayerCommandAction; +template +using RepeatOffAction = MediaPlayerCommandAction; +template +using RepeatOneAction = MediaPlayerCommandAction; +template +using RepeatAllAction = MediaPlayerCommandAction; +template +using ShuffleAction = MediaPlayerCommandAction; +template +using UnshuffleAction = MediaPlayerCommandAction; +template +using GroupJoinAction = MediaPlayerCommandAction; +template +using ClearPlaylistAction = MediaPlayerCommandAction; template class PlayMediaAction : public Action, public Parented { TEMPLATABLE_VALUE(std::string, media_url) @@ -105,5 +127,10 @@ template class IsOffCondition : public Condition, public bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_OFF; } }; +template class IsMutedCondition : public Condition, public Parented { + public: + bool check(const Ts &...x) override { return this->parent_->is_muted(); } +}; + } // namespace media_player } // namespace esphome diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index 17d9b054da..a53d598b0f 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -60,11 +60,39 @@ const char *media_player_command_to_string(MediaPlayerCommand command) { return "TURN_ON"; case MEDIA_PLAYER_COMMAND_TURN_OFF: return "TURN_OFF"; + case MEDIA_PLAYER_COMMAND_NEXT: + return "NEXT"; + case MEDIA_PLAYER_COMMAND_PREVIOUS: + return "PREVIOUS"; + case MEDIA_PLAYER_COMMAND_REPEAT_ALL: + return "REPEAT_ALL"; + case MEDIA_PLAYER_COMMAND_SHUFFLE: + return "SHUFFLE"; + case MEDIA_PLAYER_COMMAND_UNSHUFFLE: + return "UNSHUFFLE"; + case MEDIA_PLAYER_COMMAND_GROUP_JOIN: + return "GROUP_JOIN"; default: return "UNKNOWN"; } } +void MediaPlayerTraits::set_supports_pause(bool supports_pause) { + if (supports_pause) { + this->feature_flags_ |= MediaPlayerEntityFeature::PAUSE | MediaPlayerEntityFeature::PLAY; + } else { + this->feature_flags_ &= ~(MediaPlayerEntityFeature::PAUSE | MediaPlayerEntityFeature::PLAY); + } +} + +void MediaPlayerTraits::set_supports_turn_off_on(bool supports_turn_off_on) { + if (supports_turn_off_on) { + this->feature_flags_ |= MediaPlayerEntityFeature::TURN_OFF | MediaPlayerEntityFeature::TURN_ON; + } else { + this->feature_flags_ &= ~(MediaPlayerEntityFeature::TURN_OFF | MediaPlayerEntityFeature::TURN_ON); + } +} + void MediaPlayerCall::validate_() { if (this->media_url_.has_value()) { if (this->command_.has_value() && this->command_.value() != MEDIA_PLAYER_COMMAND_ENQUEUE) { @@ -125,6 +153,30 @@ MediaPlayerCall &MediaPlayerCall::set_command(const char *command) { this->set_command(MEDIA_PLAYER_COMMAND_TURN_ON); } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TURN_OFF")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_TURN_OFF); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("VOLUME_UP")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_VOLUME_UP); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("VOLUME_DOWN")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_VOLUME_DOWN); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("ENQUEUE")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_ENQUEUE); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("REPEAT_ONE")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_REPEAT_ONE); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("REPEAT_OFF")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_REPEAT_OFF); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("REPEAT_ALL")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_REPEAT_ALL); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("CLEAR_PLAYLIST")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("NEXT")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_NEXT); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("PREVIOUS")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_PREVIOUS); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("SHUFFLE")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_SHUFFLE); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("UNSHUFFLE")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_UNSHUFFLE); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("GROUP_JOIN")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_GROUP_JOIN); } else { ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command); } diff --git a/esphome/components/media_player/media_player.h b/esphome/components/media_player/media_player.h index f75a68dd85..3509747718 100644 --- a/esphome/components/media_player/media_player.h +++ b/esphome/components/media_player/media_player.h @@ -58,6 +58,12 @@ enum MediaPlayerCommand : uint8_t { MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST = 11, MEDIA_PLAYER_COMMAND_TURN_ON = 12, MEDIA_PLAYER_COMMAND_TURN_OFF = 13, + MEDIA_PLAYER_COMMAND_NEXT = 14, + MEDIA_PLAYER_COMMAND_PREVIOUS = 15, + MEDIA_PLAYER_COMMAND_REPEAT_ALL = 16, + MEDIA_PLAYER_COMMAND_SHUFFLE = 17, + MEDIA_PLAYER_COMMAND_UNSHUFFLE = 18, + MEDIA_PLAYER_COMMAND_GROUP_JOIN = 19, }; const char *media_player_command_to_string(MediaPlayerCommand command); @@ -74,38 +80,40 @@ struct MediaPlayerSupportedFormat { uint32_t sample_bytes; }; +// Base features always reported for all media players +static constexpr uint32_t BASE_MEDIA_PLAYER_FEATURES = + MediaPlayerEntityFeature::PLAY_MEDIA | MediaPlayerEntityFeature::BROWSE_MEDIA | MediaPlayerEntityFeature::STOP | + MediaPlayerEntityFeature::VOLUME_SET | MediaPlayerEntityFeature::VOLUME_MUTE | + MediaPlayerEntityFeature::MEDIA_ANNOUNCE; + class MediaPlayer; class MediaPlayerTraits { public: MediaPlayerTraits() = default; - void set_supports_pause(bool supports_pause) { this->supports_pause_ = supports_pause; } - bool get_supports_pause() const { return this->supports_pause_; } - - void set_supports_turn_off_on(bool supports_turn_off_on) { this->supports_turn_off_on_ = supports_turn_off_on; } - bool get_supports_turn_off_on() const { return this->supports_turn_off_on_; } + uint32_t get_feature_flags() const { return this->feature_flags_; } + void add_feature_flags(uint32_t feature_flags) { this->feature_flags_ |= feature_flags; } + void clear_feature_flags(uint32_t feature_flags) { this->feature_flags_ &= ~feature_flags; } + // Returns true only if all specified flags are set + bool has_feature_flags(uint32_t feature_flags) const { + return (this->feature_flags_ & feature_flags) == feature_flags; + } std::vector &get_supported_formats() { return this->supported_formats_; } - uint32_t get_feature_flags() const { - uint32_t flags = 0; - flags |= MediaPlayerEntityFeature::PLAY_MEDIA | MediaPlayerEntityFeature::BROWSE_MEDIA | - MediaPlayerEntityFeature::STOP | MediaPlayerEntityFeature::VOLUME_SET | - MediaPlayerEntityFeature::VOLUME_MUTE | MediaPlayerEntityFeature::MEDIA_ANNOUNCE; - if (this->get_supports_pause()) { - flags |= MediaPlayerEntityFeature::PAUSE | MediaPlayerEntityFeature::PLAY; - } - if (this->get_supports_turn_off_on()) { - flags |= MediaPlayerEntityFeature::TURN_OFF | MediaPlayerEntityFeature::TURN_ON; - } - return flags; + // Legacy setters/getters are kept for backward compatibility + void set_supports_pause(bool supports_pause); + bool get_supports_pause() const { return this->has_feature_flags(MediaPlayerEntityFeature::PAUSE); } + + void set_supports_turn_off_on(bool supports_turn_off_on); + bool get_supports_turn_off_on() const { + return this->has_feature_flags(MediaPlayerEntityFeature::TURN_ON | MediaPlayerEntityFeature::TURN_OFF); } protected: std::vector supported_formats_{}; - bool supports_pause_{false}; - bool supports_turn_off_on_{false}; + uint32_t feature_flags_{BASE_MEDIA_PLAYER_FEATURES}; }; class MediaPlayerCall { diff --git a/tests/components/media_player/common.yaml b/tests/components/media_player/common.yaml index 763bc231c0..c83ee89ad4 100644 --- a/tests/components/media_player/common.yaml +++ b/tests/components/media_player/common.yaml @@ -23,8 +23,27 @@ media_player: - media_player.stop: - media_player.stop: announcement: true + on_announcement: + - media_player.play: + on_turn_on: + - media_player.play: + on_turn_off: + - media_player.stop: on_pause: - media_player.toggle: + - media_player.turn_on: + - media_player.turn_off: + - media_player.next: + - media_player.previous: + - media_player.mute: + - media_player.unmute: + - media_player.repeat_off: + - media_player.repeat_one: + - media_player.repeat_all: + - media_player.shuffle: + - media_player.unshuffle: + - media_player.group_join: + - media_player.clear_playlist: - wait_until: media_player.is_idle: - wait_until: @@ -33,6 +52,12 @@ media_player: media_player.is_announcing: - wait_until: media_player.is_paused: + - wait_until: + media_player.is_on: + - wait_until: + media_player.is_off: + - wait_until: + media_player.is_muted: - media_player.volume_up: - media_player.volume_down: - media_player.volume_set: 50% From ba7134ee3f870ce00d2990d84b142b734028330f Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 18 Feb 2026 21:51:16 -0600 Subject: [PATCH 071/609] [mdns] add Sendspin advertisement support (#14013) Co-authored-by: J. Nick Koston --- esphome/components/mdns/__init__.py | 2 +- esphome/components/mdns/mdns_component.cpp | 19 ++++++++++++++++++- esphome/core/defines.h | 2 ++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 3088d8ad7e..f87f929615 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -21,7 +21,7 @@ DEPENDENCIES = ["network"] # Components that create mDNS services at runtime # IMPORTANT: If you add a new component here, you must also update the corresponding # #ifdef blocks in mdns_component.cpp compile_records_() method -COMPONENTS_WITH_MDNS_SERVICES = ("api", "prometheus", "web_server") +COMPONENTS_WITH_MDNS_SERVICES = ("api", "prometheus", "sendspin", "web_server") mdns_ns = cg.esphome_ns.namespace("mdns") MDNSComponent = mdns_ns.class_("MDNSComponent", cg.Component) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 47db92610a..5e5e1279d9 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -29,6 +29,10 @@ static const char *const TAG = "mdns"; #define USE_WEBSERVER_PORT 80 // NOLINT #endif +#ifndef USE_SENDSPIN_PORT +#define USE_SENDSPIN_PORT 8928 // NOLINT +#endif + // Define all constant strings using the macro MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp"); @@ -150,6 +154,18 @@ void MDNSComponent::compile_records_(StaticVector Date: Wed, 18 Feb 2026 21:51:33 -0600 Subject: [PATCH 072/609] [audio, speaker] Add support for decoding Ogg Opus files (#13967) --- esphome/components/audio/__init__.py | 42 ++++++++++ esphome/components/audio/audio.cpp | 4 + esphome/components/audio/audio.h | 3 + esphome/components/audio/audio_decoder.cpp | 60 +++++++++++++- esphome/components/audio/audio_decoder.h | 15 +++- esphome/components/audio/audio_reader.cpp | 13 +++ .../speaker/media_player/__init__.py | 82 ++++++++++++++++--- .../speaker/media_player/audio_pipeline.cpp | 10 +++ esphome/core/defines.h | 1 + esphome/idf_component.yml | 2 + .../speaker/audio_dac.esp32-ard.yaml | 10 --- .../speaker/common-media_player.yaml | 7 ++ .../speaker/media_player.esp32-s3-idf.yaml | 9 -- ...idf.yaml => test-audio_dac.esp32-idf.yaml} | 0 ....yaml => test-media_player.esp32-idf.yaml} | 0 tests/components/speaker/test.esp32-ard.yaml | 10 --- 16 files changed, 222 insertions(+), 46 deletions(-) delete mode 100644 tests/components/speaker/audio_dac.esp32-ard.yaml delete mode 100644 tests/components/speaker/media_player.esp32-s3-idf.yaml rename tests/components/speaker/{audio_dac.esp32-idf.yaml => test-audio_dac.esp32-idf.yaml} (100%) rename tests/components/speaker/{media_player.esp32-idf.yaml => test-media_player.esp32-idf.yaml} (100%) delete mode 100644 tests/components/speaker/test.esp32-ard.yaml diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index f48b776ddd..d8d426ec63 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -1,10 +1,14 @@ +from dataclasses import dataclass + import esphome.codegen as cg from esphome.components.esp32 import add_idf_component, include_builtin_idf_component import esphome.config_validation as cv from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE +from esphome.core import CORE import esphome.final_validate as fv CODEOWNERS = ["@kahrendt"] +DOMAIN = "audio" audio_ns = cg.esphome_ns.namespace("audio") AudioFile = audio_ns.struct("AudioFile") @@ -14,9 +18,38 @@ AUDIO_FILE_TYPE_ENUM = { "WAV": AudioFileType.WAV, "MP3": AudioFileType.MP3, "FLAC": AudioFileType.FLAC, + "OPUS": AudioFileType.OPUS, } +@dataclass +class AudioData: + flac_support: bool = False + mp3_support: bool = False + opus_support: bool = False + + +def _get_data() -> AudioData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = AudioData() + return CORE.data[DOMAIN] + + +def request_flac_support() -> None: + """Request FLAC codec support for audio decoding.""" + _get_data().flac_support = True + + +def request_mp3_support() -> None: + """Request MP3 codec support for audio decoding.""" + _get_data().mp3_support = True + + +def request_opus_support() -> None: + """Request Opus codec support for audio decoding.""" + _get_data().opus_support = True + + CONF_MIN_BITS_PER_SAMPLE = "min_bits_per_sample" CONF_MAX_BITS_PER_SAMPLE = "max_bits_per_sample" CONF_MIN_CHANNELS = "min_channels" @@ -173,3 +206,12 @@ async def to_code(config): name="esphome/esp-audio-libs", ref="2.0.3", ) + + data = _get_data() + if data.flac_support: + cg.add_define("USE_AUDIO_FLAC_SUPPORT") + if data.mp3_support: + cg.add_define("USE_AUDIO_MP3_SUPPORT") + if data.opus_support: + cg.add_define("USE_AUDIO_OPUS_SUPPORT") + add_idf_component(name="esphome/micro-opus", ref="0.3.3") diff --git a/esphome/components/audio/audio.cpp b/esphome/components/audio/audio.cpp index 9cc9b7d0da..40592f6107 100644 --- a/esphome/components/audio/audio.cpp +++ b/esphome/components/audio/audio.cpp @@ -46,6 +46,10 @@ const char *audio_file_type_to_string(AudioFileType file_type) { #ifdef USE_AUDIO_MP3_SUPPORT case AudioFileType::MP3: return "MP3"; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + case AudioFileType::OPUS: + return "OPUS"; #endif case AudioFileType::WAV: return "WAV"; diff --git a/esphome/components/audio/audio.h b/esphome/components/audio/audio.h index e01d7eb101..7d7db9e944 100644 --- a/esphome/components/audio/audio.h +++ b/esphome/components/audio/audio.h @@ -112,6 +112,9 @@ enum class AudioFileType : uint8_t { #endif #ifdef USE_AUDIO_MP3_SUPPORT MP3, +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + OPUS, #endif WAV, }; diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index 8f514468c4..ee6d7d0a15 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -3,10 +3,13 @@ #ifdef USE_ESP32 #include "esphome/core/hal.h" +#include "esphome/core/log.h" namespace esphome { namespace audio { +static const char *const TAG = "audio.decoder"; + static const uint32_t DECODING_TIMEOUT_MS = 50; // The decode function will yield after this duration static const uint32_t READ_WRITE_TIMEOUT_MS = 20; // Timeout for transferring audio data @@ -79,6 +82,14 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { // Always reallocate the output transfer buffer to the smallest necessary size this->output_transfer_buffer_->reallocate(this->free_buffer_required_); break; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + case AudioFileType::OPUS: + this->opus_decoder_ = make_unique(); + this->free_buffer_required_ = + this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header + this->decoder_buffers_internally_ = true; + break; #endif case AudioFileType::WAV: this->wav_decoder_ = make_unique(); @@ -158,8 +169,9 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { // Decode more audio // Only shift data on the first loop iteration to avoid unnecessary, slow moves - size_t bytes_read = this->input_transfer_buffer_->transfer_data_from_source(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), - first_loop_iteration); + // If the decoder buffers internally, then never shift + size_t bytes_read = this->input_transfer_buffer_->transfer_data_from_source( + pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), first_loop_iteration && !this->decoder_buffers_internally_); if (!first_loop_iteration && (this->input_transfer_buffer_->available() < bytes_processed)) { // Less data is available than what was processed in last iteration, so don't attempt to decode. @@ -195,6 +207,11 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { case AudioFileType::MP3: state = this->decode_mp3_(); break; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + case AudioFileType::OPUS: + state = this->decode_opus_(); + break; #endif case AudioFileType::WAV: state = this->decode_wav_(); @@ -339,6 +356,45 @@ FileDecoderState AudioDecoder::decode_mp3_() { } #endif +#ifdef USE_AUDIO_OPUS_SUPPORT +FileDecoderState AudioDecoder::decode_opus_() { + bool processed_header = this->opus_decoder_->is_initialized(); + + size_t bytes_consumed, samples_decoded; + + micro_opus::OggOpusResult result = this->opus_decoder_->decode( + this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available(), + this->output_transfer_buffer_->get_buffer_end(), this->output_transfer_buffer_->free(), bytes_consumed, + samples_decoded); + + if (result == micro_opus::OGG_OPUS_OK) { + if (!processed_header && this->opus_decoder_->is_initialized()) { + // Header processed and stream info is available + this->audio_stream_info_ = + audio::AudioStreamInfo(this->opus_decoder_->get_bit_depth(), this->opus_decoder_->get_channels(), + this->opus_decoder_->get_sample_rate()); + } + if (samples_decoded > 0 && this->audio_stream_info_.has_value()) { + // Some audio was processed + this->output_transfer_buffer_->increase_buffer_length( + this->audio_stream_info_.value().frames_to_bytes(samples_decoded)); + } + this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed); + } else if (result == micro_opus::OGG_OPUS_OUTPUT_BUFFER_TOO_SMALL) { + // Reallocate to decode the packet on the next call + this->free_buffer_required_ = this->opus_decoder_->get_required_output_buffer_size(); + if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) { + // Couldn't reallocate output buffer + return FileDecoderState::FAILED; + } + } else { + ESP_LOGE(TAG, "Opus decoder failed: %" PRId8, result); + return FileDecoderState::POTENTIALLY_FAILED; + } + return FileDecoderState::MORE_TO_PROCESS; +} +#endif + FileDecoderState AudioDecoder::decode_wav_() { if (!this->audio_stream_info_.has_value()) { // Header hasn't been processed diff --git a/esphome/components/audio/audio_decoder.h b/esphome/components/audio/audio_decoder.h index 2ca1d623fe..cad16110ae 100644 --- a/esphome/components/audio/audio_decoder.h +++ b/esphome/components/audio/audio_decoder.h @@ -24,6 +24,11 @@ #endif #include +// micro-opus +#ifdef USE_AUDIO_OPUS_SUPPORT +#include +#endif + namespace esphome { namespace audio { @@ -47,7 +52,7 @@ class AudioDecoder { * @brief Class that facilitates decoding an audio file. * The audio file is read from a ring buffer source, decoded, and sent to an audio sink (ring buffer or speaker * component). - * Supports wav, flac, and mp3 formats. + * Supports wav, flac, mp3, and ogg opus formats. */ public: /// @brief Allocates the input and output transfer buffers @@ -55,7 +60,7 @@ class AudioDecoder { /// @param output_buffer_size Size of the output transfer buffer in bytes. AudioDecoder(size_t input_buffer_size, size_t output_buffer_size); - /// @brief Deallocates the MP3 decoder (the flac and wav decoders are deallocated automatically) + /// @brief Deallocates the MP3 decoder (the flac, opus, and wav decoders are deallocated automatically) ~AudioDecoder(); /// @brief Adds a source ring buffer for raw file data. Takes ownership of the ring buffer in a shared_ptr. @@ -108,6 +113,10 @@ class AudioDecoder { #ifdef USE_AUDIO_MP3_SUPPORT FileDecoderState decode_mp3_(); esp_audio_libs::helix_decoder::HMP3Decoder mp3_decoder_; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + FileDecoderState decode_opus_(); + std::unique_ptr opus_decoder_; #endif FileDecoderState decode_wav_(); @@ -124,6 +133,8 @@ class AudioDecoder { bool end_of_file_{false}; bool wav_has_known_end_{false}; + bool decoder_buffers_internally_{false}; + bool pause_output_{false}; uint32_t accumulated_frames_written_{0}; diff --git a/esphome/components/audio/audio_reader.cpp b/esphome/components/audio/audio_reader.cpp index 4e4bd31f9b..78d69d7a39 100644 --- a/esphome/components/audio/audio_reader.cpp +++ b/esphome/components/audio/audio_reader.cpp @@ -197,6 +197,11 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { else if (str_endswith_ignore_case(url, ".flac")) { file_type = AudioFileType::FLAC; } +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + else if (str_endswith_ignore_case(url, ".opus")) { + file_type = AudioFileType::OPUS; + } #endif else { file_type = AudioFileType::NONE; @@ -241,6 +246,14 @@ AudioFileType AudioReader::get_audio_type(const char *content_type) { if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) { return AudioFileType::FLAC; } +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + // Match "audio/ogg" with a codecs parameter containing "opus" + // Valid forms: audio/ogg;codecs=opus, audio/ogg; codecs="opus", etc. + // Plain "audio/ogg" without a codecs parameter is not matched, as those are almost always Ogg Vorbis streams + if (strncasecmp(content_type, "audio/ogg", 9) == 0 && strcasestr(content_type + 9, "opus") != nullptr) { + return AudioFileType::OPUS; + } #endif return AudioFileType::NONE; } diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 034312236c..b302bd9b23 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -26,7 +26,6 @@ from esphome.const import ( from esphome.core import CORE, HexInt from esphome.core.entity_helpers import inherit_property_from from esphome.external_files import download_content -from esphome.final_validate import full_config _LOGGER = logging.getLogger(__name__) @@ -37,6 +36,10 @@ DEPENDENCIES = ["network"] CODEOWNERS = ["@kahrendt", "@synesthesiam"] DOMAIN = "media_player" +CODEC_SUPPORT_ALL = "all" +CODEC_SUPPORT_NEEDED = "needed" +CODEC_SUPPORT_NONE = "none" + TYPE_LOCAL = "local" TYPE_WEB = "web" @@ -110,6 +113,8 @@ def _get_supported_format_struct(pipeline, type): args.append(("format", "flac")) elif pipeline[CONF_FORMAT] == "MP3": args.append(("format", "mp3")) + elif pipeline[CONF_FORMAT] == "OPUS": + args.append(("format", "opus")) elif pipeline[CONF_FORMAT] == "WAV": args.append(("format", "wav")) @@ -173,6 +178,13 @@ def _read_audio_file_and_type(file_config): media_file_type = audio.AUDIO_FILE_TYPE_ENUM["MP3"] elif file_type in ("flac"): media_file_type = audio.AUDIO_FILE_TYPE_ENUM["FLAC"] + elif ( + file_type in ("ogg") + and len(data) >= 36 + and data.startswith(b"OggS") + and data[28:36] == b"OpusHead" + ): + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["OPUS"] return data, media_file_type @@ -199,6 +211,10 @@ def _validate_pipeline(config): inherit_property_from(CONF_NUM_CHANNELS, CONF_SPEAKER)(config) inherit_property_from(CONF_SAMPLE_RATE, CONF_SPEAKER)(config) + # Opus only supports 48 kHz + if config.get(CONF_FORMAT) == "OPUS" and config.get(CONF_SAMPLE_RATE) != 48000: + raise cv.Invalid("Opus only supports a sample rate of 48000 Hz") + # Validate the transcoder settings is compatible with the speaker audio.final_validate_audio_schema( "speaker media_player", @@ -225,12 +241,27 @@ def _validate_repeated_speaker(config): def _final_validate(config): - # Default to using codec if psram is enabled - if (use_codec := config.get(CONF_CODEC_SUPPORT_ENABLED)) is None: - use_codec = psram.DOMAIN in full_config.get() - conf_id = config[CONF_ID].id - core_data = CORE.data.setdefault(DOMAIN, {conf_id: {}}) - core_data[conf_id][CONF_CODEC_SUPPORT_ENABLED] = use_codec + # Normalize boolean values to string equivalents + codec_mode = config[CONF_CODEC_SUPPORT_ENABLED] + if codec_mode is True: + codec_mode = CODEC_SUPPORT_ALL + elif codec_mode is False: + codec_mode = CODEC_SUPPORT_NONE + + use_codec = codec_mode != CODEC_SUPPORT_NONE + + # In "needed" mode, collect formats from pipelines and files + needed_formats = set() + need_all = False + if codec_mode == CODEC_SUPPORT_NEEDED: + for pipeline_key in (CONF_ANNOUNCEMENT_PIPELINE, CONF_MEDIA_PIPELINE): + if pipeline := config.get(pipeline_key): + fmt = pipeline[CONF_FORMAT] + if fmt == "NONE": + # No preferred format means any format could arrive + need_all = True + else: + needed_formats.add(fmt) for file_config in config.get(CONF_FILES, []): _, media_file_type = _read_audio_file_and_type(file_config) @@ -243,6 +274,26 @@ def _final_validate(config): raise cv.Invalid( f"Unsupported local media file type, set {CONF_CODEC_SUPPORT_ENABLED} to true or convert the media file to wav" ) + # In "needed" mode, add file format to needed codecs + if codec_mode == CODEC_SUPPORT_NEEDED: + for fmt_name, fmt_enum in audio.AUDIO_FILE_TYPE_ENUM.items(): + if str(media_file_type) == str(fmt_enum): + if fmt_name not in ("WAV", "NONE"): + needed_formats.add(fmt_name) + break + + # Request codec support + if codec_mode == CODEC_SUPPORT_ALL or need_all: + audio.request_flac_support() + audio.request_mp3_support() + audio.request_opus_support() + elif codec_mode == CODEC_SUPPORT_NEEDED: + if "FLAC" in needed_formats: + audio.request_flac_support() + if "MP3" in needed_formats: + audio.request_mp3_support() + if "OPUS" in needed_formats: + audio.request_opus_support() return config @@ -307,7 +358,17 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range( min=4000, max=4000000 ), - cv.Optional(CONF_CODEC_SUPPORT_ENABLED): cv.boolean, + cv.Optional( + CONF_CODEC_SUPPORT_ENABLED, default=CODEC_SUPPORT_NEEDED + ): cv.Any( + cv.boolean, + cv.one_of( + CODEC_SUPPORT_ALL, + CODEC_SUPPORT_NEEDED, + CODEC_SUPPORT_NONE, + lower=True, + ), + ), cv.Optional(CONF_FILES): cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All( cv.boolean, cv.requires_component(psram.DOMAIN) @@ -340,11 +401,6 @@ FINAL_VALIDATE_SCHEMA = cv.All( async def to_code(config): - if CORE.data[DOMAIN][config[CONF_ID].id][CONF_CODEC_SUPPORT_ENABLED]: - # Compile all supported audio codecs - cg.add_define("USE_AUDIO_FLAC_SUPPORT", True) - cg.add_define("USE_AUDIO_MP3_SUPPORT", True) - var = await media_player.new_media_player(config) await cg.register_component(var, config) diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index 8be37d740a..177743feb1 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -13,7 +13,12 @@ namespace speaker { static const uint32_t INITIAL_BUFFER_MS = 1000; // Start playback after buffering this duration of the file static const uint32_t READ_TASK_STACK_SIZE = 5 * 1024; +// Opus decoding uses more stack than other codecs +#ifdef USE_AUDIO_OPUS_SUPPORT +static const uint32_t DECODE_TASK_STACK_SIZE = 5 * 1024; +#else static const uint32_t DECODE_TASK_STACK_SIZE = 3 * 1024; +#endif static const uint32_t INFO_ERROR_QUEUE_COUNT = 5; @@ -552,6 +557,11 @@ void AudioPipeline::decode_task(void *params) { case audio::AudioFileType::FLAC: initial_bytes_to_buffer /= 2; // Estimate the FLAC compression factor is 2 break; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + case audio::AudioFileType::OPUS: + initial_bytes_to_buffer /= 8; // Estimate the Opus compression factor is 8 + break; #endif default: break; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index a70c6cd0d0..5559ec88d0 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -130,6 +130,7 @@ #define USE_AUDIO_DAC #define USE_AUDIO_FLAC_SUPPORT #define USE_AUDIO_MP3_SUPPORT +#define USE_AUDIO_OPUS_SUPPORT #define USE_API #define USE_API_CLIENT_CONNECTED_TRIGGER #define USE_API_CLIENT_DISCONNECTED_TRIGGER diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index f39ea9b3ae..83b2d9d95c 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -3,6 +3,8 @@ dependencies: version: "7.4.2" esphome/esp-audio-libs: version: 2.0.3 + esphome/micro-opus: + version: 0.3.3 espressif/esp-tflite-micro: version: 1.3.3~1 espressif/esp32-camera: diff --git a/tests/components/speaker/audio_dac.esp32-ard.yaml b/tests/components/speaker/audio_dac.esp32-ard.yaml deleted file mode 100644 index 3f5d1bba7c..0000000000 --- a/tests/components/speaker/audio_dac.esp32-ard.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO27 - i2s_lrclk_pin: GPIO26 - i2s_mclk_pin: GPIO25 - i2s_dout_pin: GPIO23 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml - -<<: !include common-audio_dac.yaml diff --git a/tests/components/speaker/common-media_player.yaml b/tests/components/speaker/common-media_player.yaml index edc9f670fc..c958c0d912 100644 --- a/tests/components/speaker/common-media_player.yaml +++ b/tests/components/speaker/common-media_player.yaml @@ -1,5 +1,11 @@ <<: !include common.yaml +wifi: + ap: + +psram: + mode: quad + media_player: - platform: speaker id: speaker_media_player_id @@ -10,3 +16,4 @@ media_player: volume_max: 0.95 volume_min: 0.0 task_stack_in_psram: true + codec_support_enabled: all diff --git a/tests/components/speaker/media_player.esp32-s3-idf.yaml b/tests/components/speaker/media_player.esp32-s3-idf.yaml deleted file mode 100644 index b3eec04d23..0000000000 --- a/tests/components/speaker/media_player.esp32-s3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - scl_pin: GPIO2 - sda_pin: GPIO3 - i2s_bclk_pin: GPIO4 - i2s_lrclk_pin: GPIO5 - i2s_mclk_pin: GPIO6 - i2s_dout_pin: GPIO7 - -<<: !include common-media_player.yaml diff --git a/tests/components/speaker/audio_dac.esp32-idf.yaml b/tests/components/speaker/test-audio_dac.esp32-idf.yaml similarity index 100% rename from tests/components/speaker/audio_dac.esp32-idf.yaml rename to tests/components/speaker/test-audio_dac.esp32-idf.yaml diff --git a/tests/components/speaker/media_player.esp32-idf.yaml b/tests/components/speaker/test-media_player.esp32-idf.yaml similarity index 100% rename from tests/components/speaker/media_player.esp32-idf.yaml rename to tests/components/speaker/test-media_player.esp32-idf.yaml diff --git a/tests/components/speaker/test.esp32-ard.yaml b/tests/components/speaker/test.esp32-ard.yaml deleted file mode 100644 index 13350cd097..0000000000 --- a/tests/components/speaker/test.esp32-ard.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO27 - i2s_lrclk_pin: GPIO26 - i2s_mclk_pin: GPIO25 - i2s_dout_pin: GPIO4 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml - -<<: !include common.yaml From 4d05e4d5765c037f234ddebb8442af2007c4fc60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20H=C3=B6rsken?= Date: Thu, 19 Feb 2026 04:52:38 +0100 Subject: [PATCH 073/609] [esp32_camera] Add support for sensors without JPEG support (#9496) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/components/esp32_camera/__init__.py | 50 ++++- .../components/esp32_camera/esp32_camera.cpp | 207 +++++++++++++----- .../components/esp32_camera/esp32_camera.h | 14 ++ esphome/core/defines.h | 1 + .../common/i2c_camera/esp32-idf.yaml | 1 + 5 files changed, 212 insertions(+), 61 deletions(-) diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index db6244fb3f..3a5d87792b 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -22,8 +22,10 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_VSYNC_PIN, ) +from esphome.core import CORE from esphome.core.entity_helpers import setup_entity import esphome.final_validate as fv +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -84,6 +86,18 @@ FRAME_SIZES = { "2560X1920": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1920, "QSXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1920, } +ESP32CameraPixelFormat = esp32_camera_ns.enum("ESP32CameraPixelFormat") +PIXEL_FORMATS = { + "RGB565": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB565, + "YUV422": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_YUV422, + "YUV420": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_YUV420, + "GRAYSCALE": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_GRAYSCALE, + "JPEG": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_JPEG, + "RGB888": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB888, + "RAW": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RAW, + "RGB444": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB444, + "RGB555": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB555, +} ESP32GainControlMode = esp32_camera_ns.enum("ESP32GainControlMode") ENUM_GAIN_CONTROL_MODE = { "MANUAL": ESP32GainControlMode.ESP32_GC_MODE_MANU, @@ -131,6 +145,7 @@ CONF_EXTERNAL_CLOCK = "external_clock" CONF_I2C_PINS = "i2c_pins" CONF_POWER_DOWN_PIN = "power_down_pin" # image +CONF_PIXEL_FORMAT = "pixel_format" CONF_JPEG_QUALITY = "jpeg_quality" CONF_VERTICAL_FLIP = "vertical_flip" CONF_HORIZONTAL_MIRROR = "horizontal_mirror" @@ -171,6 +186,21 @@ def validate_fb_location_(value): return validator(value) +def validate_jpeg_quality(config: ConfigType) -> ConfigType: + quality = config.get(CONF_JPEG_QUALITY) + pixel_format = config.get(CONF_PIXEL_FORMAT, "JPEG") + + if quality == 0: + # Set default JPEG quality if not specified for backwards compatibility + if pixel_format == "JPEG": + config[CONF_JPEG_QUALITY] = 10 + # For pixel formats other than JPEG, the valid 0 means no conversion + elif quality < 6 or quality > 63: + raise cv.Invalid(f"jpeg_quality must be between 6 and 63, got {quality}") + + return config + + CONFIG_SCHEMA = cv.All( cv.ENTITY_BASE_SCHEMA.extend( { @@ -206,7 +236,12 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum( FRAME_SIZES, upper=True ), - cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=6, max=63), + cv.Optional(CONF_PIXEL_FORMAT, default="JPEG"): cv.enum( + PIXEL_FORMATS, upper=True + ), + cv.Optional(CONF_JPEG_QUALITY, default=0): cv.Any( + cv.one_of(0), cv.int_range(min=6, max=63) + ), cv.Optional(CONF_CONTRAST, default=0): camera_range_param, cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param, cv.Optional(CONF_SATURATION, default=0): camera_range_param, @@ -270,11 +305,21 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.COMPONENT_SCHEMA), + validate_jpeg_quality, cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID), ) def _final_validate(config): + # Check psram requirement for non-JPEG formats + if ( + config.get(CONF_PIXEL_FORMAT, "JPEG") != "JPEG" + and psram_domain not in CORE.loaded_integrations + ): + raise cv.Invalid( + f"Non-JPEG pixel formats require the '{psram_domain}' component for JPEG conversion" + ) + if CONF_I2C_PINS not in config: return fconf = fv.full_config.get() @@ -298,6 +343,7 @@ SETTERS = { CONF_RESET_PIN: "set_reset_pin", CONF_POWER_DOWN_PIN: "set_power_down_pin", # image + CONF_PIXEL_FORMAT: "set_pixel_format", CONF_JPEG_QUALITY: "set_jpeg_quality", CONF_VERTICAL_FLIP: "set_vertical_flip", CONF_HORIZONTAL_MIRROR: "set_horizontal_mirror", @@ -351,6 +397,8 @@ async def to_code(config): cg.add(var.set_frame_size(config[CONF_RESOLUTION])) cg.add_define("USE_CAMERA") + if config[CONF_JPEG_QUALITY] != 0 and config[CONF_PIXEL_FORMAT] != "JPEG": + cg.add_define("USE_ESP32_CAMERA_JPEG_CONVERSION") add_idf_component(name="espressif/esp32-camera", ref="2.1.1") add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index cfe06b1673..655ae54f0a 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -16,6 +16,74 @@ static constexpr size_t FRAMEBUFFER_TASK_STACK_SIZE = 1792; static constexpr uint32_t FRAME_LOG_INTERVAL_MS = 60000; #endif +static const char *frame_size_to_str(framesize_t size) { + switch (size) { + case FRAMESIZE_QQVGA: + return "160x120 (QQVGA)"; + case FRAMESIZE_QCIF: + return "176x155 (QCIF)"; + case FRAMESIZE_HQVGA: + return "240x176 (HQVGA)"; + case FRAMESIZE_QVGA: + return "320x240 (QVGA)"; + case FRAMESIZE_CIF: + return "400x296 (CIF)"; + case FRAMESIZE_VGA: + return "640x480 (VGA)"; + case FRAMESIZE_SVGA: + return "800x600 (SVGA)"; + case FRAMESIZE_XGA: + return "1024x768 (XGA)"; + case FRAMESIZE_SXGA: + return "1280x1024 (SXGA)"; + case FRAMESIZE_UXGA: + return "1600x1200 (UXGA)"; + case FRAMESIZE_FHD: + return "1920x1080 (FHD)"; + case FRAMESIZE_P_HD: + return "720x1280 (P_HD)"; + case FRAMESIZE_P_3MP: + return "864x1536 (P_3MP)"; + case FRAMESIZE_QXGA: + return "2048x1536 (QXGA)"; + case FRAMESIZE_QHD: + return "2560x1440 (QHD)"; + case FRAMESIZE_WQXGA: + return "2560x1600 (WQXGA)"; + case FRAMESIZE_P_FHD: + return "1080x1920 (P_FHD)"; + case FRAMESIZE_QSXGA: + return "2560x1920 (QSXGA)"; + default: + return "UNKNOWN"; + } +} + +static const char *pixel_format_to_str(pixformat_t format) { + switch (format) { + case PIXFORMAT_RGB565: + return "RGB565"; + case PIXFORMAT_YUV422: + return "YUV422"; + case PIXFORMAT_YUV420: + return "YUV420"; + case PIXFORMAT_GRAYSCALE: + return "GRAYSCALE"; + case PIXFORMAT_JPEG: + return "JPEG"; + case PIXFORMAT_RGB888: + return "RGB888"; + case PIXFORMAT_RAW: + return "RAW"; + case PIXFORMAT_RGB444: + return "RGB444"; + case PIXFORMAT_RGB555: + return "RGB555"; + default: + return "UNKNOWN"; + } +} + /* ---------------- public API (derivated) ---------------- */ void ESP32Camera::setup() { #ifdef USE_I2C @@ -68,64 +136,9 @@ void ESP32Camera::dump_config() { this->name_.c_str(), YESNO(this->is_internal()), conf.pin_d0, conf.pin_d1, conf.pin_d2, conf.pin_d3, conf.pin_d4, conf.pin_d5, conf.pin_d6, conf.pin_d7, conf.pin_vsync, conf.pin_href, conf.pin_pclk, conf.pin_xclk, conf.xclk_freq_hz, conf.pin_sccb_sda, conf.pin_sccb_scl, conf.pin_reset); - switch (this->config_.frame_size) { - case FRAMESIZE_QQVGA: - ESP_LOGCONFIG(TAG, " Resolution: 160x120 (QQVGA)"); - break; - case FRAMESIZE_QCIF: - ESP_LOGCONFIG(TAG, " Resolution: 176x155 (QCIF)"); - break; - case FRAMESIZE_HQVGA: - ESP_LOGCONFIG(TAG, " Resolution: 240x176 (HQVGA)"); - break; - case FRAMESIZE_QVGA: - ESP_LOGCONFIG(TAG, " Resolution: 320x240 (QVGA)"); - break; - case FRAMESIZE_CIF: - ESP_LOGCONFIG(TAG, " Resolution: 400x296 (CIF)"); - break; - case FRAMESIZE_VGA: - ESP_LOGCONFIG(TAG, " Resolution: 640x480 (VGA)"); - break; - case FRAMESIZE_SVGA: - ESP_LOGCONFIG(TAG, " Resolution: 800x600 (SVGA)"); - break; - case FRAMESIZE_XGA: - ESP_LOGCONFIG(TAG, " Resolution: 1024x768 (XGA)"); - break; - case FRAMESIZE_SXGA: - ESP_LOGCONFIG(TAG, " Resolution: 1280x1024 (SXGA)"); - break; - case FRAMESIZE_UXGA: - ESP_LOGCONFIG(TAG, " Resolution: 1600x1200 (UXGA)"); - break; - case FRAMESIZE_FHD: - ESP_LOGCONFIG(TAG, " Resolution: 1920x1080 (FHD)"); - break; - case FRAMESIZE_P_HD: - ESP_LOGCONFIG(TAG, " Resolution: 720x1280 (P_HD)"); - break; - case FRAMESIZE_P_3MP: - ESP_LOGCONFIG(TAG, " Resolution: 864x1536 (P_3MP)"); - break; - case FRAMESIZE_QXGA: - ESP_LOGCONFIG(TAG, " Resolution: 2048x1536 (QXGA)"); - break; - case FRAMESIZE_QHD: - ESP_LOGCONFIG(TAG, " Resolution: 2560x1440 (QHD)"); - break; - case FRAMESIZE_WQXGA: - ESP_LOGCONFIG(TAG, " Resolution: 2560x1600 (WQXGA)"); - break; - case FRAMESIZE_P_FHD: - ESP_LOGCONFIG(TAG, " Resolution: 1080x1920 (P_FHD)"); - break; - case FRAMESIZE_QSXGA: - ESP_LOGCONFIG(TAG, " Resolution: 2560x1920 (QSXGA)"); - break; - default: - break; - } + + ESP_LOGCONFIG(TAG, " Resolution: %s", frame_size_to_str(this->config_.frame_size)); + ESP_LOGCONFIG(TAG, " Pixel Format: %s", pixel_format_to_str(this->config_.pixel_format)); if (this->is_failed()) { ESP_LOGE(TAG, " Setup Failed: %s", esp_err_to_name(this->init_error_)); @@ -184,8 +197,19 @@ void ESP32Camera::loop() { // check if we can return the image if (this->can_return_image_()) { // return image - auto *fb = this->current_image_->get_raw_buffer(); - xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); +#ifdef USE_ESP32_CAMERA_JPEG_CONVERSION + if (this->config_.pixel_format != PIXFORMAT_JPEG && this->config_.jpeg_quality > 0) { + // for non-JPEG format, we need to free the data and raw buffer + auto *jpg_buf = this->current_image_->get_data_buffer(); + free(jpg_buf); // NOLINT(cppcoreguidelines-no-malloc) + auto *fb = this->current_image_->get_raw_buffer(); + this->fb_allocator_.deallocate(fb, 1); + } else +#endif + { + auto *fb = this->current_image_->get_raw_buffer(); + xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); + } this->current_image_.reset(); } @@ -212,6 +236,38 @@ void ESP32Camera::loop() { xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); return; } + +#ifdef USE_ESP32_CAMERA_JPEG_CONVERSION + if (this->config_.pixel_format != PIXFORMAT_JPEG && this->config_.jpeg_quality > 0) { + // for non-JPEG format, we need to convert the frame to JPEG + uint8_t *jpg_buf; + size_t jpg_buf_len; + size_t width = fb->width; + size_t height = fb->height; + struct timeval timestamp = fb->timestamp; + bool ok = frame2jpg(fb, 100 - this->config_.jpeg_quality, &jpg_buf, &jpg_buf_len); + // return the original frame buffer to the queue + xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); + if (!ok) { + ESP_LOGE(TAG, "Failed to convert frame to JPEG!"); + return; + } + // create a new camera_fb_t for the JPEG data + fb = this->fb_allocator_.allocate(1); + if (fb == nullptr) { + ESP_LOGE(TAG, "Failed to allocate memory for camera frame buffer!"); + free(jpg_buf); // NOLINT(cppcoreguidelines-no-malloc) + return; + } + memset(fb, 0, sizeof(camera_fb_t)); + fb->buf = jpg_buf; + fb->len = jpg_buf_len; + fb->width = width; + fb->height = height; + fb->format = PIXFORMAT_JPEG; + fb->timestamp = timestamp; + } +#endif this->current_image_ = std::make_shared(fb, this->single_requesters_ | this->stream_requesters_); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE @@ -342,6 +398,37 @@ void ESP32Camera::set_frame_size(ESP32CameraFrameSize size) { break; } } +void ESP32Camera::set_pixel_format(ESP32CameraPixelFormat format) { + switch (format) { + case ESP32_PIXEL_FORMAT_RGB565: + this->config_.pixel_format = PIXFORMAT_RGB565; + break; + case ESP32_PIXEL_FORMAT_YUV422: + this->config_.pixel_format = PIXFORMAT_YUV422; + break; + case ESP32_PIXEL_FORMAT_YUV420: + this->config_.pixel_format = PIXFORMAT_YUV420; + break; + case ESP32_PIXEL_FORMAT_GRAYSCALE: + this->config_.pixel_format = PIXFORMAT_GRAYSCALE; + break; + case ESP32_PIXEL_FORMAT_JPEG: + this->config_.pixel_format = PIXFORMAT_JPEG; + break; + case ESP32_PIXEL_FORMAT_RGB888: + this->config_.pixel_format = PIXFORMAT_RGB888; + break; + case ESP32_PIXEL_FORMAT_RAW: + this->config_.pixel_format = PIXFORMAT_RAW; + break; + case ESP32_PIXEL_FORMAT_RGB444: + this->config_.pixel_format = PIXFORMAT_RGB444; + break; + case ESP32_PIXEL_FORMAT_RGB555: + this->config_.pixel_format = PIXFORMAT_RGB555; + break; + } +} void ESP32Camera::set_jpeg_quality(uint8_t quality) { this->config_.jpeg_quality = quality; } void ESP32Camera::set_vertical_flip(bool vertical_flip) { this->vertical_flip_ = vertical_flip; } void ESP32Camera::set_horizontal_mirror(bool horizontal_mirror) { this->horizontal_mirror_ = horizontal_mirror; } diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index eea93b7e01..9fbd3848f2 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -41,6 +41,18 @@ enum ESP32CameraFrameSize { ESP32_CAMERA_SIZE_2560X1920, // QSXGA }; +enum ESP32CameraPixelFormat { + ESP32_PIXEL_FORMAT_RGB565, + ESP32_PIXEL_FORMAT_YUV422, + ESP32_PIXEL_FORMAT_YUV420, + ESP32_PIXEL_FORMAT_GRAYSCALE, + ESP32_PIXEL_FORMAT_JPEG, + ESP32_PIXEL_FORMAT_RGB888, + ESP32_PIXEL_FORMAT_RAW, + ESP32_PIXEL_FORMAT_RGB444, + ESP32_PIXEL_FORMAT_RGB555, +}; + enum ESP32AgcGainCeiling { ESP32_GAINCEILING_2X = GAINCEILING_2X, ESP32_GAINCEILING_4X = GAINCEILING_4X, @@ -126,6 +138,7 @@ class ESP32Camera : public camera::Camera { void set_reset_pin(uint8_t pin); void set_power_down_pin(uint8_t pin); /* -- image */ + void set_pixel_format(ESP32CameraPixelFormat format); void set_frame_size(ESP32CameraFrameSize size); void set_jpeg_quality(uint8_t quality); void set_vertical_flip(bool vertical_flip); @@ -220,6 +233,7 @@ class ESP32Camera : public camera::Camera { #ifdef USE_I2C i2c::InternalI2CBus *i2c_bus_{nullptr}; #endif // USE_I2C + RAMAllocator fb_allocator_{RAMAllocator::ALLOC_INTERNAL}; }; class ESP32CameraImageTrigger : public Trigger, public camera::CameraListener { diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 5559ec88d0..a7fb9f197c 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -43,6 +43,7 @@ #define USE_DEVICES #define USE_DISPLAY #define USE_ENTITY_ICON +#define USE_ESP32_CAMERA_JPEG_CONVERSION #define USE_ESP32_HOSTED #define USE_ESP32_IMPROV_STATE_CALLBACK #define USE_EVENT diff --git a/tests/test_build_components/common/i2c_camera/esp32-idf.yaml b/tests/test_build_components/common/i2c_camera/esp32-idf.yaml index 443ebbebd9..07ab6cdc8d 100644 --- a/tests/test_build_components/common/i2c_camera/esp32-idf.yaml +++ b/tests/test_build_components/common/i2c_camera/esp32-idf.yaml @@ -30,6 +30,7 @@ esp32_camera: resolution: 640x480 jpeg_quality: 10 frame_buffer_location: PSRAM + pixel_format: JPEG on_image: then: - lambda: |- From 4cc1e6a9103dc804e9e4516ff1f6fe313701ccf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Thu, 19 Feb 2026 15:23:22 +0100 Subject: [PATCH 074/609] [esp32_ble_server] add test for lambda characteristic (#14091) --- tests/components/esp32_ble_server/common.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/components/esp32_ble_server/common.yaml b/tests/components/esp32_ble_server/common.yaml index e9576a8262..7fe0b2eb5f 100644 --- a/tests/components/esp32_ble_server/common.yaml +++ b/tests/components/esp32_ble_server/common.yaml @@ -33,6 +33,10 @@ esp32_ble_server: - uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc42d advertise: false characteristics: + - id: test_lambda_characteristic + uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc12c + read: true + value: !lambda return { 1, 2 }; - id: test_change_characteristic uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc11c read: true From 7b53a9895000219aac5895de48c5bd15715d7aea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 08:39:44 -0600 Subject: [PATCH 075/609] [socket] Log error when UDP socket requested on LWIP TCP-only platforms (#14089) --- esphome/components/socket/lwip_raw_tcp_impl.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index aa37386d70..6e95f5bc7a 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -680,6 +680,11 @@ class LWIPRawListenImpl final : public LWIPRawImpl { }; std::unique_ptr socket(int domain, int type, int protocol) { + if (type != SOCK_STREAM) { + ESP_LOGE(TAG, "UDP sockets not supported on this platform, use WiFiUDP"); + errno = EPROTOTYPE; + return nullptr; + } auto *pcb = tcp_new(); if (pcb == nullptr) return nullptr; From 6daca09794d7d48478e4c6751907ae5a4b5aa7b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 08:40:08 -0600 Subject: [PATCH 076/609] [logger] Replace LogListener virtual interface with LogCallback struct (#14084) --- esphome/components/api/api_server.cpp | 5 +- esphome/components/api/api_server.h | 6 +- esphome/components/ble_nus/ble_nus.cpp | 5 +- esphome/components/ble_nus/ble_nus.h | 9 +-- esphome/components/logger/logger.h | 66 ++++++++++---------- esphome/components/mqtt/mqtt_client.cpp | 5 +- esphome/components/mqtt/mqtt_client.h | 9 +-- esphome/components/syslog/esphome_syslog.cpp | 7 ++- esphome/components/syslog/esphome_syslog.h | 5 +- esphome/components/web_server/web_server.cpp | 5 +- esphome/components/web_server/web_server.h | 11 +--- 11 files changed, 65 insertions(+), 68 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 67a117e68f..1a1d0b229b 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -92,7 +92,10 @@ void APIServer::setup() { #ifdef USE_LOGGER if (logger::global_logger != nullptr) { - logger::global_logger->add_log_listener(this); + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); } #endif diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 323acc2efb..3b9ba0e23b 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -37,10 +37,6 @@ struct SavedNoisePsk { class APIServer : public Component, public Controller -#ifdef USE_LOGGER - , - public logger::LogListener -#endif #ifdef USE_CAMERA , public camera::CameraListener @@ -56,7 +52,7 @@ class APIServer : public Component, void on_shutdown() override; bool teardown() override; #ifdef USE_LOGGER - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); #endif #ifdef USE_CAMERA void on_camera_image(const std::shared_ptr &image) override; diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index 0de65b623f..a10132eb3e 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -87,7 +87,10 @@ void BLENUS::setup() { global_ble_nus = this; #ifdef USE_LOGGER if (logger::global_logger != nullptr && this->expose_log_) { - logger::global_logger->add_log_listener(this); + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); } #endif } diff --git a/esphome/components/ble_nus/ble_nus.h b/esphome/components/ble_nus/ble_nus.h index ef20fc5e5b..b2b0ee7713 100644 --- a/esphome/components/ble_nus/ble_nus.h +++ b/esphome/components/ble_nus/ble_nus.h @@ -10,12 +10,7 @@ namespace esphome::ble_nus { -class BLENUS : public Component -#ifdef USE_LOGGER - , - public logger::LogListener -#endif -{ +class BLENUS : public Component { enum TxStatus { TX_DISABLED, TX_ENABLED, @@ -29,7 +24,7 @@ class BLENUS : public Component size_t write_array(const uint8_t *data, size_t len); void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } #ifdef USE_LOGGER - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); #endif protected: diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 835542dd8f..2a7552af92 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -40,26 +40,25 @@ struct device; namespace esphome::logger { -/** Interface for receiving log messages without std::function overhead. +/** Lightweight callback for receiving log messages without virtual dispatch overhead. * - * Components can implement this interface instead of using lambdas with std::function - * to reduce flash usage from std::function type erasure machinery. + * Replaces the former LogListener virtual interface to eliminate per-implementer + * vtable sub-tables and thunk code (~39 bytes saved per class that used LogListener). * * Usage: - * class MyComponent : public Component, public LogListener { - * public: - * void setup() override { - * if (logger::global_logger != nullptr) - * logger::global_logger->add_log_listener(this); - * } - * void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override { - * // Handle log message - * } - * }; + * // In your component's setup(): + * if (logger::global_logger != nullptr) + * logger::global_logger->add_log_callback( + * this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + * static_cast(self)->on_log(level, tag, message, message_len); + * }); */ -class LogListener { - public: - virtual void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) = 0; +struct LogCallback { + void *instance; + void (*fn)(void *, uint8_t, const char *, const char *, size_t); + void invoke(uint8_t level, const char *tag, const char *message, size_t message_len) const { + this->fn(this->instance, level, tag, message, message_len); + } }; #ifdef USE_LOGGER_LEVEL_LISTENERS @@ -187,11 +186,13 @@ class Logger : public Component { inline uint8_t level_for(const char *tag); #ifdef USE_LOG_LISTENERS - /// Register a log listener to receive log messages - void add_log_listener(LogListener *listener) { this->log_listeners_.push_back(listener); } + /// Register a log callback to receive log messages + void add_log_callback(void *instance, void (*fn)(void *, uint8_t, const char *, const char *, size_t)) { + this->log_callbacks_.push_back(LogCallback{instance, fn}); + } #else /// No-op when log listeners are disabled - void add_log_listener(LogListener *listener) {} + void add_log_callback(void *instance, void (*fn)(void *, uint8_t, const char *, const char *, size_t)) {} #endif #ifdef USE_LOGGER_LEVEL_LISTENERS @@ -253,11 +254,11 @@ class Logger : public Component { } #endif - // Helper to notify log listeners + // Helper to notify log callbacks inline void HOT notify_listeners_(uint8_t level, const char *tag, const LogBuffer &buf) { #ifdef USE_LOG_LISTENERS - for (auto *listener : this->log_listeners_) - listener->on_log(level, tag, buf.data, buf.pos); + for (auto &cb : this->log_callbacks_) + cb.invoke(level, tag, buf.data, buf.pos); #endif } @@ -341,8 +342,8 @@ class Logger : public Component { std::map log_levels_{}; #endif #ifdef USE_LOG_LISTENERS - StaticVector - log_listeners_; // Log message listeners (API, MQTT, syslog, etc.) + StaticVector + log_callbacks_; // Log message callbacks (API, MQTT, syslog, etc.) #endif #ifdef USE_LOGGER_LEVEL_LISTENERS std::vector level_listeners_; // Log level change listeners @@ -478,15 +479,16 @@ class Logger : public Component { }; extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -class LoggerMessageTrigger : public Trigger, public LogListener { +class LoggerMessageTrigger : public Trigger { public: - explicit LoggerMessageTrigger(Logger *parent, uint8_t level) : level_(level) { parent->add_log_listener(this); } - - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override { - (void) message_len; - if (level <= this->level_) { - this->trigger(level, tag, message); - } + explicit LoggerMessageTrigger(Logger *parent, uint8_t level) : level_(level) { + parent->add_log_callback(this, + [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + auto *trigger = static_cast(self); + if (level <= trigger->level_) { + trigger->trigger(level, tag, message); + } + }); } protected: diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 90b423c386..9905b4677e 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -64,7 +64,10 @@ void MQTTClientComponent::setup() { }); #ifdef USE_LOGGER if (this->is_log_message_enabled() && logger::global_logger != nullptr) { - logger::global_logger->add_log_listener(this); + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); } #endif diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 38bc0b4da3..21edd53eda 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -99,12 +99,7 @@ enum MQTTClientState { class MQTTComponent; -class MQTTClientComponent : public Component -#ifdef USE_LOGGER - , - public logger::LogListener -#endif -{ +class MQTTClientComponent : public Component { public: MQTTClientComponent(); @@ -252,7 +247,7 @@ class MQTTClientComponent : public Component float get_setup_priority() const override; #ifdef USE_LOGGER - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); #endif void on_message(const std::string &topic, const std::string &payload); diff --git a/esphome/components/syslog/esphome_syslog.cpp b/esphome/components/syslog/esphome_syslog.cpp index 376de54db4..790d08ffa6 100644 --- a/esphome/components/syslog/esphome_syslog.cpp +++ b/esphome/components/syslog/esphome_syslog.cpp @@ -18,7 +18,12 @@ constexpr int LOG_LEVEL_TO_SYSLOG_SEVERITY[] = { 7 // VERY_VERBOSE }; -void Syslog::setup() { logger::global_logger->add_log_listener(this); } +void Syslog::setup() { + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); +} void Syslog::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { this->log_(level, tag, message, message_len); diff --git a/esphome/components/syslog/esphome_syslog.h b/esphome/components/syslog/esphome_syslog.h index bde6ab5ed4..be4fa91436 100644 --- a/esphome/components/syslog/esphome_syslog.h +++ b/esphome/components/syslog/esphome_syslog.h @@ -2,17 +2,16 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/components/logger/logger.h" #include "esphome/components/udp/udp_component.h" #include "esphome/components/time/real_time_clock.h" #ifdef USE_NETWORK namespace esphome::syslog { -class Syslog : public Component, public Parented, public logger::LogListener { +class Syslog : public Component, public Parented { public: Syslog(int level, time::RealTimeClock *time) : log_level_(level), time_(time) {} void setup() override; - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); void set_strip(bool strip) { this->strip_ = strip; } void set_facility(int facility) { this->facility_ = facility; } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index e3d131f58e..a44a47379e 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -395,7 +395,10 @@ void WebServer::setup() { #ifdef USE_LOGGER if (logger::global_logger != nullptr && this->expose_log_) { - logger::global_logger->add_log_listener(this); + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); } #endif diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 026da763ea..6afe618b59 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -186,14 +186,7 @@ class DeferredUpdateEventSourceList : public std::list Date: Thu, 19 Feb 2026 08:40:23 -0600 Subject: [PATCH 077/609] [core] Devirtualize call_loop() and mark_failed() in Component (#14083) --- esphome/core/component.cpp | 12 +++++------- esphome/core/component.h | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index ba0f1663b9..f283a69064 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -207,7 +207,7 @@ bool Component::cancel_retry(uint32_t id) { #pragma GCC diagnostic pop } -void Component::call_loop() { this->loop(); } +void Component::call_loop_() { this->loop(); } void Component::call_setup() { this->setup(); } void Component::call_dump_config() { this->dump_config(); @@ -258,11 +258,11 @@ void Component::call() { case COMPONENT_STATE_SETUP: // State setup: Call first loop and set state to loop this->set_component_state_(COMPONENT_STATE_LOOP); - this->call_loop(); + this->call_loop_(); break; case COMPONENT_STATE_LOOP: // State loop: Call loop - this->call_loop(); + this->call_loop_(); break; case COMPONENT_STATE_FAILED: // State failed: Do nothing @@ -497,16 +497,14 @@ void Component::set_setup_priority(float priority) { bool Component::has_overridden_loop() const { #if defined(USE_HOST) || defined(CLANG_TIDY) - bool loop_overridden = true; - bool call_loop_overridden = true; + return true; #else #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpmf-conversions" bool loop_overridden = (void *) (this->*(&Component::loop)) != (void *) (&Component::loop); - bool call_loop_overridden = (void *) (this->*(&Component::call_loop)) != (void *) (&Component::call_loop); #pragma GCC diagnostic pop + return loop_overridden; #endif - return loop_overridden || call_loop_overridden; } PollingComponent::PollingComponent(uint32_t update_interval) : update_interval_(update_interval) {} diff --git a/esphome/core/component.h b/esphome/core/component.h index 6f7f77dbc1..d4dad3c9a6 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -165,7 +165,7 @@ class Component { * For example, i2c based components can check if the remote device is responding and otherwise * mark the component as failed. Eventually this will also enable smart status LEDs. */ - virtual void mark_failed(); + void mark_failed(); // Remove before 2026.6.0 ESPDEPRECATED("Use mark_failed(LOG_STR(\"static string literal\")) instead. Do NOT use .c_str() from temporary " @@ -286,7 +286,7 @@ class Component { protected: friend class Application; - virtual void call_loop(); + void call_loop_(); virtual void call_setup(); virtual void call_dump_config(); From 535980b9bd37ebf405a6305e34e980e3f16a5af6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 08:40:41 -0600 Subject: [PATCH 078/609] [cse7761] Use constexpr for compile-time constants (#14081) --- esphome/components/cse7761/cse7761.cpp | 38 +++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/esphome/components/cse7761/cse7761.cpp b/esphome/components/cse7761/cse7761.cpp index 7c5ee833a4..f4966357d4 100644 --- a/esphome/components/cse7761/cse7761.cpp +++ b/esphome/components/cse7761/cse7761.cpp @@ -15,29 +15,29 @@ static const char *const TAG = "cse7761"; * https://github.com/arendst/Tasmota/blob/development/tasmota/xnrg_19_cse7761.ino \*********************************************************************************************/ -static const int CSE7761_UREF = 42563; // RmsUc -static const int CSE7761_IREF = 52241; // RmsIAC -static const int CSE7761_PREF = 44513; // PowerPAC +static constexpr int CSE7761_UREF = 42563; // RmsUc +static constexpr int CSE7761_IREF = 52241; // RmsIAC +static constexpr int CSE7761_PREF = 44513; // PowerPAC -static const uint8_t CSE7761_REG_SYSCON = 0x00; // (2) System Control Register (0x0A04) -static const uint8_t CSE7761_REG_EMUCON = 0x01; // (2) Metering control register (0x0000) -static const uint8_t CSE7761_REG_EMUCON2 = 0x13; // (2) Metering control register 2 (0x0001) -static const uint8_t CSE7761_REG_PULSE1SEL = 0x1D; // (2) Pin function output select register (0x3210) +static constexpr uint8_t CSE7761_REG_SYSCON = 0x00; // (2) System Control Register (0x0A04) +static constexpr uint8_t CSE7761_REG_EMUCON = 0x01; // (2) Metering control register (0x0000) +static constexpr uint8_t CSE7761_REG_EMUCON2 = 0x13; // (2) Metering control register 2 (0x0001) +static constexpr uint8_t CSE7761_REG_PULSE1SEL = 0x1D; // (2) Pin function output select register (0x3210) -static const uint8_t CSE7761_REG_RMSIA = 0x24; // (3) The effective value of channel A current (0x000000) -static const uint8_t CSE7761_REG_RMSIB = 0x25; // (3) The effective value of channel B current (0x000000) -static const uint8_t CSE7761_REG_RMSU = 0x26; // (3) Voltage RMS (0x000000) -static const uint8_t CSE7761_REG_POWERPA = 0x2C; // (4) Channel A active power, update rate 27.2Hz (0x00000000) -static const uint8_t CSE7761_REG_POWERPB = 0x2D; // (4) Channel B active power, update rate 27.2Hz (0x00000000) -static const uint8_t CSE7761_REG_SYSSTATUS = 0x43; // (1) System status register +static constexpr uint8_t CSE7761_REG_RMSIA = 0x24; // (3) The effective value of channel A current (0x000000) +static constexpr uint8_t CSE7761_REG_RMSIB = 0x25; // (3) The effective value of channel B current (0x000000) +static constexpr uint8_t CSE7761_REG_RMSU = 0x26; // (3) Voltage RMS (0x000000) +static constexpr uint8_t CSE7761_REG_POWERPA = 0x2C; // (4) Channel A active power, update rate 27.2Hz (0x00000000) +static constexpr uint8_t CSE7761_REG_POWERPB = 0x2D; // (4) Channel B active power, update rate 27.2Hz (0x00000000) +static constexpr uint8_t CSE7761_REG_SYSSTATUS = 0x43; // (1) System status register -static const uint8_t CSE7761_REG_COEFFCHKSUM = 0x6F; // (2) Coefficient checksum -static const uint8_t CSE7761_REG_RMSIAC = 0x70; // (2) Channel A effective current conversion coefficient +static constexpr uint8_t CSE7761_REG_COEFFCHKSUM = 0x6F; // (2) Coefficient checksum +static constexpr uint8_t CSE7761_REG_RMSIAC = 0x70; // (2) Channel A effective current conversion coefficient -static const uint8_t CSE7761_SPECIAL_COMMAND = 0xEA; // Start special command -static const uint8_t CSE7761_CMD_RESET = 0x96; // Reset command, after receiving the command, the chip resets -static const uint8_t CSE7761_CMD_CLOSE_WRITE = 0xDC; // Close write operation -static const uint8_t CSE7761_CMD_ENABLE_WRITE = 0xE5; // Enable write operation +static constexpr uint8_t CSE7761_SPECIAL_COMMAND = 0xEA; // Start special command +static constexpr uint8_t CSE7761_CMD_RESET = 0x96; // Reset command, after receiving the command, the chip resets +static constexpr uint8_t CSE7761_CMD_CLOSE_WRITE = 0xDC; // Close write operation +static constexpr uint8_t CSE7761_CMD_ENABLE_WRITE = 0xE5; // Enable write operation enum CSE7761 { RMS_IAC, RMS_IBC, RMS_UC, POWER_PAC, POWER_PBC, POWER_SC, ENERGY_AC, ENERGY_BC }; From 01a46f665f550085022bed7d994e043045f5553a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:42:22 -0500 Subject: [PATCH 079/609] Bump esptool from 5.1.0 to 5.2.0 (#14058) Signed-off-by: dependabot[bot] 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 2e6268284a..be3445dceb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ tzlocal==5.3.1 # from time tzdata>=2021.1 # from time pyserial==3.5 platformio==6.1.19 -esptool==5.1.0 +esptool==5.2.0 click==8.1.7 esphome-dashboard==20260210.0 aioesphomeapi==44.0.0 From b5a8e1c94cfa2d82c2e9b9241ae62c1abd7dd3a7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 09:06:46 -0600 Subject: [PATCH 080/609] [ci] Update lint message to recommend constexpr over static const (#14099) --- script/ci-custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/ci-custom.py b/script/ci-custom.py index 8c405b04ae..231f587068 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -301,7 +301,7 @@ def highlight(s): ], ) def lint_no_defines(fname, match): - s = highlight(f"static const uint8_t {match.group(1)} = {match.group(2)};") + s = highlight(f"static constexpr uint8_t {match.group(1)} = {match.group(2)};") return ( "#define macros for integer constants are not allowed, please use " f"{s} style instead (replace uint8_t with the appropriate " From 0484b2852dcf76161f5ae36d447e8d9e1cdad747 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 09:27:05 -0600 Subject: [PATCH 081/609] [e131] Fix E1.31 on ESP8266 and RP2040 by restoring WiFiUDP support (#14086) --- esphome/components/e131/e131.cpp | 31 ++++++++++++++++++++++++- esphome/components/e131/e131.h | 8 +++++++ esphome/components/e131/e131_packet.cpp | 2 ++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index 4187857901..941927122c 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -14,12 +14,17 @@ static const int PORT = 5568; E131Component::E131Component() {} E131Component::~E131Component() { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) if (this->socket_) { this->socket_->close(); } +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + this->udp_.stop(); +#endif } void E131Component::setup() { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) this->socket_ = socket::socket_ip(SOCK_DGRAM, IPPROTO_IP); int enable = 1; @@ -50,6 +55,13 @@ void E131Component::setup() { this->mark_failed(); return; } +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + if (!this->udp_.begin(PORT)) { + ESP_LOGW(TAG, "Cannot bind E1.31 to port %d.", PORT); + this->mark_failed(); + return; + } +#endif join_igmp_groups_(); } @@ -59,19 +71,36 @@ void E131Component::loop() { int universe = 0; uint8_t buf[1460]; +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) ssize_t len = this->socket_->read(buf, sizeof(buf)); if (len == -1) { return; } if (!this->packet_(buf, (size_t) len, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet received of size %zd.", len); + ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); return; } if (!this->process_(universe, packet)) { ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); } +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + while (auto packet_size = this->udp_.parsePacket()) { + auto len = this->udp_.read(buf, sizeof(buf)); + if (len <= 0) + continue; + + if (!this->packet_(buf, (size_t) len, universe, packet)) { + ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); + continue; + } + + if (!this->process_(universe, packet)) { + ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); + } + } +#endif } void E131Component::add_effect(E131AddressableLightEffect *light_effect) { diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index d4b272eae2..72da9ddebe 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -1,7 +1,11 @@ #pragma once #include "esphome/core/defines.h" #ifdef USE_NETWORK +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) #include "esphome/components/socket/socket.h" +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) +#include +#endif #include "esphome/core/component.h" #include @@ -45,7 +49,11 @@ class E131Component : public esphome::Component { void leave_(int universe); E131ListenMethod listen_method_{E131_MULTICAST}; +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) std::unique_ptr socket_; +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + WiFiUDP udp_; +#endif std::vector light_effects_; std::map universe_consumers_; }; diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index ed081e5758..aa5c740454 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -62,8 +62,10 @@ const size_t E131_MIN_PACKET_SIZE = reinterpret_cast(&((E131RawPacket *) bool E131Component::join_igmp_groups_() { if (listen_method_ != E131_MULTICAST) return false; +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) if (this->socket_ == nullptr) return false; +#endif for (auto universe : universe_consumers_) { if (!universe.second) From 916cf0d8b7f945bb6e3ee30702469f22013b09c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 09:28:00 -0600 Subject: [PATCH 082/609] [e131] Replace std::map with std::vector for universe tracking (#14087) --- esphome/components/e131/e131.h | 9 ++++-- esphome/components/e131/e131_packet.cpp | 39 ++++++++++++++++--------- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index 72da9ddebe..fee447b678 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -9,7 +9,6 @@ #include "esphome/core/component.h" #include -#include #include #include @@ -27,6 +26,11 @@ struct E131Packet { uint8_t values[E131_MAX_PROPERTY_VALUES_COUNT]; }; +struct UniverseConsumer { + uint16_t universe; + uint16_t consumers; +}; + class E131Component : public esphome::Component { public: E131Component(); @@ -45,6 +49,7 @@ class E131Component : public esphome::Component { bool packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet); bool process_(int universe, const E131Packet &packet); bool join_igmp_groups_(); + UniverseConsumer *find_universe_(int universe); void join_(int universe); void leave_(int universe); @@ -55,7 +60,7 @@ class E131Component : public esphome::Component { WiFiUDP udp_; #endif std::vector light_effects_; - std::map universe_consumers_; + std::vector universe_consumers_; }; } // namespace e131 diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index aa5c740454..b90e6d5c91 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -60,19 +60,19 @@ union E131RawPacket { const size_t E131_MIN_PACKET_SIZE = reinterpret_cast(&((E131RawPacket *) nullptr)->property_values[1]); bool E131Component::join_igmp_groups_() { - if (listen_method_ != E131_MULTICAST) + if (this->listen_method_ != E131_MULTICAST) return false; #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) if (this->socket_ == nullptr) return false; #endif - for (auto universe : universe_consumers_) { - if (!universe.second) + for (auto &entry : this->universe_consumers_) { + if (!entry.consumers) continue; ip4_addr_t multicast_addr = - network::IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff)); + network::IPAddress(239, 255, ((entry.universe >> 8) & 0xff), ((entry.universe >> 0) & 0xff)); err_t err; { @@ -81,34 +81,47 @@ bool E131Component::join_igmp_groups_() { } if (err) { - ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", universe.first); + ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", entry.universe); } } return true; } +UniverseConsumer *E131Component::find_universe_(int universe) { + for (auto &entry : this->universe_consumers_) { + if (entry.universe == universe) + return &entry; + } + return nullptr; +} + void E131Component::join_(int universe) { // store only latest received packet for the given universe - auto consumers = ++universe_consumers_[universe]; - - if (consumers > 1) { - return; // we already joined before + auto *consumer = this->find_universe_(universe); + if (consumer != nullptr) { + if (consumer->consumers++ > 0) { + return; // we already joined before + } + } else { + this->universe_consumers_.push_back({static_cast(universe), 1}); } - if (join_igmp_groups_()) { + if (this->join_igmp_groups_()) { ESP_LOGD(TAG, "Joined %d universe for E1.31.", universe); } } void E131Component::leave_(int universe) { - auto consumers = --universe_consumers_[universe]; + auto *consumer = this->find_universe_(universe); + if (consumer == nullptr) + return; - if (consumers > 0) { + if (--consumer->consumers > 0) { return; // we have other consumers of the given universe } - if (listen_method_ == E131_MULTICAST) { + if (this->listen_method_ == E131_MULTICAST) { ip4_addr_t multicast_addr = network::IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff)); LwIPLock lock; From a8171da0033ce797ef59125a69b270cfe35e36eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 09:38:57 -0600 Subject: [PATCH 083/609] [web_server] Reduce set_json_id flash and stack usage (#14029) --- esphome/components/web_server/web_server.cpp | 46 ++++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index a44a47379e..f352e5896c 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -374,7 +374,7 @@ std::string WebServer::get_config_json() { json::JsonBuilder builder; JsonObject root = builder.root(); - root[ESPHOME_F("title")] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name(); + root[ESPHOME_F("title")] = App.get_friendly_name().empty() ? App.get_name().c_str() : App.get_friendly_name().c_str(); char comment_buffer[ESPHOME_COMMENT_SIZE]; App.get_comment_string(comment_buffer); root[ESPHOME_F("comment")] = comment_buffer; @@ -513,21 +513,27 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J size_t device_len = device_name ? strlen(device_name) : 0; #endif - // Build id into stack buffer - ArduinoJson copies the string - // Format: {prefix}/{device?}/{name} + // Single stack buffer for both id formats - ArduinoJson copies the string before we overwrite // Buffer sizes use constants from entity_base.h validated in core/config.py // Note: Device name uses ESPHOME_FRIENDLY_NAME_MAX_LEN (sub-device max 120), not ESPHOME_DEVICE_NAME_MAX_LEN // (hostname) + // Without USE_DEVICES: legacy id ({prefix}-{object_id}) is the largest format + // With USE_DEVICES: name_id ({prefix}/{device}/{name}) is the largest format + static constexpr size_t LEGACY_ID_SIZE = ESPHOME_DOMAIN_MAX_LEN + 1 + OBJECT_ID_MAX_LEN; #ifdef USE_DEVICES static constexpr size_t ID_BUF_SIZE = - ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1; + std::max(ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1, + LEGACY_ID_SIZE); #else - static constexpr size_t ID_BUF_SIZE = ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1; + static constexpr size_t ID_BUF_SIZE = + std::max(ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1, LEGACY_ID_SIZE); #endif char id_buf[ID_BUF_SIZE]; - char *p = id_buf; - memcpy(p, prefix, prefix_len); - p += prefix_len; + memcpy(id_buf, prefix, prefix_len); // NOLINT(bugprone-not-null-terminated-result) + + // name_id: new format {prefix}/{device?}/{name} - frontend should prefer this + // Remove in 2026.8.0 when id switches to new format permanently + char *p = id_buf + prefix_len; *p++ = '/'; #ifdef USE_DEVICES if (device_name) { @@ -538,31 +544,25 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J #endif memcpy(p, name.c_str(), name_len); p[name_len] = '\0'; - - // name_id: new format {prefix}/{device?}/{name} - frontend should prefer this - // Remove in 2026.8.0 when id switches to new format permanently root[ESPHOME_F("name_id")] = id_buf; // id: old format {prefix}-{object_id} for backward compatibility - // Will switch to new format in 2026.8.0 - char legacy_buf[ESPHOME_DOMAIN_MAX_LEN + 1 + OBJECT_ID_MAX_LEN]; - char *lp = legacy_buf; - memcpy(lp, prefix, prefix_len); - lp += prefix_len; - *lp++ = '-'; - obj->write_object_id_to(lp, sizeof(legacy_buf) - (lp - legacy_buf)); - root[ESPHOME_F("id")] = legacy_buf; + // Will switch to new format in 2026.8.0 - reuses prefix already in id_buf + id_buf[prefix_len] = '-'; + obj->write_object_id_to(id_buf + prefix_len + 1, ID_BUF_SIZE - prefix_len - 1); + root[ESPHOME_F("id")] = id_buf; if (start_config == DETAIL_ALL) { root[ESPHOME_F("domain")] = prefix; - root[ESPHOME_F("name")] = name; + // Use .c_str() to avoid instantiating set template (saves ~24B) + root[ESPHOME_F("name")] = name.c_str(); #ifdef USE_DEVICES if (device_name) { root[ESPHOME_F("device")] = device_name; } #endif #ifdef USE_ENTITY_ICON - root[ESPHOME_F("icon")] = obj->get_icon_ref(); + root[ESPHOME_F("icon")] = obj->get_icon_ref().c_str(); #endif root[ESPHOME_F("entity_category")] = obj->get_entity_category(); bool is_disabled = obj->is_disabled_by_default(); @@ -632,7 +632,7 @@ std::string WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); if (!uom_ref.empty()) - root[ESPHOME_F("uom")] = uom_ref; + root[ESPHOME_F("uom")] = uom_ref.c_str(); } return builder.serialize(); @@ -1147,7 +1147,7 @@ std::string WebServer::number_json_(number::Number *obj, float value, JsonDetail root[ESPHOME_F("step")] = (value_accuracy_to_buf(val_buf, obj->traits.get_step(), accuracy), val_buf); root[ESPHOME_F("mode")] = (int) obj->traits.get_mode(); if (!uom_ref.empty()) - root[ESPHOME_F("uom")] = uom_ref; + root[ESPHOME_F("uom")] = uom_ref.c_str(); this->add_sorting_info_(root, obj); } From 5304750215a7be803d87bb9f059d43952fe084f6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:00:34 -0500 Subject: [PATCH 084/609] [socket] Fix IPv6 compilation error on host platform (#14101) Co-authored-by: Claude Opus 4.6 --- esphome/components/socket/socket.cpp | 10 ++++++++-- tests/components/socket/common.yaml | 11 +++++++++++ tests/components/socket/test-ipv6.esp32-idf.yaml | 4 ++++ tests/components/socket/test-ipv6.host.yaml | 4 ++++ tests/components/socket/test.esp32-idf.yaml | 1 + tests/components/socket/test.host.yaml | 3 +++ 6 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 tests/components/socket/common.yaml create mode 100644 tests/components/socket/test-ipv6.esp32-idf.yaml create mode 100644 tests/components/socket/test-ipv6.host.yaml create mode 100644 tests/components/socket/test.esp32-idf.yaml create mode 100644 tests/components/socket/test.host.yaml diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index 2fcc162ead..6154c497e0 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -59,8 +59,14 @@ size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::s #if USE_NETWORK_IPV6 else if (addr_ptr->sa_family == AF_INET6 && len >= sizeof(sockaddr_in6)) { const auto *addr = reinterpret_cast(addr_ptr); -#ifndef USE_SOCKET_IMPL_LWIP_TCP - // Format IPv4-mapped IPv6 addresses as regular IPv4 (not supported on ESP8266 raw TCP) +#ifdef USE_HOST + // Format IPv4-mapped IPv6 addresses as regular IPv4 (POSIX layout, no LWIP union) + if (IN6_IS_ADDR_V4MAPPED(&addr->sin6_addr) && + esphome_inet_ntop4(&addr->sin6_addr.s6_addr[12], buf.data(), buf.size()) != nullptr) { + return strlen(buf.data()); + } +#elif !defined(USE_SOCKET_IMPL_LWIP_TCP) + // Format IPv4-mapped IPv6 addresses as regular IPv4 (LWIP layout) if (addr->sin6_addr.un.u32_addr[0] == 0 && addr->sin6_addr.un.u32_addr[1] == 0 && addr->sin6_addr.un.u32_addr[2] == htonl(0xFFFF) && esphome_inet_ntop4(&addr->sin6_addr.un.u32_addr[3], buf.data(), buf.size()) != nullptr) { diff --git a/tests/components/socket/common.yaml b/tests/components/socket/common.yaml new file mode 100644 index 0000000000..aaf49f1611 --- /dev/null +++ b/tests/components/socket/common.yaml @@ -0,0 +1,11 @@ +substitutions: + network_enable_ipv6: "false" + +socket: + +wifi: + ssid: MySSID + password: password1 + +network: + enable_ipv6: ${network_enable_ipv6} diff --git a/tests/components/socket/test-ipv6.esp32-idf.yaml b/tests/components/socket/test-ipv6.esp32-idf.yaml new file mode 100644 index 0000000000..da1324b17e --- /dev/null +++ b/tests/components/socket/test-ipv6.esp32-idf.yaml @@ -0,0 +1,4 @@ +substitutions: + network_enable_ipv6: "true" + +<<: !include common.yaml diff --git a/tests/components/socket/test-ipv6.host.yaml b/tests/components/socket/test-ipv6.host.yaml new file mode 100644 index 0000000000..fdd52c574e --- /dev/null +++ b/tests/components/socket/test-ipv6.host.yaml @@ -0,0 +1,4 @@ +socket: + +network: + enable_ipv6: true diff --git a/tests/components/socket/test.esp32-idf.yaml b/tests/components/socket/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/socket/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/socket/test.host.yaml b/tests/components/socket/test.host.yaml new file mode 100644 index 0000000000..e0c5d7cea3 --- /dev/null +++ b/tests/components/socket/test.host.yaml @@ -0,0 +1,3 @@ +socket: + +network: From f7459670d34b47f66210eaaca7586be33f84205c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 10:10:22 -0600 Subject: [PATCH 085/609] [core] Optimize WarnIfComponentBlockingGuard::finish() hot path (#14040) Co-authored-by: Claude Opus 4.6 --- esphome/core/component.cpp | 38 ++++++++++++++++++-------------------- esphome/core/component.h | 7 ++++--- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index f283a69064..b458ea2a84 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -82,8 +82,8 @@ void store_component_error_message(const Component *component, const char *messa // setup_priority, component state, and status LED constants are now // constexpr in component.h -const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning -const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again +static constexpr uint16_t WARN_IF_BLOCKING_INCREMENT_MS = + 10U; ///< How long the blocking time must be larger to warn again float Component::get_loop_priority() const { return 0.0f; } @@ -529,37 +529,35 @@ void PollingComponent::stop_poller() { uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; } void PollingComponent::set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } -WarnIfComponentBlockingGuard::WarnIfComponentBlockingGuard(Component *component, uint32_t start_time) - : started_(start_time), component_(component) {} +static void __attribute__((noinline, cold)) warn_blocking(Component *component, uint32_t blocking_time) { + bool should_warn; + if (component != nullptr) { + should_warn = component->should_warn_of_blocking(blocking_time); + } else { + should_warn = true; // Already checked > WARN_IF_BLOCKING_OVER_MS in caller + } + if (should_warn) { + ESP_LOGW(TAG, "%s took a long time for an operation (%" PRIu32 " ms), max is 30 ms", + component == nullptr ? LOG_STR_LITERAL("") : LOG_STR_ARG(component->get_component_log_str()), + blocking_time); + } +} + uint32_t WarnIfComponentBlockingGuard::finish() { uint32_t curr_time = millis(); - uint32_t blocking_time = curr_time - this->started_; - #ifdef USE_RUNTIME_STATS // Record component runtime stats if (global_runtime_stats != nullptr) { global_runtime_stats->record_component_time(this->component_, blocking_time, curr_time); } #endif - bool should_warn; - if (this->component_ != nullptr) { - should_warn = this->component_->should_warn_of_blocking(blocking_time); - } else { - should_warn = blocking_time > WARN_IF_BLOCKING_OVER_MS; + if (blocking_time > WARN_IF_BLOCKING_OVER_MS) { + warn_blocking(this->component_, blocking_time); } - if (should_warn) { - ESP_LOGW(TAG, "%s took a long time for an operation (%" PRIu32 " ms)", - component_ == nullptr ? LOG_STR_LITERAL("") : LOG_STR_ARG(component_->get_component_log_str()), - blocking_time); - ESP_LOGW(TAG, "Components should block for at most 30 ms"); - } - return curr_time; } -WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {} - #ifdef USE_SETUP_PRIORITY_OVERRIDE void clear_setup_priority_overrides() { // Free the setup priority map completely diff --git a/esphome/core/component.h b/esphome/core/component.h index d4dad3c9a6..b99641a275 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -79,7 +79,7 @@ inline constexpr uint8_t STATUS_LED_ERROR = 0x10; // Remove before 2026.8.0 enum class RetryResult { DONE, RETRY }; -extern const uint16_t WARN_IF_BLOCKING_OVER_MS; +inline constexpr uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; class Component { public: @@ -550,12 +550,13 @@ class PollingComponent : public Component { class WarnIfComponentBlockingGuard { public: - WarnIfComponentBlockingGuard(Component *component, uint32_t start_time); + WarnIfComponentBlockingGuard(Component *component, uint32_t start_time) + : started_(start_time), component_(component) {} // Finish the timing operation and return the current time uint32_t finish(); - ~WarnIfComponentBlockingGuard(); + ~WarnIfComponentBlockingGuard() = default; protected: uint32_t started_; From b11ad26c4f70050d1656200234b0642760bdea68 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 19 Feb 2026 10:20:19 -0600 Subject: [PATCH 086/609] [audio] Support decoding audio directly from flash (#14098) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/audio/audio_decoder.cpp | 102 ++++++++++-------- esphome/components/audio/audio_decoder.h | 26 +++-- .../audio/audio_transfer_buffer.cpp | 17 ++- .../components/audio/audio_transfer_buffer.h | 67 +++++++++++- 4 files changed, 159 insertions(+), 53 deletions(-) diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index ee6d7d0a15..7794b5b0d3 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -15,8 +15,8 @@ static const uint32_t READ_WRITE_TIMEOUT_MS = 20; // Timeout for transferring a static const uint32_t MAX_POTENTIALLY_FAILED_COUNT = 10; -AudioDecoder::AudioDecoder(size_t input_buffer_size, size_t output_buffer_size) { - this->input_transfer_buffer_ = AudioSourceTransferBuffer::create(input_buffer_size); +AudioDecoder::AudioDecoder(size_t input_buffer_size, size_t output_buffer_size) + : input_buffer_size_(input_buffer_size) { this->output_transfer_buffer_ = AudioSinkTransferBuffer::create(output_buffer_size); } @@ -29,11 +29,20 @@ AudioDecoder::~AudioDecoder() { } esp_err_t AudioDecoder::add_source(std::weak_ptr &input_ring_buffer) { - if (this->input_transfer_buffer_ != nullptr) { - this->input_transfer_buffer_->set_source(input_ring_buffer); - return ESP_OK; + auto source = AudioSourceTransferBuffer::create(this->input_buffer_size_); + if (source == nullptr) { + return ESP_ERR_NO_MEM; } - return ESP_ERR_NO_MEM; + source->set_source(input_ring_buffer); + this->input_buffer_ = std::move(source); + return ESP_OK; +} + +esp_err_t AudioDecoder::add_source(const uint8_t *data_pointer, size_t length) { + auto source = make_unique(); + source->set_data(data_pointer, length); + this->input_buffer_ = std::move(source); + return ESP_OK; } esp_err_t AudioDecoder::add_sink(std::weak_ptr &output_ring_buffer) { @@ -54,8 +63,16 @@ esp_err_t AudioDecoder::add_sink(speaker::Speaker *speaker) { } #endif +esp_err_t AudioDecoder::add_sink(AudioSinkCallback *callback) { + if (this->output_transfer_buffer_ != nullptr) { + this->output_transfer_buffer_->set_sink(callback); + return ESP_OK; + } + return ESP_ERR_NO_MEM; +} + esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { - if ((this->input_transfer_buffer_ == nullptr) || (this->output_transfer_buffer_ == nullptr)) { + if (this->output_transfer_buffer_ == nullptr) { return ESP_ERR_NO_MEM; } @@ -112,6 +129,10 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { } AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { + if (this->input_buffer_ == nullptr) { + return AudioDecoderState::FAILED; + } + if (stop_gracefully) { if (this->output_transfer_buffer_->available() == 0) { if (this->end_of_file_) { @@ -119,7 +140,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { return AudioDecoderState::FINISHED; } - if (!this->input_transfer_buffer_->has_buffered_data()) { + if (!this->input_buffer_->has_buffered_data()) { // If all the internal buffers are empty, the decoding is done return AudioDecoderState::FINISHED; } @@ -170,10 +191,10 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { // Only shift data on the first loop iteration to avoid unnecessary, slow moves // If the decoder buffers internally, then never shift - size_t bytes_read = this->input_transfer_buffer_->transfer_data_from_source( - pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), first_loop_iteration && !this->decoder_buffers_internally_); + size_t bytes_read = this->input_buffer_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), + first_loop_iteration && !this->decoder_buffers_internally_); - if (!first_loop_iteration && (this->input_transfer_buffer_->available() < bytes_processed)) { + if (!first_loop_iteration && (this->input_buffer_->available() < bytes_processed)) { // Less data is available than what was processed in last iteration, so don't attempt to decode. // This attempts to avoid the decoder from consistently trying to decode an incomplete frame. The transfer buffer // will shift the remaining data to the start and copy more from the source the next time the decode function is @@ -181,19 +202,21 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { break; } - bytes_available_before_processing = this->input_transfer_buffer_->available(); + bytes_available_before_processing = this->input_buffer_->available(); 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) { - // The input buffer is full. Since it previously failed on the exact same data, we can never recover + if ((this->input_buffer_->free() == 0) && first_loop_iteration) { + // The input buffer is full (or read-only, e.g. const flash source). Since it previously failed on the exact + // same data, we can never recover. For const sources this is correct: the entire file is already available, so + // a decode failure is genuine, not a transient out-of-data condition. state = FileDecoderState::FAILED; } else { // Attempt to get more data next time state = FileDecoderState::IDLE; } - } else if (this->input_transfer_buffer_->available() == 0) { + } else if (this->input_buffer_->available() == 0) { // No data to decode, attempt to get more data next time state = FileDecoderState::IDLE; } else { @@ -224,7 +247,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { } first_loop_iteration = false; - bytes_processed = bytes_available_before_processing - this->input_transfer_buffer_->available(); + bytes_processed = bytes_available_before_processing - this->input_buffer_->available(); if (state == FileDecoderState::POTENTIALLY_FAILED) { ++this->potentially_failed_count_; @@ -243,8 +266,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { FileDecoderState AudioDecoder::decode_flac_() { if (!this->audio_stream_info_.has_value()) { // Header hasn't been read - auto result = this->flac_decoder_->read_header(this->input_transfer_buffer_->get_buffer_start(), - this->input_transfer_buffer_->available()); + auto result = this->flac_decoder_->read_header(this->input_buffer_->data(), this->input_buffer_->available()); if (result > esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) { // Serrious error reading FLAC header, there is no recovery @@ -252,7 +274,7 @@ FileDecoderState AudioDecoder::decode_flac_() { } size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); - this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed); + this->input_buffer_->consume(bytes_consumed); if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) { return FileDecoderState::MORE_TO_PROCESS; @@ -273,8 +295,7 @@ FileDecoderState AudioDecoder::decode_flac_() { } uint32_t output_samples = 0; - auto result = this->flac_decoder_->decode_frame(this->input_transfer_buffer_->get_buffer_start(), - this->input_transfer_buffer_->available(), + auto result = this->flac_decoder_->decode_frame(this->input_buffer_->data(), this->input_buffer_->available(), this->output_transfer_buffer_->get_buffer_end(), &output_samples); if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) { @@ -283,7 +304,7 @@ FileDecoderState AudioDecoder::decode_flac_() { } size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); - this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed); + this->input_buffer_->consume(bytes_consumed); if (result > esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) { // Corrupted frame, don't retry with current buffer content, wait for new sync @@ -305,26 +326,25 @@ FileDecoderState AudioDecoder::decode_flac_() { #ifdef USE_AUDIO_MP3_SUPPORT FileDecoderState AudioDecoder::decode_mp3_() { // Look for the next sync word - int buffer_length = (int) this->input_transfer_buffer_->available(); - int32_t offset = - esp_audio_libs::helix_decoder::MP3FindSyncWord(this->input_transfer_buffer_->get_buffer_start(), buffer_length); + int buffer_length = (int) this->input_buffer_->available(); + int32_t offset = esp_audio_libs::helix_decoder::MP3FindSyncWord(this->input_buffer_->data(), buffer_length); if (offset < 0) { // New data may have the sync word - this->input_transfer_buffer_->decrease_buffer_length(buffer_length); + this->input_buffer_->consume(buffer_length); return FileDecoderState::POTENTIALLY_FAILED; } // Advance read pointer to match the offset for the syncword - this->input_transfer_buffer_->decrease_buffer_length(offset); - const uint8_t *buffer_start = this->input_transfer_buffer_->get_buffer_start(); + this->input_buffer_->consume(offset); + const uint8_t *buffer_start = this->input_buffer_->data(); - buffer_length = (int) this->input_transfer_buffer_->available(); + buffer_length = (int) this->input_buffer_->available(); int err = esp_audio_libs::helix_decoder::MP3Decode(this->mp3_decoder_, &buffer_start, &buffer_length, (int16_t *) this->output_transfer_buffer_->get_buffer_end(), 0); - size_t consumed = this->input_transfer_buffer_->available() - buffer_length; - this->input_transfer_buffer_->decrease_buffer_length(consumed); + size_t consumed = this->input_buffer_->available() - buffer_length; + this->input_buffer_->consume(consumed); if (err) { switch (err) { @@ -363,9 +383,8 @@ FileDecoderState AudioDecoder::decode_opus_() { size_t bytes_consumed, samples_decoded; micro_opus::OggOpusResult result = this->opus_decoder_->decode( - this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available(), - this->output_transfer_buffer_->get_buffer_end(), this->output_transfer_buffer_->free(), bytes_consumed, - samples_decoded); + this->input_buffer_->data(), this->input_buffer_->available(), this->output_transfer_buffer_->get_buffer_end(), + this->output_transfer_buffer_->free(), bytes_consumed, samples_decoded); if (result == micro_opus::OGG_OPUS_OK) { if (!processed_header && this->opus_decoder_->is_initialized()) { @@ -379,7 +398,7 @@ FileDecoderState AudioDecoder::decode_opus_() { this->output_transfer_buffer_->increase_buffer_length( this->audio_stream_info_.value().frames_to_bytes(samples_decoded)); } - this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed); + this->input_buffer_->consume(bytes_consumed); } else if (result == micro_opus::OGG_OPUS_OUTPUT_BUFFER_TOO_SMALL) { // Reallocate to decode the packet on the next call this->free_buffer_required_ = this->opus_decoder_->get_required_output_buffer_size(); @@ -399,11 +418,11 @@ FileDecoderState AudioDecoder::decode_wav_() { if (!this->audio_stream_info_.has_value()) { // Header hasn't been processed - esp_audio_libs::wav_decoder::WAVDecoderResult result = this->wav_decoder_->decode_header( - this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available()); + esp_audio_libs::wav_decoder::WAVDecoderResult result = + this->wav_decoder_->decode_header(this->input_buffer_->data(), this->input_buffer_->available()); if (result == esp_audio_libs::wav_decoder::WAV_DECODER_SUCCESS_IN_DATA) { - this->input_transfer_buffer_->decrease_buffer_length(this->wav_decoder_->bytes_processed()); + this->input_buffer_->consume(this->wav_decoder_->bytes_processed()); this->audio_stream_info_ = audio::AudioStreamInfo( this->wav_decoder_->bits_per_sample(), this->wav_decoder_->num_channels(), this->wav_decoder_->sample_rate()); @@ -419,7 +438,7 @@ FileDecoderState AudioDecoder::decode_wav_() { } } else { if (!this->wav_has_known_end_ || (this->wav_bytes_left_ > 0)) { - size_t bytes_to_copy = this->input_transfer_buffer_->available(); + size_t bytes_to_copy = this->input_buffer_->available(); if (this->wav_has_known_end_) { bytes_to_copy = std::min(bytes_to_copy, this->wav_bytes_left_); @@ -428,9 +447,8 @@ FileDecoderState AudioDecoder::decode_wav_() { bytes_to_copy = std::min(bytes_to_copy, this->output_transfer_buffer_->free()); if (bytes_to_copy > 0) { - std::memcpy(this->output_transfer_buffer_->get_buffer_end(), this->input_transfer_buffer_->get_buffer_start(), - bytes_to_copy); - this->input_transfer_buffer_->decrease_buffer_length(bytes_to_copy); + std::memcpy(this->output_transfer_buffer_->get_buffer_end(), this->input_buffer_->data(), bytes_to_copy); + this->input_buffer_->consume(bytes_to_copy); this->output_transfer_buffer_->increase_buffer_length(bytes_to_copy); if (this->wav_has_known_end_) { this->wav_bytes_left_ -= bytes_to_copy; diff --git a/esphome/components/audio/audio_decoder.h b/esphome/components/audio/audio_decoder.h index cad16110ae..726baa289e 100644 --- a/esphome/components/audio/audio_decoder.h +++ b/esphome/components/audio/audio_decoder.h @@ -50,12 +50,12 @@ enum class FileDecoderState : uint8_t { class AudioDecoder { /* * @brief Class that facilitates decoding an audio file. - * The audio file is read from a ring buffer source, decoded, and sent to an audio sink (ring buffer or speaker - * component). + * The audio file is read from a source (ring buffer or const data pointer), decoded, and sent to an audio sink + * (ring buffer, speaker component, or callback). * Supports wav, flac, mp3, and ogg opus formats. */ public: - /// @brief Allocates the input and output transfer buffers + /// @brief Allocates the output transfer buffer and stores the input buffer size for later use by add_source() /// @param input_buffer_size Size of the input transfer buffer in bytes. /// @param output_buffer_size Size of the output transfer buffer in bytes. AudioDecoder(size_t input_buffer_size, size_t output_buffer_size); @@ -80,6 +80,17 @@ class AudioDecoder { esp_err_t add_sink(speaker::Speaker *speaker); #endif + /// @brief Adds a const data pointer as the source for raw file data. Does not allocate a transfer buffer. + /// @param data_pointer Pointer to the const audio data (e.g., stored in flash memory) + /// @param length Size of the data in bytes + /// @return ESP_OK + esp_err_t add_source(const uint8_t *data_pointer, size_t length); + + /// @brief Adds a callback as the sink for decoded audio. + /// @param callback Pointer to the AudioSinkCallback implementation + /// @return ESP_OK if successful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated + esp_err_t add_sink(AudioSinkCallback *callback); + /// @brief Sets up decoding the file /// @param audio_file_type AudioFileType of the file /// @return ESP_OK if successful, ESP_ERR_NO_MEM if the transfer buffers fail to allocate, or ESP_ERR_NOT_SUPPORTED if @@ -120,25 +131,26 @@ class AudioDecoder { #endif FileDecoderState decode_wav_(); - std::unique_ptr input_transfer_buffer_; + std::unique_ptr input_buffer_; std::unique_ptr output_transfer_buffer_; AudioFileType audio_file_type_{AudioFileType::NONE}; optional audio_stream_info_{}; + size_t input_buffer_size_{0}; size_t free_buffer_required_{0}; size_t wav_bytes_left_{0}; uint32_t potentially_failed_count_{0}; + uint32_t accumulated_frames_written_{0}; + uint32_t playback_ms_{0}; + bool end_of_file_{false}; bool wav_has_known_end_{false}; bool decoder_buffers_internally_{false}; bool pause_output_{false}; - - uint32_t accumulated_frames_written_{0}; - uint32_t playback_ms_{0}; }; } // namespace audio } // namespace esphome diff --git a/esphome/components/audio/audio_transfer_buffer.cpp b/esphome/components/audio/audio_transfer_buffer.cpp index a8be55d62f..5cd7cf9e63 100644 --- a/esphome/components/audio/audio_transfer_buffer.cpp +++ b/esphome/components/audio/audio_transfer_buffer.cpp @@ -142,7 +142,7 @@ size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_ this->data_start_ = this->buffer_; } - size_t bytes_to_read = this->free(); + size_t bytes_to_read = AudioTransferBuffer::free(); size_t bytes_read = 0; if (bytes_to_read > 0) { if (this->ring_buffer_.use_count() > 0) { @@ -193,6 +193,21 @@ bool AudioSinkTransferBuffer::has_buffered_data() const { return (this->available() > 0); } +size_t AudioSourceTransferBuffer::free() const { return AudioTransferBuffer::free(); } + +bool AudioSourceTransferBuffer::has_buffered_data() const { return AudioTransferBuffer::has_buffered_data(); } + +void ConstAudioSourceBuffer::set_data(const uint8_t *data, size_t length) { + this->data_start_ = data; + this->length_ = length; +} + +void ConstAudioSourceBuffer::consume(size_t bytes) { + bytes = std::min(bytes, this->length_); + this->length_ -= bytes; + this->data_start_ += bytes; +} + } // namespace audio } // namespace esphome diff --git a/esphome/components/audio/audio_transfer_buffer.h b/esphome/components/audio/audio_transfer_buffer.h index 22c22cc9ae..c32d4d0e41 100644 --- a/esphome/components/audio/audio_transfer_buffer.h +++ b/esphome/components/audio/audio_transfer_buffer.h @@ -32,7 +32,7 @@ class AudioTransferBuffer { /// @brief Destructor that deallocates the transfer buffer ~AudioTransferBuffer(); - /// @brief Returns a pointer to the start of the transfer buffer where available() bytes of exisiting data can be read + /// @brief Returns a pointer to the start of the transfer buffer where available() bytes of existing data can be read uint8_t *get_buffer_start() const { return this->data_start_; } /// @brief Returns a pointer to the end of the transfer buffer where free() bytes of new data can be written @@ -129,10 +129,41 @@ class AudioSinkTransferBuffer : public AudioTransferBuffer { AudioSinkCallback *sink_callback_{nullptr}; }; -class AudioSourceTransferBuffer : public AudioTransferBuffer { +/// @brief Abstract interface for reading audio data from a buffer. +/// Provides a common read interface for both mutable transfer buffers and read-only const buffers. +class AudioReadableBuffer { + public: + virtual ~AudioReadableBuffer() = default; + + /// @brief Returns a pointer to the start of readable data + virtual const uint8_t *data() const = 0; + + /// @brief Returns the number of bytes available to read + virtual size_t available() const = 0; + + /// @brief Returns the number of free bytes available to write. Defaults to 0 for read-only buffers. + virtual size_t free() const { return 0; } + + /// @brief Advances past consumed data + /// @param bytes Number of bytes consumed + virtual void consume(size_t bytes) = 0; + + /// @brief Tests if there is any buffered data + virtual bool has_buffered_data() const = 0; + + /// @brief Refills the buffer from its source. No-op by default for read-only buffers. + /// @param ticks_to_wait FreeRTOS ticks to block while waiting for data + /// @param pre_shift If true, shifts existing data to the start of the buffer before reading + /// @return Number of bytes read + virtual size_t fill(TickType_t ticks_to_wait, bool pre_shift) { return 0; } + size_t fill(TickType_t ticks_to_wait) { return this->fill(ticks_to_wait, true); } +}; + +class AudioSourceTransferBuffer : public AudioTransferBuffer, public AudioReadableBuffer { /* * @brief A class that implements a transfer buffer for audio sources. * Supports reading audio data from a ring buffer into the transfer buffer for processing. + * Implements AudioReadableBuffer for use by consumers that only need read access. */ public: /// @brief Creates a new source transfer buffer. @@ -140,7 +171,7 @@ class AudioSourceTransferBuffer : public AudioTransferBuffer { /// @return unique_ptr if successfully allocated, nullptr otherwise static std::unique_ptr create(size_t buffer_size); - /// @brief Reads any available data from the sink into the transfer buffer. + /// @brief Reads any available data from the source into the transfer buffer. /// @param ticks_to_wait FreeRTOS ticks to block while waiting for the source to have enough data /// @param pre_shift If true, any unwritten data is moved to the start of the buffer before transferring from the /// source. Defaults to true. @@ -150,6 +181,36 @@ class AudioSourceTransferBuffer : public AudioTransferBuffer { /// @brief Adds a ring buffer as the transfer buffer's source. /// @param ring_buffer weak_ptr to the allocated ring buffer void set_source(const std::weak_ptr &ring_buffer) { this->ring_buffer_ = ring_buffer.lock(); }; + + // AudioReadableBuffer interface + const uint8_t *data() const override { return this->data_start_; } + size_t available() const override { return this->buffer_length_; } + size_t free() const override; + void consume(size_t bytes) override { this->decrease_buffer_length(bytes); } + bool has_buffered_data() const override; + size_t fill(TickType_t ticks_to_wait, bool pre_shift) override { + return this->transfer_data_from_source(ticks_to_wait, pre_shift); + } +}; + +/// @brief A lightweight read-only audio buffer for const data sources (e.g., flash memory). +/// Does not allocate memory or transfer data from external sources. +class ConstAudioSourceBuffer : public AudioReadableBuffer { + public: + /// @brief Sets the data pointer and length for the buffer + /// @param data Pointer to the const audio data + /// @param length Size of the data in bytes + void set_data(const uint8_t *data, size_t length); + + // AudioReadableBuffer interface + const uint8_t *data() const override { return this->data_start_; } + size_t available() const override { return this->length_; } + void consume(size_t bytes) override; + bool has_buffered_data() const override { return this->length_ > 0; } + + protected: + const uint8_t *data_start_{nullptr}; + size_t length_{0}; }; } // namespace audio From bd50b80882d990855561216548965d4c01aad9fd Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:34:40 -0500 Subject: [PATCH 087/609] [opentherm] Remove deprecated opentherm_version config option (#14103) Co-authored-by: Claude Opus 4.6 --- esphome/components/opentherm/__init__.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/esphome/components/opentherm/__init__.py b/esphome/components/opentherm/__init__.py index dddb9dc891..36f85a9766 100644 --- a/esphome/components/opentherm/__init__.py +++ b/esphome/components/opentherm/__init__.py @@ -1,4 +1,3 @@ -import logging from typing import Any from esphome import automation, pins @@ -24,7 +23,6 @@ CONF_CH2_ACTIVE = "ch2_active" CONF_SUMMER_MODE_ACTIVE = "summer_mode_active" CONF_DHW_BLOCK = "dhw_block" CONF_SYNC_MODE = "sync_mode" -CONF_OPENTHERM_VERSION = "opentherm_version" # Deprecated, will be removed CONF_BEFORE_SEND = "before_send" CONF_BEFORE_PROCESS_RESPONSE = "before_process_response" @@ -38,8 +36,6 @@ BeforeProcessResponseTrigger = generate.opentherm_ns.class_( automation.Trigger.template(generate.OpenthermData.operator("ref")), ) -_LOGGER = logging.getLogger(__name__) - CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -54,7 +50,6 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_SUMMER_MODE_ACTIVE, False): cv.boolean, cv.Optional(CONF_DHW_BLOCK, False): cv.boolean, cv.Optional(CONF_SYNC_MODE, False): cv.boolean, - cv.Optional(CONF_OPENTHERM_VERSION): cv.positive_float, # Deprecated cv.Optional(CONF_BEFORE_SEND): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BeforeSendTrigger), @@ -123,11 +118,6 @@ async def to_code(config: dict[str, Any]) -> None: cg.add(getattr(var, f"set_{key}_{const.SETTING}")(value)) settings.append(key) else: - if key == CONF_OPENTHERM_VERSION: - _LOGGER.warning( - "opentherm_version is deprecated and will be removed in esphome 2025.2.0\n" - "Please change to 'opentherm_version_controller'." - ) cg.add(getattr(var, f"set_{key}")(value)) if len(input_sensors) > 0: From bf2e22da4f7ebf660d517f27702500a67d11ea9b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:55:03 -0500 Subject: [PATCH 088/609] [esp32] Remove deprecated add_idf_component() parameters and IDF component refresh option (#14105) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32/__init__.py | 68 ++++++---------------------- 1 file changed, 14 insertions(+), 54 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 8b3e1afea6..b1b3f0dc16 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -25,7 +25,6 @@ from esphome.const import ( CONF_PLATFORM_VERSION, CONF_PLATFORMIO_OPTIONS, CONF_REF, - CONF_REFRESH, CONF_SAFE_MODE, CONF_SOURCE, CONF_TYPE, @@ -41,7 +40,7 @@ from esphome.const import ( ThreadModel, __version__, ) -from esphome.core import CORE, HexInt, TimePeriod +from esphome.core import CORE, HexInt from esphome.coroutine import CoroPriority, coroutine_with_priority import esphome.final_validate as fv from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed @@ -499,49 +498,24 @@ def add_idf_component( repo: str | None = None, ref: str | None = None, path: str | None = None, - refresh: TimePeriod | None = None, - components: list[str] | None = None, - submodules: list[str] | None = None, ): """Add an esp-idf component to the project.""" if not repo and not ref and not path: raise ValueError("Requires at least one of repo, ref or path") - if refresh or submodules or components: - _LOGGER.warning( - "The refresh, components and submodules parameters in add_idf_component() are " - "deprecated and will be removed in ESPHome 2026.1. If you are seeing this, report " - "an issue to the external_component author and ask them to update it." - ) components_registry = CORE.data[KEY_ESP32][KEY_COMPONENTS] - if components: - for comp in components: - existing = components_registry.get(comp) - if existing and existing.get(KEY_REF) != ref: - _LOGGER.warning( - "IDF component %s version conflict %s replaced by %s", - comp, - existing.get(KEY_REF), - ref, - ) - components_registry[comp] = { - KEY_REPO: repo, - KEY_REF: ref, - KEY_PATH: f"{path}/{comp}" if path else comp, - } - else: - existing = components_registry.get(name) - if existing and existing.get(KEY_REF) != ref: - _LOGGER.warning( - "IDF component %s version conflict %s replaced by %s", - name, - existing.get(KEY_REF), - ref, - ) - components_registry[name] = { - KEY_REPO: repo, - KEY_REF: ref, - KEY_PATH: path, - } + existing = components_registry.get(name) + if existing and existing.get(KEY_REF) != ref: + _LOGGER.warning( + "IDF component %s version conflict %s replaced by %s", + name, + existing.get(KEY_REF), + ref, + ) + components_registry[name] = { + KEY_REPO: repo, + KEY_REF: ref, + KEY_PATH: path, + } def exclude_builtin_idf_component(name: str) -> None: @@ -1037,16 +1011,6 @@ def _parse_idf_component(value: str) -> ConfigType: ) -def _validate_idf_component(config: ConfigType) -> ConfigType: - """Validate IDF component config and warn about deprecated options.""" - if CONF_REFRESH in config: - _LOGGER.warning( - "The 'refresh' option for IDF components is deprecated and has no effect. " - "It will be removed in ESPHome 2026.1. Please remove it from your configuration." - ) - return config - - FRAMEWORK_ESP_IDF = "esp-idf" FRAMEWORK_ARDUINO = "arduino" FRAMEWORK_SCHEMA = cv.Schema( @@ -1135,13 +1099,9 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_SOURCE): cv.git_ref, cv.Optional(CONF_REF): cv.string, cv.Optional(CONF_PATH): cv.string, - cv.Optional(CONF_REFRESH): cv.All( - cv.string, cv.source_refresh - ), } ), ), - _validate_idf_component, ) ), } From ed74790eed87afff0f252fb2330dceb9b1a293de Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:56:06 -0500 Subject: [PATCH 089/609] [i2c] Remove deprecated stop parameter overloads and readv/writev methods (#14106) Co-authored-by: Claude Opus 4.6 --- esphome/components/i2c/i2c.h | 31 ----------------- esphome/components/i2c/i2c_bus.h | 58 -------------------------------- 2 files changed, 89 deletions(-) diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index 48a6e751cf..aab98d5f46 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -267,37 +267,6 @@ class I2CDevice { bool write_byte_16(uint8_t a_register, uint16_t data) const { return write_bytes_16(a_register, &data, 1); } - // Deprecated functions - - ESPDEPRECATED("The stop argument is no longer used. This will be removed from ESPHome 2026.3.0", "2025.9.0") - ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop) { - return this->read_register(a_register, data, len); - } - - ESPDEPRECATED("The stop argument is no longer used. This will be removed from ESPHome 2026.3.0", "2025.9.0") - ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop) { - return this->read_register16(a_register, data, len); - } - - ESPDEPRECATED("The stop argument is no longer used; use write_read() for consecutive write and read. This will be " - "removed from ESPHome 2026.3.0", - "2025.9.0") - ErrorCode write(const uint8_t *data, size_t len, bool stop) const { return this->write(data, len); } - - ESPDEPRECATED("The stop argument is no longer used; use write_read() for consecutive write and read. This will be " - "removed from ESPHome 2026.3.0", - "2025.9.0") - ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) const { - return this->write_register(a_register, data, len); - } - - ESPDEPRECATED("The stop argument is no longer used; use write_read() for consecutive write and read. This will be " - "removed from ESPHome 2026.3.0", - "2025.9.0") - ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop) const { - return this->write_register16(a_register, data, len); - } - protected: uint8_t address_{0x00}; ///< store the address of the device on the bus I2CBus *bus_{nullptr}; ///< pointer to I2CBus instance diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index 3de5d5ca7b..2bc0dc1ef9 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -1,8 +1,6 @@ #pragma once #include #include -#include -#include #include #include @@ -24,18 +22,6 @@ enum ErrorCode { ERROR_CRC = 7, ///< bytes received with a CRC error }; -/// @brief the ReadBuffer structure stores a pointer to a read buffer and its length -struct ReadBuffer { - uint8_t *data; ///< pointer to the read buffer - size_t len; ///< length of the buffer -}; - -/// @brief the WriteBuffer structure stores a pointer to a write buffer and its length -struct WriteBuffer { - const uint8_t *data; ///< pointer to the write buffer - size_t len; ///< length of the buffer -}; - /// @brief This Class provides the methods to read and write bytes from an I2CBus. /// @note The I2CBus virtual class follows a *Factory design pattern* that provides all the interfaces methods required /// by clients while deferring the actual implementation of these methods to a subclasses. I2C-bus specification and @@ -68,50 +54,6 @@ class I2CBus { return this->write_readv(address, buffer, len, nullptr, 0); } - ESPDEPRECATED("This method is deprecated and will be removed in ESPHome 2026.3.0. Use write_readv() instead.", - "2025.9.0") - ErrorCode readv(uint8_t address, ReadBuffer *read_buffers, size_t count) { - size_t total_len = 0; - for (size_t i = 0; i != count; i++) { - total_len += read_buffers[i].len; - } - - SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C reads are small - uint8_t *buffer = buffer_alloc.get(); - - auto err = this->write_readv(address, nullptr, 0, buffer, total_len); - if (err != ERROR_OK) - return err; - size_t pos = 0; - for (size_t i = 0; i != count; i++) { - if (read_buffers[i].len != 0) { - std::memcpy(read_buffers[i].data, buffer + pos, read_buffers[i].len); - pos += read_buffers[i].len; - } - } - return ERROR_OK; - } - - ESPDEPRECATED("This method is deprecated and will be removed in ESPHome 2026.3.0. Use write_readv() instead.", - "2025.9.0") - ErrorCode writev(uint8_t address, const WriteBuffer *write_buffers, size_t count, bool stop = true) { - size_t total_len = 0; - for (size_t i = 0; i != count; i++) { - total_len += write_buffers[i].len; - } - - SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C writes are small - uint8_t *buffer = buffer_alloc.get(); - - size_t pos = 0; - for (size_t i = 0; i != count; i++) { - std::memcpy(buffer + pos, write_buffers[i].data, write_buffers[i].len); - pos += write_buffers[i].len; - } - - return this->write_readv(address, buffer, total_len, nullptr, 0); - } - protected: /// @brief Scans the I2C bus for devices. Devices presence is kept in an array of std::pair /// that contains the address and the corresponding bool presence flag. From d2026b4cd74bbeb9b86941bf1887f3d93d639387 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 19 Feb 2026 10:56:34 -0600 Subject: [PATCH 090/609] [audio] Disable FLAC CRC validation to improve decoding efficiency (#14108) --- esphome/components/audio/audio_decoder.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index 7794b5b0d3..bc05bc0006 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -85,6 +85,10 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { #ifdef USE_AUDIO_FLAC_SUPPORT case AudioFileType::FLAC: this->flac_decoder_ = make_unique(); + // CRC check slows down decoding by 15-20% on an ESP32-S3. FLAC sources in ESPHome are either from an http source + // or built into the firmware, so the data integrity is already verified by the time it gets to the decoder, + // making the CRC check unnecessary. + this->flac_decoder_->set_crc_check_enabled(false); this->free_buffer_required_ = this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header break; From da616e05574af7216a3b5668ab1d2d0e3dce0ca1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:00:05 -0500 Subject: [PATCH 091/609] [ethernet] Improve clk_mode deprecation warning with actionable YAML (#14104) Co-authored-by: Claude Opus 4.6 --- esphome/components/ethernet/__init__.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 52f5f44d41..935d2004d4 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -218,12 +218,19 @@ def _validate(config): ) elif config[CONF_TYPE] != "OPENETH": if CONF_CLK_MODE in config: + mode, pin = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]] LOGGER.warning( - "[ethernet] The 'clk_mode' option is deprecated and will be removed in ESPHome 2026.1. " - "Please update your configuration to use 'clk' instead." + "[ethernet] The 'clk_mode' option is deprecated. " + "Please replace 'clk_mode: %s' with:\n" + " clk:\n" + " mode: %s\n" + " pin: %s\n" + "Removal scheduled for 2026.7.0.", + config[CONF_CLK_MODE], + mode, + pin, ) - mode = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]] - config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode[0], CONF_PIN: mode[1]}) + config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode, CONF_PIN: pin}) del config[CONF_CLK_MODE] elif CONF_CLK not in config: raise cv.Invalid("'clk' is a required option for [ethernet].") From 9aa17984df3a0766c18c02ed60f4575ba69a7fb9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:25:26 -0500 Subject: [PATCH 092/609] [pulse_counter] Fix build failure when use_pcnt is false (#14111) Co-authored-by: Claude Opus 4.6 --- .../components/pulse_counter/pulse_counter_sensor.h | 4 ++-- .../pulse_counter/test-no-pcnt.esp32-idf.yaml | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index a7913d5d66..7a68858099 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -8,10 +8,10 @@ #if defined(USE_ESP32) #include -#ifdef SOC_PCNT_SUPPORTED +#if defined(SOC_PCNT_SUPPORTED) && __has_include() #include #define HAS_PCNT -#endif // SOC_PCNT_SUPPORTED +#endif // defined(SOC_PCNT_SUPPORTED) && __has_include() #endif // USE_ESP32 namespace esphome { diff --git a/tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml b/tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml new file mode 100644 index 0000000000..cd15cc781d --- /dev/null +++ b/tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml @@ -0,0 +1,10 @@ +sensor: + - platform: pulse_counter + name: Pulse Counter + pin: 4 + use_pcnt: false + count_mode: + rising_edge: INCREMENT + falling_edge: DECREMENT + internal_filter: 13us + update_interval: 15s From 7a5c3cee0d0f3fd77423438f036e8b6596a3246b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 11:41:00 -0600 Subject: [PATCH 093/609] [esp32_ble] Enable CONFIG_BT_RELEASE_IRAM on ESP32-C2 (#14109) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32_ble/__init__.py | 10 ++++++++++ tests/components/esp32_ble/test.esp32-c2-idf.yaml | 5 +++++ 2 files changed, 15 insertions(+) create mode 100644 tests/components/esp32_ble/test.esp32-c2-idf.yaml diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index dcc3ce71cf..d2020ada22 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -9,6 +9,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import socket from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant +from esphome.components.esp32.const import VARIANT_ESP32C2 import esphome.config_validation as cv from esphome.const import ( CONF_ENABLE_ON_BOOT, @@ -387,6 +388,15 @@ def final_validation(config): f"Name '{name}' is too long, maximum length is {max_length} characters" ) + # ESP32-C2 has very limited RAM (~272KB). Without releasing BLE IRAM, + # esp_bt_controller_init fails with ESP_ERR_NO_MEM. + # CONFIG_BT_RELEASE_IRAM changes the memory layout so IRAM and DRAM share + # space more flexibly, giving the BT controller enough contiguous memory. + # This requires CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT to be disabled. + if get_esp32_variant() == VARIANT_ESP32C2: + add_idf_sdkconfig_option("CONFIG_BT_RELEASE_IRAM", True) + add_idf_sdkconfig_option("CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT", False) + # Set GATT Client/Server sdkconfig options based on which components are loaded full_config = fv.full_config.get() diff --git a/tests/components/esp32_ble/test.esp32-c2-idf.yaml b/tests/components/esp32_ble/test.esp32-c2-idf.yaml new file mode 100644 index 0000000000..f8defaf28f --- /dev/null +++ b/tests/components/esp32_ble/test.esp32-c2-idf.yaml @@ -0,0 +1,5 @@ +<<: !include common.yaml + +esp32_ble: + io_capability: keyboard_only + disable_bt_logs: false From f2c98d612607d61c483d08cc77e92795af85c131 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:45:04 -0500 Subject: [PATCH 094/609] [safe_mode] Log brownout as reset reason on OTA rollback (#14113) Co-authored-by: Claude Opus 4.6 --- esphome/components/safe_mode/safe_mode.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index f32511531a..6cae4bf9d5 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -11,6 +11,7 @@ #if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) #include +#include #endif namespace esphome::safe_mode { @@ -54,6 +55,10 @@ void SafeModeComponent::dump_config() { "OTA rollback detected! Rolled back from partition '%s'\n" "The device reset before the boot was marked successful", last_invalid->label); + if (esp_reset_reason() == ESP_RST_BROWNOUT) { + ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!\n" + "See https://esphome.io/guides/faq.html#brownout-detector-was-triggered"); + } } #endif } From 4aa8f57d3609c12c10c3877c83b06d0c52975e7a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 14:08:44 -0600 Subject: [PATCH 095/609] [json] Add SerializationBuffer for stack-first JSON serialization (#13625) --- esphome/components/api/user_services.h | 4 +- esphome/components/json/json_util.cpp | 81 +++++++- esphome/components/json/json_util.h | 111 +++++++++- esphome/components/mqtt/mqtt_client.cpp | 4 +- esphome/components/web_server/web_server.cpp | 191 +++++++++--------- esphome/components/web_server/web_server.h | 138 +++++++------ .../web_server_idf/web_server_idf.cpp | 6 +- .../web_server_idf/web_server_idf.h | 3 +- tests/components/json/common.yaml | 13 +- 9 files changed, 367 insertions(+), 184 deletions(-) diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 85fba2a435..0fc529108c 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -264,9 +264,9 @@ template class APIRespondAction : public Action { // Build and send JSON response json::JsonBuilder builder; this->json_builder_(x..., builder.root()); - std::string json_str = builder.serialize(); + auto json_buf = builder.serialize(); this->parent_->send_action_response(call_id, success, StringRef(error_message), - reinterpret_cast(json_str.data()), json_str.size()); + reinterpret_cast(json_buf.data()), json_buf.size()); return; } #endif diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 69f8bfc61a..6c60a04d20 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -15,7 +15,7 @@ static const char *const TAG = "json"; static SpiRamAllocator global_json_allocator; #endif -std::string build_json(const json_build_t &f) { +SerializationBuffer<> build_json(const json_build_t &f) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson JsonBuilder builder; JsonObject root = builder.root(); @@ -66,14 +66,83 @@ JsonDocument parse_json(const uint8_t *data, size_t len) { // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } -std::string JsonBuilder::serialize() { +SerializationBuffer<> JsonBuilder::serialize() { + // =========================================================================================== + // CRITICAL: NRVO (Named Return Value Optimization) - DO NOT REFACTOR WITHOUT UNDERSTANDING + // =========================================================================================== + // + // This function is carefully structured to enable NRVO. The compiler constructs `result` + // directly in the caller's stack frame, eliminating the move constructor call entirely. + // + // WITHOUT NRVO: Each return would trigger SerializationBuffer's move constructor, which + // must memcpy up to 512 bytes of stack buffer content. This happens on EVERY JSON + // serialization (sensor updates, web server responses, MQTT publishes, etc.). + // + // WITH NRVO: Zero memcpy, zero move constructor overhead. The buffer lives directly + // where the caller needs it. + // + // Requirements for NRVO to work: + // 1. Single named variable (`result`) returned from ALL paths + // 2. All paths must return the SAME variable (not different variables) + // 3. No std::move() on the return statement + // + // If you must modify this function: + // - Keep a single `result` variable declared at the top + // - All code paths must return `result` (not a different variable) + // - Verify NRVO still works by checking the disassembly for move constructor calls + // - Test: objdump -d -C firmware.elf | grep "SerializationBuffer.*SerializationBuffer" + // Should show only destructor, NOT move constructor + // + // Try stack buffer first. 640 bytes covers 99.9% of JSON payloads (sensors ~200B, + // lights ~170B, climate ~500-700B). Only entities with 40+ options exceed this. + // + // IMPORTANT: ArduinoJson's serializeJson() with a bounded buffer returns the actual + // bytes written (truncated count), NOT the would-be size like snprintf(). When the + // payload exceeds the buffer, the return value equals the buffer capacity. The heap + // fallback doubles the buffer size until the payload fits. This avoids instantiating + // measureJson()'s DummyWriter templates (~736 bytes flash) at the cost of temporarily + // over-allocating heap (at most 2x) for the rare payloads that exceed 640 bytes. + // + // =========================================================================================== + constexpr size_t buf_size = SerializationBuffer<>::BUFFER_SIZE; + SerializationBuffer<> result(buf_size - 1); // Max content size (reserve 1 for null) + if (doc_.overflowed()) { ESP_LOGE(TAG, "JSON document overflow"); - return "{}"; + auto *buf = result.data_writable_(); + buf[0] = '{'; + buf[1] = '}'; + buf[2] = '\0'; + result.set_size_(2); + return result; } - std::string output; - serializeJson(doc_, output); - return output; + + size_t size = serializeJson(doc_, result.data_writable_(), buf_size); + if (size < buf_size) { + // Fits in stack buffer - update size to actual length + result.set_size_(size); + return result; + } + + // Payload exceeded stack buffer. Double the buffer and retry until it fits. + // In practice, one iteration (1024 bytes) covers all known entity types. + // Payloads exceeding 1024 bytes are not known to exist in real configurations. + // Cap at 5120 as a safety limit to prevent runaway allocation. + constexpr size_t max_heap_size = 5120; + size_t heap_size = buf_size * 2; + while (heap_size <= max_heap_size) { + result.reallocate_heap_(heap_size - 1); + size = serializeJson(doc_, result.data_writable_(), heap_size); + if (size < heap_size) { + result.set_size_(size); + return result; + } + heap_size *= 2; + } + // Payload exceeds 5120 bytes - return truncated result + ESP_LOGW(TAG, "JSON payload too large, truncated to %zu bytes", size); + result.set_size_(size); + return result; } } // namespace json diff --git a/esphome/components/json/json_util.h b/esphome/components/json/json_util.h index c472b9a9ec..0dc9ff883c 100644 --- a/esphome/components/json/json_util.h +++ b/esphome/components/json/json_util.h @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include "esphome/core/defines.h" @@ -14,6 +16,108 @@ namespace esphome { namespace json { +/// Buffer for JSON serialization that uses stack allocation for small payloads. +/// Template parameter STACK_SIZE specifies the stack buffer size (default 512 bytes). +/// Supports move semantics for efficient return-by-value. +template class SerializationBuffer { + public: + static constexpr size_t BUFFER_SIZE = STACK_SIZE; ///< Stack buffer size for this instantiation + + /// Construct with known size (typically from measureJson) + explicit SerializationBuffer(size_t size) : size_(size) { + if (size + 1 <= STACK_SIZE) { + buffer_ = stack_buffer_; + } else { + heap_buffer_ = new char[size + 1]; + buffer_ = heap_buffer_; + } + buffer_[0] = '\0'; + } + + ~SerializationBuffer() { delete[] heap_buffer_; } + + // Move constructor - works with same template instantiation + SerializationBuffer(SerializationBuffer &&other) noexcept : heap_buffer_(other.heap_buffer_), size_(other.size_) { + if (other.buffer_ == other.stack_buffer_) { + // Stack buffer - must copy content + std::memcpy(stack_buffer_, other.stack_buffer_, size_ + 1); + buffer_ = stack_buffer_; + } else { + // Heap buffer - steal ownership + buffer_ = heap_buffer_; + other.heap_buffer_ = nullptr; + } + // Leave moved-from object in valid empty state + other.stack_buffer_[0] = '\0'; + other.buffer_ = other.stack_buffer_; + other.size_ = 0; + } + + // Move assignment + SerializationBuffer &operator=(SerializationBuffer &&other) noexcept { + if (this != &other) { + delete[] heap_buffer_; + heap_buffer_ = other.heap_buffer_; + size_ = other.size_; + if (other.buffer_ == other.stack_buffer_) { + std::memcpy(stack_buffer_, other.stack_buffer_, size_ + 1); + buffer_ = stack_buffer_; + } else { + buffer_ = heap_buffer_; + other.heap_buffer_ = nullptr; + } + // Leave moved-from object in valid empty state + other.stack_buffer_[0] = '\0'; + other.buffer_ = other.stack_buffer_; + other.size_ = 0; + } + return *this; + } + + // Delete copy operations + SerializationBuffer(const SerializationBuffer &) = delete; + SerializationBuffer &operator=(const SerializationBuffer &) = delete; + + /// Get null-terminated C string + const char *c_str() const { return buffer_; } + /// Get data pointer + const char *data() const { return buffer_; } + /// Get string length (excluding null terminator) + size_t size() const { return size_; } + + /// Implicit conversion to std::string for backward compatibility + /// WARNING: This allocates a new std::string on the heap. Prefer using + /// c_str() or data()/size() directly when possible to avoid allocation. + operator std::string() const { return std::string(buffer_, size_); } // NOLINT(google-explicit-constructor) + + private: + friend class JsonBuilder; ///< Allows JsonBuilder::serialize() to call private methods + + /// Get writable buffer (for serialization) + char *data_writable_() { return buffer_; } + /// Set actual size after serialization (must not exceed allocated size) + /// Also ensures null termination for c_str() safety + void set_size_(size_t size) { + size_ = size; + buffer_[size] = '\0'; + } + + /// Reallocate to heap buffer with new size (for when stack buffer is too small) + /// This invalidates any previous buffer content. Used by JsonBuilder::serialize(). + void reallocate_heap_(size_t size) { + delete[] heap_buffer_; + heap_buffer_ = new char[size + 1]; + buffer_ = heap_buffer_; + size_ = size; + buffer_[0] = '\0'; + } + + char stack_buffer_[STACK_SIZE]; + char *heap_buffer_{nullptr}; + char *buffer_; + size_t size_; +}; + #ifdef USE_PSRAM // Build an allocator for the JSON Library using the RAMAllocator class // This is only compiled when PSRAM is enabled @@ -47,7 +151,8 @@ using json_parse_t = std::function; using json_build_t = std::function; /// Build a JSON string with the provided json build function. -std::string build_json(const json_build_t &f); +/// Returns SerializationBuffer for stack-first allocation; implicitly converts to std::string. +SerializationBuffer<> build_json(const json_build_t &f); /// Parse a JSON string and run the provided json parse function if it's valid. bool parse_json(const std::string &data, const json_parse_t &f); @@ -72,7 +177,9 @@ class JsonBuilder { return root_; } - std::string serialize(); + /// Serialize the JSON document to a SerializationBuffer (stack-first allocation) + /// Uses 512-byte stack buffer by default, falls back to heap for larger JSON + SerializationBuffer<> serialize(); private: #ifdef USE_PSRAM diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 9905b4677e..c433804dd9 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -543,8 +543,8 @@ bool MQTTClientComponent::publish(const char *topic, const char *payload, size_t } bool MQTTClientComponent::publish_json(const char *topic, const json::json_build_t &f, uint8_t qos, bool retain) { - std::string message = json::build_json(f); - return this->publish(topic, message.c_str(), message.length(), qos, retain); + auto message = json::build_json(f); + return this->publish(topic, message.c_str(), message.size(), qos, retain); } void MQTTClientComponent::enable() { diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index f352e5896c..4b572417c1 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -214,7 +214,7 @@ DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_gener void DeferredUpdateEventSource::process_deferred_queue_() { while (!deferred_queue_.empty()) { DeferredEvent &de = deferred_queue_.front(); - std::string message = de.message_generator_(web_server_, de.source_); + auto message = de.message_generator_(web_server_, de.source_); if (this->send(message.c_str(), "state") != DISCARDED) { // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen deferred_queue_.erase(deferred_queue_.begin()); @@ -271,7 +271,7 @@ void DeferredUpdateEventSource::deferrable_send_state(void *source, const char * // deferred queue still not empty which means downstream event queue full, no point trying to send first deq_push_back_with_dedup_(source, message_generator); } else { - std::string message = message_generator(web_server_, source); + auto message = message_generator(web_server_, source); if (this->send(message.c_str(), "state") == DISCARDED) { deq_push_back_with_dedup_(source, message_generator); } else { @@ -325,7 +325,7 @@ void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource ws->defer([ws, source]() { // Configure reconnect timeout and send config // this should always go through since the AsyncEventSourceClient event queue is empty on connect - std::string message = ws->get_config_json(); + auto message = ws->get_config_json(); source->try_send_nodefer(message.c_str(), "ping", millis(), 30000); #ifdef USE_WEBSERVER_SORTING @@ -334,10 +334,10 @@ void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource JsonObject root = builder.root(); root[ESPHOME_F("name")] = group.second.name; root[ESPHOME_F("sorting_weight")] = group.second.weight; - message = builder.serialize(); + auto group_msg = builder.serialize(); // up to 31 groups should be able to be queued initially without defer - source->try_send_nodefer(message.c_str(), "sorting_group"); + source->try_send_nodefer(group_msg.c_str(), "sorting_group"); } #endif @@ -370,7 +370,7 @@ void WebServer::set_css_include(const char *css_include) { this->css_include_ = void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_include; } #endif -std::string WebServer::get_config_json() { +json::SerializationBuffer<> WebServer::get_config_json() { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -606,20 +606,20 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM // Note: request->method() is always HTTP_GET here (canHandle ensures this) if (entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->sensor_json_(obj, obj->state, detail); + auto data = this->sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } } request->send(404); } -std::string WebServer::sensor_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::sensor_state_json_generator(WebServer *web_server, void *source) { return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_STATE); } -std::string WebServer::sensor_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::sensor_all_json_generator(WebServer *web_server, void *source) { return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_ALL); } -std::string WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -653,23 +653,23 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const // Note: request->method() is always HTTP_GET here (canHandle ensures this) if (entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->text_sensor_json_(obj, obj->state, detail); + auto data = this->text_sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } } request->send(404); } -std::string WebServer::text_sensor_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::text_sensor_state_json_generator(WebServer *web_server, void *source) { return web_server->text_sensor_json_((text_sensor::TextSensor *) (source), ((text_sensor::TextSensor *) (source))->state, DETAIL_STATE); } -std::string WebServer::text_sensor_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::text_sensor_all_json_generator(WebServer *web_server, void *source) { return web_server->text_sensor_json_((text_sensor::TextSensor *) (source), ((text_sensor::TextSensor *) (source))->state, DETAIL_ALL); } -std::string WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, - JsonDetail start_config) { +json::SerializationBuffer<> WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, + JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -714,7 +714,7 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->switch_json_(obj, obj->state, detail); + auto data = this->switch_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -739,13 +739,13 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } -std::string WebServer::switch_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::switch_state_json_generator(WebServer *web_server, void *source) { return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_STATE); } -std::string WebServer::switch_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::switch_all_json_generator(WebServer *web_server, void *source) { return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_ALL); } -std::string WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -767,7 +767,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM continue; if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->button_json_(obj, detail); + auto data = this->button_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals(ESPHOME_F("press"))) { DEFER_ACTION(obj, obj->press()); @@ -780,10 +780,10 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } -std::string WebServer::button_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::button_all_json_generator(WebServer *web_server, void *source) { return web_server->button_json_((button::Button *) (source), DETAIL_ALL); } -std::string WebServer::button_json_(button::Button *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::button_json_(button::Button *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -810,22 +810,23 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con // Note: request->method() is always HTTP_GET here (canHandle ensures this) if (entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->binary_sensor_json_(obj, obj->state, detail); + auto data = this->binary_sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } } request->send(404); } -std::string WebServer::binary_sensor_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::binary_sensor_state_json_generator(WebServer *web_server, void *source) { return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source), ((binary_sensor::BinarySensor *) (source))->state, DETAIL_STATE); } -std::string WebServer::binary_sensor_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::binary_sensor_all_json_generator(WebServer *web_server, void *source) { return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source), ((binary_sensor::BinarySensor *) (source))->state, DETAIL_ALL); } -std::string WebServer::binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, + JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -852,7 +853,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->fan_json_(obj, detail); + auto data = this->fan_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals(ESPHOME_F("toggle"))) { DEFER_ACTION(obj, obj->toggle().perform()); @@ -893,13 +894,13 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } request->send(404); } -std::string WebServer::fan_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::fan_state_json_generator(WebServer *web_server, void *source) { return web_server->fan_json_((fan::Fan *) (source), DETAIL_STATE); } -std::string WebServer::fan_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::fan_all_json_generator(WebServer *web_server, void *source) { return web_server->fan_json_((fan::Fan *) (source), DETAIL_ALL); } -std::string WebServer::fan_json_(fan::Fan *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::fan_json_(fan::Fan *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -933,7 +934,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->light_json_(obj, detail); + auto data = this->light_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals(ESPHOME_F("toggle"))) { DEFER_ACTION(obj, obj->toggle().perform()); @@ -972,13 +973,13 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa } request->send(404); } -std::string WebServer::light_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::light_state_json_generator(WebServer *web_server, void *source) { return web_server->light_json_((light::LightState *) (source), DETAIL_STATE); } -std::string WebServer::light_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::light_all_json_generator(WebServer *web_server, void *source) { return web_server->light_json_((light::LightState *) (source), DETAIL_ALL); } -std::string WebServer::light_json_(light::LightState *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::light_json_(light::LightState *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1012,7 +1013,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->cover_json_(obj, detail); + auto data = this->cover_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1060,13 +1061,13 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa } request->send(404); } -std::string WebServer::cover_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::cover_state_json_generator(WebServer *web_server, void *source) { return web_server->cover_json_((cover::Cover *) (source), DETAIL_STATE); } -std::string WebServer::cover_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::cover_all_json_generator(WebServer *web_server, void *source) { return web_server->cover_json_((cover::Cover *) (source), DETAIL_ALL); } -std::string WebServer::cover_json_(cover::Cover *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::cover_json_(cover::Cover *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1101,7 +1102,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->number_json_(obj, obj->state, detail); + auto data = this->number_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1120,13 +1121,13 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM request->send(404); } -std::string WebServer::number_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::number_state_json_generator(WebServer *web_server, void *source) { return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_STATE); } -std::string WebServer::number_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::number_all_json_generator(WebServer *web_server, void *source) { return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_ALL); } -std::string WebServer::number_json_(number::Number *obj, float value, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::number_json_(number::Number *obj, float value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1168,7 +1169,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat continue; if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->date_json_(obj, detail); + auto data = this->date_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1194,13 +1195,13 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat request->send(404); } -std::string WebServer::date_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::date_state_json_generator(WebServer *web_server, void *source) { return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_STATE); } -std::string WebServer::date_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::date_all_json_generator(WebServer *web_server, void *source) { return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_ALL); } -std::string WebServer::date_json_(datetime::DateEntity *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::date_json_(datetime::DateEntity *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1229,7 +1230,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat continue; if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->time_json_(obj, detail); + auto data = this->time_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1254,13 +1255,13 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat } request->send(404); } -std::string WebServer::time_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::time_state_json_generator(WebServer *web_server, void *source) { return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_STATE); } -std::string WebServer::time_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::time_all_json_generator(WebServer *web_server, void *source) { return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_ALL); } -std::string WebServer::time_json_(datetime::TimeEntity *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::time_json_(datetime::TimeEntity *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1289,7 +1290,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur continue; if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->datetime_json_(obj, detail); + auto data = this->datetime_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1314,13 +1315,13 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur } request->send(404); } -std::string WebServer::datetime_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::datetime_state_json_generator(WebServer *web_server, void *source) { return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_STATE); } -std::string WebServer::datetime_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::datetime_all_json_generator(WebServer *web_server, void *source) { return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_ALL); } -std::string WebServer::datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1351,7 +1352,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->text_json_(obj, obj->state, detail); + auto data = this->text_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1370,13 +1371,13 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat request->send(404); } -std::string WebServer::text_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::text_state_json_generator(WebServer *web_server, void *source) { return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_STATE); } -std::string WebServer::text_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::text_all_json_generator(WebServer *web_server, void *source) { return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_ALL); } -std::string WebServer::text_json_(text::Text *obj, const std::string &value, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::text_json_(text::Text *obj, const std::string &value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1408,7 +1409,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), detail); + auto data = this->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), detail); request->send(200, "application/json", data.c_str()); return; } @@ -1427,15 +1428,15 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } -std::string WebServer::select_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::select_state_json_generator(WebServer *web_server, void *source) { auto *obj = (select::Select *) (source); return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), DETAIL_STATE); } -std::string WebServer::select_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::select_all_json_generator(WebServer *web_server, void *source) { auto *obj = (select::Select *) (source); return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), DETAIL_ALL); } -std::string WebServer::select_json_(select::Select *obj, StringRef value, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::select_json_(select::Select *obj, StringRef value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1467,7 +1468,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->climate_json_(obj, detail); + auto data = this->climate_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1500,15 +1501,15 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url } request->send(404); } -std::string WebServer::climate_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::climate_state_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return web_server->climate_json_((climate::Climate *) (source), DETAIL_STATE); } -std::string WebServer::climate_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::climate_all_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return web_server->climate_json_((climate::Climate *) (source), DETAIL_ALL); } -std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::climate_json_(climate::Climate *obj, JsonDetail start_config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1641,7 +1642,7 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->lock_json_(obj, obj->state, detail); + auto data = this->lock_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1666,13 +1667,13 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat } request->send(404); } -std::string WebServer::lock_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::lock_state_json_generator(WebServer *web_server, void *source) { return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_STATE); } -std::string WebServer::lock_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::lock_all_json_generator(WebServer *web_server, void *source) { return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_ALL); } -std::string WebServer::lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1700,7 +1701,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->valve_json_(obj, detail); + auto data = this->valve_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1746,13 +1747,13 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa } request->send(404); } -std::string WebServer::valve_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::valve_state_json_generator(WebServer *web_server, void *source) { return web_server->valve_json_((valve::Valve *) (source), DETAIL_STATE); } -std::string WebServer::valve_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::valve_all_json_generator(WebServer *web_server, void *source) { return web_server->valve_json_((valve::Valve *) (source), DETAIL_ALL); } -std::string WebServer::valve_json_(valve::Valve *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::valve_json_(valve::Valve *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1785,7 +1786,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->alarm_control_panel_json_(obj, obj->get_state(), detail); + auto data = this->alarm_control_panel_json_(obj, obj->get_state(), detail); request->send(200, "application/json", data.c_str()); return; } @@ -1825,19 +1826,19 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques } request->send(404); } -std::string WebServer::alarm_control_panel_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::alarm_control_panel_state_json_generator(WebServer *web_server, void *source) { return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source), ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(), DETAIL_STATE); } -std::string WebServer::alarm_control_panel_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::alarm_control_panel_all_json_generator(WebServer *web_server, void *source) { return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source), ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(), DETAIL_ALL); } -std::string WebServer::alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj, - alarm_control_panel::AlarmControlPanelState value, - JsonDetail start_config) { +json::SerializationBuffer<> WebServer::alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj, + alarm_control_panel::AlarmControlPanelState value, + JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1866,7 +1867,7 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->water_heater_json_(obj, detail); + auto data = this->water_heater_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1902,14 +1903,14 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons request->send(404); } -std::string WebServer::water_heater_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::water_heater_state_json_generator(WebServer *web_server, void *source) { return web_server->water_heater_json_(static_cast(source), DETAIL_STATE); } -std::string WebServer::water_heater_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::water_heater_all_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return web_server->water_heater_json_(static_cast(source), DETAIL_ALL); } -std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); char buf[PSTR_LOCAL_SIZE]; @@ -1971,7 +1972,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->infrared_json_(obj, detail); + auto data = this->infrared_json_(obj, detail); request->send(200, ESPHOME_F("application/json"), data.c_str()); return; } @@ -2031,12 +2032,12 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur request->send(404); } -std::string WebServer::infrared_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::infrared_all_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return web_server->infrared_json_(static_cast(source), DETAIL_ALL); } -std::string WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -2071,7 +2072,7 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa // Note: request->method() is always HTTP_GET here (canHandle ensures this) if (entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->event_json_(obj, StringRef(), detail); + auto data = this->event_json_(obj, StringRef(), detail); request->send(200, "application/json", data.c_str()); return; } @@ -2081,16 +2082,16 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa static StringRef get_event_type(event::Event *event) { return event ? event->get_last_event_type() : StringRef(); } -std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::event_state_json_generator(WebServer *web_server, void *source) { auto *event = static_cast(source); return web_server->event_json_(event, get_event_type(event), DETAIL_STATE); } // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson -std::string WebServer::event_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::event_all_json_generator(WebServer *web_server, void *source) { auto *event = static_cast(source); return web_server->event_json_(event, get_event_type(event), DETAIL_ALL); } -std::string WebServer::event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -2124,7 +2125,7 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->update_json_(obj, detail); + auto data = this->update_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -2140,15 +2141,15 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } -std::string WebServer::update_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::update_state_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE); } -std::string WebServer::update_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::update_all_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE); } -std::string WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson json::JsonBuilder builder; JsonObject root = builder.root(); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 6afe618b59..76c1c8b0bd 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -2,6 +2,7 @@ #include "list_entities.h" +#include "esphome/components/json/json_util.h" #include "esphome/components/web_server_base/web_server_base.h" #ifdef USE_WEBSERVER #include "esphome/core/component.h" @@ -103,7 +104,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; can be forgotten. */ #if !defined(USE_ESP32) && defined(USE_ARDUINO) -using message_generator_t = std::string(WebServer *, void *); +using message_generator_t = json::SerializationBuffer<>(WebServer *, void *); class DeferredUpdateEventSourceList; class DeferredUpdateEventSource : public AsyncEventSource { @@ -257,7 +258,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { void handle_index_request(AsyncWebServerRequest *request); /// Return the webserver configuration as JSON. - std::string get_config_json(); + json::SerializationBuffer<> get_config_json(); #ifdef USE_WEBSERVER_CSS_INCLUDE /// Handle included css request under '/0.css'. @@ -279,8 +280,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a sensor request under '/sensor/'. void handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string sensor_state_json_generator(WebServer *web_server, void *source); - static std::string sensor_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> sensor_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> sensor_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_SWITCH @@ -289,8 +290,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a switch request under '/switch//'. void handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string switch_state_json_generator(WebServer *web_server, void *source); - static std::string switch_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> switch_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> switch_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_BUTTON @@ -298,7 +299,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { void handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match); // Buttons are stateless, so there is no button_state_json_generator - static std::string button_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> button_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_BINARY_SENSOR @@ -307,8 +308,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a binary sensor request under '/binary_sensor/'. void handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string binary_sensor_state_json_generator(WebServer *web_server, void *source); - static std::string binary_sensor_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> binary_sensor_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> binary_sensor_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_FAN @@ -317,8 +318,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a fan request under '/fan//'. void handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string fan_state_json_generator(WebServer *web_server, void *source); - static std::string fan_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> fan_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> fan_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_LIGHT @@ -327,8 +328,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a light request under '/light//'. void handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string light_state_json_generator(WebServer *web_server, void *source); - static std::string light_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> light_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> light_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_TEXT_SENSOR @@ -337,8 +338,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a text sensor request under '/text_sensor/'. void handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string text_sensor_state_json_generator(WebServer *web_server, void *source); - static std::string text_sensor_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> text_sensor_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> text_sensor_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_COVER @@ -347,8 +348,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a cover request under '/cover//'. void handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string cover_state_json_generator(WebServer *web_server, void *source); - static std::string cover_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> cover_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> cover_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_NUMBER @@ -356,8 +357,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a number request under '/number/'. void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string number_state_json_generator(WebServer *web_server, void *source); - static std::string number_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> number_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> number_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_DATETIME_DATE @@ -365,8 +366,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a date request under '/date/'. void handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string date_state_json_generator(WebServer *web_server, void *source); - static std::string date_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> date_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> date_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_DATETIME_TIME @@ -374,8 +375,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a time request under '/time/'. void handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string time_state_json_generator(WebServer *web_server, void *source); - static std::string time_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> time_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> time_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_DATETIME_DATETIME @@ -383,8 +384,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a datetime request under '/datetime/'. void handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string datetime_state_json_generator(WebServer *web_server, void *source); - static std::string datetime_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> datetime_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> datetime_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_TEXT @@ -392,8 +393,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a text input request under '/text/'. void handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string text_state_json_generator(WebServer *web_server, void *source); - static std::string text_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> text_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> text_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_SELECT @@ -401,8 +402,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a select request under '/select/'. void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string select_state_json_generator(WebServer *web_server, void *source); - static std::string select_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> select_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> select_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_CLIMATE @@ -410,8 +411,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a climate request under '/climate/'. void handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string climate_state_json_generator(WebServer *web_server, void *source); - static std::string climate_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> climate_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> climate_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_LOCK @@ -420,8 +421,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a lock request under '/lock//'. void handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string lock_state_json_generator(WebServer *web_server, void *source); - static std::string lock_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> lock_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> lock_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_VALVE @@ -430,8 +431,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a valve request under '/valve//'. void handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string valve_state_json_generator(WebServer *web_server, void *source); - static std::string valve_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> valve_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> valve_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_ALARM_CONTROL_PANEL @@ -440,8 +441,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a alarm_control_panel request under '/alarm_control_panel/'. void handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string alarm_control_panel_state_json_generator(WebServer *web_server, void *source); - static std::string alarm_control_panel_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> alarm_control_panel_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> alarm_control_panel_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_WATER_HEATER @@ -450,22 +451,22 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a water_heater request under '/water_heater//'. void handle_water_heater_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string water_heater_state_json_generator(WebServer *web_server, void *source); - static std::string water_heater_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> water_heater_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> water_heater_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_INFRARED /// Handle an infrared request under '/infrared//transmit'. void handle_infrared_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string infrared_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> infrared_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_EVENT void on_event(event::Event *obj) override; - static std::string event_state_json_generator(WebServer *web_server, void *source); - static std::string event_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> event_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> event_all_json_generator(WebServer *web_server, void *source); /// Handle a event request under '/event'. void handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -477,8 +478,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a update request under '/update/'. void handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string update_state_json_generator(WebServer *web_server, void *source); - static std::string update_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> update_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> update_all_json_generator(WebServer *web_server, void *source); #endif /// Override the web handler's canHandle method. @@ -586,71 +587,74 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { private: #ifdef USE_SENSOR - std::string sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config); + json::SerializationBuffer<> sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config); #endif #ifdef USE_SWITCH - std::string switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config); + json::SerializationBuffer<> switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config); #endif #ifdef USE_BUTTON - std::string button_json_(button::Button *obj, JsonDetail start_config); + json::SerializationBuffer<> button_json_(button::Button *obj, JsonDetail start_config); #endif #ifdef USE_BINARY_SENSOR - std::string binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config); + json::SerializationBuffer<> binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, + JsonDetail start_config); #endif #ifdef USE_FAN - std::string fan_json_(fan::Fan *obj, JsonDetail start_config); + json::SerializationBuffer<> fan_json_(fan::Fan *obj, JsonDetail start_config); #endif #ifdef USE_LIGHT - std::string light_json_(light::LightState *obj, JsonDetail start_config); + json::SerializationBuffer<> light_json_(light::LightState *obj, JsonDetail start_config); #endif #ifdef USE_TEXT_SENSOR - std::string text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, JsonDetail start_config); + json::SerializationBuffer<> text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, + JsonDetail start_config); #endif #ifdef USE_COVER - std::string cover_json_(cover::Cover *obj, JsonDetail start_config); + json::SerializationBuffer<> cover_json_(cover::Cover *obj, JsonDetail start_config); #endif #ifdef USE_NUMBER - std::string number_json_(number::Number *obj, float value, JsonDetail start_config); + json::SerializationBuffer<> number_json_(number::Number *obj, float value, JsonDetail start_config); #endif #ifdef USE_DATETIME_DATE - std::string date_json_(datetime::DateEntity *obj, JsonDetail start_config); + json::SerializationBuffer<> date_json_(datetime::DateEntity *obj, JsonDetail start_config); #endif #ifdef USE_DATETIME_TIME - std::string time_json_(datetime::TimeEntity *obj, JsonDetail start_config); + json::SerializationBuffer<> time_json_(datetime::TimeEntity *obj, JsonDetail start_config); #endif #ifdef USE_DATETIME_DATETIME - std::string datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config); + json::SerializationBuffer<> datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config); #endif #ifdef USE_TEXT - std::string text_json_(text::Text *obj, const std::string &value, JsonDetail start_config); + json::SerializationBuffer<> text_json_(text::Text *obj, const std::string &value, JsonDetail start_config); #endif #ifdef USE_SELECT - std::string select_json_(select::Select *obj, StringRef value, JsonDetail start_config); + json::SerializationBuffer<> select_json_(select::Select *obj, StringRef value, JsonDetail start_config); #endif #ifdef USE_CLIMATE - std::string climate_json_(climate::Climate *obj, JsonDetail start_config); + json::SerializationBuffer<> climate_json_(climate::Climate *obj, JsonDetail start_config); #endif #ifdef USE_LOCK - std::string lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config); + json::SerializationBuffer<> lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config); #endif #ifdef USE_VALVE - std::string valve_json_(valve::Valve *obj, JsonDetail start_config); + json::SerializationBuffer<> valve_json_(valve::Valve *obj, JsonDetail start_config); #endif #ifdef USE_ALARM_CONTROL_PANEL - std::string alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj, - alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config); + json::SerializationBuffer<> alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj, + alarm_control_panel::AlarmControlPanelState value, + JsonDetail start_config); #endif #ifdef USE_EVENT - std::string event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config); + json::SerializationBuffer<> event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config); #endif #ifdef USE_WATER_HEATER - std::string water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config); + json::SerializationBuffer<> water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config); #endif #ifdef USE_INFRARED - std::string infrared_json_(infrared::Infrared *obj, JsonDetail start_config); + json::SerializationBuffer<> infrared_json_(infrared::Infrared *obj, JsonDetail start_config); #endif #ifdef USE_UPDATE - std::string update_json_(update::UpdateEntity *obj, JsonDetail start_config); + json::SerializationBuffer<> update_json_(update::UpdateEntity *obj, JsonDetail start_config); #endif }; diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 1798159e7f..4034a22586 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -563,7 +563,7 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * // Configure reconnect timeout and send config // this should always go through since the tcp send buffer is empty on connect - std::string message = ws->get_config_json(); + auto message = ws->get_config_json(); this->try_send_nodefer(message.c_str(), "ping", millis(), 30000); #ifdef USE_WEBSERVER_SORTING @@ -617,7 +617,7 @@ void AsyncEventSourceResponse::deq_push_back_with_dedup_(void *source, message_g void AsyncEventSourceResponse::process_deferred_queue_() { while (!deferred_queue_.empty()) { DeferredEvent &de = deferred_queue_.front(); - std::string message = de.message_generator_(web_server_, de.source_); + auto message = de.message_generator_(web_server_, de.source_); if (this->try_send_nodefer(message.c_str(), "state")) { // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen deferred_queue_.erase(deferred_queue_.begin()); @@ -854,7 +854,7 @@ void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *e // trying to send first deq_push_back_with_dedup_(source, message_generator); } else { - std::string message = message_generator(web_server_, source); + auto message = message_generator(web_server_, source); if (!this->try_send_nodefer(message.c_str(), "state")) { deq_push_back_with_dedup_(source, message_generator); } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 74601ffda8..76ddfa35fd 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -16,6 +16,7 @@ #include #ifdef USE_WEBSERVER +#include "esphome/components/json/json_util.h" #include "esphome/components/web_server/list_entities.h" #endif @@ -250,7 +251,7 @@ class AsyncWebHandler { class AsyncEventSource; class AsyncEventSourceResponse; -using message_generator_t = std::string(esphome::web_server::WebServer *, void *); +using message_generator_t = json::SerializationBuffer<>(esphome::web_server::WebServer *, void *); /* This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function diff --git a/tests/components/json/common.yaml b/tests/components/json/common.yaml index c36c7f2a5a..c4bf6c3831 100644 --- a/tests/components/json/common.yaml +++ b/tests/components/json/common.yaml @@ -4,15 +4,16 @@ interval: - interval: 60s then: - lambda: |- - // Test build_json - std::string json_str = esphome::json::build_json([](JsonObject root) { + // Test build_json - returns SerializationBuffer, use auto to avoid heap allocation + auto json_buf = esphome::json::build_json([](JsonObject root) { root["sensor"] = "temperature"; root["value"] = 23.5; root["unit"] = "°C"; }); - ESP_LOGD("test", "Built JSON: %s", json_str.c_str()); + ESP_LOGD("test", "Built JSON: %s", json_buf.c_str()); - // Test parse_json + // Test parse_json - implicit conversion to std::string for backward compatibility + std::string json_str = json_buf; bool parse_ok = esphome::json::parse_json(json_str, [](JsonObject root) { if (root["sensor"].is() && root["value"].is()) { const char* sensor = root["sensor"]; @@ -26,10 +27,10 @@ interval: }); ESP_LOGD("test", "Parse result (JSON syntax only): %s", parse_ok ? "success" : "failed"); - // Test JsonBuilder class + // Test JsonBuilder class - returns SerializationBuffer esphome::json::JsonBuilder builder; JsonObject obj = builder.root(); obj["test"] = "direct_builder"; obj["count"] = 42; - std::string result = builder.serialize(); + auto result = builder.serialize(); ESP_LOGD("test", "JsonBuilder result: %s", result.c_str()); From 17a810b939ba7ab0019fa65149e2db2622d2466c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:14:48 -0500 Subject: [PATCH 096/609] [wifi] Sync output_power with PHY max TX power to prevent brownout (#14118) Co-authored-by: Claude Opus 4.6 --- esphome/components/wifi/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index e865de8663..afceec6c54 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -1,4 +1,5 @@ import logging +import math from esphome import automation from esphome.automation import Condition @@ -493,6 +494,13 @@ async def to_code(config): cg.add(var.set_passive_scan(True)) if CONF_OUTPUT_POWER in config: cg.add(var.set_output_power(config[CONF_OUTPUT_POWER])) + if CORE.is_esp32: + # Set PHY max TX power to match output_power so calibration also uses + # reduced power. This prevents brownout during PHY init on marginal + # power supplies, which is critical for OTA updates with rollback enabled. + # Kconfig range is 10-20, ESPHome allows 8.5-20.5 + phy_tx_power = max(10, min(20, math.ceil(config[CONF_OUTPUT_POWER]))) + add_idf_sdkconfig_option("CONFIG_ESP_PHY_MAX_WIFI_TX_POWER", phy_tx_power) # enable_on_boot defaults to true in C++ - only set if false if not config[CONF_ENABLE_ON_BOOT]: cg.add(var.set_enable_on_boot(False)) From cceb109303f16a8723b11e3e9bf9a26f985be197 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 17:48:18 -0600 Subject: [PATCH 097/609] [uart] Always call pin setup for UART0 default pins on ESP-IDF (#14130) --- .../uart/uart_component_esp_idf.cpp | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 6c242220a6..ea7a09fee6 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -19,6 +19,13 @@ namespace esphome::uart { static const char *const TAG = "uart.idf"; +/// Check if a pin number matches one of the default UART0 GPIO pins. +/// These pins may have residual state from the boot console that requires +/// explicit reset before UART reconfiguration (ESP-IDF issue #17459). +static constexpr bool is_default_uart0_pin(int8_t pin_num) { + return pin_num == U0TXD_GPIO_NUM || pin_num == U0RXD_GPIO_NUM; +} + uart_config_t IDFUARTComponent::get_config_() { uart_parity_t parity = UART_PARITY_DISABLE; if (this->parity_ == UART_CONFIG_PARITY_EVEN) { @@ -150,20 +157,26 @@ void IDFUARTComponent::load_settings(bool dump_config) { // Commit 9ed617fb17 removed gpio_func_sel() calls from uart_set_pin(), which breaks // UART on default UART0 pins that may have residual state from boot console. // Reset these pins before configuring UART to ensure they're in a clean state. - if (tx == U0TXD_GPIO_NUM || tx == U0RXD_GPIO_NUM) { + if (is_default_uart0_pin(tx)) { gpio_reset_pin(static_cast(tx)); } - if (rx == U0TXD_GPIO_NUM || rx == U0RXD_GPIO_NUM) { + if (is_default_uart0_pin(rx)) { gpio_reset_pin(static_cast(rx)); } - // Setup pins after reset to preserve open drain/pullup/pulldown flags + // Setup pins after reset to configure GPIO direction and pull resistors. + // For UART0 default pins, setup() must always be called because gpio_reset_pin() + // above sets GPIO_MODE_DISABLE which disables the input buffer. Without setup(), + // uart_set_pin() on ESP-IDF 5.4.2+ does not re-enable the input buffer for + // IOMUX-connected pins, so the RX pin cannot receive data (see issue #10132). + // For other pins, only call setup() if pull or open-drain flags are set to avoid + // disturbing the default pin state which breaks some external components (#11823). auto setup_pin_if_needed = [](InternalGPIOPin *pin) { if (!pin) { return; } const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN; - if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { + if (is_default_uart0_pin(pin->get_pin()) || (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { pin->setup(); } }; From 1b4de55efda47b59e1572f432ef40b932311da38 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:12:37 -0500 Subject: [PATCH 098/609] [pulse_counter] Fix PCNT glitch filter calculation off by 1000x (#14132) Co-authored-by: Claude Opus 4.6 --- esphome/components/pulse_counter/pulse_counter_sensor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index ef4cc980f6..5e62c0a410 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -117,7 +117,7 @@ bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { } if (this->filter_us != 0) { - uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / (uint32_t) esp_clk_apb_freq(); + uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000u / ((uint32_t) esp_clk_apb_freq() / 1000000u); pcnt_glitch_filter_config_t filter_config = { .max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns), }; From d29288547ec2d5b9e68b61c2186522efaad041b7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 18:54:33 -0600 Subject: [PATCH 099/609] [core] Use constexpr for PROGMEM arrays (#14127) Co-authored-by: Claude Opus 4.6 --- esphome/cpp_generator.py | 2 +- tests/unit_tests/test_cpp_generator.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 83f2d6cf81..fe666bdd6e 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -424,7 +424,7 @@ class ProgmemAssignmentExpression(AssignmentExpression): super().__init__(type_, "", name, rhs) def __str__(self): - return f"static const {self.type} {self.name}[] PROGMEM = {self.rhs}" + return f"static constexpr {self.type} {self.name}[] PROGMEM = {self.rhs}" class StaticConstAssignmentExpression(AssignmentExpression): diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 8755e6e2a1..049d21027f 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -325,7 +325,7 @@ class TestStatements: ), ( cg.ProgmemAssignmentExpression(ct.uint16, "foo", "bar"), - 'static const uint16_t foo[] PROGMEM = "bar"', + 'static constexpr uint16_t foo[] PROGMEM = "bar"', ), ), ) From 94712b3961dd962d0a43636301c5edccd08121b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 18:54:44 -0600 Subject: [PATCH 100/609] [esp8266][web_server] Use constexpr for PROGMEM arrays in codegen (#14128) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp8266/gpio.py | 4 ++-- esphome/components/web_server/__init__.py | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index 2e8d6496bc..43508afaf9 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -202,11 +202,11 @@ async def add_pin_initial_states_array(): cg.add_global( cg.RawExpression( - f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16] PROGMEM = {{{initial_modes_s}}}" + f"constexpr uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16] PROGMEM = {{{initial_modes_s}}}" ) ) cg.add_global( cg.RawExpression( - f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16] PROGMEM = {{{initial_levels_s}}}" + f"constexpr uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16] PROGMEM = {{{initial_levels_s}}}" ) ) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 294a5e0a15..9305a2de61 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -280,10 +280,8 @@ def add_resource_as_progmem( content_encoded = gzip.compress(content_encoded) content_encoded_size = len(content_encoded) bytes_as_int = ", ".join(str(x) for x in content_encoded) - uint8_t = f"const uint8_t ESPHOME_WEBSERVER_{resource_name}[{content_encoded_size}] PROGMEM = {{{bytes_as_int}}}" - size_t = ( - f"const size_t ESPHOME_WEBSERVER_{resource_name}_SIZE = {content_encoded_size}" - ) + uint8_t = f"constexpr uint8_t ESPHOME_WEBSERVER_{resource_name}[{content_encoded_size}] PROGMEM = {{{bytes_as_int}}}" + size_t = f"constexpr size_t ESPHOME_WEBSERVER_{resource_name}_SIZE = {content_encoded_size}" cg.add_global(cg.RawExpression(uint8_t)) cg.add_global(cg.RawExpression(size_t)) From c1265a9490711813a6036b8a772dd36e3e871089 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 18:54:57 -0600 Subject: [PATCH 101/609] [core] Use constexpr for hand-written PROGMEM arrays in C++ (#14129) Co-authored-by: Claude Opus 4.6 --- .../components/api/api_frame_helper_noise.cpp | 2 +- esphome/components/bme680/bme680.cpp | 8 +++--- .../components/captive_portal/captive_index.h | 4 +-- esphome/components/display/display.cpp | 6 ++-- esphome/components/ili9xxx/ili9xxx_init.h | 28 +++++++++---------- esphome/components/max7219/max7219.cpp | 2 +- esphome/components/max7219digit/max7219font.h | 2 +- .../components/ssd1306_base/ssd1306_base.cpp | 2 +- esphome/components/st7735/st7735.cpp | 2 +- esphome/components/tm1621/tm1621.cpp | 2 +- esphome/components/tm1637/tm1637.cpp | 2 +- esphome/components/tm1638/sevenseg.h | 2 +- .../components/web_server/server_index_v2.h | 4 +-- .../components/web_server/server_index_v3.h | 4 +-- 14 files changed, 35 insertions(+), 35 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 492988128a..2aad732f7f 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -19,7 +19,7 @@ namespace esphome::api { static const char *const TAG = "api.noise"; #ifdef USE_ESP8266 -static const char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit"; +static constexpr char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit"; #else static const char *const PROLOGUE_INIT = "NoiseAPIInit"; #endif diff --git a/esphome/components/bme680/bme680.cpp b/esphome/components/bme680/bme680.cpp index 5e52c84b3d..e3cd80de00 100644 --- a/esphome/components/bme680/bme680.cpp +++ b/esphome/components/bme680/bme680.cpp @@ -22,11 +22,11 @@ static const uint8_t BME680_REGISTER_CHIPID = 0xD0; static const uint8_t BME680_REGISTER_FIELD0 = 0x1D; -const float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, -0.8, - 0.0, 0.0, -0.2, -0.5, 0.0, -1.0, 0.0, 0.0}; +constexpr float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, -0.8, + 0.0, 0.0, -0.2, -0.5, 0.0, -1.0, 0.0, 0.0}; -const float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.1, 0.7, 0.0, -0.8, - -0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; +constexpr float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.1, 0.7, 0.0, -0.8, + -0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; [[maybe_unused]] static const char *oversampling_to_str(BME680Oversampling oversampling) { switch (oversampling) { diff --git a/esphome/components/captive_portal/captive_index.h b/esphome/components/captive_portal/captive_index.h index 645ebb7a2f..a81edc1900 100644 --- a/esphome/components/captive_portal/captive_index.h +++ b/esphome/components/captive_portal/captive_index.h @@ -6,7 +6,7 @@ namespace esphome::captive_portal { #ifdef USE_CAPTIVE_PORTAL_GZIP -const uint8_t INDEX_GZ[] PROGMEM = { +constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7e, 0x05, 0x8f, 0x49, 0xbb, 0x52, 0xb3, 0x7a, 0x7a, 0xed, 0x6c, 0x24, 0x51, 0x45, 0x9a, 0xbb, 0xa2, 0x05, 0x9a, 0x36, 0xc0, 0x6e, 0x73, 0x1f, 0x82, 0x00, 0x4b, 0x53, 0x23, 0x8b, 0x31, 0x45, 0xea, 0x48, 0xca, 0x8f, 0x18, 0xbe, 0xdf, @@ -86,7 +86,7 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0xfc, 0xda, 0xd1, 0xf8, 0xe9, 0xa3, 0xe1, 0xa6, 0xfb, 0x1f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00}; #else // Brotli (default, smaller) -const uint8_t INDEX_BR[] PROGMEM = { +constexpr uint8_t INDEX_BR[] PROGMEM = { 0x1b, 0xf8, 0x0a, 0x00, 0x64, 0x5a, 0xd3, 0xfa, 0xe7, 0xf3, 0x62, 0xd8, 0x06, 0x1b, 0xe9, 0x6a, 0x8a, 0x81, 0x2b, 0xb5, 0x49, 0x14, 0x37, 0xdc, 0x9e, 0x1a, 0xcb, 0x56, 0x87, 0xfb, 0xff, 0xf7, 0x73, 0x75, 0x12, 0x0a, 0xd6, 0x48, 0x84, 0xc6, 0x21, 0xa4, 0x6d, 0xb5, 0x71, 0xef, 0x13, 0xbe, 0x4e, 0x54, 0xf1, 0x64, 0x8f, 0x3f, 0xcc, 0x9a, 0x78, diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 53a087803c..2bd7d03600 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -810,9 +810,9 @@ bool Display::clamp_y_(int y, int h, int &min_y, int &max_y) { return min_y < max_y; } -const uint8_t TESTCARD_FONT[3][8] PROGMEM = {{0x41, 0x7F, 0x7F, 0x09, 0x19, 0x7F, 0x66, 0x00}, // 'R' - {0x1C, 0x3E, 0x63, 0x41, 0x51, 0x73, 0x72, 0x00}, // 'G' - {0x41, 0x7F, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00}}; // 'B' +constexpr uint8_t TESTCARD_FONT[3][8] PROGMEM = {{0x41, 0x7F, 0x7F, 0x09, 0x19, 0x7F, 0x66, 0x00}, // 'R' + {0x1C, 0x3E, 0x63, 0x41, 0x51, 0x73, 0x72, 0x00}, // 'G' + {0x41, 0x7F, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00}}; // 'B' void Display::test_card() { int w = get_width(), h = get_height(), image_w, image_h; diff --git a/esphome/components/ili9xxx/ili9xxx_init.h b/esphome/components/ili9xxx/ili9xxx_init.h index 7b176ed57a..f0c6a94a65 100644 --- a/esphome/components/ili9xxx/ili9xxx_init.h +++ b/esphome/components/ili9xxx/ili9xxx_init.h @@ -7,7 +7,7 @@ namespace esphome { namespace ili9xxx { // clang-format off -static const uint8_t PROGMEM INITCMD_M5STACK[] = { +static constexpr uint8_t PROGMEM INITCMD_M5STACK[] = { 0xEF, 3, 0x03, 0x80, 0x02, 0xCF, 3, 0x00, 0xC1, 0x30, 0xED, 4, 0x64, 0x03, 0x12, 0x81, @@ -37,7 +37,7 @@ static const uint8_t PROGMEM INITCMD_M5STACK[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_M5CORE[] = { +static constexpr uint8_t PROGMEM INITCMD_M5CORE[] = { ILI9XXX_SETEXTC, 3, 0xFF,0x93,0x42, // Turn on the external command ILI9XXX_PWCTR1 , 2, 0x12, 0x12, ILI9XXX_PWCTR2 , 1, 0x03, @@ -56,7 +56,7 @@ static const uint8_t PROGMEM INITCMD_M5CORE[] = { -static const uint8_t PROGMEM INITCMD_ILI9341[] = { +static constexpr uint8_t PROGMEM INITCMD_ILI9341[] = { 0xEF, 3, 0x03, 0x80, 0x02, 0xCF, 3, 0x00, 0xC1, 0x30, 0xED, 4, 0x64, 0x03, 0x12, 0x81, @@ -86,7 +86,7 @@ static const uint8_t PROGMEM INITCMD_ILI9341[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_ILI9481[] = { +static constexpr uint8_t PROGMEM INITCMD_ILI9481[] = { ILI9XXX_SLPOUT , 0x80, // Exit sleep mode ILI9XXX_PWSET , 3, 0x07, 0x41, 0x1D, ILI9XXX_VMCTR , 3, 0x00, 0x1C, 0x1F, @@ -105,7 +105,7 @@ static const uint8_t PROGMEM INITCMD_ILI9481[] = { 0x00 // end }; -static const uint8_t PROGMEM INITCMD_ILI9481_18[] = { +static constexpr uint8_t PROGMEM INITCMD_ILI9481_18[] = { ILI9XXX_SLPOUT , 0x80, // Exit sleep mode ILI9XXX_PWSET , 3, 0x07, 0x41, 0x1D, ILI9XXX_VMCTR , 3, 0x00, 0x1C, 0x1F, @@ -124,7 +124,7 @@ static const uint8_t PROGMEM INITCMD_ILI9481_18[] = { 0x00 // end }; -static const uint8_t PROGMEM INITCMD_ILI9486[] = { +static constexpr uint8_t PROGMEM INITCMD_ILI9486[] = { ILI9XXX_SLPOUT, 0x80, ILI9XXX_PIXFMT, 1, 0x55, ILI9XXX_PWCTR3, 1, 0x44, @@ -173,7 +173,7 @@ static const uint8_t INITCMD_WAVESHARE_RES_3_5[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_ILI9488_A[] = { +static constexpr uint8_t PROGMEM INITCMD_ILI9488_A[] = { ILI9XXX_GMCTRP1,15, 0x00, 0x03, 0x09, 0x08, 0x16, 0x0A, 0x3F, 0x78, 0x4C, 0x09, 0x0A, 0x08, 0x16, 0x1A, 0x0F, ILI9XXX_GMCTRN1,15, 0x00, 0x16, 0x19, 0x03, 0x0F, 0x05, 0x32, 0x45, 0x46, 0x04, 0x0E, 0x0D, 0x35, 0x37, 0x0F, @@ -206,7 +206,7 @@ static const uint8_t PROGMEM INITCMD_ILI9488_A[] = { 0x00 // end }; -static const uint8_t PROGMEM INITCMD_ST7796[] = { +static constexpr uint8_t PROGMEM INITCMD_ST7796[] = { // This ST7796S initilization routine was copied from https://github.com/prenticedavid/Adafruit_ST7796S_kbv/blob/master/Adafruit_ST7796S_kbv.cpp ILI9XXX_SWRESET, 0x80, // Soft reset, then delay 150 ms ILI9XXX_CSCON, 1, 0xC3, // ?? Unlock Manufacturer @@ -226,7 +226,7 @@ static const uint8_t PROGMEM INITCMD_ST7796[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_S3BOX[] = { +static constexpr uint8_t PROGMEM INITCMD_S3BOX[] = { 0xEF, 3, 0x03, 0x80, 0x02, 0xCF, 3, 0x00, 0xC1, 0x30, 0xED, 4, 0x64, 0x03, 0x12, 0x81, @@ -256,7 +256,7 @@ static const uint8_t PROGMEM INITCMD_S3BOX[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_S3BOXLITE[] = { +static constexpr uint8_t PROGMEM INITCMD_S3BOXLITE[] = { 0xEF, 3, 0x03, 0x80, 0x02, 0xCF, 3, 0x00, 0xC1, 0x30, 0xED, 4, 0x64, 0x03, 0x12, 0x81, @@ -286,7 +286,7 @@ static const uint8_t PROGMEM INITCMD_S3BOXLITE[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_ST7789V[] = { +static constexpr uint8_t PROGMEM INITCMD_ST7789V[] = { ILI9XXX_SLPOUT , 0x80, // Exit Sleep ILI9XXX_DISPON , 0x80, // Display on ILI9XXX_MADCTL , 1, 0x08, // Memory Access Control, BGR @@ -313,7 +313,7 @@ static const uint8_t PROGMEM INITCMD_ST7789V[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_GC9A01A[] = { +static constexpr uint8_t PROGMEM INITCMD_GC9A01A[] = { 0xEF, 0, 0xEB, 1, 0x14, // ? 0xFE, 0, @@ -367,7 +367,7 @@ static const uint8_t PROGMEM INITCMD_GC9A01A[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_GC9D01N[] = { +static constexpr uint8_t PROGMEM INITCMD_GC9D01N[] = { // Enable Inter_command 0xFE, 0, // Inter Register Enable 1 (FEh) 0xEF, 0, // Inter Register Enable 2 (EFh) @@ -426,7 +426,7 @@ static const uint8_t PROGMEM INITCMD_GC9D01N[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_ST7735[] = { +static constexpr uint8_t PROGMEM INITCMD_ST7735[] = { ILI9XXX_SWRESET, 0, // Soft reset, then delay 10ms ILI9XXX_DELAY(10), ILI9XXX_SLPOUT , 0, // Exit Sleep, delay diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp index d701e6fc86..bec62ea005 100644 --- a/esphome/components/max7219/max7219.cpp +++ b/esphome/components/max7219/max7219.cpp @@ -15,7 +15,7 @@ static const uint8_t MAX7219_REGISTER_SHUTDOWN = 0x0C; static const uint8_t MAX7219_REGISTER_TEST = 0x0F; static const uint8_t MAX7219_UNKNOWN_CHAR = 0b11111111; -const uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = { +constexpr uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = { 0b00000000, // ' ', ord 0x20 0b10110000, // '!', ord 0x21 0b00100010, // '"', ord 0x22 diff --git a/esphome/components/max7219digit/max7219font.h b/esphome/components/max7219digit/max7219font.h index 22d64d1ecd..53674dc60f 100644 --- a/esphome/components/max7219digit/max7219font.h +++ b/esphome/components/max7219digit/max7219font.h @@ -7,7 +7,7 @@ namespace max7219digit { // bit patterns for the CP437 font -const uint8_t MAX7219_DOT_MATRIX_FONT[256][8] PROGMEM = { +constexpr uint8_t MAX7219_DOT_MATRIX_FONT[256][8] PROGMEM = { {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x00 {0x7E, 0x81, 0x95, 0xB1, 0xB1, 0x95, 0x81, 0x7E}, // 0x01 {0x7E, 0xFF, 0xEB, 0xCF, 0xCF, 0xEB, 0xFF, 0x7E}, // 0x02 diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index be99bd93da..5bd83ec8a8 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -49,7 +49,7 @@ struct ModelDimensions { uint8_t width; uint8_t height; }; -static const ModelDimensions MODEL_DIMS[] PROGMEM = { +static constexpr ModelDimensions MODEL_DIMS[] PROGMEM = { {128, 32}, // SSD1306_MODEL_128_32 {128, 64}, // SSD1306_MODEL_128_64 {96, 16}, // SSD1306_MODEL_96_16 diff --git a/esphome/components/st7735/st7735.cpp b/esphome/components/st7735/st7735.cpp index 1a74b5ce1e..58459b79bb 100644 --- a/esphome/components/st7735/st7735.cpp +++ b/esphome/components/st7735/st7735.cpp @@ -68,7 +68,7 @@ static const uint8_t ST7735_GMCTRP1 = 0xE0; static const uint8_t ST7735_GMCTRN1 = 0xE1; // clang-format off -static const uint8_t PROGMEM +static constexpr uint8_t PROGMEM BCMD[] = { // Init commands for 7735B screens 18, // 18 commands in list: ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, no args, w/delay diff --git a/esphome/components/tm1621/tm1621.cpp b/esphome/components/tm1621/tm1621.cpp index 6859973857..c82d306460 100644 --- a/esphome/components/tm1621/tm1621.cpp +++ b/esphome/components/tm1621/tm1621.cpp @@ -23,7 +23,7 @@ enum Tm1621Device { TM1621_USER, TM1621_POWR316D, TM1621_THR316D }; const uint8_t TM1621_COMMANDS[] = {TM1621_SYS_EN, TM1621_LCD_ON, TM1621_BIAS, TM1621_TIMER_DIS, TM1621_WDT_DIS, TM1621_TONE_OFF, TM1621_IRQ_DIS}; -const char TM1621_KCHAR[] PROGMEM = {"0|1|2|3|4|5|6|7|8|9|-| "}; +constexpr char TM1621_KCHAR[] PROGMEM = {"0|1|2|3|4|5|6|7|8|9|-| "}; // 0 1 2 3 4 5 6 7 8 9 - off const uint8_t TM1621_DIGIT_ROW[2][12] = {{0x5F, 0x50, 0x3D, 0x79, 0x72, 0x6B, 0x6F, 0x51, 0x7F, 0x7B, 0x20, 0x00}, {0xF5, 0x05, 0xB6, 0x97, 0x47, 0xD3, 0xF3, 0x85, 0xF7, 0xD7, 0x02, 0x00}}; diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index 49da01472f..f9c876f40c 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -27,7 +27,7 @@ const uint8_t TM1637_DATA_FIXED_ADDR = 0x04; //!< Fixed address // --- // D X // XABCDEFG -const uint8_t TM1637_ASCII_TO_RAW[] PROGMEM = { +constexpr uint8_t TM1637_ASCII_TO_RAW[] PROGMEM = { 0b00000000, // ' ', ord 0x20 0b10110000, // '!', ord 0x21 0b00100010, // '"', ord 0x22 diff --git a/esphome/components/tm1638/sevenseg.h b/esphome/components/tm1638/sevenseg.h index e20a55a69f..a4c16c7422 100644 --- a/esphome/components/tm1638/sevenseg.h +++ b/esphome/components/tm1638/sevenseg.h @@ -4,7 +4,7 @@ namespace esphome { namespace tm1638 { namespace TM1638Translation { -const unsigned char SEVEN_SEG[] PROGMEM = { +constexpr unsigned char SEVEN_SEG[] PROGMEM = { 0x00, /* (space) */ 0x86, /* ! */ 0x22, /* " */ diff --git a/esphome/components/web_server/server_index_v2.h b/esphome/components/web_server/server_index_v2.h index cc37db6a6b..b5dac9ae4c 100644 --- a/esphome/components/web_server/server_index_v2.h +++ b/esphome/components/web_server/server_index_v2.h @@ -9,7 +9,7 @@ namespace esphome::web_server { #ifdef USE_WEBSERVER_GZIP -const uint8_t INDEX_GZ[] PROGMEM = { +constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xed, 0x7d, 0xd9, 0x72, 0xdb, 0x48, 0xb6, 0xe0, 0xf3, 0xd4, 0x57, 0x40, 0x28, 0xb5, 0x8c, 0x2c, 0x26, 0xc1, 0x45, 0x92, 0x2d, 0x83, 0x4a, 0xb2, 0x65, 0xd9, 0xd5, 0x76, 0x97, 0xb7, 0xb6, 0xec, 0xda, 0x58, 0x6c, 0x09, 0x02, 0x92, 0x44, 0x96, 0x41, 0x80, 0x05, 0x24, 0xb5, 0x14, 0x89, @@ -697,7 +697,7 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0x56, 0x78, 0xff, 0xff, 0x01, 0xa2, 0x89, 0x8c, 0x0d, 0xc4, 0x97, 0x00, 0x00}; #else // Brotli (default, smaller) -const uint8_t INDEX_BR[] PROGMEM = { +constexpr uint8_t INDEX_BR[] PROGMEM = { 0x1b, 0xc3, 0x97, 0x11, 0x55, 0xb5, 0x65, 0x2c, 0x8a, 0x8a, 0x55, 0x0b, 0xd0, 0xba, 0x80, 0x1b, 0x32, 0xb0, 0x81, 0x4f, 0x27, 0x63, 0xf1, 0x7e, 0x88, 0xe3, 0xd8, 0x52, 0x84, 0x55, 0xe8, 0x35, 0x5b, 0x2b, 0x82, 0xe1, 0xed, 0x1f, 0xfd, 0xde, 0x63, 0x38, 0x3a, 0x71, 0x78, 0xb0, 0x42, 0x17, 0x15, 0x54, 0x23, 0xe1, 0xaa, 0x28, 0x11, 0x94, 0x23, diff --git a/esphome/components/web_server/server_index_v3.h b/esphome/components/web_server/server_index_v3.h index bd47071dce..1f61b19fb5 100644 --- a/esphome/components/web_server/server_index_v3.h +++ b/esphome/components/web_server/server_index_v3.h @@ -9,7 +9,7 @@ namespace esphome::web_server { #ifdef USE_WEBSERVER_GZIP -const uint8_t INDEX_GZ[] PROGMEM = { +constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xcc, 0xbd, 0x7b, 0x7f, 0x1a, 0xb9, 0xb2, 0x28, 0xfa, 0xf7, 0x3d, 0x9f, 0xc2, 0xee, 0x9d, 0xf1, 0xb4, 0x8c, 0x68, 0x03, 0x36, 0x8e, 0xd3, 0x58, 0xe6, 0xe4, 0x39, 0xc9, 0x3c, 0x92, 0x4c, 0x9c, 0x64, 0x26, 0xc3, 0xb0, 0x33, 0xa2, 0x11, 0xa0, 0xa4, 0x91, 0x98, 0x96, 0x88, 0xed, 0x01, @@ -4104,7 +4104,7 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0x37, 0x7a, 0x03, 0x00}; #else // Brotli (default, smaller) -const uint8_t INDEX_BR[] PROGMEM = { +constexpr uint8_t INDEX_BR[] PROGMEM = { 0x5b, 0x36, 0x7a, 0x53, 0xc2, 0x36, 0x06, 0x5a, 0x1f, 0xd4, 0x4e, 0x00, 0xb3, 0xd6, 0xea, 0xff, 0x0a, 0xab, 0x51, 0x94, 0xb1, 0xe6, 0xb0, 0x2e, 0x61, 0xbb, 0x1a, 0x70, 0x3b, 0xd8, 0x06, 0xfd, 0x7d, 0x2f, 0x1a, 0x00, 0x55, 0x35, 0xe3, 0xa8, 0x1c, 0x62, 0xca, 0xd3, 0xb4, 0x00, 0xdb, 0x5e, 0x43, 0xa7, 0x14, 0x08, 0xa4, 0x51, 0x99, 0x96, 0xb6, From afbc45bf32fe8146183d0d44fc16213cdebddbdb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 20:35:42 -0600 Subject: [PATCH 102/609] [e131] Drain all queued packets per loop iteration (#14133) --- esphome/components/e131/e131.cpp | 26 +++++--------------------- esphome/components/e131/e131.h | 9 +++++++++ 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index 941927122c..a7a695c167 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -70,27 +70,12 @@ void E131Component::loop() { E131Packet packet; int universe = 0; uint8_t buf[1460]; + ssize_t len; -#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) - ssize_t len = this->socket_->read(buf, sizeof(buf)); - if (len == -1) { - return; - } - - if (!this->packet_(buf, (size_t) len, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); - return; - } - - if (!this->process_(universe, packet)) { - ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); - } -#elif defined(USE_SOCKET_IMPL_LWIP_TCP) - while (auto packet_size = this->udp_.parsePacket()) { - auto len = this->udp_.read(buf, sizeof(buf)); - if (len <= 0) - continue; - + // Drain all queued packets so multi-universe frames are applied + // atomically before the light writes. Without this, each universe + // packet would trigger a separate full-strip write causing tearing. + while ((len = this->read_(buf, sizeof(buf))) > 0) { if (!this->packet_(buf, (size_t) len, universe, packet)) { ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); continue; @@ -100,7 +85,6 @@ void E131Component::loop() { ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); } } -#endif } void E131Component::add_effect(E131AddressableLightEffect *light_effect) { diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index fee447b678..8f0b808946 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -46,6 +46,15 @@ class E131Component : public esphome::Component { void set_method(E131ListenMethod listen_method) { this->listen_method_ = listen_method; } protected: + inline ssize_t read_(uint8_t *buf, size_t len) { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) + return this->socket_->read(buf, len); +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + if (!this->udp_.parsePacket()) + return -1; + return this->udp_.read(buf, len); +#endif + } bool packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet); bool process_(int universe, const E131Packet &packet); bool join_igmp_groups_(); From 7a2a149061c3b9480a3d1900f3e8cad1f1ad75ac Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:43:29 -0500 Subject: [PATCH 103/609] [esp32] Bump ESP-IDF to 5.5.3 (#14122) Co-authored-by: Claude Opus 4.6 --- .clang-tidy.hash | 2 +- esphome/components/esp32/__init__.py | 17 +++++++++++++---- esphome/components/esp32_ble/__init__.py | 15 +++++++++------ platformio.ini | 4 ++-- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index d6d401ee66..df584fa716 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -ce05c28e9dc0b12c4f6e7454986ffea5123ac974a949da841be698c535f2083e +3258307fa645ba77307e502075c02c4d710e92c48250839db3526d36a9655444 diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b1b3f0dc16..cdde1c4ed5 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -643,7 +643,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = { # These versions correspond to pioarduino/esp-idf releases # See: https://github.com/pioarduino/esp-idf/releases ARDUINO_IDF_VERSION_LOOKUP = { - cv.Version(3, 3, 7): cv.Version(5, 5, 2), + cv.Version(3, 3, 7): cv.Version(5, 5, 3), cv.Version(3, 3, 6): cv.Version(5, 5, 2), cv.Version(3, 3, 5): cv.Version(5, 5, 2), cv.Version(3, 3, 4): cv.Version(5, 5, 1), @@ -662,11 +662,12 @@ ARDUINO_IDF_VERSION_LOOKUP = { # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(5, 5, 2), - "latest": cv.Version(5, 5, 2), - "dev": cv.Version(5, 5, 2), + "recommended": cv.Version(5, 5, 3), + "latest": cv.Version(5, 5, 3), + "dev": cv.Version(5, 5, 3), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { + cv.Version(5, 5, 3): cv.Version(55, 3, 37), cv.Version(5, 5, 2): cv.Version(55, 3, 37), cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"), cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"), @@ -1471,6 +1472,14 @@ async def to_code(config): f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True ) + # ESP32-P4: ESP-IDF 5.5.3 changed the default of ESP32P4_SELECTS_REV_LESS_V3 + # from y to n. PlatformIO uses sections.ld.in (for rev <3) or + # sections.rev3.ld.in (for rev >=3) based on board definition. + # Set the sdkconfig option to match the board's revision. + if variant == VARIANT_ESP32P4: + is_rev3 = "_r3" in config[CONF_BOARD] + add_idf_sdkconfig_option("CONFIG_ESP32P4_SELECTS_REV_LESS_V3", not is_rev3) + # Set minimum chip revision for ESP32 variant # Setting this to 3.0 or higher reduces flash size by excluding workaround code, # and for PSRAM users saves significant IRAM by keeping C library functions in ROM. diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index d2020ada22..80fcf051b9 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -413,18 +413,21 @@ def final_validation(config): add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID", True) add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_BLUEDROID_HCI_VHCI", True) - # Check if BLE Server is needed - has_ble_server = "esp32_ble_server" in full_config - # Check if BLE Client is needed (via esp32_ble_tracker or esp32_ble_client) has_ble_client = ( "esp32_ble_tracker" in full_config or "esp32_ble_client" in full_config ) - # ESP-IDF BLE stack requires GATT Server to be enabled when GATT Client is enabled - # This is an internal dependency in the Bluedroid stack (tested ESP-IDF 5.4.2-5.5.1) + # Always enable GATTS: ESP-IDF 5.5.2.260206 has a bug in gatt_main.c where a + # GATT_TRACE_DEBUG references 'msg_len' outside the GATTS_INCLUDED/GATTC_INCLUDED + # guard, causing a compile error when both are disabled. + # Additionally, when GATT Client is enabled, GATT Server must also be enabled + # as an internal dependency in the Bluedroid stack. # See: https://github.com/espressif/esp-idf/issues/17724 - add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client) + # TODO: Revert to conditional once the gatt_main.c bug is fixed upstream: + # has_ble_server = "esp32_ble_server" in full_config + # add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client) + add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", True) add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client) # Handle max_connections: check for deprecated location in esp32_ble_tracker diff --git a/platformio.ini b/platformio.ini index 09b3d8722d..fdd6a36428 100644 --- a/platformio.ini +++ b/platformio.ini @@ -136,7 +136,7 @@ extends = common:arduino platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip platform_packages = pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.7/esp32-core-3.3.7.tar.xz - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3/esp-idf-v5.5.3.tar.xz framework = arduino, espidf ; Arduino as an ESP-IDF component lib_deps = @@ -171,7 +171,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script extends = common:idf platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip platform_packages = - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3/esp-idf-v5.5.3.tar.xz framework = espidf lib_deps = From b67b2cc3ab86ebbc7cfb3d0a893bf96bd50ef9a6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:56:20 -0500 Subject: [PATCH 104/609] [ld2450] Add frame header synchronization to fix initialization regression (#14135) Co-authored-by: Claude Opus 4.6 Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/ld2450/ld2450.cpp | 22 ++- esphome/components/ld2450/ld2450.h | 1 + tests/components/ld2450/common.h | 61 +++++++ tests/components/ld2450/ld2450_readline.cpp | 181 ++++++++++++++++++++ 4 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 tests/components/ld2450/common.h create mode 100644 tests/components/ld2450/ld2450_readline.cpp diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 1ea5c18271..2af45235a3 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -769,15 +769,33 @@ void LD2450Component::readline_(int readch) { return; // No data available } + // Frame header synchronization: verify first 4 bytes match a known frame header. + // This prevents the parser from accumulating mid-frame data after losing sync + // (e.g. after module restart or UART noise). + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { + const uint8_t byte = static_cast(readch); + // Verify header bytes match the frame type established by byte 0 + if (this->buffer_pos_ > 0) { + const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; + if (byte != expected[this->buffer_pos_]) { + this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame + } + } + // First byte must match start of a data or command frame header + if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { + return; + } + } + if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { - // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; + return; } - if (this->buffer_pos_ < 4) { + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { return; // Not enough data to process yet } if (this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[0] && diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index fe69cd81d0..44e5912b2a 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/defines.h" #include "esphome/core/component.h" #ifdef USE_SENSOR diff --git a/tests/components/ld2450/common.h b/tests/components/ld2450/common.h new file mode 100644 index 0000000000..d5ffbe1295 --- /dev/null +++ b/tests/components/ld2450/common.h @@ -0,0 +1,61 @@ +#pragma once +#include +#include +#include +#include +#include +#include "esphome/components/ld2450/ld2450.h" +#include "esphome/components/uart/uart_component.h" + +namespace esphome::ld2450::testing { + +// Mock UART component to satisfy UARTDevice parent requirement. +class MockUARTComponent : public uart::UARTComponent { + public: + void write_array(const uint8_t *data, size_t len) override {} + MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); + MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); + MOCK_METHOD(size_t, available, (), (override)); + MOCK_METHOD(void, flush, (), (override)); + MOCK_METHOD(void, check_logger_conflict, (), (override)); +}; + +// Expose protected members for testing. +class TestableLD2450 : public LD2450Component { + public: + using LD2450Component::buffer_data_; + using LD2450Component::buffer_pos_; + using LD2450Component::readline_; + + void feed(const std::vector &data) { + for (uint8_t byte : data) { + this->readline_(byte); + } + } +}; + +// LD2450 periodic data frame: header (4) + 3 targets * 8 bytes + footer (2) = 30 bytes +// All-zero targets means no presence detected. +inline std::vector make_periodic_frame(uint8_t fill = 0x00) { + std::vector frame = {0xAA, 0xFF, 0x03, 0x00}; // DATA_FRAME_HEADER + for (int i = 0; i < 24; i++) { + frame.push_back(fill); // 3 targets * 8 bytes + } + frame.push_back(0x55); // DATA_FRAME_FOOTER + frame.push_back(0xCC); + return frame; +} + +// LD2450 command ACK frame for CMD_ENABLE_CONF (0xFF), successful. +// header (4) + length (2) + command (2) + result (2) + footer (4) = 14 bytes +inline std::vector make_ack_frame() { + return { + 0xFD, 0xFC, 0xFB, 0xFA, // CMD_FRAME_HEADER + 0x04, 0x00, // length = 4 + 0xFF, 0x01, // command = enable_conf, status = success + 0x00, 0x00, // result = ok + 0x04, 0x03, 0x02, 0x01 // CMD_FRAME_FOOTER + }; +} + +} // namespace esphome::ld2450::testing diff --git a/tests/components/ld2450/ld2450_readline.cpp b/tests/components/ld2450/ld2450_readline.cpp new file mode 100644 index 0000000000..68b1dd6881 --- /dev/null +++ b/tests/components/ld2450/ld2450_readline.cpp @@ -0,0 +1,181 @@ +#include "common.h" + +namespace esphome::ld2450::testing { + +class LD2450ReadlineTest : public ::testing::Test { + protected: + void SetUp() override { + this->ld2450_.set_uart_parent(&this->mock_uart_); + // Ensure clean state + ASSERT_EQ(this->ld2450_.buffer_pos_, 0); + } + + MockUARTComponent mock_uart_; + TestableLD2450 ld2450_; +}; + +// --- Good data tests --- + +TEST_F(LD2450ReadlineTest, ValidPeriodicFrame) { + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + // After a complete valid frame, buffer should be reset + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, ValidCommandAckFrame) { + auto frame = make_ack_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, BackToBackPeriodicFrames) { + auto frame = make_periodic_frame(); + for (int i = 0; i < 5; i++) { + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Frame " << i << " not processed"; + } +} + +TEST_F(LD2450ReadlineTest, BackToBackMixedFrames) { + auto periodic = make_periodic_frame(); + auto ack = make_ack_frame(); + this->ld2450_.feed(periodic); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + this->ld2450_.feed(ack); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + this->ld2450_.feed(periodic); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +// --- Garbage rejection tests --- + +TEST_F(LD2450ReadlineTest, GarbageDiscarded) { + // Feed bytes that don't match any header start byte + std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99, 0x00, 0xFF, 0x7F}; + this->ld2450_.feed(garbage); + // Header sync should discard all of these + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) { + std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99}; + this->ld2450_.feed(garbage); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +// --- Header synchronization tests --- + +TEST_F(LD2450ReadlineTest, PartialDataHeaderThenMismatch) { + // Start of a data frame header, then invalid byte + this->ld2450_.feed({0xAA, 0xFF, 0x42}); // 0x42 doesn't match DATA_FRAME_HEADER[2] (0x03) + // Parser should have reset + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, PartialCmdHeaderThenMismatch) { + // Start of a command frame header, then invalid byte + this->ld2450_.feed({0xFD, 0xFC, 0xFB, 0x42}); // 0x42 doesn't match CMD_FRAME_HEADER[3] (0xFA) + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, PartialHeaderThenValidFrame) { + // Partial header that fails, then a complete valid frame + this->ld2450_.feed({0xAA, 0xFF, 0x42}); // Fails at byte 3 + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, HeaderMismatchRecoveryOnNewHeaderByte) { + // Start data header, mismatch at byte 2, but mismatch byte is start of command header + this->ld2450_.feed({0xAA, 0xFF}); + EXPECT_EQ(this->ld2450_.buffer_pos_, 2); // Accumulating header + + this->ld2450_.feed({0xFD}); // Doesn't match DATA_FRAME_HEADER[2]=0x03, but IS CMD_FRAME_HEADER[0] + // Parser should reset and start new frame with 0xFD + EXPECT_EQ(this->ld2450_.buffer_pos_, 1); + EXPECT_EQ(this->ld2450_.buffer_data_[0], 0xFD); +} + +// --- Mid-frame / overflow recovery tests --- + +TEST_F(LD2450ReadlineTest, MidFrameDataRecovery) { + // Simulate starting mid-frame: feed the tail end of a periodic frame (no valid header) + // These bytes would be part of target data in a real frame + std::vector mid_frame = {0x10, 0x20, 0x30, 0x40, 0x55, 0xCC}; + this->ld2450_.feed(mid_frame); + // All discarded (none match header start bytes) + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + // Now feed a valid frame + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, OverflowRecovery) { + // Feed a valid data frame header followed by enough filler to cause overflow. + // Header (4) + 36 filler = 40 bytes in buffer. The 41st byte triggers overflow. + std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; // Valid header + for (int i = 0; i < 37; i++) { + overflow_data.push_back(0x11); // Filler that won't match any footer + } + // 41 bytes total: 40 stored, 41st triggers overflow and resets buffer_pos_ to 0 + this->ld2450_.feed(overflow_data); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + // Feed a valid frame and verify recovery + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, RepeatedOverflowDoesNotLoop) { + // Simulate the bug scenario: repeated overflows should not prevent recovery. + // Feed 3 rounds of overflow-inducing data. + for (int round = 0; round < 3; round++) { + std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; + for (int i = 0; i < 37; i++) { + overflow_data.push_back(0x22); + } + this->ld2450_.feed(overflow_data); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Overflow round " << round; + } + + // Parser should still recover and process a valid frame + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, SimulatedRestartGarbageThenFrames) { + // Simulate LD2450 restart: burst of garbage bytes (partial frames, noise) + // followed by normal periodic data. + // Partial periodic frame (as if we started reading mid-frame), a stale footer, and more garbage + std::vector restart_noise = { + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, // mid-frame data + 0x55, 0xCC, // stale footer bytes + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, // more garbage + }; + + this->ld2450_.feed(restart_noise); + // All garbage should be discarded + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + // Now the LD2450 starts sending valid frames + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +} // namespace esphome::ld2450::testing From a2f0607c1e860d1173c80e0a763fda99e71e69f6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:04:38 -0500 Subject: [PATCH 105/609] [ld2410] Add frame header synchronization to readline_() (#14136) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2410/ld2410.cpp | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 95a04f768a..a3c2193d67 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -601,15 +601,33 @@ void LD2410Component::readline_(int readch) { return; // No data available } + // Frame header synchronization: verify first 4 bytes match a known frame header. + // This prevents the parser from getting stuck in an overflow loop after losing sync + // (e.g. after module restart or UART noise). + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { + const uint8_t byte = static_cast(readch); + // Verify header bytes match the frame type established by byte 0 + if (this->buffer_pos_ > 0) { + const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; + if (byte != expected[this->buffer_pos_]) { + this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame + } + } + // First byte must match start of a data or command frame header + if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { + return; + } + } + if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { - // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; + return; } - if (this->buffer_pos_ < 4) { + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { return; // Not enough data to process yet } if (ld2410::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { From 5af871acce9c33284d483620131056af2ad48071 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:36:12 -0500 Subject: [PATCH 106/609] [ld2420] Increase MAX_LINE_LENGTH to allow footer-based resync (#14137) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2420/ld2420.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 02250c5911..358793fe64 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -21,7 +21,9 @@ namespace esphome::ld2420 { static constexpr uint8_t CALIBRATE_SAMPLES = 64; -static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer +// Energy frame is 45 bytes; +1 for null terminator, +4 so that a frame footer always lands +// inside the buffer during footer-based resynchronization after losing sync. +static constexpr uint8_t MAX_LINE_LENGTH = 50; static constexpr uint8_t TOTAL_GATES = 16; enum OpMode : uint8_t { From efe54e3b5e3d3ddefee703048ff37bb4ec9b0b5f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:25:25 -0500 Subject: [PATCH 107/609] [ld2410/ld2450] Replace header sync with buffer size increase for frame resync (#14138) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2410/ld2410.cpp | 19 +-- esphome/components/ld2410/ld2410.h | 6 +- esphome/components/ld2450/ld2450.cpp | 19 +-- esphome/components/ld2450/ld2450.h | 8 +- tests/components/ld2450/ld2450_readline.cpp | 158 ++++++++------------ 5 files changed, 72 insertions(+), 138 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index a3c2193d67..f8f782f804 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -601,28 +601,11 @@ void LD2410Component::readline_(int readch) { return; // No data available } - // Frame header synchronization: verify first 4 bytes match a known frame header. - // This prevents the parser from getting stuck in an overflow loop after losing sync - // (e.g. after module restart or UART noise). - if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { - const uint8_t byte = static_cast(readch); - // Verify header bytes match the frame type established by byte 0 - if (this->buffer_pos_ > 0) { - const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; - if (byte != expected[this->buffer_pos_]) { - this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame - } - } - // First byte must match start of a data or command frame header - if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { - return; - } - } - if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { + // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; return; diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index efe585fb76..687ed21d1d 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -33,8 +33,10 @@ namespace esphome::ld2410 { using namespace ld24xx; -static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer -static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410 +// Engineering data frame is 45 bytes; +1 for null terminator, +4 so that a frame footer always +// lands inside the buffer during footer-based resynchronization after losing sync. +static constexpr uint8_t MAX_LINE_LENGTH = 50; +static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410 class LD2410Component : public Component, public uart::UARTDevice { #ifdef USE_BINARY_SENSOR diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 2af45235a3..d30c164769 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -769,28 +769,11 @@ void LD2450Component::readline_(int readch) { return; // No data available } - // Frame header synchronization: verify first 4 bytes match a known frame header. - // This prevents the parser from accumulating mid-frame data after losing sync - // (e.g. after module restart or UART noise). - if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { - const uint8_t byte = static_cast(readch); - // Verify header bytes match the frame type established by byte 0 - if (this->buffer_pos_ > 0) { - const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; - if (byte != expected[this->buffer_pos_]) { - this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame - } - } - // First byte must match start of a data or command frame header - if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { - return; - } - } - if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { + // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; return; diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 44e5912b2a..30f96c0a9c 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -38,9 +38,11 @@ using namespace ld24xx; // Constants static constexpr uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Timeout to reset presense status 5 sec. -static constexpr uint8_t MAX_LINE_LENGTH = 41; // Max characters for serial buffer -static constexpr uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450 -static constexpr uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450 +// Zone query response is 40 bytes; +1 for null terminator, +4 so that a frame footer always +// lands inside the buffer during footer-based resynchronization after losing sync. +static constexpr uint8_t MAX_LINE_LENGTH = 45; +static constexpr uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450 +static constexpr uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450 enum Direction : uint8_t { DIRECTION_APPROACHING = 0, diff --git a/tests/components/ld2450/ld2450_readline.cpp b/tests/components/ld2450/ld2450_readline.cpp index 68b1dd6881..cb97f633bf 100644 --- a/tests/components/ld2450/ld2450_readline.cpp +++ b/tests/components/ld2450/ld2450_readline.cpp @@ -48,19 +48,39 @@ TEST_F(LD2450ReadlineTest, BackToBackMixedFrames) { EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -// --- Garbage rejection tests --- - -TEST_F(LD2450ReadlineTest, GarbageDiscarded) { - // Feed bytes that don't match any header start byte - std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99, 0x00, 0xFF, 0x7F}; - this->ld2450_.feed(garbage); - // Header sync should discard all of these - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} +// --- Garbage then valid frame tests --- TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) { + // Garbage bytes accumulate in the buffer but don't match any footer. + // A valid frame follows; its footer resets the buffer and resyncs. std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99}; this->ld2450_.feed(garbage); + EXPECT_GT(this->ld2450_.buffer_pos_, 0); // Garbage accumulated + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + // Footer from the valid frame resyncs the parser + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +// --- Footer-based resynchronization tests --- + +TEST_F(LD2450ReadlineTest, FooterInGarbageResyncs) { + // Garbage containing a periodic frame footer (0x55 0xCC) triggers + // a buffer reset, allowing the next frame to be parsed cleanly. + std::vector garbage_with_footer = {0x01, 0x02, 0x03, 0x04, 0x55, 0xCC}; + this->ld2450_.feed(garbage_with_footer); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); // Footer reset the buffer + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, CmdFooterInGarbageResyncs) { + // Garbage containing a command frame footer (04 03 02 01) also resyncs. + std::vector garbage_with_footer = {0x10, 0x20, 0x30, 0x40, 0x04, 0x03, 0x02, 0x01}; + this->ld2450_.feed(garbage_with_footer); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); auto frame = make_periodic_frame(); @@ -68,112 +88,56 @@ TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) { EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -// --- Header synchronization tests --- +// --- Overflow recovery tests --- -TEST_F(LD2450ReadlineTest, PartialDataHeaderThenMismatch) { - // Start of a data frame header, then invalid byte - this->ld2450_.feed({0xAA, 0xFF, 0x42}); // 0x42 doesn't match DATA_FRAME_HEADER[2] (0x03) - // Parser should have reset - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, PartialCmdHeaderThenMismatch) { - // Start of a command frame header, then invalid byte - this->ld2450_.feed({0xFD, 0xFC, 0xFB, 0x42}); // 0x42 doesn't match CMD_FRAME_HEADER[3] (0xFA) - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, PartialHeaderThenValidFrame) { - // Partial header that fails, then a complete valid frame - this->ld2450_.feed({0xAA, 0xFF, 0x42}); // Fails at byte 3 - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - - auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, HeaderMismatchRecoveryOnNewHeaderByte) { - // Start data header, mismatch at byte 2, but mismatch byte is start of command header - this->ld2450_.feed({0xAA, 0xFF}); - EXPECT_EQ(this->ld2450_.buffer_pos_, 2); // Accumulating header - - this->ld2450_.feed({0xFD}); // Doesn't match DATA_FRAME_HEADER[2]=0x03, but IS CMD_FRAME_HEADER[0] - // Parser should reset and start new frame with 0xFD - EXPECT_EQ(this->ld2450_.buffer_pos_, 1); - EXPECT_EQ(this->ld2450_.buffer_data_[0], 0xFD); -} - -// --- Mid-frame / overflow recovery tests --- - -TEST_F(LD2450ReadlineTest, MidFrameDataRecovery) { - // Simulate starting mid-frame: feed the tail end of a periodic frame (no valid header) - // These bytes would be part of target data in a real frame - std::vector mid_frame = {0x10, 0x20, 0x30, 0x40, 0x55, 0xCC}; - this->ld2450_.feed(mid_frame); - // All discarded (none match header start bytes) - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - - // Now feed a valid frame - auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, OverflowRecovery) { - // Feed a valid data frame header followed by enough filler to cause overflow. - // Header (4) + 36 filler = 40 bytes in buffer. The 41st byte triggers overflow. - std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; // Valid header - for (int i = 0; i < 37; i++) { - overflow_data.push_back(0x11); // Filler that won't match any footer - } - // 41 bytes total: 40 stored, 41st triggers overflow and resets buffer_pos_ to 0 +TEST_F(LD2450ReadlineTest, OverflowResetsBuffer) { + // Fill the buffer to capacity with filler that won't match any footer. + // MAX_LINE_LENGTH is 45, usable is 44. The 45th byte triggers overflow. + std::vector overflow_data(MAX_LINE_LENGTH, 0x11); + this->ld2450_.feed(overflow_data); + // After overflow, buffer_pos_ resets to 0 (via the < 4 early return path) + EXPECT_LT(this->ld2450_.buffer_pos_, 4); +} + +TEST_F(LD2450ReadlineTest, OverflowThenValidFrame) { + // Overflow, then a valid frame should be processed. + std::vector overflow_data(MAX_LINE_LENGTH, 0x11); this->ld2450_.feed(overflow_data); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - // Feed a valid frame and verify recovery auto frame = make_periodic_frame(); this->ld2450_.feed(frame); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -TEST_F(LD2450ReadlineTest, RepeatedOverflowDoesNotLoop) { - // Simulate the bug scenario: repeated overflows should not prevent recovery. - // Feed 3 rounds of overflow-inducing data. - for (int round = 0; round < 3; round++) { - std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; - for (int i = 0; i < 37; i++) { - overflow_data.push_back(0x22); - } - this->ld2450_.feed(overflow_data); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Overflow round " << round; - } - - // Parser should still recover and process a valid frame +TEST_F(LD2450ReadlineTest, BufferLargeEnoughForDesyncedFooter) { + // The key fix: the buffer (45) is large enough that a desynced periodic frame's + // footer (at most 30 bytes into the stream) will land inside the buffer before overflow. + // Simulate starting 10 bytes into a periodic frame, then a full frame follows. + std::vector mid_frame = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39}; + // Then a complete periodic frame whose footer will land at position 40 (10 + 30), + // well within the buffer size of 45. auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); + mid_frame.insert(mid_frame.end(), frame.begin(), frame.end()); + + this->ld2450_.feed(mid_frame); + // The footer from the frame should have triggered a reset EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -TEST_F(LD2450ReadlineTest, SimulatedRestartGarbageThenFrames) { - // Simulate LD2450 restart: burst of garbage bytes (partial frames, noise) - // followed by normal periodic data. - // Partial periodic frame (as if we started reading mid-frame), a stale footer, and more garbage +TEST_F(LD2450ReadlineTest, SimulatedRestartThenFrames) { + // Simulate LD2450 restart: burst of garbage followed by valid periodic frames. + // The garbage + first frame should fit in the buffer so the footer resyncs. std::vector restart_noise = { - 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, // mid-frame data - 0x55, 0xCC, // stale footer bytes - 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, // more garbage + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // 8 bytes of mid-frame data }; + auto frame = make_periodic_frame(); + // 8 garbage + 30 frame = 38 bytes, well within buffer of 45 + restart_noise.insert(restart_noise.end(), frame.begin(), frame.end()); this->ld2450_.feed(restart_noise); - // All garbage should be discarded - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - - // Now the LD2450 starts sending valid frames - auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + // Subsequent frames should work normally this->ld2450_.feed(frame); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } From b0a35559b3aa905a46b662b5834689a45b840e33 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 10:19:01 -0600 Subject: [PATCH 108/609] [esp32] Bump ESP-IDF to 5.5.3.1, revert GATTS workaround (#14147) --- .clang-tidy.hash | 2 +- esphome/components/esp32/__init__.py | 25 +++++++++++++++--------- esphome/components/esp32_ble/__init__.py | 15 ++++++-------- platformio.ini | 4 ++-- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index df584fa716..777c846371 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -3258307fa645ba77307e502075c02c4d710e92c48250839db3526d36a9655444 +5eb1e5852765114ad06533220d3160b6c23f5ccefc4de41828699de5dfff5ad6 diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index cdde1c4ed5..b6682100f7 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -587,16 +587,22 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: return f"{ARDUINO_FRAMEWORK_PKG}@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}" -def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: +def _format_framework_espidf_version( + ver: cv.Version, release: str | None = None +) -> str: # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to # a PIO platformio/framework-espidf value if ver == cv.Version(5, 4, 3) or ver >= cv.Version(5, 5, 1): ext = "tar.xz" else: ext = "zip" + # Build version string with dot-separated extra (e.g., "5.5.3.1" not "5.5.3-1") + ver_str = f"{ver.major}.{ver.minor}.{ver.patch}" + if ver.extra: + ver_str += f".{ver.extra}" if release: - return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.{ext}" - return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}" + return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{ver_str}.{release}/esp-idf-v{ver_str}.{ext}" + return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{ver_str}/esp-idf-v{ver_str}.{ext}" def _is_framework_url(source: str) -> bool: @@ -643,7 +649,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = { # These versions correspond to pioarduino/esp-idf releases # See: https://github.com/pioarduino/esp-idf/releases ARDUINO_IDF_VERSION_LOOKUP = { - cv.Version(3, 3, 7): cv.Version(5, 5, 3), + cv.Version(3, 3, 7): cv.Version(5, 5, 3, "1"), cv.Version(3, 3, 6): cv.Version(5, 5, 2), cv.Version(3, 3, 5): cv.Version(5, 5, 2), cv.Version(3, 3, 4): cv.Version(5, 5, 1), @@ -662,11 +668,12 @@ ARDUINO_IDF_VERSION_LOOKUP = { # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(5, 5, 3), - "latest": cv.Version(5, 5, 3), - "dev": cv.Version(5, 5, 3), + "recommended": cv.Version(5, 5, 3, "1"), + "latest": cv.Version(5, 5, 3, "1"), + "dev": cv.Version(5, 5, 3, "1"), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { + cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37), cv.Version(5, 5, 3): cv.Version(55, 3, 37), cv.Version(5, 5, 2): cv.Version(55, 3, 37), cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"), @@ -730,7 +737,7 @@ def _check_versions(config): platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version) value[CONF_SOURCE] = value.get( CONF_SOURCE, - _format_framework_espidf_version(version, value.get(CONF_RELEASE, None)), + _format_framework_espidf_version(version, value.get(CONF_RELEASE)), ) if _is_framework_url(value[CONF_SOURCE]): value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}" @@ -1428,7 +1435,7 @@ async def to_code(config): if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None: cg.add_platformio_option( "platform_packages", - [_format_framework_espidf_version(idf_ver, None)], + [_format_framework_espidf_version(idf_ver)], ) # Use stub package to skip downloading precompiled libs stubs_dir = CORE.relative_build_path("arduino_libs_stub") diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 80fcf051b9..c0e2f78bde 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -418,16 +418,13 @@ def final_validation(config): "esp32_ble_tracker" in full_config or "esp32_ble_client" in full_config ) - # Always enable GATTS: ESP-IDF 5.5.2.260206 has a bug in gatt_main.c where a - # GATT_TRACE_DEBUG references 'msg_len' outside the GATTS_INCLUDED/GATTC_INCLUDED - # guard, causing a compile error when both are disabled. - # Additionally, when GATT Client is enabled, GATT Server must also be enabled - # as an internal dependency in the Bluedroid stack. + # Check if BLE Server is needed + has_ble_server = "esp32_ble_server" in full_config + + # ESP-IDF BLE stack requires GATT Server to be enabled when GATT Client is enabled + # This is an internal dependency in the Bluedroid stack # See: https://github.com/espressif/esp-idf/issues/17724 - # TODO: Revert to conditional once the gatt_main.c bug is fixed upstream: - # has_ble_server = "esp32_ble_server" in full_config - # add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client) - add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", True) + add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client) add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client) # Handle max_connections: check for deprecated location in esp32_ble_tracker diff --git a/platformio.ini b/platformio.ini index fdd6a36428..e35dce2228 100644 --- a/platformio.ini +++ b/platformio.ini @@ -136,7 +136,7 @@ extends = common:arduino platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip platform_packages = pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.7/esp32-core-3.3.7.tar.xz - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3/esp-idf-v5.5.3.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3.1/esp-idf-v5.5.3.1.tar.xz framework = arduino, espidf ; Arduino as an ESP-IDF component lib_deps = @@ -171,7 +171,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script extends = common:idf platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip platform_packages = - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3/esp-idf-v5.5.3.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3.1/esp-idf-v5.5.3.1.tar.xz framework = espidf lib_deps = From 9ce01fc369c5451e1f980e49cea08e51b6a0cc53 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:20:05 -0500 Subject: [PATCH 109/609] [esp32] Add engineering_sample option for ESP32-P4 (#14139) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32/__init__.py | 43 +++++++++++++++++-- esphome/components/esp32/boards.py | 5 ++- script/generate-esp32-boards.py | 15 ++++++- tests/components/esp32/test.esp32-p4-idf.yaml | 1 + 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b6682100f7..06677006ea 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -87,6 +87,7 @@ IS_TARGET_PLATFORM = True CONF_ASSERTION_LEVEL = "assertion_level" CONF_COMPILER_OPTIMIZATION = "compiler_optimization" CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" +CONF_ENGINEERING_SAMPLE = "engineering_sample" CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" @@ -785,6 +786,15 @@ def _detect_variant(value): # variant has already been validated against the known set value = value.copy() value[CONF_BOARD] = STANDARD_BOARDS[variant] + if variant == VARIANT_ESP32P4: + engineering_sample = value.get(CONF_ENGINEERING_SAMPLE) + if engineering_sample is None: + _LOGGER.warning( + "No board specified for ESP32-P4. Defaulting to production silicon (rev3). " + "If you have an early engineering sample (pre-rev3), set 'engineering_sample: true'." + ) + elif engineering_sample: + value[CONF_BOARD] = "esp32-p4-evboard" elif board in BOARDS: variant = variant or BOARDS[board][KEY_VARIANT] if variant != BOARDS[board][KEY_VARIANT]: @@ -848,6 +858,30 @@ def final_validate(config): path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_MINIMUM_CHIP_REVISION], ) ) + if ( + config[CONF_VARIANT] != VARIANT_ESP32P4 + and config.get(CONF_ENGINEERING_SAMPLE) is not None + ): + errs.append( + cv.Invalid( + f"'{CONF_ENGINEERING_SAMPLE}' is only supported on {VARIANT_ESP32P4}", + path=[CONF_ENGINEERING_SAMPLE], + ) + ) + if ( + config[CONF_VARIANT] == VARIANT_ESP32P4 + and config.get(CONF_ENGINEERING_SAMPLE) is not None + ): + board_is_es = BOARDS.get(config[CONF_BOARD], {}).get( + "engineering_sample", False + ) + if config[CONF_ENGINEERING_SAMPLE] != board_is_es: + errs.append( + cv.Invalid( + f"'{CONF_ENGINEERING_SAMPLE}' does not match board '{config[CONF_BOARD]}'", + path=[CONF_ENGINEERING_SAMPLE], + ) + ) if advanced[CONF_EXECUTE_FROM_PSRAM]: if config[CONF_VARIANT] != VARIANT_ESP32S3: errs.append( @@ -1197,6 +1231,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_CPU_FREQUENCY): cv.one_of( *FULL_CPU_FREQUENCIES, upper=True ), + cv.Optional(CONF_ENGINEERING_SAMPLE): cv.boolean, cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of( *FLASH_SIZES, upper=True ), @@ -1482,10 +1517,12 @@ async def to_code(config): # ESP32-P4: ESP-IDF 5.5.3 changed the default of ESP32P4_SELECTS_REV_LESS_V3 # from y to n. PlatformIO uses sections.ld.in (for rev <3) or # sections.rev3.ld.in (for rev >=3) based on board definition. - # Set the sdkconfig option to match the board's revision. + # Set the sdkconfig option to match the board's chip revision. if variant == VARIANT_ESP32P4: - is_rev3 = "_r3" in config[CONF_BOARD] - add_idf_sdkconfig_option("CONFIG_ESP32P4_SELECTS_REV_LESS_V3", not is_rev3) + is_eng_sample = BOARDS.get(config[CONF_BOARD], {}).get( + "engineering_sample", False + ) + add_idf_sdkconfig_option("CONFIG_ESP32P4_SELECTS_REV_LESS_V3", is_eng_sample) # Set minimum chip revision for ESP32 variant # Setting this to 3.0 or higher reduces flash size by excluding workaround code, diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index 66367d63ae..2bd08e7c39 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -20,7 +20,7 @@ STANDARD_BOARDS = { VARIANT_ESP32C6: "esp32-c6-devkitm-1", VARIANT_ESP32C61: "esp32-c61-devkitc1-n8r2", VARIANT_ESP32H2: "esp32-h2-devkitm-1", - VARIANT_ESP32P4: "esp32-p4-evboard", + VARIANT_ESP32P4: "esp32-p4_r3-evboard", VARIANT_ESP32S2: "esp32-s2-kaluga-1", VARIANT_ESP32S3: "esp32-s3-devkitc-1", } @@ -1713,10 +1713,12 @@ BOARDS = { "esp32-p4": { "name": "Espressif ESP32-P4 ES (pre rev.300) generic", "variant": VARIANT_ESP32P4, + "engineering_sample": True, }, "esp32-p4-evboard": { "name": "Espressif ESP32-P4 Function EV Board (ES pre rev.300)", "variant": VARIANT_ESP32P4, + "engineering_sample": True, }, "esp32-p4_r3": { "name": "Espressif ESP32-P4 rev.300 generic", @@ -2141,6 +2143,7 @@ BOARDS = { "m5stack-tab5-p4": { "name": "M5STACK Tab5 esp32-p4 Board (ES pre rev.300)", "variant": VARIANT_ESP32P4, + "engineering_sample": True, }, "m5stack-timer-cam": { "name": "M5Stack Timer CAM", diff --git a/script/generate-esp32-boards.py b/script/generate-esp32-boards.py index 81b78b04be..ab4a38ced5 100755 --- a/script/generate-esp32-boards.py +++ b/script/generate-esp32-boards.py @@ -43,10 +43,14 @@ def get_boards(): name = board_info["name"] board = fname.stem variant = mcu.upper() - boards[board] = { + chip_variant = board_info["build"].get("chip_variant", "") + entry = { "name": name, "variant": f"VARIANT_{variant}", } + if chip_variant.endswith("_es"): + entry["engineering_sample"] = True + boards[board] = entry return boards @@ -55,6 +59,12 @@ TEMPLATE = """ "%s": { "variant": %s, },""" +TEMPLATE_ES = """ "%s": { + "name": "%s", + "variant": %s, + "engineering_sample": True, + },""" + def main(check: bool): boards = get_boards() @@ -66,7 +76,8 @@ def main(check: bool): if line == "BOARDS = {": parts.append(line) parts.extend( - TEMPLATE % (board, info["name"], info["variant"]) + (TEMPLATE_ES if info.get("engineering_sample") else TEMPLATE) + % (board, info["name"], info["variant"]) for board, info in sorted(boards.items()) ) parts.append("}") diff --git a/tests/components/esp32/test.esp32-p4-idf.yaml b/tests/components/esp32/test.esp32-p4-idf.yaml index bc054f5aee..fd42fac5a3 100644 --- a/tests/components/esp32/test.esp32-p4-idf.yaml +++ b/tests/components/esp32/test.esp32-p4-idf.yaml @@ -1,5 +1,6 @@ esp32: variant: esp32p4 + engineering_sample: true flash_size: 32MB cpu_frequency: 400MHz framework: From 403235e2d4ec6a27ac4ad35f79b7dc2afb413611 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:20:29 -0500 Subject: [PATCH 110/609] [wifi] Add band_mode configuration for ESP32-C5 dual-band WiFi (#14148) Co-authored-by: Claude Opus 4.6 --- esphome/components/wifi/__init__.py | 22 ++++++++++++++++++- esphome/components/wifi/wifi_component.cpp | 16 ++++++++++++++ esphome/components/wifi/wifi_component.h | 13 +++++++++++ .../wifi/wifi_component_esp_idf.cpp | 7 ++++++ tests/components/wifi/test.esp32-c5-idf.yaml | 5 +++++ 5 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 tests/components/wifi/test.esp32-c5-idf.yaml diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index afceec6c54..540d0a0ab1 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -5,7 +5,12 @@ from esphome import automation from esphome.automation import Condition import esphome.codegen as cg from esphome.components.const import CONF_USE_PSRAM -from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant +from esphome.components.esp32 import ( + add_idf_sdkconfig_option, + const, + get_esp32_variant, + only_on_variant, +) from esphome.components.network import ( has_high_performance_networking, ip_address_literal, @@ -64,6 +69,7 @@ _LOGGER = logging.getLogger(__name__) NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] CONF_SAVE = "save" +CONF_BAND_MODE = "band_mode" CONF_MIN_AUTH_MODE = "min_auth_mode" CONF_POST_CONNECT_ROAMING = "post_connect_roaming" @@ -90,6 +96,13 @@ WIFI_POWER_SAVE_MODES = { "HIGH": WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH, } +WiFiBandMode = cg.global_ns.enum("wifi_band_mode_t") +WIFI_BAND_MODES = { + "AUTO": WiFiBandMode.WIFI_BAND_MODE_AUTO, + "2.4GHZ": WiFiBandMode.WIFI_BAND_MODE_2G_ONLY, + "5GHZ": WiFiBandMode.WIFI_BAND_MODE_5G_ONLY, +} + WifiMinAuthMode = wifi_ns.enum("WifiMinAuthMode") WIFI_MIN_AUTH_MODES = { "WPA": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA, @@ -353,6 +366,11 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault(CONF_ENABLE_RRM, esp32=False): cv.All( cv.boolean, cv.only_on_esp32 ), + cv.Optional(CONF_BAND_MODE): cv.All( + cv.enum(WIFI_BAND_MODES, upper=True), + cv.only_on_esp32, + only_on_variant(supported=[const.VARIANT_ESP32C5]), + ), cv.Optional(CONF_PASSIVE_SCAN, default=False): cv.boolean, cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, cv.Optional(CONF_POST_CONNECT_ROAMING, default=True): cv.boolean, @@ -527,6 +545,8 @@ async def to_code(config): cg.add(var.set_btm(config[CONF_ENABLE_BTM])) if config[CONF_ENABLE_RRM]: cg.add(var.set_rrm(config[CONF_ENABLE_RRM])) + if CONF_BAND_MODE in config: + cg.add(var.set_band_mode(config[CONF_BAND_MODE])) if config.get(CONF_USE_PSRAM): add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a2efac8d26..8b3060c7c3 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1469,6 +1469,22 @@ void WiFiComponent::dump_config() { ESP_LOGCONFIG(TAG, " Disabled"); return; } +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) + const char *band_mode_s; + switch (this->band_mode_) { + case WIFI_BAND_MODE_2G_ONLY: + band_mode_s = "2.4GHz"; + break; + case WIFI_BAND_MODE_5G_ONLY: + band_mode_s = "5GHz"; + break; + case WIFI_BAND_MODE_AUTO: + default: + band_mode_s = "Auto"; + break; + } + ESP_LOGCONFIG(TAG, " Band Mode: %s", band_mode_s); +#endif if (this->is_connected()) { this->print_connect_params_(); } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 4a038f602c..5f903e092a 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -45,6 +45,10 @@ extern "C" { #include #endif +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) +#include +#endif + #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) #include #include @@ -435,6 +439,9 @@ class WiFiComponent : public Component { void set_power_save_mode(WiFiPowerSaveMode power_save); void set_min_auth_mode(WifiMinAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; } void set_output_power(float output_power) { output_power_ = output_power; } +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) + void set_band_mode(wifi_band_mode_t band_mode) { this->band_mode_ = band_mode; } +#endif void set_passive_scan(bool passive); @@ -652,6 +659,9 @@ class WiFiComponent : public Component { bool wifi_sta_pre_setup_(); bool wifi_apply_output_power_(float output_power); bool wifi_apply_power_save_(); +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) + bool wifi_apply_band_mode_(); +#endif bool wifi_sta_ip_config_(const optional &manual_ip); bool wifi_apply_hostname_(); bool wifi_sta_connect_(const WiFiAP &ap); @@ -774,6 +784,9 @@ class WiFiComponent : public Component { // 1-byte enums and integers WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) + wifi_band_mode_t band_mode_{WIFI_BAND_MODE_AUTO}; +#endif WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2}; WiFiRetryPhase retry_phase_{WiFiRetryPhase::INITIAL_CONNECT}; uint8_t num_retried_{0}; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 52ee482121..57bbceb1b8 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -292,6 +292,10 @@ bool WiFiComponent::wifi_apply_power_save_() { return success; } +#ifdef SOC_WIFI_SUPPORT_5G +bool WiFiComponent::wifi_apply_band_mode_() { return esp_wifi_set_band_mode(this->band_mode_) == ESP_OK; } +#endif + bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // enable STA if (!this->wifi_mode_(true, {})) @@ -726,6 +730,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { s_sta_started = true; // re-apply power save mode wifi_apply_power_save_(); +#ifdef SOC_WIFI_SUPPORT_5G + wifi_apply_band_mode_(); +#endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) { ESP_LOGV(TAG, "STA stop"); diff --git a/tests/components/wifi/test.esp32-c5-idf.yaml b/tests/components/wifi/test.esp32-c5-idf.yaml new file mode 100644 index 0000000000..92a52db09e --- /dev/null +++ b/tests/components/wifi/test.esp32-c5-idf.yaml @@ -0,0 +1,5 @@ +wifi: + band_mode: 5GHZ + +packages: + - !include common.yaml From 9c0eed8a67a392868d2a8e6c8a220314271c847e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:03:39 -0500 Subject: [PATCH 111/609] [e131] Remove dead LWIP TCP code path from loop() (#14155) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/e131/e131.cpp | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index d04ab8a58d..a7a695c167 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -85,22 +85,6 @@ void E131Component::loop() { ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); } } -#elif defined(USE_SOCKET_IMPL_LWIP_TCP) - while (auto packet_size = this->udp_.parsePacket()) { - auto len = this->udp_.read(buf, sizeof(buf)); - if (len <= 0) - continue; - - if (!this->packet_(buf, (size_t) len, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); - continue; - } - - if (!this->process_(universe, packet)) { - ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); - } - } -#endif } void E131Component::add_effect(E131AddressableLightEffect *light_effect) { From b85a49cdb3c65c24ffdb725a56dc9f48c2fd7f3a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:27:15 -0600 Subject: [PATCH 112/609] Bump github/codeql-action from 4.32.3 to 4.32.4 (#14161) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 376825bad6..5d7c32eaa9 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 + uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 + uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 with: category: "/language:${{matrix.language}}" From 1a376328911b184cbc5b7251688607f7ff62384a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:27:45 +0000 Subject: [PATCH 113/609] Bump pylint from 4.0.4 to 4.0.5 (#14160) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 9e99855f6f..611e552829 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==4.0.4 +pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating ruff==0.15.1 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating From edfc3e3501ec3b0652a7a341d5542f70c488b79c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:32:41 +0000 Subject: [PATCH 114/609] Bump ruff from 0.15.1 to 0.15.2 (#14159) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6d89060b0d..07d02e0e3c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.1 + rev: v0.15.2 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 611e552829..3e5dc8a90c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.1 # also change in .pre-commit-config.yaml when updating +ruff==0.15.2 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From 48115eca18b8c9937595a0fe26ce50fde4ebb15f Mon Sep 17 00:00:00 2001 From: Pawelo <81100874+pgolawsk@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:08:31 +0100 Subject: [PATCH 115/609] [safe_mode] Extract RTC_KEY constant for shared use (#14121) Co-authored-by: J. Nick Koston --- esphome/components/safe_mode/safe_mode.cpp | 2 +- esphome/components/safe_mode/safe_mode.h | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index 6cae4bf9d5..bd80048c64 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -104,7 +104,7 @@ bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t en this->safe_mode_enable_time_ = enable_time; this->safe_mode_boot_is_good_after_ = boot_is_good_after; this->safe_mode_num_attempts_ = num_attempts; - this->rtc_ = global_preferences->make_preference(233825507UL, false); + this->rtc_ = global_preferences->make_preference(RTC_KEY, false); #if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) // Check partition state to detect if bootloader supports rollback diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index d6f669f39f..a4d27c15da 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -11,6 +11,9 @@ namespace esphome::safe_mode { +/// RTC key for storing boot loop counter - used by safe_mode and preferences backends +constexpr uint32_t RTC_KEY = 233825507UL; + /// SafeModeComponent provides a safe way to recover from repeated boot failures class SafeModeComponent : public Component { public: From db6aa58f40b4c6be322f8c26312605bb2fca8dd6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:06:46 -0500 Subject: [PATCH 116/609] [max7219digit] Fix typo in action names (#14162) Co-authored-by: Claude Opus 4.6 --- esphome/components/max7219digit/display.py | 22 +++++++++++----------- tests/components/max7219digit/common.yaml | 14 +++++++------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index e6d53efc5d..a251eaccea 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -133,12 +133,12 @@ MAX7219_ON_ACTION_SCHEMA = automation.maybe_simple_id( @automation.register_action( - "max7129digit.invert_off", DisplayInvertAction, MAX7219_OFF_ACTION_SCHEMA + "max7219digit.invert_off", DisplayInvertAction, MAX7219_OFF_ACTION_SCHEMA ) @automation.register_action( - "max7129digit.invert_on", DisplayInvertAction, MAX7219_ON_ACTION_SCHEMA + "max7219digit.invert_on", DisplayInvertAction, MAX7219_ON_ACTION_SCHEMA ) -async def max7129digit_invert_to_code(config, action_id, template_arg, args): +async def max7219digit_invert_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_state(config[CONF_STATE])) @@ -146,12 +146,12 @@ async def max7129digit_invert_to_code(config, action_id, template_arg, args): @automation.register_action( - "max7129digit.turn_off", DisplayVisibilityAction, MAX7219_OFF_ACTION_SCHEMA + "max7219digit.turn_off", DisplayVisibilityAction, MAX7219_OFF_ACTION_SCHEMA ) @automation.register_action( - "max7129digit.turn_on", DisplayVisibilityAction, MAX7219_ON_ACTION_SCHEMA + "max7219digit.turn_on", DisplayVisibilityAction, MAX7219_ON_ACTION_SCHEMA ) -async def max7129digit_visible_to_code(config, action_id, template_arg, args): +async def max7219digit_visible_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_state(config[CONF_STATE])) @@ -159,12 +159,12 @@ async def max7129digit_visible_to_code(config, action_id, template_arg, args): @automation.register_action( - "max7129digit.reverse_off", DisplayReverseAction, MAX7219_OFF_ACTION_SCHEMA + "max7219digit.reverse_off", DisplayReverseAction, MAX7219_OFF_ACTION_SCHEMA ) @automation.register_action( - "max7129digit.reverse_on", DisplayReverseAction, MAX7219_ON_ACTION_SCHEMA + "max7219digit.reverse_on", DisplayReverseAction, MAX7219_ON_ACTION_SCHEMA ) -async def max7129digit_reverse_to_code(config, action_id, template_arg, args): +async def max7219digit_reverse_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_state(config[CONF_STATE])) @@ -183,9 +183,9 @@ MAX7219_INTENSITY_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "max7129digit.intensity", DisplayIntensityAction, MAX7219_INTENSITY_SCHEMA + "max7219digit.intensity", DisplayIntensityAction, MAX7219_INTENSITY_SCHEMA ) -async def max7129digit_intensity_to_code(config, action_id, template_arg, args): +async def max7219digit_intensity_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_INTENSITY], args, cg.uint8) diff --git a/tests/components/max7219digit/common.yaml b/tests/components/max7219digit/common.yaml index 525b7b8d3e..4d2dbb781d 100644 --- a/tests/components/max7219digit/common.yaml +++ b/tests/components/max7219digit/common.yaml @@ -13,10 +13,10 @@ esphome: on_boot: - priority: 100 then: - - max7129digit.invert_off: - - max7129digit.invert_on: - - max7129digit.turn_on: - - max7129digit.turn_off: - - max7129digit.reverse_on: - - max7129digit.reverse_off: - - max7129digit.intensity: 10 + - max7219digit.invert_off: + - max7219digit.invert_on: + - max7219digit.turn_on: + - max7219digit.turn_off: + - max7219digit.reverse_on: + - max7219digit.reverse_off: + - max7219digit.intensity: 10 From 1d3054ef5e463f1b018e297ffe547c7e621ddfd4 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Fri, 20 Feb 2026 23:12:50 +0100 Subject: [PATCH 117/609] [nrf52,logger] Early debug (#11685) --- esphome/components/debug/debug_zephyr.cpp | 45 +-------- esphome/components/logger/__init__.py | 24 +++++ esphome/components/logger/logger.cpp | 3 + esphome/components/logger/logger.h | 1 + esphome/components/logger/logger_zephyr.cpp | 97 +++++++++++++++++++ esphome/components/nrf52/__init__.py | 1 + esphome/components/zephyr/gpio.h | 6 +- esphome/components/zephyr/preferences.h | 6 +- esphome/components/zephyr/reset_reason.cpp | 63 ++++++++++++ esphome/components/zephyr/reset_reason.h | 18 ++++ esphome/config_validation.py | 2 + esphome/core/defines.h | 2 + .../logger/test.nrf52-adafruit.yaml | 2 + 13 files changed, 219 insertions(+), 51 deletions(-) create mode 100644 esphome/components/zephyr/reset_reason.cpp create mode 100644 esphome/components/zephyr/reset_reason.h diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index 0291cc3061..ecca7150bd 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -2,6 +2,7 @@ #ifdef USE_ZEPHYR #include #include "esphome/core/log.h" +#include #include #include #include @@ -15,16 +16,6 @@ static const char *const TAG = "debug"; constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC; constexpr std::uintptr_t MBR_BOOTLOADER_ADDR = 0xFF8; -static size_t append_reset_reason(char *buf, size_t size, size_t pos, bool set, const char *reason) { - if (!set) { - return pos; - } - if (pos > 0) { - pos = buf_append_printf(buf, size, pos, ", "); - } - return buf_append_printf(buf, size, pos, "%s", reason); -} - static inline uint32_t read_mem_u32(uintptr_t addr) { return *reinterpret_cast(addr); // NOLINT(performance-no-int-to-ptr) } @@ -57,39 +48,7 @@ static inline uint32_t sd_version_get() { } const char *DebugComponent::get_reset_reason_(std::span buffer) { - char *buf = buffer.data(); - const size_t size = RESET_REASON_BUFFER_SIZE; - - uint32_t cause; - auto ret = hwinfo_get_reset_cause(&cause); - if (ret) { - ESP_LOGE(TAG, "Unable to get reset cause: %d", ret); - buf[0] = '\0'; - return buf; - } - size_t pos = 0; - - pos = append_reset_reason(buf, size, pos, cause & RESET_PIN, "External pin"); - pos = append_reset_reason(buf, size, pos, cause & RESET_SOFTWARE, "Software reset"); - pos = append_reset_reason(buf, size, pos, cause & RESET_BROWNOUT, "Brownout (drop in voltage)"); - pos = append_reset_reason(buf, size, pos, cause & RESET_POR, "Power-on reset (POR)"); - pos = append_reset_reason(buf, size, pos, cause & RESET_WATCHDOG, "Watchdog timer expiration"); - pos = append_reset_reason(buf, size, pos, cause & RESET_DEBUG, "Debug event"); - pos = append_reset_reason(buf, size, pos, cause & RESET_SECURITY, "Security violation"); - pos = append_reset_reason(buf, size, pos, cause & RESET_LOW_POWER_WAKE, "Waking up from low power mode"); - pos = append_reset_reason(buf, size, pos, cause & RESET_CPU_LOCKUP, "CPU lock-up detected"); - pos = append_reset_reason(buf, size, pos, cause & RESET_PARITY, "Parity error"); - pos = append_reset_reason(buf, size, pos, cause & RESET_PLL, "PLL error"); - pos = append_reset_reason(buf, size, pos, cause & RESET_CLOCK, "Clock error"); - pos = append_reset_reason(buf, size, pos, cause & RESET_HARDWARE, "Hardware reset"); - pos = append_reset_reason(buf, size, pos, cause & RESET_USER, "User reset"); - pos = append_reset_reason(buf, size, pos, cause & RESET_TEMPERATURE, "Temperature reset"); - - // Ensure null termination if nothing was written - if (pos == 0) { - buf[0] = '\0'; - } - + const char *buf = zephyr::get_reset_reason(buffer); ESP_LOGD(TAG, "Reset Reason: %s", buf); return buf; } diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index b2952d7995..c8f3c52911 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -101,6 +101,8 @@ CONF_INITIAL_LEVEL = "initial_level" CONF_LOGGER_ID = "logger_id" CONF_RUNTIME_TAG_LEVELS = "runtime_tag_levels" CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size" +CONF_WAIT_FOR_CDC = "wait_for_cdc" +CONF_EARLY_MESSAGE = "early_message" UART_SELECTION_ESP32 = { VARIANT_ESP32: [UART0, UART1, UART2], @@ -208,6 +210,12 @@ def validate_initial_no_higher_than_global(config): return config +def validate_wait_for_cdc(config): + if config.get(CONF_WAIT_FOR_CDC) and config.get(CONF_HARDWARE_UART) != USB_CDC: + raise cv.Invalid("wait_for_cdc requires hardware_uart: USB_CDC") + return config + + Logger = logger_ns.class_("Logger", cg.Component) LoggerMessageTrigger = logger_ns.class_( "LoggerMessageTrigger", @@ -300,10 +308,18 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault( CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH, esp8266=True ): cv.All(cv.only_on_esp8266, cv.boolean), + cv.SplitDefault(CONF_WAIT_FOR_CDC, nrf52=False): cv.All( + cv.only_on(PLATFORM_NRF52), + cv.boolean, + ), + cv.SplitDefault(CONF_EARLY_MESSAGE, nrf52=False): cv.All( + cv.only_on(PLATFORM_NRF52), cv.boolean + ), } ).extend(cv.COMPONENT_SCHEMA), validate_local_no_higher_than_global, validate_initial_no_higher_than_global, + validate_wait_for_cdc, ) @@ -425,13 +441,21 @@ async def to_code(config): except cv.Invalid: pass + if config.get(CONF_WAIT_FOR_CDC): + cg.add_define("USE_LOGGER_WAIT_FOR_CDC") + if config.get(CONF_EARLY_MESSAGE): + cg.add_define("USE_LOGGER_EARLY_MESSAGE") + if CORE.is_nrf52: + # esphome implement own fatal error handler which save PC/LR before reset + zephyr_add_prj_conf("RESET_ON_FATAL_ERROR", False) zephyr_add_prj_conf("THREAD_LOCAL_STORAGE", True) if config[CONF_HARDWARE_UART] == UART0: zephyr_add_overlay("""&uart0 { status = "okay";};""") if config[CONF_HARDWARE_UART] == UART1: zephyr_add_overlay("""&uart1 { status = "okay";};""") if config[CONF_HARDWARE_UART] == USB_CDC: + cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC") zephyr_add_prj_conf("UART_LINE_CTRL", True) zephyr_add_cdc_acm(config, 0) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index e1b49bcb61..87963c5fc5 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -261,6 +261,9 @@ void Logger::dump_config() { ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(get_log_level_str(it.second))); } #endif +#ifdef USE_ZEPHYR + dump_crash_(); +#endif } void Logger::set_log_level(uint8_t level) { diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 2a7552af92..c6c379f6c6 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -317,6 +317,7 @@ class Logger : public Component { Stream *hw_serial_{nullptr}; #endif #if defined(USE_ZEPHYR) + void dump_crash_(); const device *uart_dev_{nullptr}; #endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index f565c5760c..d6193ff36b 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -8,9 +8,30 @@ #include #include #include +#ifdef USE_LOGGER_EARLY_MESSAGE +#include +#endif + +namespace esphome::zephyr_coredump { + +__attribute__((weak)) void print_coredump() {} + +} // namespace esphome::zephyr_coredump namespace esphome::logger { +static const uint32_t CRASH_MAGIC = 0xDEADBEEF; + +__attribute__((section(".noinit"))) struct { + uint32_t magic; + uint32_t reason; + uint32_t pc; + uint32_t lr; +#if defined(CONFIG_THREAD_NAME) + char thread[CONFIG_THREAD_MAX_NAME_LEN]; +#endif +} crash_buf; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + static const char *const TAG = "logger"; #ifdef USE_LOGGER_USB_CDC @@ -57,10 +78,26 @@ void Logger::pre_setup() { ESP_LOGE(TAG, "%s is not ready.", LOG_STR_ARG(get_uart_selection_())); } else { this->uart_dev_ = uart_dev; +#if defined(USE_LOGGER_WAIT_FOR_CDC) && defined(USE_LOGGER_UART_SELECTION_USB_CDC) + uint32_t dtr = 0; + uint32_t count = (10 * 100); // wait 10 sec for USB CDC to have early logs + while (dtr == 0 && count-- != 0) { + uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr); + delay(10); + arch_feed_wdt(); + } +#endif } } global_logger = this; ESP_LOGI(TAG, "Log initialized"); +#ifdef USE_LOGGER_EARLY_MESSAGE + char reason_buffer[zephyr::RESET_REASON_BUFFER_SIZE]; + const char *reset_reason = zephyr::get_reset_reason(std::span(reason_buffer)); + ESP_LOGI(TAG, "Reset reason: %s", reset_reason); + dump_crash_(); + zephyr_coredump::print_coredump(); +#endif } void HOT Logger::write_msg_(const char *msg, uint16_t len) { @@ -93,6 +130,66 @@ const LogString *Logger::get_uart_selection_() { } } +static const uint8_t REASON_BUF_SIZE = 32; + +static const char *reason_to_str(unsigned int reason, char *buf) { + switch (reason) { + case K_ERR_CPU_EXCEPTION: + return "CPU exception"; + case K_ERR_SPURIOUS_IRQ: + return "Unhandled interrupt"; + case K_ERR_STACK_CHK_FAIL: + return "Stack overflow"; + case K_ERR_KERNEL_OOPS: + return "Kernel oops"; + case K_ERR_KERNEL_PANIC: + return "Kernel panic"; + default: + snprintf(buf, REASON_BUF_SIZE, "Unknown error (%u)", reason); + return buf; + } +} + +void Logger::dump_crash_() { + ESP_LOGD(TAG, "Crash buffer address %p", &crash_buf); + if (crash_buf.magic == CRASH_MAGIC) { + char reason_buf[REASON_BUF_SIZE]; + ESP_LOGE(TAG, "Last crash:"); + ESP_LOGE(TAG, "Reason=%s PC=0x%08x LR=0x%08x", reason_to_str(crash_buf.reason, reason_buf), crash_buf.pc, + crash_buf.lr); +#if defined(CONFIG_THREAD_NAME) + ESP_LOGE(TAG, "Thread: %s", crash_buf.thread); +#endif + } +} + +void k_sys_fatal_error_handler(unsigned int reason, const z_arch_esf_t *esf) { + crash_buf.magic = CRASH_MAGIC; + crash_buf.reason = reason; + if (esf) { + crash_buf.pc = esf->basic.pc; + crash_buf.lr = esf->basic.lr; + } +#if defined(CONFIG_THREAD_NAME) + auto thread = k_current_get(); + const char *name = k_thread_name_get(thread); + if (name) { + strncpy(crash_buf.thread, name, sizeof(crash_buf.thread) - 1); + crash_buf.thread[sizeof(crash_buf.thread) - 1] = '\0'; + } else { + crash_buf.thread[0] = '\0'; + } +#endif + arch_restart(); +} + } // namespace esphome::logger +extern "C" { + +void k_sys_fatal_error_handler(unsigned int reason, const z_arch_esf_t *esf) { + esphome::logger::k_sys_fatal_error_handler(reason, esf); +} +} + #endif diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 7d3d59f0ad..95e3670124 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -266,6 +266,7 @@ async def to_code(config: ConfigType) -> None: }; """ ) + zephyr_add_prj_conf("REBOOT", True) @coroutine_with_priority(CoroPriority.DIAGNOSTICS) diff --git a/esphome/components/zephyr/gpio.h b/esphome/components/zephyr/gpio.h index 94f25f02ac..907fbe9f9c 100644 --- a/esphome/components/zephyr/gpio.h +++ b/esphome/components/zephyr/gpio.h @@ -3,8 +3,7 @@ #ifdef USE_ZEPHYR #include "esphome/core/hal.h" #include -namespace esphome { -namespace zephyr { +namespace esphome::zephyr { class ZephyrGPIOPin : public InternalGPIOPin { public: @@ -39,7 +38,6 @@ class ZephyrGPIOPin : public InternalGPIOPin { bool value_{false}; }; -} // namespace zephyr -} // namespace esphome +} // namespace esphome::zephyr #endif // USE_ZEPHYR diff --git a/esphome/components/zephyr/preferences.h b/esphome/components/zephyr/preferences.h index 6a37e41b46..4bee96d79e 100644 --- a/esphome/components/zephyr/preferences.h +++ b/esphome/components/zephyr/preferences.h @@ -2,12 +2,10 @@ #ifdef USE_ZEPHYR -namespace esphome { -namespace zephyr { +namespace esphome::zephyr { void setup_preferences(); -} // namespace zephyr -} // namespace esphome +} #endif diff --git a/esphome/components/zephyr/reset_reason.cpp b/esphome/components/zephyr/reset_reason.cpp new file mode 100644 index 0000000000..24b32196db --- /dev/null +++ b/esphome/components/zephyr/reset_reason.cpp @@ -0,0 +1,63 @@ +#include "reset_reason.h" + +#if defined(USE_ZEPHYR) && (defined(USE_LOGGER_EARLY_MESSAGE) || defined(USE_DEBUG)) +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include + +namespace esphome::zephyr { + +static const char *const TAG = "zephyr"; + +static size_t append_reset_reason(char *buf, size_t size, size_t pos, bool set, const char *reason) { + if (!set) { + return pos; + } + if (pos > 0) { + pos = buf_append_printf(buf, size, pos, ", "); + } + return buf_append_printf(buf, size, pos, "%s", reason); +} + +const char *get_reset_reason(std::span buffer) { + char *buf = buffer.data(); + const size_t size = RESET_REASON_BUFFER_SIZE; + + uint32_t cause; + auto ret = hwinfo_get_reset_cause(&cause); + if (ret) { + ESP_LOGE(TAG, "Unable to get reset cause: %d", ret); + buf[0] = '\0'; + return buf; + } + size_t pos = 0; + + if (cause == 0) { + pos = append_reset_reason(buf, size, pos, true, "None"); + } else { + pos = append_reset_reason(buf, size, pos, cause & RESET_PIN, "External pin"); + pos = append_reset_reason(buf, size, pos, cause & RESET_SOFTWARE, "Software reset"); + pos = append_reset_reason(buf, size, pos, cause & RESET_BROWNOUT, "Brownout (drop in voltage)"); + pos = append_reset_reason(buf, size, pos, cause & RESET_POR, "Power-on reset (POR)"); + pos = append_reset_reason(buf, size, pos, cause & RESET_WATCHDOG, "Watchdog timer expiration"); + pos = append_reset_reason(buf, size, pos, cause & RESET_DEBUG, "Debug event"); + pos = append_reset_reason(buf, size, pos, cause & RESET_SECURITY, "Security violation"); + pos = append_reset_reason(buf, size, pos, cause & RESET_LOW_POWER_WAKE, "Waking up from low power mode"); + pos = append_reset_reason(buf, size, pos, cause & RESET_CPU_LOCKUP, "CPU lock-up detected"); + pos = append_reset_reason(buf, size, pos, cause & RESET_PARITY, "Parity error"); + pos = append_reset_reason(buf, size, pos, cause & RESET_PLL, "PLL error"); + pos = append_reset_reason(buf, size, pos, cause & RESET_CLOCK, "Clock error"); + pos = append_reset_reason(buf, size, pos, cause & RESET_HARDWARE, "Hardware reset"); + pos = append_reset_reason(buf, size, pos, cause & RESET_USER, "User reset"); + pos = append_reset_reason(buf, size, pos, cause & RESET_TEMPERATURE, "Temperature reset"); + } + + // Ensure null termination if nothing was written + if (pos == 0) { + buf[0] = '\0'; + } + return buf; +} +} // namespace esphome::zephyr + +#endif diff --git a/esphome/components/zephyr/reset_reason.h b/esphome/components/zephyr/reset_reason.h new file mode 100644 index 0000000000..2c2e7b8470 --- /dev/null +++ b/esphome/components/zephyr/reset_reason.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/core/defines.h" + +#if defined(USE_ZEPHYR) && (defined(USE_LOGGER_EARLY_MESSAGE) || defined(USE_DEBUG)) + +#include +#include + +namespace esphome::zephyr { + +static constexpr size_t RESET_REASON_BUFFER_SIZE = 128; + +const char *get_reset_reason(std::span buffer); + +} // namespace esphome::zephyr + +#endif diff --git a/esphome/config_validation.py b/esphome/config_validation.py index a9d1a72e5a..ef1c66a20e 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -70,6 +70,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_NRF52, PLATFORM_RP2040, SCHEDULER_DONT_RUN, TYPE_GIT, @@ -695,6 +696,7 @@ def only_with_framework( only_on_esp32 = only_on(PLATFORM_ESP32) only_on_esp8266 = only_on(PLATFORM_ESP8266) +only_on_nrf52 = only_on(PLATFORM_NRF52) only_on_rp2040 = only_on(PLATFORM_RP2040) only_with_arduino = only_with_framework(Framework.ARDUINO) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index a7fb9f197c..c82b222a3d 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -334,6 +334,8 @@ #ifdef USE_NRF52 #define USE_ESPHOME_TASK_LOG_BUFFER +#define USE_LOGGER_EARLY_MESSAGE +#define USE_LOGGER_WAIT_FOR_CDC #define USE_NRF52_DFU #define USE_NRF52_REG0_VOUT 5 #define USE_NRF52_UICR_ERASE diff --git a/tests/components/logger/test.nrf52-adafruit.yaml b/tests/components/logger/test.nrf52-adafruit.yaml index 821a136250..062451188f 100644 --- a/tests/components/logger/test.nrf52-adafruit.yaml +++ b/tests/components/logger/test.nrf52-adafruit.yaml @@ -5,4 +5,6 @@ esphome: logger: level: DEBUG + wait_for_cdc: true + early_message: true task_log_buffer_size: 0 From d206c75b0b815c1cdd5d4f0741f0b04f22918138 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 19:20:44 -0600 Subject: [PATCH 118/609] [logger] Fix loop disable optimization using wrong preprocessor guard (#14158) --- esphome/components/logger/logger.cpp | 13 ++++++------- esphome/components/logger/logger.h | 10 +++++----- esphome/components/logger/logger_zephyr.cpp | 2 +- esphome/core/defines.h | 4 ++++ 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 87963c5fc5..22a95e4835 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -170,19 +170,19 @@ void Logger::init_log_buffer(size_t total_buffer_size) { // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size); -// Zephyr needs loop working to check when CDC port is open -#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC)) - // Start with loop disabled when using task buffer (unless using USB CDC on ESP32) +#if !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) + // Start with loop disabled when using task buffer // The loop will be enabled automatically when messages arrive + // Zephyr with USB CDC needs loop active to poll port readiness via cdc_loop_() this->disable_loop_when_buffer_empty_(); #endif } #endif -#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)) +#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void Logger::loop() { this->process_messages_(); -#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC) +#if defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC) this->cdc_loop_(); #endif } @@ -204,8 +204,7 @@ void Logger::process_messages_() { this->write_log_buffer_to_console_(buf); } } -// Zephyr needs loop working to check when CDC port is open -#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC)) +#if !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) else { // No messages to process, disable loop if appropriate // This reduces overhead when there's no async logging activity diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index c6c379f6c6..8bf1edebb8 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -147,7 +147,7 @@ class Logger : public Component { #ifdef USE_ESPHOME_TASK_LOG_BUFFER void init_log_buffer(size_t total_buffer_size); #endif -#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)) +#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void loop() override; #endif /// Manually set the baud rate for serial, set to 0 to disable. @@ -229,7 +229,7 @@ class Logger : public Component { void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args, const char *thread_name); #endif -#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC) +#if defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC) void cdc_loop_(); #endif void process_messages_(); @@ -465,9 +465,9 @@ class Logger : public Component { inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); } #endif -// Zephyr needs loop working to check when CDC port is open -#if defined(USE_ESPHOME_TASK_LOG_BUFFER) && !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC)) - // Disable loop when task buffer is empty (with USB CDC check on ESP32) +#if defined(USE_ESPHOME_TASK_LOG_BUFFER) && !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) + // Disable loop when task buffer is empty + // Zephyr with USB CDC needs loop active to poll port readiness via cdc_loop_() inline void disable_loop_when_buffer_empty_() { // Thread safety note: This is safe even if another task calls enable_loop_soon_any_context() // concurrently. If that happens between our check and disable_loop(), the enable request diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index d6193ff36b..c2d24d6efc 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -34,7 +34,7 @@ __attribute__((section(".noinit"))) struct { static const char *const TAG = "logger"; -#ifdef USE_LOGGER_USB_CDC +#ifdef USE_LOGGER_UART_SELECTION_USB_CDC void Logger::cdc_loop_() { if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) { return; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index c82b222a3d..5109dd36f4 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -270,10 +270,12 @@ #if defined(USE_ESP32_VARIANT_ESP32S2) #define USE_LOGGER_USB_CDC +#define USE_LOGGER_UART_SELECTION_USB_CDC #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \ defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \ defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S3) #define USE_LOGGER_USB_CDC +#define USE_LOGGER_UART_SELECTION_USB_CDC #define USE_LOGGER_USB_SERIAL_JTAG #endif #endif @@ -335,6 +337,8 @@ #ifdef USE_NRF52 #define USE_ESPHOME_TASK_LOG_BUFFER #define USE_LOGGER_EARLY_MESSAGE +#define USE_LOGGER_UART_SELECTION_USB_CDC +#define USE_LOGGER_USB_CDC #define USE_LOGGER_WAIT_FOR_CDC #define USE_NRF52_DFU #define USE_NRF52_REG0_VOUT 5 From 35037d1a5b7c5ad7f4dbabaaa60dab2e7d76af01 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 19:20:58 -0600 Subject: [PATCH 119/609] [core] Deduplicate base64 encode/decode logic (#14143) Co-authored-by: Claude Opus 4.6 --- esphome/core/helpers.cpp | 88 ++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 48 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 09e755ca71..9f850b5df8 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -545,38 +545,36 @@ static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == std::string base64_encode(const std::vector &buf) { return base64_encode(buf.data(), buf.size()); } +// Encode 3 input bytes to 4 base64 characters, append 'count' to ret. +static inline void base64_encode_triple(const char *char_array_3, int count, std::string &ret) { + char char_array_4[4]; + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for (int j = 0; j < count; j++) + ret += BASE64_CHARS[static_cast(char_array_4[j])]; +} + std::string base64_encode(const uint8_t *buf, size_t buf_len) { std::string ret; int i = 0; - int j = 0; char char_array_3[3]; - char char_array_4[4]; while (buf_len--) { char_array_3[i++] = *(buf++); if (i == 3) { - char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; - char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); - char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); - char_array_4[3] = char_array_3[2] & 0x3f; - - for (i = 0; (i < 4); i++) - ret += BASE64_CHARS[static_cast(char_array_4[i])]; + base64_encode_triple(char_array_3, 4, ret); i = 0; } } if (i) { - for (j = i; j < 3; j++) + for (int j = i; j < 3; j++) char_array_3[j] = '\0'; - char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; - char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); - char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); - char_array_4[3] = char_array_3[2] & 0x3f; - - for (j = 0; (j < i + 1); j++) - ret += BASE64_CHARS[static_cast(char_array_4[j])]; + base64_encode_triple(char_array_3, i + 1, ret); while ((i++ < 3)) ret += '='; @@ -589,13 +587,33 @@ size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf return base64_decode(reinterpret_cast(encoded_string.data()), encoded_string.size(), buf, buf_len); } +// Decode 4 base64 characters to up to 'count' output bytes, returns true if truncated. +static inline bool base64_decode_quad(uint8_t *char_array_4, int count, uint8_t *buf, size_t buf_len, size_t &out) { + for (int i = 0; i < 4; i++) + char_array_4[i] = base64_find_char(char_array_4[i]); + + uint8_t char_array_3[3]; + char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; + + bool truncated = false; + for (int j = 0; j < count; j++) { + if (out < buf_len) { + buf[out++] = char_array_3[j]; + } else { + truncated = true; + } + } + return truncated; +} + size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len) { size_t in_len = encoded_len; int i = 0; - int j = 0; size_t in = 0; size_t out = 0; - uint8_t char_array_4[4], char_array_3[3]; + uint8_t char_array_4[4]; bool truncated = false; // SAFETY: The loop condition checks is_base64() before processing each character. @@ -605,42 +623,16 @@ size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *b char_array_4[i++] = encoded_data[in]; in++; if (i == 4) { - for (i = 0; i < 4; i++) - char_array_4[i] = base64_find_char(char_array_4[i]); - - char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); - char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); - char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; - - for (i = 0; i < 3; i++) { - if (out < buf_len) { - buf[out++] = char_array_3[i]; - } else { - truncated = true; - } - } + truncated |= base64_decode_quad(char_array_4, 3, buf, buf_len, out); i = 0; } } if (i) { - for (j = i; j < 4; j++) + for (int j = i; j < 4; j++) char_array_4[j] = 0; - for (j = 0; j < 4; j++) - char_array_4[j] = base64_find_char(char_array_4[j]); - - char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); - char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); - char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; - - for (j = 0; j < i - 1; j++) { - if (out < buf_len) { - buf[out++] = char_array_3[j]; - } else { - truncated = true; - } - } + truncated |= base64_decode_quad(char_array_4, i - 1, buf, buf_len, out); } if (truncated) { From a3f279c1cf150f6908b84df9976e72821995e7b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 19:21:14 -0600 Subject: [PATCH 120/609] [usb_host] Implement disable_loop/enable_loop pattern for USB components (#14163) --- esphome/components/usb_host/usb_host.h | 21 +++++++++++-------- .../components/usb_host/usb_host_client.cpp | 17 ++++++++++++++- esphome/components/usb_uart/usb_uart.cpp | 11 +++++++++- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index d1ec356613..a6a97d0bd7 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -73,12 +73,12 @@ static constexpr UBaseType_t USB_TASK_PRIORITY = 5; // Higher priority than mai // used to report a transfer status struct TransferStatus { - bool success; - uint16_t error_code; uint8_t *data; size_t data_len; - uint8_t endpoint; void *user_data; + uint16_t error_code; + uint8_t endpoint; + bool success; }; using transfer_cb_t = std::function; @@ -127,7 +127,7 @@ class USBClient : public Component { friend class USBHost; public: - USBClient(uint16_t vid, uint16_t pid) : vid_(vid), pid_(pid), trq_in_use_(0) {} + USBClient(uint16_t vid, uint16_t pid) : trq_in_use_(0), vid_(vid), pid_(pid) {} void setup() override; void loop() override; // setup must happen after the host bus has been setup @@ -148,6 +148,10 @@ class USBClient : public Component { EventPool event_pool; protected: + // Process USB events from the queue. Returns true if any work was done. + // Subclasses should call this instead of USBClient::loop() to combine + // with their own work check for a single disable_loop() decision. + bool process_usb_events_(); void handle_open_state_(); TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe) virtual void disconnect(); @@ -161,20 +165,19 @@ class USBClient : public Component { static void usb_task_fn(void *arg); [[noreturn]] void usb_task_loop() const; + // Members ordered to minimize struct padding on 32-bit platforms + TransferRequest requests_[MAX_REQUESTS]{}; TaskHandle_t usb_task_handle_{nullptr}; - usb_host_client_handle_t handle_{}; usb_device_handle_t device_handle_{}; int device_addr_{-1}; int state_{USB_CLIENT_INIT}; - uint16_t vid_{}; - uint16_t pid_{}; // Lock-free pool management using atomic bitmask (no dynamic allocation) // Bit i = 1: requests_[i] is in use, Bit i = 0: requests_[i] is available // Supports multiple concurrent consumers and producers (both threads can allocate/deallocate) - // Bitmask type automatically selected: uint16_t for <= 16 slots, uint32_t for 17-32 slots std::atomic trq_in_use_; - TransferRequest requests_[MAX_REQUESTS]{}; + uint16_t vid_{}; + uint16_t pid_{}; }; class USBHost : public Component { public: diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 0612d7a841..a9be38fb03 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -197,6 +197,9 @@ static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void * // Push to lock-free queue (always succeeds since pool size == queue size) client->event_queue.push(event); + // Re-enable component loop to process the queued event + client->enable_loop_soon_any_context(); + // Wake main loop immediately to process USB event instead of waiting for select() timeout #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); @@ -243,10 +246,13 @@ void USBClient::usb_task_loop() const { } } -void USBClient::loop() { +bool USBClient::process_usb_events_() { + bool had_work = false; + // Process any events from the USB task UsbEvent *event; while ((event = this->event_queue.pop()) != nullptr) { + had_work = true; switch (event->type) { case EVENT_DEVICE_NEW: this->on_opened(event->data.device_new.address); @@ -266,8 +272,17 @@ void USBClient::loop() { } if (this->state_ == USB_CLIENT_OPEN) { + had_work = true; this->handle_open_state_(); } + + return had_work; +} + +void USBClient::loop() { + if (!this->process_usb_events_()) { + this->disable_loop(); + } } void USBClient::handle_open_state_() { diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index edd01c26c6..5c2806c456 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -172,11 +172,12 @@ bool USBUartChannel::read_array(uint8_t *data, size_t len) { } void USBUartComponent::setup() { USBClient::setup(); } void USBUartComponent::loop() { - USBClient::loop(); + bool had_work = this->process_usb_events_(); // Process USB data from the lock-free queue UsbDataChunk *chunk; while ((chunk = this->usb_data_queue_.pop()) != nullptr) { + had_work = true; auto *channel = chunk->channel; #ifdef USE_UART_DEBUGGER @@ -198,6 +199,11 @@ void USBUartComponent::loop() { if (dropped > 0) { ESP_LOGW(TAG, "Dropped %u USB data chunks due to buffer overflow", dropped); } + + // Disable loop when idle. Callbacks re-enable via enable_loop_soon_any_context(). + if (!had_work) { + this->disable_loop(); + } } void USBUartComponent::dump_config() { USBClient::dump_config(); @@ -264,6 +270,9 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // Push always succeeds because pool size == queue size this->usb_data_queue_.push(chunk); + // Re-enable component loop to process the queued data + this->enable_loop_soon_any_context(); + // Wake main loop immediately to process USB data instead of waiting for select() timeout #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); From 0e38acd67a178ca6490284093c7034e8bfde5532 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 19:21:56 -0600 Subject: [PATCH 121/609] [api] Warn when clients connect with outdated API version (#14145) --- esphome/components/api/api_connection.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 5a7994a322..5b02bee537 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1534,6 +1534,12 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) { ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu16 ".%" PRIu16, this->helper_->get_client_name(), this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_); + // TODO: Remove before 2026.8.0 (one version after get_object_id backward compat removal) + if (!this->client_supports_api_version(1, 14)) { + ESP_LOGW(TAG, "'%s' using outdated API %" PRIu16 ".%" PRIu16 ", update to 1.14+", this->helper_->get_client_name(), + this->client_api_version_major_, this->client_api_version_minor_); + } + HelloResponse resp; resp.api_version_major = 1; resp.api_version_minor = 14; From 8589f80d8fea4751aeac15134c91ca79fdf850a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 20:59:49 -0600 Subject: [PATCH 122/609] [api,ota,captive_portal] Fix fd leaks and clean up socket_ip_loop_monitored setup paths (#14167) --- esphome/components/api/api_server.cpp | 30 +++++++++---------- esphome/components/api/api_server.h | 9 +++++- .../captive_portal/dns_server_esp32_idf.cpp | 9 ++---- .../captive_portal/dns_server_esp32_idf.h | 9 ++++-- .../components/esphome/ota/ota_esphome.cpp | 26 +++++++++------- esphome/components/esphome/ota/ota_esphome.h | 3 +- 6 files changed, 49 insertions(+), 37 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 1a1d0b229b..5b096788f5 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -30,6 +30,12 @@ APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-c APIServer::APIServer() { global_api_server = this; } +void APIServer::socket_failed_(const LogString *msg) { + ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno); + this->destroy_socket_(); + this->mark_failed(); +} + void APIServer::setup() { ControllerRegistry::register_controller(this); @@ -48,22 +54,20 @@ void APIServer::setup() { #endif #endif - this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections + this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections if (this->socket_ == nullptr) { - ESP_LOGW(TAG, "Could not create socket"); - this->mark_failed(); + this->socket_failed_(LOG_STR("creation")); return; } int enable = 1; int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); + ESP_LOGW(TAG, "Socket reuseaddr: errno %d", errno); // we can still continue } err = this->socket_->setblocking(false); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); - this->mark_failed(); + this->socket_failed_(LOG_STR("nonblocking")); return; } @@ -71,22 +75,19 @@ void APIServer::setup() { socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); if (sl == 0) { - ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno); - this->mark_failed(); + this->socket_failed_(LOG_STR("set sockaddr")); return; } err = this->socket_->bind((struct sockaddr *) &server, sl); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); - this->mark_failed(); + this->socket_failed_(LOG_STR("bind")); return; } err = this->socket_->listen(this->listen_backlog_); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); - this->mark_failed(); + this->socket_failed_(LOG_STR("listen")); return; } @@ -622,10 +623,7 @@ void APIServer::on_shutdown() { this->shutting_down_ = true; // Close the listening socket to prevent new connections - if (this->socket_) { - this->socket_->close(); - this->socket_ = nullptr; - } + this->destroy_socket_(); // Change batch delay to 5ms for quick flushing during shutdown this->batch_delay_ = 5; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 3b9ba0e23b..fed29016b3 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -249,8 +249,15 @@ class APIServer : public Component, void add_state_subscription_(std::string entity_id, optional attribute, std::function f, bool once); #endif // USE_API_HOMEASSISTANT_STATES + // No explicit close() needed — listen sockets have no active connections on + // failure/shutdown. Destructor handles fd cleanup (close or abort per platform). + inline void destroy_socket_() { + delete this->socket_; + this->socket_ = nullptr; + } + void socket_failed_(const LogString *msg); // Pointers and pointer-like types first (4 bytes each) - std::unique_ptr socket_ = nullptr; + socket::Socket *socket_{nullptr}; #ifdef USE_API_CLIENT_CONNECTED_TRIGGER Trigger client_connected_trigger_; #endif diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.cpp b/esphome/components/captive_portal/dns_server_esp32_idf.cpp index 5743cbd671..bd9989a40c 100644 --- a/esphome/components/captive_portal/dns_server_esp32_idf.cpp +++ b/esphome/components/captive_portal/dns_server_esp32_idf.cpp @@ -53,7 +53,7 @@ void DNSServer::start(const network::IPAddress &ip) { #endif // Create loop-monitored UDP socket - this->socket_ = socket::socket_ip_loop_monitored(SOCK_DGRAM, IPPROTO_UDP); + this->socket_ = socket::socket_ip_loop_monitored(SOCK_DGRAM, IPPROTO_UDP).release(); if (this->socket_ == nullptr) { ESP_LOGE(TAG, "Socket create failed"); return; @@ -70,17 +70,14 @@ void DNSServer::start(const network::IPAddress &ip) { int err = this->socket_->bind((struct sockaddr *) &server_addr, addr_len); if (err != 0) { ESP_LOGE(TAG, "Bind failed: %d", errno); - this->socket_ = nullptr; + this->destroy_socket_(); return; } ESP_LOGV(TAG, "Bound to port %d", DNS_PORT); } void DNSServer::stop() { - if (this->socket_ != nullptr) { - this->socket_->close(); - this->socket_ = nullptr; - } + this->destroy_socket_(); ESP_LOGV(TAG, "Stopped"); } diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.h b/esphome/components/captive_portal/dns_server_esp32_idf.h index 3e0ac07373..f8e4cfec84 100644 --- a/esphome/components/captive_portal/dns_server_esp32_idf.h +++ b/esphome/components/captive_portal/dns_server_esp32_idf.h @@ -1,7 +1,6 @@ #pragma once #ifdef USE_ESP32 -#include #include "esphome/core/helpers.h" #include "esphome/components/network/ip_address.h" #include "esphome/components/socket/socket.h" @@ -15,9 +14,15 @@ class DNSServer { void process_next_request(); protected: + // No explicit close() needed — listen sockets have no active connections on + // failure/shutdown. Destructor handles fd cleanup (close or abort per platform). + inline void destroy_socket_() { + delete this->socket_; + this->socket_ = nullptr; + } static constexpr size_t DNS_BUFFER_SIZE = 192; - std::unique_ptr socket_{nullptr}; + socket::Socket *socket_{nullptr}; network::IPAddress server_ip_; uint8_t buffer_[DNS_BUFFER_SIZE]; }; diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index df2ea98f2c..a1cdf59d2b 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -28,10 +28,9 @@ static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer void ESPHomeOTAComponent::setup() { - this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections + this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections if (this->server_ == nullptr) { - this->log_socket_error_(LOG_STR("creation")); - this->mark_failed(); + this->server_failed_(LOG_STR("creation")); return; } int enable = 1; @@ -42,8 +41,7 @@ void ESPHomeOTAComponent::setup() { } err = this->server_->setblocking(false); if (err != 0) { - this->log_socket_error_(LOG_STR("non-blocking")); - this->mark_failed(); + this->server_failed_(LOG_STR("nonblocking")); return; } @@ -51,22 +49,19 @@ void ESPHomeOTAComponent::setup() { socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); if (sl == 0) { - this->log_socket_error_(LOG_STR("set sockaddr")); - this->mark_failed(); + this->server_failed_(LOG_STR("set sockaddr")); return; } err = this->server_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { - this->log_socket_error_(LOG_STR("bind")); - this->mark_failed(); + this->server_failed_(LOG_STR("bind")); return; } err = this->server_->listen(1); // Only one client at a time if (err != 0) { - this->log_socket_error_(LOG_STR("listen")); - this->mark_failed(); + this->server_failed_(LOG_STR("listen")); return; } } @@ -455,6 +450,15 @@ void ESPHomeOTAComponent::log_remote_closed_(const LogString *during) { ESP_LOGW(TAG, "Remote closed at %s", LOG_STR_ARG(during)); } +void ESPHomeOTAComponent::server_failed_(const LogString *msg) { + this->log_socket_error_(msg); + // No explicit close() needed — listen sockets have no active connections on + // failure/shutdown. Destructor handles fd cleanup (close or abort per platform). + delete this->server_; + this->server_ = nullptr; + this->mark_failed(); +} + bool ESPHomeOTAComponent::handle_read_error_(ssize_t read, const LogString *desc) { if (read == -1 && this->would_block_(errno)) { return false; // No data yet, try again next loop diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index e199b7e406..c9e89c82ba 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -66,6 +66,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { this->handshake_buf_pos_ = 0; // Reset buffer position for next state } + void server_failed_(const LogString *msg); void log_socket_error_(const LogString *msg); void log_read_error_(const LogString *what); void log_start_(const LogString *phase); @@ -83,7 +84,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { std::unique_ptr auth_buf_; #endif // USE_OTA_PASSWORD - std::unique_ptr server_; + socket::Socket *server_{nullptr}; std::unique_ptr client_; std::unique_ptr backend_; From abe37c98413c6b78a7a01bec82f4b2f14e68cd77 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 21:08:49 -0600 Subject: [PATCH 123/609] [uptime] Use scheduler millis_64() for rollover-safe uptime tracking (#14170) --- .../uptime/sensor/uptime_seconds_sensor.cpp | 27 +++++-------------- .../uptime/sensor/uptime_seconds_sensor.h | 9 ++----- .../uptime/sensor/uptime_timestamp_sensor.cpp | 6 ++--- .../uptime/sensor/uptime_timestamp_sensor.h | 6 ++--- .../uptime/text_sensor/uptime_text_sensor.cpp | 24 ++++------------- .../uptime/text_sensor/uptime_text_sensor.h | 8 ++---- esphome/core/scheduler.cpp | 2 ++ esphome/core/scheduler.h | 3 +++ 8 files changed, 24 insertions(+), 61 deletions(-) diff --git a/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp b/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp index 54260d7e80..20e8ed8fda 100644 --- a/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp +++ b/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp @@ -1,30 +1,16 @@ #include "uptime_seconds_sensor.h" -#include "esphome/core/hal.h" -#include "esphome/core/helpers.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { static const char *const TAG = "uptime.sensor"; void UptimeSecondsSensor::update() { - const uint32_t ms = millis(); - const uint64_t ms_mask = (1ULL << 32) - 1ULL; - const uint32_t last_ms = this->uptime_ & ms_mask; - if (ms < last_ms) { - this->uptime_ += ms_mask + 1ULL; - ESP_LOGD(TAG, "Detected roll-over \xf0\x9f\xa6\x84"); - } - this->uptime_ &= ~ms_mask; - this->uptime_ |= ms; - - // Do separate second and milliseconds conversion to avoid floating point division errors - // Probably some IEEE standard already guarantees this division can be done without loss - // of precision in a single division, but let's do it like this to be sure. - const uint64_t seconds_int = this->uptime_ / 1000ULL; - const float seconds = float(seconds_int) + (this->uptime_ % 1000ULL) / 1000.0f; + const uint64_t uptime = App.scheduler.millis_64(); + const uint64_t seconds_int = uptime / 1000ULL; + const float seconds = float(seconds_int) + (uptime % 1000ULL) / 1000.0f; this->publish_state(seconds); } float UptimeSecondsSensor::get_setup_priority() const { return setup_priority::HARDWARE; } @@ -33,5 +19,4 @@ void UptimeSecondsSensor::dump_config() { ESP_LOGCONFIG(TAG, " Type: Seconds"); } -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime diff --git a/esphome/components/uptime/sensor/uptime_seconds_sensor.h b/esphome/components/uptime/sensor/uptime_seconds_sensor.h index 210195052f..1b80a4480a 100644 --- a/esphome/components/uptime/sensor/uptime_seconds_sensor.h +++ b/esphome/components/uptime/sensor/uptime_seconds_sensor.h @@ -3,8 +3,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { class UptimeSecondsSensor : public sensor::Sensor, public PollingComponent { public: @@ -12,10 +11,6 @@ class UptimeSecondsSensor : public sensor::Sensor, public PollingComponent { void dump_config() override; float get_setup_priority() const override; - - protected: - uint64_t uptime_{0}; }; -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime diff --git a/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp b/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp index 69033be11c..4e0f06be1c 100644 --- a/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp +++ b/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp @@ -6,8 +6,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { static const char *const TAG = "uptime.sensor"; @@ -33,7 +32,6 @@ void UptimeTimestampSensor::dump_config() { ESP_LOGCONFIG(TAG, " Type: Timestamp"); } -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime #endif // USE_TIME diff --git a/esphome/components/uptime/sensor/uptime_timestamp_sensor.h b/esphome/components/uptime/sensor/uptime_timestamp_sensor.h index f38b5d53b4..912c0b7655 100644 --- a/esphome/components/uptime/sensor/uptime_timestamp_sensor.h +++ b/esphome/components/uptime/sensor/uptime_timestamp_sensor.h @@ -8,8 +8,7 @@ #include "esphome/components/time/real_time_clock.h" #include "esphome/core/component.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { class UptimeTimestampSensor : public sensor::Sensor, public Component { public: @@ -24,7 +23,6 @@ class UptimeTimestampSensor : public sensor::Sensor, public Component { time::RealTimeClock *time_; }; -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime #endif // USE_TIME diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp index acd3980a1a..88ae53fbfc 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp @@ -1,11 +1,10 @@ #include "uptime_text_sensor.h" -#include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { static const char *const TAG = "uptime.sensor"; @@ -17,22 +16,10 @@ static void append_unit(char *buf, size_t buf_size, size_t &pos, const char *sep pos = buf_append_printf(buf, buf_size, pos, "%u%s", value, label); } -void UptimeTextSensor::setup() { - this->last_ms_ = millis(); - if (this->last_ms_ < 60 * 1000) - this->last_ms_ = 0; - this->update(); -} +void UptimeTextSensor::setup() { this->update(); } void UptimeTextSensor::update() { - auto now = millis(); - // get whole seconds since last update. Note that even if the millis count has overflowed between updates, - // the difference will still be correct due to the way twos-complement arithmetic works. - uint32_t delta = now - this->last_ms_; - this->last_ms_ = now - delta % 1000; // save remainder for next update - delta /= 1000; - this->uptime_ += delta; - uint32_t uptime = this->uptime_; + uint32_t uptime = static_cast(App.scheduler.millis_64() / 1000); unsigned interval = this->get_update_interval() / 1000; // Calculate all time units @@ -89,5 +76,4 @@ void UptimeTextSensor::update() { float UptimeTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } void UptimeTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Uptime Text Sensor", this); } -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.h b/esphome/components/uptime/text_sensor/uptime_text_sensor.h index 947d9c91e9..a97ba332bb 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.h +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.h @@ -5,8 +5,7 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/core/component.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { class UptimeTextSensor : public text_sensor::TextSensor, public PollingComponent { public: @@ -35,9 +34,6 @@ class UptimeTextSensor : public text_sensor::TextSensor, public PollingComponent const char *seconds_text_; const char *separator_; bool expand_{}; - uint32_t uptime_{0}; // uptime in seconds, will overflow after 136 years - uint32_t last_ms_{0}; }; -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 3294f689e8..e82efcc520 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -675,6 +675,8 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type return total_cancelled > 0; } +uint64_t Scheduler::millis_64() { return this->millis_64_(millis()); } + uint64_t Scheduler::millis_64_(uint32_t now) { // THREAD SAFETY NOTE: // This function has three implementations, based on the precompiler flags diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 394178a831..afe11aaca6 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -116,6 +116,9 @@ class Scheduler { ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0") bool cancel_retry(Component *component, uint32_t id); + /// Get 64-bit millisecond timestamp (handles 32-bit millis() rollover) + uint64_t millis_64(); + // Calculate when the next scheduled item should run // @param now Fresh timestamp from millis() - must not be stale/cached // Returns the time in milliseconds until the next scheduled item, or nullopt if no items From f8f98bf428e1db199f4b5bf79a4b3362b7d5672d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 21:16:49 -0600 Subject: [PATCH 124/609] [logger] Reduce UART driver heap waste on ESP32 (#14168) --- esphome/components/logger/logger_esp32.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index dfa643d5e9..9d64771aec 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -77,9 +77,10 @@ void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) { uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; uart_config.source_clk = UART_SCLK_DEFAULT; uart_param_config(uart_num, &uart_config); - const int uart_buffer_size = tx_buffer_size; - // Install UART driver using an event queue here - uart_driver_install(uart_num, uart_buffer_size, uart_buffer_size, 10, nullptr, 0); + // The logger only writes to UART, never reads, so use the minimum RX buffer. + // ESP-IDF requires rx_buffer_size > UART_HW_FIFO_LEN (128 bytes). + const int min_rx_buffer_size = UART_HW_FIFO_LEN(uart_num) + 1; + uart_driver_install(uart_num, min_rx_buffer_size, tx_buffer_size, 0, nullptr, 0); } void Logger::pre_setup() { From f77da803c9b316c78a51e602563dcd6d916682f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 21:39:18 -0600 Subject: [PATCH 125/609] [api] Write protobuf encode output to pre-sized buffer directly (#14018) --- esphome/components/api/api_connection.cpp | 35 ++--- esphome/components/api/api_pb2.cpp | 180 +++++++++++----------- esphome/components/api/api_pb2.h | 176 ++++++++++----------- esphome/components/api/proto.cpp | 15 ++ esphome/components/api/proto.h | 108 ++++++++----- script/api_protobuf/api_protobuf.py | 12 +- 6 files changed, 286 insertions(+), 240 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 5b02bee537..9fc263abbd 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -347,9 +347,7 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess #endif // Calculate size - ProtoSize size_calc; - msg.calculate_size(size_calc); - uint32_t calculated_size = size_calc.get_size(); + uint32_t calculated_size = msg.calculated_size(); // Cache frame sizes to avoid repeated virtual calls const uint8_t header_padding = conn->helper_->frame_header_padding(); @@ -377,19 +375,14 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess shared_buf.resize(current_size + footer_size + header_padding); } - // Encode directly into buffer - size_t size_before_encode = shared_buf.size(); - msg.encode({&shared_buf}); + // Pre-resize buffer to include payload, then encode through raw pointer + size_t write_start = shared_buf.size(); + shared_buf.resize(write_start + calculated_size); + ProtoWriteBuffer buffer{&shared_buf, write_start}; + msg.encode(buffer); - // Calculate actual encoded size (not including header that was already added) - size_t actual_payload_size = shared_buf.size() - size_before_encode; - - // Return actual total size (header + actual payload + footer) - size_t actual_total_size = header_padding + actual_payload_size + footer_size; - - // Verify that calculate_size() returned the correct value - assert(calculated_size == actual_payload_size); - return static_cast(actual_total_size); + // Return total size (header + payload + footer) + return static_cast(header_padding + calculated_size + footer_size); } #ifdef USE_BINARY_SENSOR @@ -1854,12 +1847,14 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { return false; } bool APIConnection::send_message_impl(const ProtoMessage &msg, uint8_t message_type) { - ProtoSize size; - msg.calculate_size(size); + uint32_t payload_size = msg.calculated_size(); std::vector &shared_buf = this->parent_->get_shared_buffer_ref(); - this->prepare_first_message_buffer(shared_buf, size.get_size()); - msg.encode({&shared_buf}); - return this->send_buffer({&shared_buf}, message_type); + this->prepare_first_message_buffer(shared_buf, payload_size); + size_t write_start = shared_buf.size(); + shared_buf.resize(write_start + payload_size); + ProtoWriteBuffer buffer{&shared_buf, write_start}; + msg.encode(buffer); + return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type); } bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 743f51dac7..5c50a8aa5b 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -31,7 +31,7 @@ bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) } return true; } -void HelloResponse::encode(ProtoWriteBuffer buffer) const { +void HelloResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->api_version_major); buffer.encode_uint32(2, this->api_version_minor); buffer.encode_string(3, this->server_info); @@ -44,7 +44,7 @@ void HelloResponse::calculate_size(ProtoSize &size) const { size.add_length(1, this->name.size()); } #ifdef USE_AREAS -void AreaInfo::encode(ProtoWriteBuffer buffer) const { +void AreaInfo::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->area_id); buffer.encode_string(2, this->name); } @@ -54,7 +54,7 @@ void AreaInfo::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_DEVICES -void DeviceInfo::encode(ProtoWriteBuffer buffer) const { +void DeviceInfo::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->device_id); buffer.encode_string(2, this->name); buffer.encode_uint32(3, this->area_id); @@ -65,7 +65,7 @@ void DeviceInfo::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->area_id); } #endif -void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { +void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(2, this->name); buffer.encode_string(3, this->mac_address); buffer.encode_string(4, this->esphome_version); @@ -111,7 +111,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { } #endif #ifdef USE_AREAS - buffer.encode_message(22, this->area); + buffer.encode_message(22, this->area, false); #endif #ifdef USE_ZWAVE_PROXY buffer.encode_uint32(23, this->zwave_proxy_feature_flags); @@ -176,7 +176,7 @@ void DeviceInfoResponse::calculate_size(ProtoSize &size) const { #endif } #ifdef USE_BINARY_SENSOR -void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -206,7 +206,7 @@ void ListEntitiesBinarySensorResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void BinarySensorStateResponse::encode(ProtoWriteBuffer buffer) const { +void BinarySensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -224,7 +224,7 @@ void BinarySensorStateResponse::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_COVER -void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -260,7 +260,7 @@ void ListEntitiesCoverResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { +void CoverStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(3, this->position); buffer.encode_float(4, this->tilt); @@ -317,7 +317,7 @@ bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_FAN -void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -359,7 +359,7 @@ void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void FanStateResponse::encode(ProtoWriteBuffer buffer) const { +void FanStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->oscillating); @@ -443,7 +443,7 @@ bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_LIGHT -void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -489,7 +489,7 @@ void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const { size.add_uint32(2, this->device_id); #endif } -void LightStateResponse::encode(ProtoWriteBuffer buffer) const { +void LightStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_float(3, this->brightness); @@ -635,7 +635,7 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_SENSOR -void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -671,7 +671,7 @@ void ListEntitiesSensorResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void SensorStateResponse::encode(ProtoWriteBuffer buffer) const { +void SensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -689,7 +689,7 @@ void SensorStateResponse::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_SWITCH -void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -719,7 +719,7 @@ void ListEntitiesSwitchResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void SwitchStateResponse::encode(ProtoWriteBuffer buffer) const { +void SwitchStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); #ifdef USE_DEVICES @@ -760,7 +760,7 @@ bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_TEXT_SENSOR -void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -788,7 +788,7 @@ void ListEntitiesTextSensorResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void TextSensorStateResponse::encode(ProtoWriteBuffer buffer) const { +void TextSensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -818,7 +818,7 @@ bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } return true; } -void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const { +void SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast(this->level)); buffer.encode_bytes(3, this->message_ptr_, this->message_len_); } @@ -839,11 +839,11 @@ bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthD } return true; } -void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } +void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->success); } void NoiseEncryptionSetKeyResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); } #endif #ifdef USE_API_HOMEASSISTANT_SERVICES -void HomeassistantServiceMap::encode(ProtoWriteBuffer buffer) const { +void HomeassistantServiceMap::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->key); buffer.encode_string(2, this->value); } @@ -851,7 +851,7 @@ void HomeassistantServiceMap::calculate_size(ProtoSize &size) const { size.add_length(1, this->key.size()); size.add_length(1, this->value.size()); } -void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const { +void HomeassistantActionRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->service); for (auto &it : this->data) { buffer.encode_message(2, it); @@ -924,7 +924,7 @@ bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDe } #endif #ifdef USE_API_HOMEASSISTANT_STATES -void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { +void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->entity_id); buffer.encode_string(2, this->attribute); buffer.encode_bool(3, this->once); @@ -976,7 +976,7 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { return true; } #ifdef USE_API_USER_DEFINED_ACTIONS -void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesServicesArgument::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); buffer.encode_uint32(2, static_cast(this->type)); } @@ -984,7 +984,7 @@ void ListEntitiesServicesArgument::calculate_size(ProtoSize &size) const { size.add_length(1, this->name.size()); size.add_uint32(1, static_cast(this->type)); } -void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); buffer.encode_fixed32(2, this->key); for (auto &it : this->args) { @@ -1103,7 +1103,7 @@ void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) { } #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES -void ExecuteServiceResponse::encode(ProtoWriteBuffer buffer) const { +void ExecuteServiceResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->call_id); buffer.encode_bool(2, this->success); buffer.encode_string(3, this->error_message); @@ -1121,7 +1121,7 @@ void ExecuteServiceResponse::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_CAMERA -void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1147,7 +1147,7 @@ void ListEntitiesCameraResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void CameraImageResponse::encode(ProtoWriteBuffer buffer) const { +void CameraImageResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bytes(2, this->data_ptr_, this->data_len_); buffer.encode_bool(3, this->done); @@ -1178,7 +1178,7 @@ bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } #endif #ifdef USE_CLIMATE -void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1276,7 +1276,7 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { #endif size.add_uint32(2, this->feature_flags); } -void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { +void ClimateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->mode)); buffer.encode_float(3, this->current_temperature); @@ -1407,7 +1407,7 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_WATER_HEATER -void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1449,7 +1449,7 @@ void ListEntitiesWaterHeaterResponse::calculate_size(ProtoSize &size) const { } size.add_uint32(1, this->supported_features); } -void WaterHeaterStateResponse::encode(ProtoWriteBuffer buffer) const { +void WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->current_temperature); buffer.encode_float(3, this->target_temperature); @@ -1515,7 +1515,7 @@ bool WaterHeaterCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value } #endif #ifdef USE_NUMBER -void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1553,7 +1553,7 @@ void ListEntitiesNumberResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void NumberStateResponse::encode(ProtoWriteBuffer buffer) const { +void NumberStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -1596,7 +1596,7 @@ bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_SELECT -void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1630,7 +1630,7 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void SelectStateResponse::encode(ProtoWriteBuffer buffer) const { +void SelectStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -1681,7 +1681,7 @@ bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_SIREN -void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1719,7 +1719,7 @@ void ListEntitiesSirenResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void SirenStateResponse::encode(ProtoWriteBuffer buffer) const { +void SirenStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); #ifdef USE_DEVICES @@ -1789,7 +1789,7 @@ bool SirenCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_LOCK -void ListEntitiesLockResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1823,7 +1823,7 @@ void ListEntitiesLockResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void LockStateResponse::encode(ProtoWriteBuffer buffer) const { +void LockStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->state)); #ifdef USE_DEVICES @@ -1878,7 +1878,7 @@ bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_BUTTON -void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1930,7 +1930,7 @@ bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_MEDIA_PLAYER -void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer buffer) const { +void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->format); buffer.encode_uint32(2, this->sample_rate); buffer.encode_uint32(3, this->num_channels); @@ -1944,7 +1944,7 @@ void MediaPlayerSupportedFormat::calculate_size(ProtoSize &size) const { size.add_uint32(1, static_cast(this->purpose)); size.add_uint32(1, this->sample_bytes); } -void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1978,7 +1978,7 @@ void ListEntitiesMediaPlayerResponse::calculate_size(ProtoSize &size) const { #endif size.add_uint32(1, this->feature_flags); } -void MediaPlayerStateResponse::encode(ProtoWriteBuffer buffer) const { +void MediaPlayerStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->state)); buffer.encode_float(3, this->volume); @@ -2062,7 +2062,7 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, } return true; } -void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer buffer) const { +void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_sint32(2, this->rssi); buffer.encode_uint32(3, this->address_type); @@ -2074,7 +2074,7 @@ void BluetoothLERawAdvertisement::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->address_type); size.add_length(1, this->data_len); } -void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const { for (uint16_t i = 0; i < this->advertisements_len; i++) { buffer.encode_message(1, this->advertisements[i]); } @@ -2103,7 +2103,7 @@ bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) } return true; } -void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->connected); buffer.encode_uint32(3, this->mtu); @@ -2125,7 +2125,7 @@ bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, ProtoVarI } return true; } -void BluetoothGATTDescriptor::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTDescriptor::encode(ProtoWriteBuffer &buffer) const { if (this->uuid[0] != 0 || this->uuid[1] != 0) { buffer.encode_uint64(1, this->uuid[0], true); buffer.encode_uint64(1, this->uuid[1], true); @@ -2141,7 +2141,7 @@ void BluetoothGATTDescriptor::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->handle); size.add_uint32(1, this->short_uuid); } -void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer &buffer) const { if (this->uuid[0] != 0 || this->uuid[1] != 0) { buffer.encode_uint64(1, this->uuid[0], true); buffer.encode_uint64(1, this->uuid[1], true); @@ -2163,7 +2163,7 @@ void BluetoothGATTCharacteristic::calculate_size(ProtoSize &size) const { size.add_repeated_message(1, this->descriptors); size.add_uint32(1, this->short_uuid); } -void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTService::encode(ProtoWriteBuffer &buffer) const { if (this->uuid[0] != 0 || this->uuid[1] != 0) { buffer.encode_uint64(1, this->uuid[0], true); buffer.encode_uint64(1, this->uuid[1], true); @@ -2183,7 +2183,7 @@ void BluetoothGATTService::calculate_size(ProtoSize &size) const { size.add_repeated_message(1, this->characteristics); size.add_uint32(1, this->short_uuid); } -void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); for (auto &it : this->services) { buffer.encode_message(2, it); @@ -2193,7 +2193,7 @@ void BluetoothGATTGetServicesResponse::calculate_size(ProtoSize &size) const { size.add_uint64(1, this->address); size.add_repeated_message(1, this->services); } -void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); } void BluetoothGATTGetServicesDoneResponse::calculate_size(ProtoSize &size) const { size.add_uint64(1, this->address); } @@ -2210,7 +2210,7 @@ bool BluetoothGATTReadRequest::decode_varint(uint32_t field_id, ProtoVarInt valu } return true; } -void BluetoothGATTReadResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTReadResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); buffer.encode_bytes(3, this->data_ptr_, this->data_len_); @@ -2302,7 +2302,7 @@ bool BluetoothGATTNotifyRequest::decode_varint(uint32_t field_id, ProtoVarInt va } return true; } -void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); buffer.encode_bytes(3, this->data_ptr_, this->data_len_); @@ -2312,7 +2312,7 @@ void BluetoothGATTNotifyDataResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->handle); size.add_length(1, this->data_len_); } -void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->free); buffer.encode_uint32(2, this->limit); for (const auto &it : this->allocated) { @@ -2330,7 +2330,7 @@ void BluetoothConnectionsFreeResponse::calculate_size(ProtoSize &size) const { } } } -void BluetoothGATTErrorResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTErrorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); buffer.encode_int32(3, this->error); @@ -2340,7 +2340,7 @@ void BluetoothGATTErrorResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->handle); size.add_int32(1, this->error); } -void BluetoothGATTWriteResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTWriteResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } @@ -2348,7 +2348,7 @@ void BluetoothGATTWriteResponse::calculate_size(ProtoSize &size) const { size.add_uint64(1, this->address); size.add_uint32(1, this->handle); } -void BluetoothGATTNotifyResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTNotifyResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } @@ -2356,7 +2356,7 @@ void BluetoothGATTNotifyResponse::calculate_size(ProtoSize &size) const { size.add_uint64(1, this->address); size.add_uint32(1, this->handle); } -void BluetoothDevicePairingResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothDevicePairingResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->paired); buffer.encode_int32(3, this->error); @@ -2366,7 +2366,7 @@ void BluetoothDevicePairingResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->paired); size.add_int32(1, this->error); } -void BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->success); buffer.encode_int32(3, this->error); @@ -2376,7 +2376,7 @@ void BluetoothDeviceUnpairingResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); size.add_int32(1, this->error); } -void BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->success); buffer.encode_int32(3, this->error); @@ -2386,7 +2386,7 @@ void BluetoothDeviceClearCacheResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); size.add_int32(1, this->error); } -void BluetoothScannerStateResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothScannerStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast(this->state)); buffer.encode_uint32(2, static_cast(this->mode)); buffer.encode_uint32(3, static_cast(this->configured_mode)); @@ -2421,7 +2421,7 @@ bool SubscribeVoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarIn } return true; } -void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->noise_suppression_level); buffer.encode_uint32(2, this->auto_gain); buffer.encode_float(3, this->volume_multiplier); @@ -2431,11 +2431,11 @@ void VoiceAssistantAudioSettings::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->auto_gain); size.add_float(1, this->volume_multiplier); } -void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->start); buffer.encode_string(2, this->conversation_id); buffer.encode_uint32(3, this->flags); - buffer.encode_message(4, this->audio_settings); + buffer.encode_message(4, this->audio_settings, false); buffer.encode_string(5, this->wake_word_phrase); } void VoiceAssistantRequest::calculate_size(ProtoSize &size) const { @@ -2516,7 +2516,7 @@ bool VoiceAssistantAudio::decode_length(uint32_t field_id, ProtoLengthDelimited } return true; } -void VoiceAssistantAudio::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantAudio::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(1, this->data, this->data_len); buffer.encode_bool(2, this->end); } @@ -2587,9 +2587,9 @@ bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLength } return true; } -void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } +void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->success); } void VoiceAssistantAnnounceFinished::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); } -void VoiceAssistantWakeWord::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantWakeWord::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->id); buffer.encode_string(2, this->wake_word); for (auto &it : this->trained_languages) { @@ -2656,7 +2656,7 @@ bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoL } return true; } -void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer &buffer) const { for (auto &it : this->available_wake_words) { buffer.encode_message(1, it); } @@ -2686,7 +2686,7 @@ bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengt } #endif #ifdef USE_ALARM_CONTROL_PANEL -void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -2718,7 +2718,7 @@ void ListEntitiesAlarmControlPanelResponse::calculate_size(ProtoSize &size) cons size.add_uint32(1, this->device_id); #endif } -void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer buffer) const { +void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->state)); #ifdef USE_DEVICES @@ -2770,7 +2770,7 @@ bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit } #endif #ifdef USE_TEXT -void ListEntitiesTextResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -2804,7 +2804,7 @@ void ListEntitiesTextResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void TextStateResponse::encode(ProtoWriteBuffer buffer) const { +void TextStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -2855,7 +2855,7 @@ bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_DATETIME_DATE -void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -2881,7 +2881,7 @@ void ListEntitiesDateResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void DateStateResponse::encode(ProtoWriteBuffer buffer) const { +void DateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_uint32(3, this->year); @@ -2934,7 +2934,7 @@ bool DateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_DATETIME_TIME -void ListEntitiesTimeResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -2960,7 +2960,7 @@ void ListEntitiesTimeResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void TimeStateResponse::encode(ProtoWriteBuffer buffer) const { +void TimeStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_uint32(3, this->hour); @@ -3013,7 +3013,7 @@ bool TimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_EVENT -void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -3049,7 +3049,7 @@ void ListEntitiesEventResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void EventResponse::encode(ProtoWriteBuffer buffer) const { +void EventResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->event_type); #ifdef USE_DEVICES @@ -3065,7 +3065,7 @@ void EventResponse::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_VALVE -void ListEntitiesValveResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -3099,7 +3099,7 @@ void ListEntitiesValveResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void ValveStateResponse::encode(ProtoWriteBuffer buffer) const { +void ValveStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->position); buffer.encode_uint32(3, static_cast(this->current_operation)); @@ -3148,7 +3148,7 @@ bool ValveCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_DATETIME_DATETIME -void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -3174,7 +3174,7 @@ void ListEntitiesDateTimeResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void DateTimeStateResponse::encode(ProtoWriteBuffer buffer) const { +void DateTimeStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_fixed32(3, this->epoch_seconds); @@ -3217,7 +3217,7 @@ bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_UPDATE -void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -3245,7 +3245,7 @@ void ListEntitiesUpdateResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void UpdateStateResponse::encode(ProtoWriteBuffer buffer) const { +void UpdateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_bool(3, this->in_progress); @@ -3314,7 +3314,7 @@ bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited valu } return true; } -void ZWaveProxyFrame::encode(ProtoWriteBuffer buffer) const { buffer.encode_bytes(1, this->data, this->data_len); } +void ZWaveProxyFrame::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(1, this->data, this->data_len); } void ZWaveProxyFrame::calculate_size(ProtoSize &size) const { size.add_length(1, this->data_len); } bool ZWaveProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -3338,7 +3338,7 @@ bool ZWaveProxyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited va } return true; } -void ZWaveProxyRequest::encode(ProtoWriteBuffer buffer) const { +void ZWaveProxyRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast(this->type)); buffer.encode_bytes(2, this->data, this->data_len); } @@ -3348,7 +3348,7 @@ void ZWaveProxyRequest::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_INFRARED -void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -3419,7 +3419,7 @@ bool InfraredRFTransmitRawTimingsRequest::decode_32bit(uint32_t field_id, Proto3 } return true; } -void InfraredRFReceiveEvent::encode(ProtoWriteBuffer buffer) const { +void InfraredRFReceiveEvent::encode(ProtoWriteBuffer &buffer) const { #ifdef USE_DEVICES buffer.encode_uint32(1, this->device_id); #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index d001f869c5..c90873d993 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -382,7 +382,7 @@ class HelloResponse final : public ProtoMessage { uint32_t api_version_minor{0}; StringRef server_info{}; StringRef name{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -447,7 +447,7 @@ class AreaInfo final : public ProtoMessage { public: uint32_t area_id{0}; StringRef name{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -462,7 +462,7 @@ class DeviceInfo final : public ProtoMessage { uint32_t device_id{0}; StringRef name{}; uint32_t area_id{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -527,7 +527,7 @@ class DeviceInfoResponse final : public ProtoMessage { #ifdef USE_ZWAVE_PROXY uint32_t zwave_home_id{0}; #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -558,7 +558,7 @@ class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage { #endif StringRef device_class{}; bool is_status_binary_sensor{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -575,7 +575,7 @@ class BinarySensorStateResponse final : public StateResponseProtoMessage { #endif bool state{false}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -597,7 +597,7 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage { bool supports_tilt{false}; StringRef device_class{}; bool supports_stop{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -615,7 +615,7 @@ class CoverStateResponse final : public StateResponseProtoMessage { float position{0.0f}; float tilt{0.0f}; enums::CoverOperation current_operation{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -657,7 +657,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { bool supports_direction{false}; int32_t supported_speed_count{0}; const std::vector *supported_preset_modes{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -677,7 +677,7 @@ class FanStateResponse final : public StateResponseProtoMessage { enums::FanDirection direction{}; int32_t speed_level{0}; StringRef preset_mode{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -724,7 +724,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage { float min_mireds{0.0f}; float max_mireds{0.0f}; const FixedVector *effects{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -751,7 +751,7 @@ class LightStateResponse final : public StateResponseProtoMessage { float cold_white{0.0f}; float warm_white{0.0f}; StringRef effect{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -815,7 +815,7 @@ class ListEntitiesSensorResponse final : public InfoResponseProtoMessage { bool force_update{false}; StringRef device_class{}; enums::SensorStateClass state_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -832,7 +832,7 @@ class SensorStateResponse final : public StateResponseProtoMessage { #endif float state{0.0f}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -851,7 +851,7 @@ class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage { #endif bool assumed_state{false}; StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -867,7 +867,7 @@ class SwitchStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "switch_state_response"; } #endif bool state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -901,7 +901,7 @@ class ListEntitiesTextSensorResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_text_sensor_response"; } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -918,7 +918,7 @@ class TextSensorStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -957,7 +957,7 @@ class SubscribeLogsResponse final : public ProtoMessage { this->message_ptr_ = data; this->message_len_ = len; } - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -990,7 +990,7 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage { const char *message_name() const override { return "noise_encryption_set_key_response"; } #endif bool success{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1004,7 +1004,7 @@ class HomeassistantServiceMap final : public ProtoMessage { public: StringRef key{}; StringRef value{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1033,7 +1033,7 @@ class HomeassistantActionRequest final : public ProtoMessage { #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON StringRef response_template{}; #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1077,7 +1077,7 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage { StringRef entity_id{}; StringRef attribute{}; bool once{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1138,7 +1138,7 @@ class ListEntitiesServicesArgument final : public ProtoMessage { public: StringRef name{}; enums::ServiceArgType type{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1157,7 +1157,7 @@ class ListEntitiesServicesResponse final : public ProtoMessage { uint32_t key{0}; FixedVector args{}; enums::SupportsResponseType supports_response{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1227,7 +1227,7 @@ class ExecuteServiceResponse final : public ProtoMessage { const uint8_t *response_data{nullptr}; uint16_t response_data_len{0}; #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1244,7 +1244,7 @@ class ListEntitiesCameraResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_camera_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1266,7 +1266,7 @@ class CameraImageResponse final : public StateResponseProtoMessage { this->data_len_ = len; } bool done{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1317,7 +1317,7 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { float visual_min_humidity{0.0f}; float visual_max_humidity{0.0f}; uint32_t feature_flags{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1345,7 +1345,7 @@ class ClimateStateResponse final : public StateResponseProtoMessage { StringRef custom_preset{}; float current_humidity{0.0f}; float target_humidity{0.0f}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1403,7 +1403,7 @@ class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage { float target_temperature_step{0.0f}; const water_heater::WaterHeaterModeMask *supported_modes{}; uint32_t supported_features{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1424,7 +1424,7 @@ class WaterHeaterStateResponse final : public StateResponseProtoMessage { uint32_t state{0}; float target_temperature_low{0.0f}; float target_temperature_high{0.0f}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1468,7 +1468,7 @@ class ListEntitiesNumberResponse final : public InfoResponseProtoMessage { StringRef unit_of_measurement{}; enums::NumberMode mode{}; StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1485,7 +1485,7 @@ class NumberStateResponse final : public StateResponseProtoMessage { #endif float state{0.0f}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1519,7 +1519,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_select_response"; } #endif const FixedVector *options{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1536,7 +1536,7 @@ class SelectStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1573,7 +1573,7 @@ class ListEntitiesSirenResponse final : public InfoResponseProtoMessage { const FixedVector *tones{}; bool supports_duration{false}; bool supports_volume{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1589,7 +1589,7 @@ class SirenStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "siren_state_response"; } #endif bool state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1634,7 +1634,7 @@ class ListEntitiesLockResponse final : public InfoResponseProtoMessage { bool supports_open{false}; bool requires_code{false}; StringRef code_format{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1650,7 +1650,7 @@ class LockStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "lock_state_response"; } #endif enums::LockState state{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1687,7 +1687,7 @@ class ListEntitiesButtonResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_button_response"; } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1719,7 +1719,7 @@ class MediaPlayerSupportedFormat final : public ProtoMessage { uint32_t num_channels{0}; enums::MediaPlayerFormatPurpose purpose{}; uint32_t sample_bytes{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1737,7 +1737,7 @@ class ListEntitiesMediaPlayerResponse final : public InfoResponseProtoMessage { bool supports_pause{false}; std::vector supported_formats{}; uint32_t feature_flags{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1755,7 +1755,7 @@ class MediaPlayerStateResponse final : public StateResponseProtoMessage { enums::MediaPlayerState state{}; float volume{0.0f}; bool muted{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1811,7 +1811,7 @@ class BluetoothLERawAdvertisement final : public ProtoMessage { uint32_t address_type{0}; uint8_t data[62]{}; uint8_t data_len{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1828,7 +1828,7 @@ class BluetoothLERawAdvertisementsResponse final : public ProtoMessage { #endif std::array advertisements{}; uint16_t advertisements_len{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1865,7 +1865,7 @@ class BluetoothDeviceConnectionResponse final : public ProtoMessage { bool connected{false}; uint32_t mtu{0}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1893,7 +1893,7 @@ class BluetoothGATTDescriptor final : public ProtoMessage { std::array uuid{}; uint32_t handle{0}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1908,7 +1908,7 @@ class BluetoothGATTCharacteristic final : public ProtoMessage { uint32_t properties{0}; FixedVector descriptors{}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1922,7 +1922,7 @@ class BluetoothGATTService final : public ProtoMessage { uint32_t handle{0}; FixedVector characteristics{}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1939,7 +1939,7 @@ class BluetoothGATTGetServicesResponse final : public ProtoMessage { #endif uint64_t address{0}; std::vector services{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1955,7 +1955,7 @@ class BluetoothGATTGetServicesDoneResponse final : public ProtoMessage { const char *message_name() const override { return "bluetooth_gatt_get_services_done_response"; } #endif uint64_t address{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1994,7 +1994,7 @@ class BluetoothGATTReadResponse final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2089,7 +2089,7 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2107,7 +2107,7 @@ class BluetoothConnectionsFreeResponse final : public ProtoMessage { uint32_t free{0}; uint32_t limit{0}; std::array allocated{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2125,7 +2125,7 @@ class BluetoothGATTErrorResponse final : public ProtoMessage { uint64_t address{0}; uint32_t handle{0}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2142,7 +2142,7 @@ class BluetoothGATTWriteResponse final : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2159,7 +2159,7 @@ class BluetoothGATTNotifyResponse final : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2177,7 +2177,7 @@ class BluetoothDevicePairingResponse final : public ProtoMessage { uint64_t address{0}; bool paired{false}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2195,7 +2195,7 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage { uint64_t address{0}; bool success{false}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2213,7 +2213,7 @@ class BluetoothDeviceClearCacheResponse final : public ProtoMessage { uint64_t address{0}; bool success{false}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2231,7 +2231,7 @@ class BluetoothScannerStateResponse final : public ProtoMessage { enums::BluetoothScannerState state{}; enums::BluetoothScannerMode mode{}; enums::BluetoothScannerMode configured_mode{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2277,7 +2277,7 @@ class VoiceAssistantAudioSettings final : public ProtoMessage { uint32_t noise_suppression_level{0}; uint32_t auto_gain{0}; float volume_multiplier{0.0f}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2297,7 +2297,7 @@ class VoiceAssistantRequest final : public ProtoMessage { uint32_t flags{0}; VoiceAssistantAudioSettings audio_settings{}; StringRef wake_word_phrase{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2359,7 +2359,7 @@ class VoiceAssistantAudio final : public ProtoDecodableMessage { const uint8_t *data{nullptr}; uint16_t data_len{0}; bool end{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2417,7 +2417,7 @@ class VoiceAssistantAnnounceFinished final : public ProtoMessage { const char *message_name() const override { return "voice_assistant_announce_finished"; } #endif bool success{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2430,7 +2430,7 @@ class VoiceAssistantWakeWord final : public ProtoMessage { StringRef id{}; StringRef wake_word{}; std::vector trained_languages{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2480,7 +2480,7 @@ class VoiceAssistantConfigurationResponse final : public ProtoMessage { std::vector available_wake_words{}; const std::vector *active_wake_words{}; uint32_t max_active_wake_words{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2515,7 +2515,7 @@ class ListEntitiesAlarmControlPanelResponse final : public InfoResponseProtoMess uint32_t supported_features{0}; bool requires_code{false}; bool requires_code_to_arm{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2531,7 +2531,7 @@ class AlarmControlPanelStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "alarm_control_panel_state_response"; } #endif enums::AlarmControlPanelState state{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2570,7 +2570,7 @@ class ListEntitiesTextResponse final : public InfoResponseProtoMessage { uint32_t max_length{0}; StringRef pattern{}; enums::TextMode mode{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2587,7 +2587,7 @@ class TextStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2621,7 +2621,7 @@ class ListEntitiesDateResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_date_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2640,7 +2640,7 @@ class DateStateResponse final : public StateResponseProtoMessage { uint32_t year{0}; uint32_t month{0}; uint32_t day{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2675,7 +2675,7 @@ class ListEntitiesTimeResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_time_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2694,7 +2694,7 @@ class TimeStateResponse final : public StateResponseProtoMessage { uint32_t hour{0}; uint32_t minute{0}; uint32_t second{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2731,7 +2731,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage { #endif StringRef device_class{}; const FixedVector *event_types{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2747,7 +2747,7 @@ class EventResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "event_response"; } #endif StringRef event_type{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2768,7 +2768,7 @@ class ListEntitiesValveResponse final : public InfoResponseProtoMessage { bool assumed_state{false}; bool supports_position{false}; bool supports_stop{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2785,7 +2785,7 @@ class ValveStateResponse final : public StateResponseProtoMessage { #endif float position{0.0f}; enums::ValveOperation current_operation{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2820,7 +2820,7 @@ class ListEntitiesDateTimeResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_date_time_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2837,7 +2837,7 @@ class DateTimeStateResponse final : public StateResponseProtoMessage { #endif bool missing_state{false}; uint32_t epoch_seconds{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2871,7 +2871,7 @@ class ListEntitiesUpdateResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_update_response"; } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2895,7 +2895,7 @@ class UpdateStateResponse final : public StateResponseProtoMessage { StringRef title{}; StringRef release_summary{}; StringRef release_url{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2930,7 +2930,7 @@ class ZWaveProxyFrame final : public ProtoDecodableMessage { #endif const uint8_t *data{nullptr}; uint16_t data_len{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2949,7 +2949,7 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage { enums::ZWaveProxyRequestType type{}; const uint8_t *data{nullptr}; uint16_t data_len{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2969,7 +2969,7 @@ class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_infrared_response"; } #endif uint32_t capabilities{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -3016,7 +3016,7 @@ class InfraredRFReceiveEvent final : public ProtoMessage { #endif uint32_t key{0}; const std::vector *timings{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 764dd3f391..73a3bab12a 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -70,6 +70,21 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size return count; } +#ifdef ESPHOME_DEBUG_API +void ProtoWriteBuffer::debug_check_bounds_(size_t bytes, const char *caller) { + if (this->pos_ + bytes > this->buffer_->data() + this->buffer_->size()) { + ESP_LOGE(TAG, "ProtoWriteBuffer bounds check failed in %s: bytes=%zu offset=%td buf_size=%zu", caller, bytes, + this->pos_ - this->buffer_->data(), this->buffer_->size()); + abort(); + } +} +void ProtoWriteBuffer::debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual) { + ESP_LOGE(TAG, "encode_message: size mismatch for field %" PRIu32 ": calculated=%" PRIu32 " actual=%td", field_id, + expected, actual); + abort(); +} +#endif + void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { const uint8_t *ptr = buffer; const uint8_t *end = buffer + length; diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 8ac79633cf..4522fc9665 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -217,21 +217,26 @@ class Proto32Bit { class ProtoWriteBuffer { public: - ProtoWriteBuffer(std::vector *buffer) : buffer_(buffer) {} - void write(uint8_t value) { this->buffer_->push_back(value); } + ProtoWriteBuffer(std::vector *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {} + ProtoWriteBuffer(std::vector *buffer, size_t write_pos) + : buffer_(buffer), pos_(buffer->data() + write_pos) {} void encode_varint_raw(uint32_t value) { while (value > 0x7F) { - this->buffer_->push_back(static_cast(value | 0x80)); + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value | 0x80); value >>= 7; } - this->buffer_->push_back(static_cast(value)); + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value); } void encode_varint_raw_64(uint64_t value) { while (value > 0x7F) { - this->buffer_->push_back(static_cast(value | 0x80)); + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value | 0x80); value >>= 7; } - this->buffer_->push_back(static_cast(value)); + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value); } /** * Encode a field key (tag/wire type combination). @@ -245,23 +250,18 @@ class ProtoWriteBuffer { * * Following https://protobuf.dev/programming-guides/encoding/#structure */ - void encode_field_raw(uint32_t field_id, uint32_t type) { - uint32_t val = (field_id << 3) | (type & WIRE_TYPE_MASK); - this->encode_varint_raw(val); - } + void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); } void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) { if (len == 0 && !force) return; this->encode_field_raw(field_id, 2); // type 2: Length-delimited string this->encode_varint_raw(len); - - // Using resize + memcpy instead of insert provides significant performance improvement: - // ~10-11x faster for 16-32 byte strings, ~3x faster for 64-byte strings - // as it avoids iterator checks and potential element moves that insert performs - size_t old_size = this->buffer_->size(); - this->buffer_->resize(old_size + len); - std::memcpy(this->buffer_->data() + old_size, string, len); + // Direct memcpy into pre-sized buffer — avoids push_back() per-byte capacity checks + // and vector::insert() iterator overhead. ~10-11x faster for 16-32 byte strings. + this->debug_check_bounds_(len); + std::memcpy(this->pos_, string, len); + this->pos_ += len; } void encode_string(uint32_t field_id, const std::string &value, bool force = false) { this->encode_string(field_id, value.data(), value.size(), force); @@ -288,17 +288,26 @@ class ProtoWriteBuffer { if (!value && !force) return; this->encode_field_raw(field_id, 0); // type 0: Varint - bool - this->buffer_->push_back(value ? 0x01 : 0x00); + this->debug_check_bounds_(1); + *this->pos_++ = value ? 0x01 : 0x00; } - void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { + // noinline: 51 call sites; inlining causes net code growth vs a single out-of-line copy + __attribute__((noinline)) void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { if (value == 0 && !force) return; this->encode_field_raw(field_id, 5); // type 5: 32-bit fixed32 - this->write((value >> 0) & 0xFF); - this->write((value >> 8) & 0xFF); - this->write((value >> 16) & 0xFF); - this->write((value >> 24) & 0xFF); + this->debug_check_bounds_(4); +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + // Protobuf fixed32 is little-endian, so direct copy works + std::memcpy(this->pos_, &value, 4); + this->pos_ += 4; +#else + *this->pos_++ = (value >> 0) & 0xFF; + *this->pos_++ = (value >> 8) & 0xFF; + *this->pos_++ = (value >> 16) & 0xFF; + *this->pos_++ = (value >> 24) & 0xFF; +#endif } // NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally // not supported to reduce overhead on embedded systems. All ESPHome devices are @@ -334,11 +343,20 @@ class ProtoWriteBuffer { } /// Encode a packed repeated sint32 field (zero-copy from vector) void encode_packed_sint32(uint32_t field_id, const std::vector &values); - void encode_message(uint32_t field_id, const ProtoMessage &value); + /// Encode a nested message field (force=true for repeated, false for singular) + void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = true); std::vector *get_buffer() const { return buffer_; } protected: +#ifdef ESPHOME_DEBUG_API + void debug_check_bounds_(size_t bytes, const char *caller = __builtin_FUNCTION()); + void debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual); +#else + void debug_check_bounds_([[maybe_unused]] size_t bytes) {} +#endif + std::vector *buffer_; + uint8_t *pos_; }; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -416,9 +434,11 @@ class ProtoMessage { public: virtual ~ProtoMessage() = default; // Default implementation for messages with no fields - virtual void encode(ProtoWriteBuffer buffer) const {} + virtual void encode(ProtoWriteBuffer &buffer) const {} // Default implementation for messages with no fields virtual void calculate_size(ProtoSize &size) const {} + // Convenience: calculate and return size directly (defined after ProtoSize) + uint32_t calculated_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP virtual const char *dump_to(DumpBuffer &out) const = 0; virtual const char *message_name() const { return "unknown"; } @@ -877,6 +897,14 @@ class ProtoSize { } }; +// Implementation of methods that depend on ProtoSize being fully defined + +inline uint32_t ProtoMessage::calculated_size() const { + ProtoSize size; + this->calculate_size(size); + return size.get_size(); +} + // Implementation of encode_packed_sint32 - must be after ProtoSize is defined inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector &values) { if (values.empty()) @@ -897,30 +925,30 @@ inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std: } // Implementation of encode_message - must be after ProtoMessage is defined -inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) { - this->encode_field_raw(field_id, 2); // type 2: Length-delimited message - +inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) { // Calculate the message size first ProtoSize msg_size; value.calculate_size(msg_size); uint32_t msg_length_bytes = msg_size.get_size(); - // Calculate how many bytes the length varint needs - uint32_t varint_length_bytes = ProtoSize::varint(msg_length_bytes); + // Skip empty singular messages (matches add_message_field which skips when nested_size == 0) + // Repeated messages (force=true) are always encoded since an empty item is meaningful + if (msg_length_bytes == 0 && !force) + return; - // Reserve exact space for the length varint - size_t begin = this->buffer_->size(); - this->buffer_->resize(this->buffer_->size() + varint_length_bytes); + this->encode_field_raw(field_id, 2); // type 2: Length-delimited message - // Write the length varint directly - encode_varint_to_buffer(msg_length_bytes, this->buffer_->data() + begin); - - // Now encode the message content - it will append to the buffer - value.encode(*this); + // Write the length varint directly through pos_ + this->encode_varint_raw(msg_length_bytes); + // Encode nested message - pos_ advances directly through the reference #ifdef ESPHOME_DEBUG_API - // Verify that the encoded size matches what we calculated - assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes); + uint8_t *start = this->pos_; + value.encode(*this); + if (static_cast(this->pos_ - start) != msg_length_bytes) + this->debug_check_encode_size_(field_id, msg_length_bytes, this->pos_ - start); +#else + value.encode(*this); #endif } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 4fbee49dae..cc881caa5c 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -689,6 +689,14 @@ class MessageType(TypeInfo): def encode_func(self) -> str: return "encode_message" + @property + def encode_content(self) -> str: + # Singular message fields pass force=false (skip empty messages) + # The default for encode_nested_message is force=true (for repeated fields) + return ( + f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, false);" + ) + @property def decode_length(self) -> str: # Override to return None for message types because we can't use template-based @@ -2186,7 +2194,7 @@ def build_message_type( # Only generate encode method if this message needs encoding and has fields if needs_encode and encode: - o = f"void {desc.name}::encode(ProtoWriteBuffer buffer) const {{" + o = f"void {desc.name}::encode(ProtoWriteBuffer &buffer) const {{" if len(encode) == 1 and len(encode[0]) + len(o) + 3 < 120: o += f" {encode[0]} }}\n" else: @@ -2194,7 +2202,7 @@ def build_message_type( o += indent("\n".join(encode)) + "\n" o += "}\n" cpp += o - prot = "void encode(ProtoWriteBuffer buffer) const override;" + prot = "void encode(ProtoWriteBuffer &buffer) const override;" public_content.append(prot) # If no fields to encode or message doesn't need encoding, the default implementation in ProtoMessage will be used From 2eac106f119627e2ac58d39f4ebd92990895366a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Sat, 21 Feb 2026 05:20:27 +0100 Subject: [PATCH 126/609] [mqtt] add missing precision in HA autodiscovery (#14010) --- esphome/components/mqtt/mqtt_const.h | 1 + esphome/components/mqtt/mqtt_sensor.cpp | 4 ++++ esphome/components/sensor/sensor.h | 2 ++ 3 files changed, 7 insertions(+) diff --git a/esphome/components/mqtt/mqtt_const.h b/esphome/components/mqtt/mqtt_const.h index 221af00371..36a7dfb41a 100644 --- a/esphome/components/mqtt/mqtt_const.h +++ b/esphome/components/mqtt/mqtt_const.h @@ -243,6 +243,7 @@ X(MQTT_STATE_VALUE_TEMPLATE, "stat_val_tpl", "state_value_template") \ X(MQTT_STEP, "step", "step") \ X(MQTT_SUBTYPE, "stype", "subtype") \ + X(MQTT_SUGGESTED_DISPLAY_PRECISION, "sug_dsp_prc", "suggested_display_precision") \ X(MQTT_SUPPORTED_COLOR_MODES, "sup_clrm", "supported_color_modes") \ X(MQTT_SUPPORTED_FEATURES, "sup_feat", "supported_features") \ X(MQTT_SWING_MODE_COMMAND_TEMPLATE, "swing_mode_cmd_tpl", "swing_mode_command_template") \ diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index e83eab6732..a7d311d194 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -49,6 +49,10 @@ void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_DEVICE_CLASS] = device_class; } + if (this->sensor_->has_accuracy_decimals()) { + root[MQTT_SUGGESTED_DISPLAY_PRECISION] = this->sensor_->get_accuracy_decimals(); + } + const auto unit_of_measurement = this->sensor_->get_unit_of_measurement_ref(); if (!unit_of_measurement.empty()) { root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index f9a45cb1d0..80981b8e28 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -48,6 +48,8 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa int8_t get_accuracy_decimals(); /// Manually set the accuracy in decimals. void set_accuracy_decimals(int8_t accuracy_decimals); + /// Check if the accuracy in decimals has been manually set. + bool has_accuracy_decimals() const { return this->sensor_flags_.has_accuracy_override; } /// Get the state class, using the manual override if set. StateClass get_state_class(); From 518f08b9097bdb3a980483f1358e309987de7402 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 20 Feb 2026 22:51:13 -1000 Subject: [PATCH 127/609] [mipi_dsi] Disallow swap_xy (#14124) --- esphome/components/mipi_dsi/display.py | 26 +++++-------------- .../mipi_dsi/fixtures/mipi_dsi.yaml | 7 ++++- .../mipi_dsi/test_mipi_dsi_config.py | 6 +++-- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/esphome/components/mipi_dsi/display.py b/esphome/components/mipi_dsi/display.py index c288b33cd2..de3791b3a4 100644 --- a/esphome/components/mipi_dsi/display.py +++ b/esphome/components/mipi_dsi/display.py @@ -87,38 +87,24 @@ COLOR_DEPTHS = { def model_schema(config): model = MODELS[config[CONF_MODEL].upper()] + model.defaults[CONF_SWAP_XY] = cv.UNDEFINED transform = cv.Schema( { cv.Required(CONF_MIRROR_X): cv.boolean, cv.Required(CONF_MIRROR_Y): cv.boolean, + cv.Optional(CONF_SWAP_XY): cv.invalid( + "Axis swapping not supported by DSI displays" + ), } ) - if model.get_default(CONF_SWAP_XY) != 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) ) - swap_xy = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY, False) - - # Dimensions are optional if the model has a default width and the swap_xy transform is not overridden - cv_dimensions = ( - cv.Optional if model.get_default(CONF_WIDTH) and not swap_xy else cv.Required - ) + # Dimensions are optional if the model has a default width + cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_24BIT, "16", "24") schema = display.FULL_DISPLAY_SCHEMA.extend( { diff --git a/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml b/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml index 7d1fc84121..6de2bd5a77 100644 --- a/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml +++ b/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml @@ -15,8 +15,13 @@ esp_ldo: display: - platform: mipi_dsi + id: p4_nano model: WAVESHARE-P4-NANO-10.1 - + rotation: 90 + - platform: mipi_dsi + id: p4_86 + model: "WAVESHARE-P4-86-PANEL" + rotation: 180 i2c: sda: GPIO7 scl: GPIO8 diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py index f8a9af0279..d465a8c81b 100644 --- a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -119,9 +119,11 @@ def test_code_generation( main_cpp = generate_main(component_fixture_path("mipi_dsi.yaml")) assert ( - "mipi_dsi_mipi_dsi_id = new mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);" + "p4_nano = new mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);" in main_cpp ) assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp - assert "mipi_dsi_mipi_dsi_id->set_lane_bit_rate(1500);" in main_cpp + assert "p4_nano->set_lane_bit_rate(1500);" in main_cpp + assert "p4_nano->set_rotation(display::DISPLAY_ROTATION_90_DEGREES);" in main_cpp + assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" in main_cpp # assert "backlight_id = new light::LightState(mipi_dsi_dsibacklight_id);" in main_cpp From 6ecb01dedc00595aa2565bcf302cde3d7ad1d4e9 Mon Sep 17 00:00:00 2001 From: Sxt Fov <140247217+sxtfov@users.noreply.github.com> Date: Sat, 21 Feb 2026 14:45:15 +0100 Subject: [PATCH 128/609] [cc1101] actions to change general and tuner settings (#14141) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/cc1101/__init__.py | 92 +++++++++++++++++++++++++ esphome/components/cc1101/cc1101.h | 78 ++++++++++++++++++++++ tests/components/cc1101/common.yaml | 96 +++++++++++++++++++++++++++ 3 files changed, 266 insertions(+) diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index fbdd7010b4..14b92a18a4 100644 --- a/esphome/components/cc1101/__init__.py +++ b/esphome/components/cc1101/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_DATA, CONF_FREQUENCY, CONF_ID, + CONF_VALUE, CONF_WAIT_TIME, ) from esphome.core import ID @@ -333,3 +334,94 @@ async def send_packet_action_to_code(config, action_id, template_arg, args): arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) cg.add(var.set_data_static(arr, len(data))) return var + + +# Setter action definitions: (setter_name, validator, template_type, enum_map) +_SETTER_ACTIONS = [ + ( + "set_frequency", + cv.All(cv.frequency, cv.float_range(min=300.0e6, max=928.0e6)), + float, + None, + ), + ("set_output_power", cv.float_range(min=-30.0, max=11.0), float, None), + ("set_modulation_type", cv.enum(MODULATION, upper=False), Modulation, MODULATION), + ("set_symbol_rate", cv.float_range(min=600, max=500000), float, None), + ( + "set_rx_attenuation", + cv.enum(RX_ATTENUATION, upper=False), + RxAttenuation, + RX_ATTENUATION, + ), + ("set_dc_blocking_filter", cv.boolean, bool, None), + ("set_manchester", cv.boolean, bool, None), + ( + "set_filter_bandwidth", + cv.All(cv.frequency, cv.float_range(min=58000, max=812000)), + float, + None, + ), + ( + "set_fsk_deviation", + cv.All(cv.frequency, cv.float_range(min=1500, max=381000)), + float, + None, + ), + ("set_msk_deviation", cv.int_range(min=1, max=8), cg.uint8, None), + ("set_channel", cv.uint8_t, cg.uint8, None), + ( + "set_channel_spacing", + cv.All(cv.frequency, cv.float_range(min=25000, max=405000)), + float, + None, + ), + ( + "set_if_frequency", + cv.All(cv.frequency, cv.float_range(min=25000, max=788000)), + float, + None, + ), +] + + +def _register_setter_actions(): + for setter_name, validator, templ_type, enum_map in _SETTER_ACTIONS: + class_name = ( + "".join(word.capitalize() for word in setter_name.split("_")) + "Action" + ) + action_cls = ns.class_( + class_name, automation.Action, cg.Parented.template(CC1101Component) + ) + schema = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(CC1101Component), + cv.Required(CONF_VALUE): cv.templatable(validator), + }, + key=CONF_VALUE, + ) + + async def _setter_action_to_code( + config, + action_id, + template_arg, + args, + _setter=setter_name, + _type=templ_type, + _map=enum_map, + ): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + data = config[CONF_VALUE] + if cg.is_template(data): + templ_ = await cg.templatable(data, args, _type) + cg.add(getattr(var, _setter)(templ_)) + else: + cg.add(getattr(var, _setter)(_map[data] if _map else data)) + return var + + automation.register_action(f"cc1101.{setter_name}", action_cls, schema)( + _setter_action_to_code + ) + + +_register_setter_actions() diff --git a/esphome/components/cc1101/cc1101.h b/esphome/components/cc1101/cc1101.h index e55071e7e3..2efd9e082d 100644 --- a/esphome/components/cc1101/cc1101.h +++ b/esphome/components/cc1101/cc1101.h @@ -161,4 +161,82 @@ template class SendPacketAction : public Action, public P size_t data_static_len_{0}; }; +template class SetSymbolRateAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, symbol_rate) + void play(const Ts &...x) override { this->parent_->set_symbol_rate(this->symbol_rate_.value(x...)); } +}; + +template class SetFrequencyAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, frequency) + void play(const Ts &...x) override { this->parent_->set_frequency(this->frequency_.value(x...)); } +}; + +template class SetOutputPowerAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, output_power) + void play(const Ts &...x) override { this->parent_->set_output_power(this->output_power_.value(x...)); } +}; + +template class SetModulationTypeAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(Modulation, modulation_type) + void play(const Ts &...x) override { this->parent_->set_modulation_type(this->modulation_type_.value(x...)); } +}; + +template class SetRxAttenuationAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(RxAttenuation, rx_attenuation) + void play(const Ts &...x) override { this->parent_->set_rx_attenuation(this->rx_attenuation_.value(x...)); } +}; + +template class SetDcBlockingFilterAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(bool, dc_blocking_filter) + void play(const Ts &...x) override { this->parent_->set_dc_blocking_filter(this->dc_blocking_filter_.value(x...)); } +}; + +template class SetManchesterAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(bool, manchester) + void play(const Ts &...x) override { this->parent_->set_manchester(this->manchester_.value(x...)); } +}; + +template class SetFilterBandwidthAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, filter_bandwidth) + void play(const Ts &...x) override { this->parent_->set_filter_bandwidth(this->filter_bandwidth_.value(x...)); } +}; + +template class SetFskDeviationAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, fsk_deviation) + void play(const Ts &...x) override { this->parent_->set_fsk_deviation(this->fsk_deviation_.value(x...)); } +}; + +template class SetMskDeviationAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, msk_deviation) + void play(const Ts &...x) override { this->parent_->set_msk_deviation(this->msk_deviation_.value(x...)); } +}; + +template class SetChannelAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, channel) + void play(const Ts &...x) override { this->parent_->set_channel(this->channel_.value(x...)); } +}; + +template class SetChannelSpacingAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, channel_spacing) + void play(const Ts &...x) override { this->parent_->set_channel_spacing(this->channel_spacing_.value(x...)); } +}; + +template class SetIfFrequencyAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, if_frequency) + void play(const Ts &...x) override { this->parent_->set_if_frequency(this->if_frequency_.value(x...)); } +}; + } // namespace esphome::cc1101 diff --git a/tests/components/cc1101/common.yaml b/tests/components/cc1101/common.yaml index 42ec50911f..9784bfce8b 100644 --- a/tests/components/cc1101/common.yaml +++ b/tests/components/cc1101/common.yaml @@ -35,3 +35,99 @@ button: data: [0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef] - cc1101.send_packet: !lambda |- return {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; + + - cc1101.set_frequency: !lambda |- + return 433.91e6; + - cc1101.set_frequency: + value: "433.91MHz" + - cc1101.set_frequency: + value: 433911000 + - cc1101.set_frequency: 433912000 + + - cc1101.set_output_power: !lambda |- + return -29.9; + - cc1101.set_output_power: + value: "-28" + - cc1101.set_output_power: + value: 10 + - cc1101.set_output_power: 11 + + - cc1101.set_modulation_type: !lambda |- + return cc1101::Modulation::MODULATION_2_FSK; + - cc1101.set_modulation_type: + value: "4-FSK" + - cc1101.set_modulation_type: "GFSK" + + - cc1101.set_symbol_rate: !lambda |- + return 6000.0; + - cc1101.set_symbol_rate: + value: "7000.0" + - cc1101.set_symbol_rate: + value: 8000.0 + - cc1101.set_symbol_rate: 9000 + + - cc1101.set_rx_attenuation: !lambda |- + return cc1101::RxAttenuation::RX_ATTENUATION_0DB; + - cc1101.set_rx_attenuation: + value: "6dB" + - cc1101.set_rx_attenuation: "12dB" + + - cc1101.set_dc_blocking_filter: !lambda |- + return false; + - cc1101.set_dc_blocking_filter: + value: true + - cc1101.set_dc_blocking_filter: false + + - cc1101.set_manchester: !lambda |- + return false; + - cc1101.set_manchester: + value: true + - cc1101.set_manchester: false + + - cc1101.set_filter_bandwidth: !lambda |- + return 58e3; + - cc1101.set_filter_bandwidth: + value: "59kHz" + - cc1101.set_filter_bandwidth: + value: 60000 + - cc1101.set_filter_bandwidth: "61kHz" + + - cc1101.set_fsk_deviation: !lambda |- + return 1.5e3; + - cc1101.set_fsk_deviation: + value: "1.6kHz" + - cc1101.set_fsk_deviation: + value: 1700 + - cc1101.set_fsk_deviation: "1.8kHz" + + - cc1101.set_msk_deviation: !lambda |- + return 1; + - cc1101.set_msk_deviation: + value: "2" + - cc1101.set_msk_deviation: + value: 3 + - cc1101.set_msk_deviation: "4" + + - cc1101.set_channel: !lambda |- + return 0; + - cc1101.set_channel: + value: "1" + - cc1101.set_channel: + value: 3 + - cc1101.set_channel: 3 + + - cc1101.set_channel_spacing: !lambda |- + return 25e3; + - cc1101.set_channel_spacing: + value: "26kHz" + - cc1101.set_channel_spacing: + value: 27000 + - cc1101.set_channel_spacing: "28kHz" + + - cc1101.set_if_frequency: !lambda |- + return 25e3; + - cc1101.set_if_frequency: + value: "26kHz" + - cc1101.set_if_frequency: + value: 27000 + - cc1101.set_if_frequency: "28kHz" From 7fb09da7cf228b3a5d2bbdfd60b3e6fb571b0331 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 11:08:13 -0600 Subject: [PATCH 129/609] [dsmr] Add deprecated std::string overload for set_decryption_key (#14180) --- esphome/components/dsmr/dsmr.h | 3 +++ tests/components/dsmr/common.yaml | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h index fafcf62b87..dc81ba9b2a 100644 --- a/esphome/components/dsmr/dsmr.h +++ b/esphome/components/dsmr/dsmr.h @@ -64,6 +64,9 @@ class Dsmr : public Component, public uart::UARTDevice { void dump_config() override; void set_decryption_key(const char *decryption_key); + // Remove before 2026.8.0 + ESPDEPRECATED("Pass .c_str() - e.g. set_decryption_key(key.c_str()). Removed in 2026.8.0", "2026.2.0") + void set_decryption_key(const std::string &decryption_key) { this->set_decryption_key(decryption_key.c_str()); } void set_max_telegram_length(size_t length) { this->max_telegram_len_ = length; } void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; } void set_request_interval(uint32_t interval) { this->request_interval_ = interval; } diff --git a/tests/components/dsmr/common.yaml b/tests/components/dsmr/common.yaml index 038bf2806b..d11ce37d59 100644 --- a/tests/components/dsmr/common.yaml +++ b/tests/components/dsmr/common.yaml @@ -1,4 +1,13 @@ +esphome: + on_boot: + then: + - lambda: |- + // Test deprecated std::string overload still compiles + std::string key = "00112233445566778899aabbccddeeff"; + id(dsmr_instance).set_decryption_key(key); + dsmr: + id: dsmr_instance decryption_key: 00112233445566778899aabbccddeeff max_telegram_length: 1000 request_pin: ${request_pin} From 416b97311b800e9563e48da0e403ed22e717ff61 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 11:12:35 -0600 Subject: [PATCH 130/609] [mqtt] Remove broken ESP8266 ssl_fingerprints option (#14182) --- esphome/__main__.py | 14 --------- esphome/components/mqtt/__init__.py | 21 -------------- .../components/mqtt/mqtt_backend_esp8266.h | 5 ---- .../components/mqtt/mqtt_backend_libretiny.h | 5 ---- esphome/components/mqtt/mqtt_client.cpp | 7 ----- esphome/components/mqtt/mqtt_client.h | 15 ---------- esphome/const.py | 1 - esphome/mqtt.py | 29 ++----------------- 8 files changed, 2 insertions(+), 95 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index c86b5604e1..488955f503 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -944,12 +944,6 @@ def command_clean_all(args: ArgsProtocol) -> int | None: return 0 -def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None: - from esphome import mqtt - - return mqtt.get_fingerprint(config) - - def command_version(args: ArgsProtocol) -> int | None: safe_print(f"Version: {const.__version__}") return 0 @@ -1237,7 +1231,6 @@ POST_CONFIG_ACTIONS = { "run": command_run, "clean": command_clean, "clean-mqtt": command_clean_mqtt, - "mqtt-fingerprint": command_mqtt_fingerprint, "idedata": command_idedata, "rename": command_rename, "discover": command_discover, @@ -1451,13 +1444,6 @@ def parse_args(argv): ) parser_wizard.add_argument("configuration", help="Your YAML configuration file.") - parser_fingerprint = subparsers.add_parser( - "mqtt-fingerprint", help="Get the SSL fingerprint from a MQTT broker." - ) - parser_fingerprint.add_argument( - "configuration", help="Your YAML configuration file(s).", nargs="+" - ) - subparsers.add_parser("version", help="Print the ESPHome version and exit.") parser_clean = subparsers.add_parser( diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index fe153fedfa..44e8836487 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -1,5 +1,3 @@ -import re - from esphome import automation from esphome.automation import Condition import esphome.codegen as cg @@ -46,7 +44,6 @@ from esphome.const import ( CONF_RETAIN, CONF_SHUTDOWN_MESSAGE, CONF_SKIP_CERT_CN_CHECK, - CONF_SSL_FINGERPRINTS, CONF_STATE_TOPIC, CONF_SUBSCRIBE_QOS, CONF_TOPIC, @@ -221,13 +218,6 @@ def validate_config(value): return out -def validate_fingerprint(value): - value = cv.string(value) - if re.match(r"^[0-9a-f]{40}$", value) is None: - raise cv.Invalid("fingerprint must be valid SHA1 hash") - return value - - def _consume_mqtt_sockets(config: ConfigType) -> ConfigType: """Register socket needs for MQTT component.""" # MQTT needs 1 socket for the broker connection @@ -291,9 +281,6 @@ CONFIG_SCHEMA = cv.All( ), validate_message_just_topic, ), - cv.Optional(CONF_SSL_FINGERPRINTS): cv.All( - cv.only_on_esp8266, cv.ensure_list(validate_fingerprint) - ), cv.Optional(CONF_KEEPALIVE, default="15s"): cv.positive_time_period_seconds, cv.Optional( CONF_REBOOT_TIMEOUT, default="15min" @@ -444,14 +431,6 @@ async def to_code(config): if CONF_LEVEL in log_topic: cg.add(var.set_log_level(logger.LOG_LEVELS[log_topic[CONF_LEVEL]])) - if CONF_SSL_FINGERPRINTS in config: - for fingerprint in config[CONF_SSL_FINGERPRINTS]: - arr = [ - cg.RawExpression(f"0x{fingerprint[i : i + 2]}") for i in range(0, 40, 2) - ] - cg.add(var.add_ssl_fingerprint(arr)) - cg.add_build_flag("-DASYNC_TCP_SSL_ENABLED=1") - cg.add(var.set_keep_alive(config[CONF_KEEPALIVE])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) diff --git a/esphome/components/mqtt/mqtt_backend_esp8266.h b/esphome/components/mqtt/mqtt_backend_esp8266.h index 470d1e6a8b..0bf5b510a4 100644 --- a/esphome/components/mqtt/mqtt_backend_esp8266.h +++ b/esphome/components/mqtt/mqtt_backend_esp8266.h @@ -21,11 +21,6 @@ class MQTTBackendESP8266 final : public MQTTBackend { } void set_server(network::IPAddress ip, uint16_t port) final { mqtt_client_.setServer(ip, port); } void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); } -#if ASYNC_TCP_SSL_ENABLED - void set_secure(bool secure) { mqtt_client.setSecure(secure); } - void add_server_fingerprint(const uint8_t *fingerprint) { mqtt_client.addServerFingerprint(fingerprint); } -#endif - void set_on_connect(std::function &&callback) final { this->mqtt_client_.onConnect(std::move(callback)); } diff --git a/esphome/components/mqtt/mqtt_backend_libretiny.h b/esphome/components/mqtt/mqtt_backend_libretiny.h index 24bf018a90..5fa3406193 100644 --- a/esphome/components/mqtt/mqtt_backend_libretiny.h +++ b/esphome/components/mqtt/mqtt_backend_libretiny.h @@ -21,11 +21,6 @@ class MQTTBackendLibreTiny final : public MQTTBackend { } void set_server(network::IPAddress ip, uint16_t port) final { mqtt_client_.setServer(IPAddress(ip), port); } void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); } -#if ASYNC_TCP_SSL_ENABLED - void set_secure(bool secure) { mqtt_client.setSecure(secure); } - void add_server_fingerprint(const uint8_t *fingerprint) { mqtt_client.addServerFingerprint(fingerprint); } -#endif - void set_on_connect(std::function &&callback) final { this->mqtt_client_.onConnect(std::move(callback)); } diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index c433804dd9..1a03c5329e 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -749,13 +749,6 @@ void MQTTClientComponent::set_on_disconnect(mqtt_on_disconnect_callback_t &&call this->on_disconnect_.add(std::move(callback_copy)); } -#if ASYNC_TCP_SSL_ENABLED -void MQTTClientComponent::add_ssl_fingerprint(const std::array &fingerprint) { - this->mqtt_backend_.setSecure(true); - this->mqtt_backend_.addServerFingerprint(fingerprint.data()); -} -#endif - MQTTClientComponent *global_mqtt_client = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) // MQTTMessageTrigger diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 21edd53eda..127e4073b0 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -137,21 +137,6 @@ class MQTTClientComponent : public Component { bool is_discovery_enabled() const; bool is_discovery_ip_enabled() const; -#if ASYNC_TCP_SSL_ENABLED - /** Add a SSL fingerprint to use for TCP SSL connections to the MQTT broker. - * - * To use this feature you first have to globally enable the `ASYNC_TCP_SSL_ENABLED` define flag. - * This function can be called multiple times and any certificate that matches any of the provided fingerprints - * will match. Calling this method will also automatically disable all non-ssl connections. - * - * @warning This is *not* secure and *not* how SSL is usually done. You'll have to add - * a separate fingerprint for every certificate you use. Additionally, the hashing - * algorithm used here due to the constraints of the MCU, SHA1, is known to be insecure. - * - * @param fingerprint The SSL fingerprint as a 20 value long std::array. - */ - void add_ssl_fingerprint(const std::array &fingerprint); -#endif #ifdef USE_ESP32 void set_ca_certificate(const char *cert) { this->mqtt_backend_.set_ca_certificate(cert); } void set_cl_certificate(const char *cert) { this->mqtt_backend_.set_cl_certificate(cert); } diff --git a/esphome/const.py b/esphome/const.py index f72cbc8893..0179aa129e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -943,7 +943,6 @@ CONF_SPI = "spi" CONF_SPI_ID = "spi_id" CONF_SPIKE_REJECTION = "spike_rejection" CONF_SSID = "ssid" -CONF_SSL_FINGERPRINTS = "ssl_fingerprints" CONF_STARTUP_DELAY = "startup_delay" CONF_STATE = "state" CONF_STATE_CLASS = "state_class" diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 042df12d67..cbf78bd3f6 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -1,6 +1,5 @@ import contextlib from datetime import datetime -import hashlib import json import logging import ssl @@ -22,14 +21,12 @@ from esphome.const import ( CONF_PASSWORD, CONF_PORT, CONF_SKIP_CERT_CN_CHECK, - CONF_SSL_FINGERPRINTS, CONF_TOPIC, CONF_TOPIC_PREFIX, CONF_USERNAME, ) -from esphome.core import CORE, EsphomeError +from esphome.core import EsphomeError from esphome.helpers import get_int_env, get_str_env -from esphome.log import AnsiFore, color from esphome.types import ConfigType from esphome.util import safe_print @@ -102,9 +99,7 @@ def prepare( elif username: client.username_pw_set(username, password) - if config[CONF_MQTT].get(CONF_SSL_FINGERPRINTS) or config[CONF_MQTT].get( - CONF_CERTIFICATE_AUTHORITY - ): + if config[CONF_MQTT].get(CONF_CERTIFICATE_AUTHORITY): context = ssl.create_default_context( cadata=config[CONF_MQTT].get(CONF_CERTIFICATE_AUTHORITY) ) @@ -283,23 +278,3 @@ def clear_topic(config, topic, username=None, password=None, client_id=None): client.publish(msg.topic, None, retain=True) return initialize(config, [topic], on_message, None, username, password, client_id) - - -# From marvinroger/async-mqtt-client -> scripts/get-fingerprint/get-fingerprint.py -def get_fingerprint(config): - addr = str(config[CONF_MQTT][CONF_BROKER]), int(config[CONF_MQTT][CONF_PORT]) - _LOGGER.info("Getting fingerprint from %s:%s", addr[0], addr[1]) - try: - cert_pem = ssl.get_server_certificate(addr) - except OSError as err: - _LOGGER.error("Unable to connect to server: %s", err) - return 1 - cert_der = ssl.PEM_cert_to_DER_cert(cert_pem) - - sha1 = hashlib.sha1(cert_der).hexdigest() - - safe_print(f"SHA1 Fingerprint: {color(AnsiFore.CYAN, sha1)}") - safe_print( - f"Copy the string above into mqtt.ssl_fingerprints section of {CORE.config_path}" - ) - return 0 From 6ff17fbf7c3d4b24ae0e20103c0828823d49b1dc Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 21 Feb 2026 07:17:54 -1000 Subject: [PATCH 131/609] [epaper_spi] Fix color mapping for weact (#14134) --- esphome/components/epaper_spi/epaper_spi.h | 2 +- .../components/epaper_spi/epaper_weact_3c.cpp | 43 ++++++++++++------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h index a8c2fe9b56..a743985518 100644 --- a/esphome/components/epaper_spi/epaper_spi.h +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -76,7 +76,7 @@ class EPaperBase : public Display, static uint8_t color_to_bit(Color color) { // It's always a shade of gray. Map to BLACK or WHITE. // We split the luminance at a suitable point - if ((static_cast(color.r) + color.g + color.b) > 512) { + if ((color.r + color.g + color.b) >= 382) { return 1; } return 0; diff --git a/esphome/components/epaper_spi/epaper_weact_3c.cpp b/esphome/components/epaper_spi/epaper_weact_3c.cpp index bd83105dd7..d4dac7076c 100644 --- a/esphome/components/epaper_spi/epaper_weact_3c.cpp +++ b/esphome/components/epaper_spi/epaper_weact_3c.cpp @@ -5,9 +5,24 @@ namespace esphome::epaper_spi { static constexpr const char *const TAG = "epaper_weact_3c"; +enum class BwrState : uint8_t { + BWR_BLACK, + BWR_WHITE, + BWR_RED, +}; + +static BwrState color_to_bwr(Color color) { + if (color.r > color.g + color.b && color.r > 127) { + return BwrState::BWR_RED; + } + if (color.r + color.g + color.b >= 382) { + return BwrState::BWR_WHITE; + } + return BwrState::BWR_BLACK; +} // SSD1680 3-color display notes: // - Buffer uses 1 bit per pixel, 8 pixels per byte -// - Buffer first half (black_offset): Black/White plane (1=black, 0=white) +// - Buffer first half (black_offset): Black/White plane (0=black, 1=white) // - Buffer second half (red_offset): Red plane (1=red, 0=no red) // - Total buffer: width * height / 4 bytes = 2 * (width * height / 8) // - For 128x296: 128*296/4 = 9472 bytes total (4736 per color) @@ -23,20 +38,20 @@ void EPaperWeAct3C::draw_pixel_at(int x, int y, Color color) { // Use luminance threshold for B/W mapping // Split at halfway point (382 = (255*3)/2) - bool is_white = (static_cast(color.r) + color.g + color.b) > 382; + auto bwr = color_to_bwr(color); // Update black/white plane (first half of buffer) - if (is_white) { - // White pixel - clear bit in black plane - this->buffer_[pos] &= ~bit; - } else { - // Black pixel - set bit in black plane + if (bwr == BwrState::BWR_WHITE) { + // White pixel - set bit in black plane this->buffer_[pos] |= bit; + } else { + // Black pixel - clear bit in black plane + this->buffer_[pos] &= ~bit; } // Update red plane (second half of buffer) // Red if red component is dominant (r > g+b) - if (color.r > color.g + color.b) { + if (bwr == BwrState::BWR_RED) { // Red pixel - set bit in red plane this->buffer_[red_offset + pos] |= bit; } else { @@ -53,21 +68,20 @@ void EPaperWeAct3C::fill(Color color) { const size_t half_buffer = this->buffer_length_ / 2u; // Use luminance threshold for B/W mapping - bool is_white = (static_cast(color.r) + color.g + color.b) > 382; - bool is_red = color.r > color.g + color.b; + auto bits = color_to_bwr(color); // Fill both planes - if (is_white) { - // White - both planes = 0x00 + if (bits == BwrState::BWR_BLACK) { + // Black - both planes = 0x00 this->buffer_.fill(0x00); - } else if (is_red) { + } else if (bits == BwrState::BWR_RED) { // Red - black plane = 0x00, red plane = 0xFF for (size_t i = 0; i < half_buffer; i++) this->buffer_[i] = 0x00; for (size_t i = 0; i < half_buffer; i++) this->buffer_[half_buffer + i] = 0xFF; } else { - // Black - black plane = 0xFF, red plane = 0x00 + // White - black plane = 0xFF, red plane = 0x00 for (size_t i = 0; i < half_buffer; i++) this->buffer_[i] = 0xFF; for (size_t i = 0; i < half_buffer; i++) @@ -112,7 +126,6 @@ bool HOT EPaperWeAct3C::transfer_data() { ESP_LOGV(TAG, "transfer_data: buffer_length=%u, half_buffer=%u", buffer_length, half_buffer); // Use a local buffer for SPI transfers - static constexpr size_t MAX_TRANSFER_SIZE = 128; uint8_t bytes_to_send[MAX_TRANSFER_SIZE]; // First, send the RED buffer (0x26 = WRITE_COLOR) From 48ba007c22d7c542e9de8740aacb8c94ac438a10 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Sat, 21 Feb 2026 20:16:20 +0100 Subject: [PATCH 132/609] [nrf52] print line number after crash in logs (#14165) --- esphome/__main__.py | 19 ++++++++++--- esphome/components/nrf52/__init__.py | 40 ++++++++++++++++++++++++++++ tests/unit_tests/test_main.py | 6 +++++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 488955f503..ffedb90bde 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -431,6 +431,14 @@ def run_miniterm(config: ConfigType, port: str, args) -> int: return 1 _LOGGER.info("Starting log output from %s with baud rate %s", port, baud_rate) + process_stacktrace = None + + try: + module = importlib.import_module("esphome.components." + CORE.target_platform) + process_stacktrace = getattr(module, "process_stacktrace") + except AttributeError: + pass + backtrace_state = False ser = serial.Serial() ser.baudrate = baud_rate @@ -472,9 +480,14 @@ def run_miniterm(config: ConfigType, port: str, args) -> int: ) safe_print(parser.parse_line(line, time_str)) - backtrace_state = platformio_api.process_stacktrace( - config, line, backtrace_state=backtrace_state - ) + if process_stacktrace: + backtrace_state = process_stacktrace( + config, line, backtrace_state + ) + else: + backtrace_state = platformio_api.process_stacktrace( + config, line, backtrace_state=backtrace_state + ) except serial.SerialException: _LOGGER.error("Serial port closed!") return 0 diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 95e3670124..a12d1db1ab 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -3,6 +3,8 @@ from __future__ import annotations import asyncio import logging from pathlib import Path +import re +import subprocess from esphome import pins import esphome.codegen as cg @@ -380,3 +382,41 @@ def show_logs(config: ConfigType, args, devices: list[str]) -> bool: asyncio.run(logger_connect(address)) return True return False + + +def _addr2line(addr2line: str, elf: Path, addr: str) -> str: + try: + result = subprocess.run( + [addr2line, "-e", elf, addr], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip().splitlines()[0] + except Exception as err: # pylint: disable=broad-except + _LOGGER.error("Running command failed: %s", err) + return "" + + +def process_stacktrace(config: ConfigType, line: str, backtrace_state: bool) -> bool: + if "Last crash:" in line: + return True + if backtrace_state: + match = re.search(r"PC=(0x[0-9a-fA-F]+)\s+LR=(0x[0-9a-fA-F]+)", line) + if match: + pc = match.group(1) + lr = match.group(2) + from esphome.analyze_memory.toolchain import find_tool + + addr2line = find_tool("addr2line") + if addr2line is None: + return False + elf = CORE.relative_pioenvs_path(CORE.name, "firmware.elf") + if not elf.exists(): + _LOGGER.warning("%s does not exists", elf) + return False + _LOGGER.error("=== CRASH ===") + _LOGGER.error("PC: %s", _addr2line(addr2line, elf, pc)) + _LOGGER.error("LR: %s", _addr2line(addr2line, elf, lr)) + + return False diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index c9aa446323..cef561c54b 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -2951,6 +2951,7 @@ def test_run_miniterm_batches_lines_with_same_timestamp( mock_serial = MockSerial([chunk, MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, @@ -2989,6 +2990,7 @@ def test_run_miniterm_different_chunks_different_timestamps( mock_serial = MockSerial([chunk1, chunk2, MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, @@ -3019,6 +3021,7 @@ def test_run_miniterm_handles_split_lines() -> None: mock_serial = MockSerial([chunk1, chunk2, MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, @@ -3057,6 +3060,7 @@ def test_run_miniterm_backtrace_state_maintained() -> None: mock_serial = MockSerial([backtrace_chunk, MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, @@ -3122,6 +3126,7 @@ def test_run_miniterm_handles_empty_reads( mock_serial = MockSerial([b"", chunk, b"", MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, @@ -3194,6 +3199,7 @@ def test_run_miniterm_buffer_limit_prevents_unbounded_growth() -> None: mock_serial = MockSerial([large_data_no_newline, final_line, MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, From 9571a979eb7ff1d881a4a833a639ff54c861174e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 13:53:45 -0600 Subject: [PATCH 133/609] [ci] Suggest StringRef instead of std::string_view (#14183) --- script/ci-custom.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/script/ci-custom.py b/script/ci-custom.py index 231f587068..df819e0f04 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -494,6 +494,22 @@ def lint_no_byte_datatype(fname, match): ) +@lint_re_check( + r"(?:std\s*::\s*string_view|#include\s*)" + CPP_RE_EOL, + include=cpp_include, +) +def lint_no_std_string_view(fname, match): + return ( + f"{highlight('std::string_view')} is not allowed in ESPHome. " + f"It pulls in significant STL template machinery that bloats flash on " + f"resource-constrained embedded targets, does not work well with ArduinoJson, " + f"and duplicates functionality already provided by {highlight('StringRef')}.\n" + f"Please use {highlight('StringRef')} from {highlight('esphome/core/string_ref.h')} " + f"for non-owning string references, or {highlight('const char *')} for simple cases.\n" + f"(If strictly necessary, add `{highlight('// NOLINT')}` to the end of the line)" + ) + + @lint_post_check def lint_constants_usage(): errs = [] From 5a07908dfa9ef499b20e8a53f23e5ea94b9eb682 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 13:54:20 -0600 Subject: [PATCH 134/609] [api] Fix build error when lambda returns StringRef in homeassistant.event data (#14187) --- esphome/components/api/homeassistant_service.h | 2 ++ tests/components/homeassistant/common.yaml | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 2322d96eef..340699e1a6 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -36,6 +36,8 @@ template class TemplatableStringValue : public TemplatableValue() {} diff --git a/tests/components/homeassistant/common.yaml b/tests/components/homeassistant/common.yaml index 9c6cb71b8b..60e3defd49 100644 --- a/tests/components/homeassistant/common.yaml +++ b/tests/components/homeassistant/common.yaml @@ -90,6 +90,19 @@ text_sensor: id: ha_hello_world_text2 attribute: some_attribute +event: + - platform: template + name: Test Event + id: test_event + event_types: + - test_event_type + on_event: + - homeassistant.event: + event: esphome.test_event + data: + event_name: !lambda |- + return event_type; + time: - platform: homeassistant on_time: From e521522b388dce7066ade34d96869187e7c47ca6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 13:54:43 -0600 Subject: [PATCH 135/609] [haier] Fix uninitialized HonSettings causing API connection failures (#14188) --- esphome/components/haier/hon_climate.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index a567ab1d89..608d5e7f21 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -29,10 +29,10 @@ enum class CleaningState : uint8_t { enum class HonControlMethod { MONITOR_ONLY = 0, SET_GROUP_PARAMETERS, SET_SINGLE_PARAMETER }; struct HonSettings { - hon_protocol::VerticalSwingMode last_vertiacal_swing; - hon_protocol::HorizontalSwingMode last_horizontal_swing; - bool beeper_state; - bool quiet_mode_state; + hon_protocol::VerticalSwingMode last_vertiacal_swing{hon_protocol::VerticalSwingMode::CENTER}; + hon_protocol::HorizontalSwingMode last_horizontal_swing{hon_protocol::HorizontalSwingMode::CENTER}; + bool beeper_state{true}; + bool quiet_mode_state{false}; }; class HonClimate : public HaierClimateBase { @@ -189,7 +189,7 @@ class HonClimate : public HaierClimateBase { int big_data_sensors_{0}; esphome::optional current_vertical_swing_{}; esphome::optional current_horizontal_swing_{}; - HonSettings settings_; + HonSettings settings_{}; ESPPreferenceObject hon_rtc_; SwitchState quiet_mode_state_{SwitchState::OFF}; }; From 6f198adb0c79bcc8e14cdb07cbf484bb23aa1e52 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 14:29:50 -0600 Subject: [PATCH 136/609] [scheduler] Reduce lock acquisitions in process_defer_queue_ (#14107) --- esphome/core/scheduler.cpp | 23 +++++++++++ esphome/core/scheduler.h | 79 ++++++++++++++++++-------------------- 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index e82efcc520..b810df7e1c 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -421,6 +421,29 @@ void Scheduler::full_cleanup_removed_items_() { this->to_remove_ = 0; } +#ifndef ESPHOME_THREAD_SINGLE +void Scheduler::compact_defer_queue_locked_() { + // Rare case: new items were added during processing - compact the vector + // This only happens when: + // 1. A deferred callback calls defer() again, or + // 2. Another thread calls defer() while we're processing + // + // Move unprocessed items (added during this loop) to the front for next iteration + // + // SAFETY: Compacted items may include cancelled items (marked for removal via + // cancel_item_locked_() during execution). This is safe because should_skip_item_() + // checks is_item_removed_() before executing, so cancelled items will be skipped + // and recycled on the next loop iteration. + size_t remaining = this->defer_queue_.size() - this->defer_queue_front_; + for (size_t i = 0; i < remaining; i++) { + this->defer_queue_[i] = std::move(this->defer_queue_[this->defer_queue_front_ + i]); + } + // Use erase() instead of resize() to avoid instantiating _M_default_append + // (saves ~156 bytes flash). Erasing from the end is O(1) - no shifting needed. + this->defer_queue_.erase(this->defer_queue_.begin() + remaining, this->defer_queue_.end()); +} +#endif /* not ESPHOME_THREAD_SINGLE */ + void HOT Scheduler::call(uint32_t now) { #ifndef ESPHOME_THREAD_SINGLE this->process_defer_queue_(now); diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index afe11aaca6..041c6f4687 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -387,41 +387,46 @@ class Scheduler { // No lock needed: single consumer (main loop), stale read just means we process less this iteration size_t defer_queue_end = this->defer_queue_.size(); + // Fast path: nothing to process, avoid lock entirely. + // Safe without lock: single consumer (main loop) reads front_, and a stale size() read + // from a concurrent push can only make us see fewer items — they'll be processed next loop. + if (this->defer_queue_front_ >= defer_queue_end) + return; + + // Merge lock acquisitions: instead of separate locks for move-out and recycle (2N+1 total), + // recycle each item after re-acquiring the lock for the next iteration (N+1 total). + // The lock is held across: recycle → loop condition → move-out, then released for execution. + std::unique_ptr item; + + this->lock_.lock(); while (this->defer_queue_front_ < defer_queue_end) { - std::unique_ptr item; - { - LockGuard lock(this->lock_); - // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. - // This is intentional and safe because: - // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function - // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_ - // and has_cancelled_timeout_in_container_locked_ in scheduler.h) - // 3. The lock protects concurrent access, but the nullptr remains until cleanup - item = std::move(this->defer_queue_[this->defer_queue_front_]); - this->defer_queue_front_++; - } + // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. + // This is intentional and safe because: + // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function + // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_ + // and has_cancelled_timeout_in_container_locked_ in scheduler.h) + // 3. The lock protects concurrent access, but the nullptr remains until cleanup + item = std::move(this->defer_queue_[this->defer_queue_front_]); + this->defer_queue_front_++; + this->lock_.unlock(); // Execute callback without holding lock to prevent deadlocks // if the callback tries to call defer() again if (!this->should_skip_item_(item.get())) { now = this->execute_item_(item.get(), now); } - // Recycle the defer item after execution - { - LockGuard lock(this->lock_); - this->recycle_item_main_loop_(std::move(item)); - } - } - // If we've consumed all items up to the snapshot point, clean up the dead space - // Single consumer (main loop), so no lock needed for this check - if (this->defer_queue_front_ >= defer_queue_end) { - LockGuard lock(this->lock_); - this->cleanup_defer_queue_locked_(); + this->lock_.lock(); + this->recycle_item_main_loop_(std::move(item)); } + // Clean up the queue (lock already held from last recycle or initial acquisition) + this->cleanup_defer_queue_locked_(); + this->lock_.unlock(); } - // Helper to cleanup defer_queue_ after processing + // Helper to cleanup defer_queue_ after processing. + // Keeps the common clear() path inline, outlines the rare compaction to keep + // cold code out of the hot instruction cache lines. // IMPORTANT: Caller must hold the scheduler lock before calling this function. inline void cleanup_defer_queue_locked_() { // Check if new items were added by producers during processing @@ -429,27 +434,17 @@ class Scheduler { // Common case: no new items - clear everything this->defer_queue_.clear(); } else { - // Rare case: new items were added during processing - compact the vector - // This only happens when: - // 1. A deferred callback calls defer() again, or - // 2. Another thread calls defer() while we're processing - // - // Move unprocessed items (added during this loop) to the front for next iteration - // - // SAFETY: Compacted items may include cancelled items (marked for removal via - // cancel_item_locked_() during execution). This is safe because should_skip_item_() - // checks is_item_removed_() before executing, so cancelled items will be skipped - // and recycled on the next loop iteration. - size_t remaining = this->defer_queue_.size() - this->defer_queue_front_; - for (size_t i = 0; i < remaining; i++) { - this->defer_queue_[i] = std::move(this->defer_queue_[this->defer_queue_front_ + i]); - } - // Use erase() instead of resize() to avoid instantiating _M_default_append - // (saves ~156 bytes flash). Erasing from the end is O(1) - no shifting needed. - this->defer_queue_.erase(this->defer_queue_.begin() + remaining, this->defer_queue_.end()); + // Rare case: new items were added during processing - outlined to keep cold code + // out of the hot instruction cache lines + this->compact_defer_queue_locked_(); } this->defer_queue_front_ = 0; } + + // Cold path for compacting defer_queue_ when new items were added during processing. + // IMPORTANT: Caller must hold the scheduler lock before calling this function. + // IMPORTANT: Must not be inlined - rare path, outlined to keep it out of the hot instruction cache lines. + void __attribute__((noinline)) compact_defer_queue_locked_(); #endif /* not ESPHOME_THREAD_SINGLE */ // Helper to check if item is marked for removal (platform-specific) From 462ac2956312f21e506b1e2e13a6960e18d0d813 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 17:29:41 -0600 Subject: [PATCH 137/609] [scheduler] Use relaxed memory ordering for atomic reads under lock (#14140) --- esphome/core/scheduler.cpp | 10 +++++----- esphome/core/scheduler.h | 18 +++++++++++++++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index b810df7e1c..36b65f6ff7 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -406,7 +406,7 @@ void Scheduler::full_cleanup_removed_items_() { // Compact in-place: move valid items forward, recycle removed ones size_t write = 0; for (size_t read = 0; read < this->items_.size(); ++read) { - if (!is_item_removed_(this->items_[read].get())) { + if (!is_item_removed_locked_(this->items_[read].get())) { if (write != read) { this->items_[write] = std::move(this->items_[read]); } @@ -531,7 +531,7 @@ void HOT Scheduler::call(uint32_t now) { // Multi-threaded platforms without atomics: must take lock to safely read remove flag { LockGuard guard{this->lock_}; - if (is_item_removed_(item.get())) { + if (is_item_removed_locked_(item.get())) { this->recycle_item_main_loop_(this->pop_raw_locked_()); this->to_remove_--; continue; @@ -568,7 +568,7 @@ void HOT Scheduler::call(uint32_t now) { // during the function call and know if we were cancelled. auto executed_item = this->pop_raw_locked_(); - if (executed_item->remove) { + if (this->is_item_removed_locked_(executed_item.get())) { // We were removed/cancelled in the function call, recycle and continue this->to_remove_--; this->recycle_item_main_loop_(std::move(executed_item)); @@ -595,7 +595,7 @@ void HOT Scheduler::call(uint32_t now) { void HOT Scheduler::process_to_add() { LockGuard guard{this->lock_}; for (auto &it : this->to_add_) { - if (is_item_removed_(it.get())) { + if (is_item_removed_locked_(it.get())) { // Recycle cancelled items this->recycle_item_main_loop_(std::move(it)); continue; @@ -628,7 +628,7 @@ size_t HOT Scheduler::cleanup_() { LockGuard guard{this->lock_}; while (!this->items_.empty()) { auto &item = this->items_[0]; - if (!item->remove) + if (!this->is_item_removed_locked_(item.get())) break; this->to_remove_--; this->recycle_item_main_loop_(this->pop_raw_locked_()); diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 041c6f4687..384d76b6b0 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -314,8 +314,8 @@ class Scheduler { // Fixes: https://github.com/esphome/esphome/issues/11940 if (!item) return false; - if (item->component != component || item->type != type || (skip_removed && item->remove) || - (match_retry && !item->is_retry)) { + if (item->component != component || item->type != type || + (skip_removed && this->is_item_removed_locked_(item.get())) || (match_retry && !item->is_retry)) { return false; } // Name type must match @@ -463,6 +463,18 @@ class Scheduler { #endif } + // Helper to check if item is marked for removal when lock is already held. + // Uses relaxed ordering since the mutex provides all necessary synchronization. + // IMPORTANT: Caller must hold the scheduler lock before calling this function. + bool is_item_removed_locked_(SchedulerItem *item) const { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + // Lock already held - relaxed is sufficient, mutex provides ordering + return item->remove.load(std::memory_order_relaxed); +#else + return item->remove; +#endif + } + // Helper to set item removal flag (platform-specific) // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this // function. Uses memory_order_release when setting to true (for cancellation synchronization), @@ -519,7 +531,7 @@ class Scheduler { // it will iterate over these nullptr items. This check prevents crashes. if (!item) continue; - if (is_item_removed_(item.get()) && + if (this->is_item_removed_locked_(item.get()) && this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT, match_retry, /* skip_removed= */ false)) { return true; From a4682615234ac61ff7825e2a922807b11e6772fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 19:41:26 -0600 Subject: [PATCH 138/609] [scheduler] De-template and consolidate scheduler helper functions (#14164) --- esphome/core/scheduler.cpp | 14 +++++++++---- esphome/core/scheduler.h | 41 +++++++------------------------------- 2 files changed, 17 insertions(+), 38 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 36b65f6ff7..e4e0751e10 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -119,10 +119,16 @@ uint32_t Scheduler::calculate_interval_offset_(uint32_t delay) { // Remove before 2026.8.0 along with all retry code bool Scheduler::is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id) { - return has_cancelled_timeout_in_container_locked_(this->items_, component, name_type, static_name, hash_or_id, - /* match_retry= */ true) || - has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_type, static_name, hash_or_id, - /* match_retry= */ true); + for (auto *container : {&this->items_, &this->to_add_}) { + for (auto &item : *container) { + if (item && this->is_item_removed_locked_(item.get()) && + this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT, + /* match_retry= */ true, /* skip_removed= */ false)) { + return true; + } + } + } + return false; } // Common implementation for both timeout and interval diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 384d76b6b0..16b0ded312 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -308,8 +308,8 @@ class Scheduler { SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const { // THREAD SAFETY: Check for nullptr first to prevent LoadProhibited crashes. On multi-threaded // platforms, items can be moved out of defer_queue_ during processing, leaving nullptr entries. - // PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_() and - // has_cancelled_timeout_in_container_locked_()), but this check provides defense-in-depth: helper + // PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_()), but this check + // provides defense-in-depth: helper // functions should be safe regardless of caller behavior. // Fixes: https://github.com/esphome/esphome/issues/11940 if (!item) @@ -403,8 +403,7 @@ class Scheduler { // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. // This is intentional and safe because: // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function - // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_ - // and has_cancelled_timeout_in_container_locked_ in scheduler.h) + // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_) // 3. The lock protects concurrent access, but the nullptr remains until cleanup item = std::move(this->defer_queue_[this->defer_queue_front_]); this->defer_queue_front_++; @@ -497,19 +496,16 @@ class Scheduler { // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id // Returns the number of items marked for removal // IMPORTANT: Must be called with scheduler lock held - template - size_t mark_matching_items_removed_locked_(Container &container, Component *component, NameType name_type, - const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type, - bool match_retry) { + size_t mark_matching_items_removed_locked_(std::vector> &container, + Component *component, NameType name_type, const char *static_name, + uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry) { size_t count = 0; for (auto &item : container) { // Skip nullptr items (can happen in defer_queue_ when items are being processed) // The defer_queue_ uses index-based processing: items are std::moved out but left in the // vector as nullptr until cleanup. Even though this function is called with lock held, // the vector can still contain nullptr items from the processing loop. This check prevents crashes. - if (!item) - continue; - if (this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) { + if (item && this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) { this->set_item_removed_(item.get(), true); count++; } @@ -517,29 +513,6 @@ class Scheduler { return count; } - // Template helper to check if any item in a container matches our criteria - // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id - // IMPORTANT: Must be called with scheduler lock held - template - bool has_cancelled_timeout_in_container_locked_(const Container &container, Component *component, NameType name_type, - const char *static_name, uint32_t hash_or_id, - bool match_retry) const { - for (const auto &item : container) { - // Skip nullptr items (can happen in defer_queue_ when items are being processed) - // The defer_queue_ uses index-based processing: items are std::moved out but left in the - // vector as nullptr until cleanup. If this function is called during defer queue processing, - // it will iterate over these nullptr items. This check prevents crashes. - if (!item) - continue; - if (this->is_item_removed_locked_(item.get()) && - this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT, - match_retry, /* skip_removed= */ false)) { - return true; - } - } - return false; - } - Mutex lock_; std::vector> items_; std::vector> to_add_; From d5c9c56fdfcdd0112e1913f05f84e297908460a7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 19:41:43 -0600 Subject: [PATCH 139/609] [platformio] Add exponential backoff and session reset to download retries (#14191) --- esphome/platformio_api.py | 41 ++++- tests/unit_tests/test_platformio_api.py | 196 +++++++++++++++++++++++- 2 files changed, 231 insertions(+), 6 deletions(-) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index d42f89d029..5d4065207f 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -5,6 +5,7 @@ import os from pathlib import Path import re import subprocess +import time from typing import Any from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE @@ -44,31 +45,61 @@ def patch_structhash(): def patch_file_downloader(): - """Patch PlatformIO's FileDownloader to retry on PackageException errors.""" + """Patch PlatformIO's FileDownloader to retry on PackageException errors. + + PlatformIO's FileDownloader uses HTTPSession which lacks built-in retry + for 502/503 errors. We add retries with exponential backoff and close the + session between attempts to force a fresh TCP connection, which may route + to a different CDN edge node. + """ from platformio.package.download import FileDownloader from platformio.package.exception import PackageException + if getattr(FileDownloader.__init__, "_esphome_patched", False): + return + original_init = FileDownloader.__init__ def patched_init(self, *args: Any, **kwargs: Any) -> None: - max_retries = 3 + max_retries = 5 for attempt in range(max_retries): try: - return original_init(self, *args, **kwargs) + original_init(self, *args, **kwargs) + return except PackageException as e: if attempt < max_retries - 1: + # Exponential backoff: 2, 4, 8, 16 seconds + delay = 2 ** (attempt + 1) _LOGGER.warning( - "Package download failed: %s. Retrying... (attempt %d/%d)", + "Package download failed: %s. " + "Retrying in %d seconds... (attempt %d/%d)", str(e), + delay, attempt + 1, max_retries, ) + # Close the response and session to free resources + # and force a new TCP connection on retry, which may + # route to a different CDN edge node + # pylint: disable=protected-access,broad-except + try: + if ( + hasattr(self, "_http_response") + and self._http_response is not None + ): + self._http_response.close() + if hasattr(self, "_http_session"): + self._http_session.close() + except Exception: + pass + # pylint: enable=protected-access,broad-except + time.sleep(delay) else: # Final attempt - re-raise raise - return None + patched_init._esphome_patched = True # type: ignore[attr-defined] # pylint: disable=protected-access FileDownloader.__init__ = patched_init diff --git a/tests/unit_tests/test_platformio_api.py b/tests/unit_tests/test_platformio_api.py index 4d7b635e59..1686144277 100644 --- a/tests/unit_tests/test_platformio_api.py +++ b/tests/unit_tests/test_platformio_api.py @@ -6,7 +6,7 @@ import os from pathlib import Path import shutil from types import SimpleNamespace -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock, Mock, call, patch import pytest @@ -673,6 +673,200 @@ def test_process_stacktrace_bad_alloc( assert state is False +def test_patch_file_downloader_succeeds_first_try() -> None: + """Test patch_file_downloader succeeds on first attempt.""" + mock_exception_cls = type("PackageException", (Exception,), {}) + original_init = MagicMock() + + with patch.dict( + "sys.modules", + { + "platformio": MagicMock(), + "platformio.package": MagicMock(), + "platformio.package.download": SimpleNamespace( + FileDownloader=type("FileDownloader", (), {"__init__": original_init}) + ), + "platformio.package.exception": SimpleNamespace( + PackageException=mock_exception_cls + ), + }, + ): + platformio_api.patch_file_downloader() + + from platformio.package.download import FileDownloader + + instance = object.__new__(FileDownloader) + FileDownloader.__init__(instance, "http://example.com/file.zip") + + original_init.assert_called_once() + + +def test_patch_file_downloader_retries_on_failure() -> None: + """Test patch_file_downloader retries with backoff on PackageException.""" + mock_exception_cls = type("PackageException", (Exception,), {}) + call_count = 0 + + def failing_init(self, *args, **kwargs): + nonlocal call_count + call_count += 1 + if call_count < 3: + raise mock_exception_cls(f"502 error attempt {call_count}") + + with ( + patch.dict( + "sys.modules", + { + "platformio": MagicMock(), + "platformio.package": MagicMock(), + "platformio.package.download": SimpleNamespace( + FileDownloader=type( + "FileDownloader", (), {"__init__": failing_init} + ) + ), + "platformio.package.exception": SimpleNamespace( + PackageException=mock_exception_cls + ), + }, + ), + patch("time.sleep") as mock_sleep, + ): + platformio_api.patch_file_downloader() + + from platformio.package.download import FileDownloader + + instance = object.__new__(FileDownloader) + FileDownloader.__init__(instance, "http://example.com/file.zip") + + # Should have been called 3 times (2 failures + 1 success) + assert call_count == 3 + + # Should have slept with exponential backoff: 2s, 4s + assert mock_sleep.call_count == 2 + mock_sleep.assert_any_call(2) + mock_sleep.assert_any_call(4) + + +def test_patch_file_downloader_raises_after_max_retries() -> None: + """Test patch_file_downloader raises after exhausting all retries.""" + mock_exception_cls = type("PackageException", (Exception,), {}) + + def always_failing_init(self, *args, **kwargs): + raise mock_exception_cls("502 error") + + with ( + patch.dict( + "sys.modules", + { + "platformio": MagicMock(), + "platformio.package": MagicMock(), + "platformio.package.download": SimpleNamespace( + FileDownloader=type( + "FileDownloader", (), {"__init__": always_failing_init} + ) + ), + "platformio.package.exception": SimpleNamespace( + PackageException=mock_exception_cls + ), + }, + ), + patch("time.sleep") as mock_sleep, + ): + platformio_api.patch_file_downloader() + + from platformio.package.download import FileDownloader + + instance = object.__new__(FileDownloader) + with pytest.raises(mock_exception_cls, match="502 error"): + FileDownloader.__init__(instance, "http://example.com/file.zip") + + # Should have slept 4 times (before attempts 2-5), not on final attempt + assert mock_sleep.call_count == 4 + mock_sleep.assert_has_calls([call(2), call(4), call(8), call(16)]) + + +def test_patch_file_downloader_closes_session_and_response_between_retries() -> None: + """Test patch_file_downloader closes HTTP session and response between retries.""" + mock_exception_cls = type("PackageException", (Exception,), {}) + mock_session = MagicMock() + mock_response = MagicMock() + call_count = 0 + + def failing_init_with_session(self, *args, **kwargs): + nonlocal call_count + call_count += 1 + self._http_session = mock_session + self._http_response = mock_response + if call_count < 2: + raise mock_exception_cls("502 error") + + with ( + patch.dict( + "sys.modules", + { + "platformio": MagicMock(), + "platformio.package": MagicMock(), + "platformio.package.download": SimpleNamespace( + FileDownloader=type( + "FileDownloader", + (), + {"__init__": failing_init_with_session}, + ) + ), + "platformio.package.exception": SimpleNamespace( + PackageException=mock_exception_cls + ), + }, + ), + patch("time.sleep"), + ): + platformio_api.patch_file_downloader() + + from platformio.package.download import FileDownloader + + instance = object.__new__(FileDownloader) + FileDownloader.__init__(instance, "http://example.com/file.zip") + + # Both response and session should have been closed between retries + mock_response.close.assert_called_once() + mock_session.close.assert_called_once() + + +def test_patch_file_downloader_idempotent() -> None: + """Test patch_file_downloader does not stack wrappers when called multiple times.""" + mock_exception_cls = type("PackageException", (Exception,), {}) + call_count = 0 + + def counting_init(self, *args, **kwargs): + nonlocal call_count + call_count += 1 + + with patch.dict( + "sys.modules", + { + "platformio": MagicMock(), + "platformio.package": MagicMock(), + "platformio.package.download": SimpleNamespace( + FileDownloader=type("FileDownloader", (), {"__init__": counting_init}) + ), + "platformio.package.exception": SimpleNamespace( + PackageException=mock_exception_cls + ), + }, + ): + # Patch multiple times + platformio_api.patch_file_downloader() + platformio_api.patch_file_downloader() + platformio_api.patch_file_downloader() + + from platformio.package.download import FileDownloader + + instance = object.__new__(FileDownloader) + FileDownloader.__init__(instance, "http://example.com/file.zip") + + # Should only be called once, not 3 times from stacked wrappers + assert call_count == 1 + + def test_platformio_log_filter_allows_non_platformio_messages() -> None: """Test that non-platformio logger messages are allowed through.""" log_filter = platformio_api.PlatformioLogFilter() From 49e4ae54beb20f7de57a24274c851c13a382d0e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 23:22:59 -0600 Subject: [PATCH 140/609] [bme68x_bsec2] Fix compilation on ESP32 Arduino (#14194) --- esphome/components/bme68x_bsec2/__init__.py | 5 ++++- tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml diff --git a/esphome/components/bme68x_bsec2/__init__.py b/esphome/components/bme68x_bsec2/__init__.py index e421efb2d6..4200b2f0b8 100644 --- a/esphome/components/bme68x_bsec2/__init__.py +++ b/esphome/components/bme68x_bsec2/__init__.py @@ -178,8 +178,11 @@ async def to_code_base(config): bsec2_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) cg.add(var.set_bsec2_configuration(bsec2_arr, len(rhs))) - # Although this component does not use SPI, the BSEC2 Arduino library requires the SPI library + # The BSEC2 and BME68x Arduino libraries unconditionally include Wire.h and + # SPI.h in their source files, so these libraries must be available even though + # ESPHome uses its own I2C/SPI abstractions instead of the Arduino ones. if core.CORE.using_arduino: + cg.add_library("Wire", None) cg.add_library("SPI", None) cg.add_library( "BME68x Sensor library", diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..7c503b0ccb --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml + +<<: !include common.yaml From e013b4867519c44b570fd6cf05872a7324845009 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Sun, 22 Feb 2026 06:44:06 +0100 Subject: [PATCH 141/609] [nextion] Add error log for failed HTTP status during TFT upload (#14190) --- esphome/components/nextion/nextion_upload_arduino.cpp | 1 + esphome/components/nextion/nextion_upload_esp32.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index 220c75f9d3..a433eff883 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -220,6 +220,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { } if (code != 200 and code != 206) { + ESP_LOGE(TAG, "HTTP request failed with status %d", code); return this->upload_end_(false); } diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index c4e6ff7182..46352afd75 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -238,6 +238,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { esp_get_free_heap_size()); int status_code = esp_http_client_get_status_code(http_client); if (status_code != 200 && status_code != 206) { + ESP_LOGE(TAG, "HTTP request failed with status %d", status_code); return this->upload_end_(false); } From 1753074eef3ea117dff2cb02995330227b242379 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 15:01:07 -0600 Subject: [PATCH 142/609] [web_server_base] Remove unnecessary Component inheritance and modernize (#14204) --- esphome/components/web_server_base/__init__.py | 3 +-- .../web_server_base/web_server_base.cpp | 18 ++---------------- .../web_server_base/web_server_base.h | 18 +++++++----------- 3 files changed, 10 insertions(+), 29 deletions(-) diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 7986ac964d..183b907ae6 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -20,7 +20,7 @@ def AUTO_LOAD(): web_server_base_ns = cg.esphome_ns.namespace("web_server_base") -WebServerBase = web_server_base_ns.class_("WebServerBase", cg.Component) +WebServerBase = web_server_base_ns.class_("WebServerBase") CONF_WEB_SERVER_BASE_ID = "web_server_base_id" CONFIG_SCHEMA = cv.Schema( @@ -33,7 +33,6 @@ CONFIG_SCHEMA = cv.Schema( @coroutine_with_priority(CoroPriority.WEB_SERVER_BASE) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) cg.add(cg.RawExpression(f"{web_server_base_ns}::global_web_server_base = {var}")) if CORE.is_esp32: diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp index 6e7097338c..dbbcd10d8d 100644 --- a/esphome/components/web_server_base/web_server_base.cpp +++ b/esphome/components/web_server_base/web_server_base.cpp @@ -1,19 +1,11 @@ #include "web_server_base.h" #ifdef USE_NETWORK -#include "esphome/core/application.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" -namespace esphome { -namespace web_server_base { - -static const char *const TAG = "web_server_base"; +namespace esphome::web_server_base { WebServerBase *global_web_server_base = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void WebServerBase::add_handler(AsyncWebHandler *handler) { - // remove all handlers - #ifdef USE_WEBSERVER_AUTH if (!credentials_.username.empty()) { handler = new internal::AuthMiddlewareHandler(handler, &credentials_); @@ -25,11 +17,5 @@ void WebServerBase::add_handler(AsyncWebHandler *handler) { } } -float WebServerBase::get_setup_priority() const { - // Before WiFi (captive portal) - return setup_priority::WIFI + 2.0f; -} - -} // namespace web_server_base -} // namespace esphome +} // namespace esphome::web_server_base #endif diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index 0c25467f1b..54421c851e 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -1,11 +1,9 @@ #pragma once #include "esphome/core/defines.h" #ifdef USE_NETWORK -#include #include #include -#include "esphome/core/component.h" #include "esphome/core/progmem.h" #if USE_ESP32 @@ -21,8 +19,7 @@ using PlatformString = std::string; using PlatformString = String; #endif -namespace esphome { -namespace web_server_base { +namespace esphome::web_server_base { class WebServerBase; extern WebServerBase *global_web_server_base; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -91,14 +88,14 @@ class AuthMiddlewareHandler : public MiddlewareHandler { } // namespace internal -class WebServerBase : public Component { +class WebServerBase { public: void init() { if (this->initialized_) { this->initialized_++; return; } - this->server_ = std::make_unique(this->port_); + this->server_ = new AsyncWebServer(this->port_); // All content is controlled and created by user - so allowing all origins is fine here. // NOTE: Currently 1 header. If more are added, update in __init__.py: // cg.add_define("WEB_SERVER_DEFAULT_HEADERS_COUNT", 1) @@ -113,11 +110,11 @@ class WebServerBase : public Component { void deinit() { this->initialized_--; if (this->initialized_ == 0) { + delete this->server_; this->server_ = nullptr; } } - AsyncWebServer *get_server() const { return this->server_.get(); } - float get_setup_priority() const override; + AsyncWebServer *get_server() const { return this->server_; } #ifdef USE_WEBSERVER_AUTH void set_auth_username(std::string auth_username) { credentials_.username = std::move(auth_username); } @@ -132,13 +129,12 @@ class WebServerBase : public Component { protected: int initialized_{0}; uint16_t port_{80}; - std::unique_ptr server_{nullptr}; + AsyncWebServer *server_{nullptr}; std::vector handlers_; #ifdef USE_WEBSERVER_AUTH internal::Credentials credentials_; #endif }; -} // namespace web_server_base -} // namespace esphome +} // namespace esphome::web_server_base #endif From 509f06afac19fc54b3fae2a67802771312b91133 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 15:56:09 -0600 Subject: [PATCH 143/609] [network] Improve IPAddress::str() deprecation warning with usage example (#14195) --- esphome/components/network/ip_address.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index d0ac8164af..b2a2c563e2 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -61,7 +61,9 @@ struct IPAddress { IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); } IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; } // Remove before 2026.8.0 - ESPDEPRECATED("Use str_to() instead. Removed in 2026.8.0", "2026.2.0") + ESPDEPRECATED( + "str() is deprecated: use 'char buf[IP_ADDRESS_BUFFER_SIZE]; ip.str_to(buf);' instead. Removed in 2026.8.0", + "2026.2.0") std::string str() const { char buf[IP_ADDRESS_BUFFER_SIZE]; this->str_to(buf); @@ -150,7 +152,9 @@ struct IPAddress { bool is_ip6() const { return IP_IS_V6(&ip_addr_); } bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); } // Remove before 2026.8.0 - ESPDEPRECATED("Use str_to() instead. Removed in 2026.8.0", "2026.2.0") + ESPDEPRECATED( + "str() is deprecated: use 'char buf[IP_ADDRESS_BUFFER_SIZE]; ip.str_to(buf);' instead. Removed in 2026.8.0", + "2026.2.0") std::string str() const { char buf[IP_ADDRESS_BUFFER_SIZE]; this->str_to(buf); From ede2da2fbc167814c5598adb6d866c239a0772c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 15:57:23 -0600 Subject: [PATCH 144/609] [core] Conditionally compile get_loop_priority with USE_LOOP_PRIORITY (#14210) --- esphome/components/ch422g/ch422g.cpp | 2 ++ esphome/components/ch422g/ch422g.h | 2 ++ esphome/components/ch423/ch423.cpp | 2 ++ esphome/components/ch423/ch423.h | 2 ++ esphome/components/deep_sleep/deep_sleep_component.cpp | 2 ++ esphome/components/deep_sleep/deep_sleep_component.h | 2 ++ esphome/components/pca9554/pca9554.cpp | 2 ++ esphome/components/pca9554/pca9554.h | 2 ++ esphome/components/pcf8574/pcf8574.cpp | 2 ++ esphome/components/pcf8574/pcf8574.h | 2 ++ esphome/components/status_led/light/status_led_light.h | 2 ++ esphome/components/status_led/status_led.cpp | 2 ++ esphome/components/status_led/status_led.h | 2 ++ esphome/components/wifi/__init__.py | 7 +++++++ esphome/components/wifi/wifi_component.cpp | 2 ++ esphome/components/wifi/wifi_component.h | 2 ++ esphome/core/application.cpp | 2 ++ esphome/core/component.cpp | 2 ++ esphome/core/component.h | 3 +++ esphome/core/defines.h | 1 + 20 files changed, 45 insertions(+) diff --git a/esphome/components/ch422g/ch422g.cpp b/esphome/components/ch422g/ch422g.cpp index eef95b9ba2..5f5e848c76 100644 --- a/esphome/components/ch422g/ch422g.cpp +++ b/esphome/components/ch422g/ch422g.cpp @@ -124,9 +124,11 @@ bool CH422GComponent::write_outputs_() { float CH422GComponent::get_setup_priority() const { return setup_priority::IO; } +#ifdef USE_LOOP_PRIORITY // Run our loop() method very early in the loop, so that we cache read values // before other components call our digital_read() method. float CH422GComponent::get_loop_priority() const { return 9.0f; } // Just after WIFI +#endif void CH422GGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; } diff --git a/esphome/components/ch422g/ch422g.h b/esphome/components/ch422g/ch422g.h index 8ed63db90a..1b96568209 100644 --- a/esphome/components/ch422g/ch422g.h +++ b/esphome/components/ch422g/ch422g.h @@ -23,7 +23,9 @@ class CH422GComponent : public Component, public i2c::I2CDevice { void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif void dump_config() override; protected: diff --git a/esphome/components/ch423/ch423.cpp b/esphome/components/ch423/ch423.cpp index 4abbbe7adf..805d8df877 100644 --- a/esphome/components/ch423/ch423.cpp +++ b/esphome/components/ch423/ch423.cpp @@ -129,9 +129,11 @@ bool CH423Component::write_outputs_() { float CH423Component::get_setup_priority() const { return setup_priority::IO; } +#ifdef USE_LOOP_PRIORITY // Run our loop() method very early in the loop, so that we cache read values // before other components call our digital_read() method. float CH423Component::get_loop_priority() const { return 9.0f; } // Just after WIFI +#endif void CH423GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool CH423GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; } diff --git a/esphome/components/ch423/ch423.h b/esphome/components/ch423/ch423.h index 7adc7de6a1..d85648a8f9 100644 --- a/esphome/components/ch423/ch423.h +++ b/esphome/components/ch423/ch423.h @@ -22,7 +22,9 @@ class CH423Component : public Component, public i2c::I2CDevice { void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif void dump_config() override; protected: diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 8066b411ff..0511518419 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -40,9 +40,11 @@ void DeepSleepComponent::loop() { this->begin_sleep(); } +#ifdef USE_LOOP_PRIORITY float DeepSleepComponent::get_loop_priority() const { return -100.0f; // run after everything else is ready } +#endif void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; } diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 3e6eda2257..1998b815f3 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -113,7 +113,9 @@ class DeepSleepComponent : public Component { void setup() override; void dump_config() override; void loop() override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif float get_setup_priority() const override; /// Helper to enter deep sleep mode diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index c574ce6593..d94767ef07 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -122,8 +122,10 @@ bool PCA9554Component::write_register_(uint8_t reg, uint16_t value) { float PCA9554Component::get_setup_priority() const { return setup_priority::IO; } +#ifdef USE_LOOP_PRIORITY // Run our loop() method early to invalidate cache before any other components access the pins float PCA9554Component::get_loop_priority() const { return 9.0f; } // Just after WIFI +#endif void PCA9554GPIOPin::setup() { pin_mode(flags_); } void PCA9554GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } diff --git a/esphome/components/pca9554/pca9554.h b/esphome/components/pca9554/pca9554.h index bf752e50c9..6dd15ccb4b 100644 --- a/esphome/components/pca9554/pca9554.h +++ b/esphome/components/pca9554/pca9554.h @@ -23,7 +23,9 @@ class PCA9554Component : public Component, float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif void dump_config() override; diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index b7d3848f0e..fa9496e7e4 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -99,8 +99,10 @@ bool PCF8574Component::write_gpio_() { } float PCF8574Component::get_setup_priority() const { return setup_priority::IO; } +#ifdef USE_LOOP_PRIORITY // Run our loop() method early to invalidate cache before any other components access the pins float PCF8574Component::get_loop_priority() const { return 9.0f; } // Just after WIFI +#endif void PCF8574GPIOPin::setup() { pin_mode(flags_); } void PCF8574GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } diff --git a/esphome/components/pcf8574/pcf8574.h b/esphome/components/pcf8574/pcf8574.h index 5203030142..23bccc26c9 100644 --- a/esphome/components/pcf8574/pcf8574.h +++ b/esphome/components/pcf8574/pcf8574.h @@ -26,7 +26,9 @@ class PCF8574Component : public Component, void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif void dump_config() override; diff --git a/esphome/components/status_led/light/status_led_light.h b/esphome/components/status_led/light/status_led_light.h index bfa144526a..a5c98d90d4 100644 --- a/esphome/components/status_led/light/status_led_light.h +++ b/esphome/components/status_led/light/status_led_light.h @@ -30,7 +30,9 @@ class StatusLEDLightOutput : public light::LightOutput, public Component { void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override { return 50.0f; } +#endif protected: GPIOPin *pin_{nullptr}; diff --git a/esphome/components/status_led/status_led.cpp b/esphome/components/status_led/status_led.cpp index 344c1e3070..93a8d4b38e 100644 --- a/esphome/components/status_led/status_led.cpp +++ b/esphome/components/status_led/status_led.cpp @@ -28,7 +28,9 @@ void StatusLED::loop() { } } float StatusLED::get_setup_priority() const { return setup_priority::HARDWARE; } +#ifdef USE_LOOP_PRIORITY float StatusLED::get_loop_priority() const { return 50.0f; } +#endif } // namespace status_led } // namespace esphome diff --git a/esphome/components/status_led/status_led.h b/esphome/components/status_led/status_led.h index 490557f3e7..f262eb260c 100644 --- a/esphome/components/status_led/status_led.h +++ b/esphome/components/status_led/status_led.h @@ -14,7 +14,9 @@ class StatusLED : public Component { void dump_config() override; void loop() override; float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif protected: GPIOPin *pin_; diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 540d0a0ab1..8c1deb6217 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -536,6 +536,13 @@ async def to_code(config): cg.add_library("ESP8266WiFi", None) elif CORE.is_rp2040: cg.add_library("WiFi", None) + # RP2040's mDNS library (LEAmDNS) relies on LwipIntf::stateUpCB() to restart + # mDNS when the network interface reconnects. However, this callback is disabled + # in the arduino-pico framework. As a workaround, we block component setup until + # WiFi is connected via can_proceed(), ensuring mDNS.begin() is called with an + # active connection. This define enables the loop priority sorting infrastructure + # used during the setup blocking phase. + cg.add_define("USE_LOOP_PRIORITY") if CORE.is_esp32: if config[CONF_ENABLE_BTM] or config[CONF_ENABLE_RRM]: diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8b3060c7c3..d5d0419395 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -968,9 +968,11 @@ void WiFiComponent::set_ap(const WiFiAP &ap) { } #endif // USE_WIFI_AP +#ifdef USE_LOOP_PRIORITY float WiFiComponent::get_loop_priority() const { return 10.0f; // before other loop components } +#endif void WiFiComponent::init_sta(size_t count) { this->sta_.init(count); } void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 5f903e092a..984930c80c 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -458,7 +458,9 @@ class WiFiComponent : public Component { void restart_adapter(); /// WIFI setup_priority. float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif /// Reconnect WiFi if required. void loop() override; diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index b216233f9b..c6597897dc 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -109,9 +109,11 @@ void Application::setup() { if (component->can_proceed()) continue; +#ifdef USE_LOOP_PRIORITY // Sort components 0 through i by loop priority insertion_sort_by_prioritycomponents_.begin()), &Component::get_loop_priority>( this->components_.begin(), this->components_.begin() + i + 1); +#endif do { uint8_t new_app_state = STATUS_LED_WARNING; diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index b458ea2a84..b4a19a0776 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -85,7 +85,9 @@ void store_component_error_message(const Component *component, const char *messa static constexpr uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again +#ifdef USE_LOOP_PRIORITY float Component::get_loop_priority() const { return 0.0f; } +#endif float Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/core/component.h b/esphome/core/component.h index b99641a275..7ea9fdf3b3 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -5,6 +5,7 @@ #include #include +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/optional.h" @@ -117,7 +118,9 @@ class Component { * * @return The loop priority of this component */ +#ifdef USE_LOOP_PRIORITY virtual float get_loop_priority() const; +#endif void call(); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 5109dd36f4..02335e2745 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -312,6 +312,7 @@ #ifdef USE_RP2040 #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 0) +#define USE_LOOP_PRIORITY #define USE_HTTP_REQUEST_RESPONSE #define USE_I2C #define USE_LOGGER_USB_CDC From ee1f52132573ab5498eaa566d3289d1103a4a186 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 16:59:54 -0600 Subject: [PATCH 145/609] [http_request] Replace std::list
with std::vector in perform() chain (#14027) --- .../components/http_request/http_request.h | 89 +++++++++++++++---- .../http_request/http_request_arduino.cpp | 2 +- .../http_request/http_request_arduino.h | 2 +- .../http_request/http_request_host.cpp | 2 +- .../http_request/http_request_host.h | 2 +- .../http_request/http_request_idf.cpp | 2 +- .../http_request/http_request_idf.h | 2 +- .../components/online_image/online_image.cpp | 2 +- 8 files changed, 79 insertions(+), 24 deletions(-) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 2b2d05c63f..8bdea470b5 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -330,38 +330,72 @@ class HttpRequestComponent : public Component { void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; } void set_redirect_limit(uint16_t limit) { this->redirect_limit_ = limit; } - std::shared_ptr get(const std::string &url) { return this->start(url, "GET", "", {}); } - std::shared_ptr get(const std::string &url, const std::list
&request_headers) { + std::shared_ptr get(const std::string &url) { + return this->start(url, "GET", "", std::vector
{}); + } + std::shared_ptr get(const std::string &url, const std::vector
&request_headers) { return this->start(url, "GET", "", request_headers); } - std::shared_ptr get(const std::string &url, const std::list
&request_headers, + std::shared_ptr get(const std::string &url, const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { return this->start(url, "GET", "", request_headers, lower_case_collect_headers); } std::shared_ptr post(const std::string &url, const std::string &body) { - return this->start(url, "POST", body, {}); + return this->start(url, "POST", body, std::vector
{}); } std::shared_ptr post(const std::string &url, const std::string &body, - const std::list
&request_headers) { + const std::vector
&request_headers) { return this->start(url, "POST", body, request_headers); } std::shared_ptr post(const std::string &url, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { return this->start(url, "POST", body, request_headers, lower_case_collect_headers); } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr get(const std::string &url, const std::list
&request_headers) { + return this->get(url, std::vector
(request_headers.begin(), request_headers.end())); + } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr get(const std::string &url, const std::list
&request_headers, + const std::vector &collect_headers) { + return this->get(url, std::vector
(request_headers.begin(), request_headers.end()), collect_headers); + } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr post(const std::string &url, const std::string &body, + const std::list
&request_headers) { + return this->post(url, body, std::vector
(request_headers.begin(), request_headers.end())); + } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr post(const std::string &url, const std::string &body, + const std::list
&request_headers, + const std::vector &collect_headers) { + return this->post(url, body, std::vector
(request_headers.begin(), request_headers.end()), collect_headers); + } + + std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, + const std::vector
&request_headers) { + // Call perform() directly to avoid ambiguity with the deprecated overloads + return this->perform(url, method, body, request_headers, {}); + } + + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers) { - // Call perform() directly to avoid ambiguity with the std::set overload - return this->perform(url, method, body, request_headers, {}); + return this->start(url, method, body, std::vector
(request_headers.begin(), request_headers.end())); } // Remove before 2027.1.0 ESPDEPRECATED("Pass collect_headers as std::vector instead of std::set. Removed in 2027.1.0.", "2026.7.0") std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::set &collect_headers) { std::vector lower; lower.reserve(collect_headers.size()); @@ -371,15 +405,39 @@ class HttpRequestComponent : public Component { return this->perform(url, method, body, request_headers, lower); } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list, and collect_headers as " + "std::vector instead of std::set. Removed in 2027.1.0.", + "2026.7.0") std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, + const std::set &collect_headers) { + std::vector lower; + lower.reserve(collect_headers.size()); + for (const auto &h : collect_headers) { + lower.push_back(str_lower_case(h)); + } + return this->perform(url, method, body, std::vector
(request_headers.begin(), request_headers.end()), lower); + } + + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, + const std::list
&request_headers, + const std::vector &lower_case_collect_headers) { + return this->perform(url, method, body, std::vector
(request_headers.begin(), request_headers.end()), + lower_case_collect_headers); + } + + std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { return this->perform(url, method, body, request_headers, lower_case_collect_headers); } protected: virtual std::shared_ptr perform(const std::string &url, const std::string &method, - const std::string &body, const std::list
&request_headers, + const std::string &body, const std::vector
&request_headers, const std::vector &lower_case_collect_headers) = 0; const char *useragent_{nullptr}; bool follow_redirects_{}; @@ -436,13 +494,10 @@ template class HttpRequestSendAction : public Action { auto f = std::bind(&HttpRequestSendAction::encode_json_func_, this, x..., std::placeholders::_1); body = json::build_json(f); } - std::list
request_headers; - for (const auto &item : this->request_headers_) { - auto val = item.second; - Header header; - header.name = item.first; - header.value = val.value(x...); - request_headers.push_back(header); + std::vector
request_headers; + request_headers.reserve(this->request_headers_.size()); + for (const auto &[key, val] : this->request_headers_) { + request_headers.push_back({key, val.value(x...)}); } auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers, diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 3f60b76b58..56b51e8b5a 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -26,7 +26,7 @@ static constexpr int ESP8266_SSL_ERR_OOM = -1000; std::shared_ptr HttpRequestArduino::perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index dbd61de364..d5ce5c0ff3 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -49,7 +49,7 @@ class HttpContainerArduino : public HttpContainer { class HttpRequestArduino : public HttpRequestComponent { protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) override; }; diff --git a/esphome/components/http_request/http_request_host.cpp b/esphome/components/http_request/http_request_host.cpp index 714a73fc31..60ab4d68a0 100644 --- a/esphome/components/http_request/http_request_host.cpp +++ b/esphome/components/http_request/http_request_host.cpp @@ -18,7 +18,7 @@ static const char *const TAG = "http_request.host"; std::shared_ptr HttpRequestHost::perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); diff --git a/esphome/components/http_request/http_request_host.h b/esphome/components/http_request/http_request_host.h index 79f5b7e817..52be0e8a16 100644 --- a/esphome/components/http_request/http_request_host.h +++ b/esphome/components/http_request/http_request_host.h @@ -19,7 +19,7 @@ class HttpContainerHost : public HttpContainer { class HttpRequestHost : public HttpRequestComponent { public: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) override; void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; } diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 0921c50b9f..dda61e2400 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -54,7 +54,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { std::shared_ptr HttpRequestIDF::perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index 9206ba6f5d..9ed1a97b1a 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -37,7 +37,7 @@ class HttpRequestIDF : public HttpRequestComponent { protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) override; // if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE uint16_t buffer_size_rx_{}; diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 6f5b82116d..da866599c9 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -49,7 +49,7 @@ void OnlineImage::update() { ESP_LOGD(TAG, "Updating image from %s", this->url_.c_str()); - std::list headers; + std::vector headers; // Add caching headers if we have them if (!this->etag_.empty()) { From 5fddce6638f47c296e1267bbeeadaf677277d635 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 17:02:05 -0600 Subject: [PATCH 146/609] [logger] Make tx_buffer_ compile-time sized (#14205) --- esphome/components/logger/__init__.py | 3 ++- esphome/components/logger/logger.cpp | 7 ++----- esphome/components/logger/logger.h | 9 +++++---- esphome/components/logger/logger_esp32.cpp | 6 +++--- esphome/components/logger/logger_host.cpp | 4 ++-- esphome/core/defines.h | 1 + tests/dummy_main.cpp | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index c8f3c52911..264197c175 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -329,10 +329,11 @@ async def to_code(config): level = config[CONF_LEVEL] CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)] + tx_buffer_size = config[CONF_TX_BUFFER_SIZE] + cg.add_define("ESPHOME_LOGGER_TX_BUFFER_SIZE", tx_buffer_size) log = cg.new_Pvariable( config[CONF_ID], baud_rate, - config[CONF_TX_BUFFER_SIZE], ) if CORE.is_esp32: cg.add(log.create_pthread_key()) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 22a95e4835..497809cd2e 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -152,10 +152,7 @@ inline uint8_t Logger::level_for(const char *tag) { return this->current_level_; } -Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size) { - // add 1 to buffer size for null terminator - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed - this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; +Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) { #if defined(USE_ESP32) || defined(USE_LIBRETINY) this->main_task_ = xTaskGetCurrentTaskHandle(); #elif defined(USE_ZEPHYR) @@ -196,7 +193,7 @@ void Logger::process_messages_() { uint16_t text_length; while (this->log_buffer_->borrow_message_main_loop(message, text_length)) { const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; - LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; + LogBuffer buf{this->tx_buffer_, ESPHOME_LOGGER_TX_BUFFER_SIZE}; this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, message->text_data(), text_length, buf); // Release the message to allow other tasks to use it as soon as possible diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 8bf1edebb8..90722ee79c 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -143,7 +143,7 @@ enum UARTSelection : uint8_t { */ class Logger : public Component { public: - explicit Logger(uint32_t baud_rate, size_t tx_buffer_size); + explicit Logger(uint32_t baud_rate); #ifdef USE_ESPHOME_TASK_LOG_BUFFER void init_log_buffer(size_t total_buffer_size); #endif @@ -281,7 +281,7 @@ class Logger : public Component { inline void HOT log_message_to_buffer_and_send_(bool &recursion_guard, uint8_t level, const char *tag, int line, FormatType format, va_list args, const char *thread_name) { RecursionGuard guard(recursion_guard); - LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; + LogBuffer buf{this->tx_buffer_, ESPHOME_LOGGER_TX_BUFFER_SIZE}; #ifdef USE_STORE_LOG_STR_IN_FLASH if constexpr (std::is_same_v) { this->format_log_to_buffer_with_terminator_P_(level, tag, line, format, args, buf); @@ -312,7 +312,6 @@ class Logger : public Component { // Group 4-byte aligned members first uint32_t baud_rate_; - char *tx_buffer_{nullptr}; #if defined(USE_ARDUINO) && !defined(USE_ESP32) Stream *hw_serial_{nullptr}; #endif @@ -354,7 +353,6 @@ class Logger : public Component { #endif // Group smaller types together at the end - uint16_t tx_buffer_size_{0}; uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) UARTSelection uart_{UART_SELECTION_UART0}; @@ -371,6 +369,9 @@ class Logger : public Component { bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms #endif + // Large buffer placed last to keep frequently-accessed member offsets small + char tx_buffer_[ESPHOME_LOGGER_TX_BUFFER_SIZE + 1]; // +1 for null terminator + // --- get_thread_name_ overloads (per-platform) --- #if defined(USE_ESP32) || defined(USE_LIBRETINY) diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 9d64771aec..d6ad77ff4f 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -89,16 +89,16 @@ void Logger::pre_setup() { switch (this->uart_) { case UART_SELECTION_UART0: this->uart_num_ = UART_NUM_0; - init_uart(this->uart_num_, baud_rate_, tx_buffer_size_); + init_uart(this->uart_num_, baud_rate_, ESPHOME_LOGGER_TX_BUFFER_SIZE); break; case UART_SELECTION_UART1: this->uart_num_ = UART_NUM_1; - init_uart(this->uart_num_, baud_rate_, tx_buffer_size_); + init_uart(this->uart_num_, baud_rate_, ESPHOME_LOGGER_TX_BUFFER_SIZE); break; #ifdef USE_ESP32_VARIANT_ESP32 case UART_SELECTION_UART2: this->uart_num_ = UART_NUM_2; - init_uart(this->uart_num_, baud_rate_, tx_buffer_size_); + init_uart(this->uart_num_, baud_rate_, ESPHOME_LOGGER_TX_BUFFER_SIZE); break; #endif #ifdef USE_LOGGER_USB_CDC diff --git a/esphome/components/logger/logger_host.cpp b/esphome/components/logger/logger_host.cpp index be12b6df6a..fe094f6e9e 100644 --- a/esphome/components/logger/logger_host.cpp +++ b/esphome/components/logger/logger_host.cpp @@ -5,8 +5,8 @@ namespace esphome::logger { void HOT Logger::write_msg_(const char *msg, uint16_t len) { static constexpr size_t TIMESTAMP_LEN = 10; // "[HH:MM:SS]" - // tx_buffer_size_ defaults to 512, so 768 covers default + headroom - char buffer[TIMESTAMP_LEN + 768]; + static constexpr size_t HEADROOM = 128; // Extra space for ANSI codes, newline, etc. + char buffer[TIMESTAMP_LEN + ESPHOME_LOGGER_TX_BUFFER_SIZE + HEADROOM]; time_t rawtime; time(&rawtime); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 02335e2745..1128df94f0 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -21,6 +21,7 @@ // logger #define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_VERY_VERBOSE +#define ESPHOME_LOGGER_TX_BUFFER_SIZE 512 #define USE_LOG_LISTENERS #define ESPHOME_LOG_MAX_LISTENERS 8 diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index e6fe733807..52f1fbd319 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -13,7 +13,7 @@ using namespace esphome; void setup() { App.pre_setup("livingroom", "LivingRoom", false); - auto *log = new logger::Logger(115200, 512); // NOLINT + auto *log = new logger::Logger(115200); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); App.register_component(log); From b539a5aa51e08db837d30be5310b917fa61f631b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 17:07:56 -0600 Subject: [PATCH 147/609] [water_heater] Fix device_id missing from state responses (#14212) --- esphome/components/api/api_connection.cpp | 3 +- .../fixtures/device_id_in_state.yaml | 115 ++++++++++++ tests/integration/test_device_id_in_state.py | 173 ++++++++++++------ 3 files changed, 232 insertions(+), 59 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 9fc263abbd..7bc9c45c05 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1346,9 +1346,8 @@ uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConne resp.target_temperature_low = wh->get_target_temperature_low(); resp.target_temperature_high = wh->get_target_temperature_high(); resp.state = wh->get_state(); - resp.key = wh->get_object_id_hash(); - return encode_message_to_buffer(resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(wh, resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size); } uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *wh = static_cast(entity); diff --git a/tests/integration/fixtures/device_id_in_state.yaml b/tests/integration/fixtures/device_id_in_state.yaml index c8548617b8..a5dfb7ee45 100644 --- a/tests/integration/fixtures/device_id_in_state.yaml +++ b/tests/integration/fixtures/device_id_in_state.yaml @@ -46,6 +46,7 @@ sensor: binary_sensor: - platform: template + id: motion_detected name: Motion Detected device_id: motion_sensor lambda: return true; @@ -82,3 +83,117 @@ output: write_action: - lambda: |- ESP_LOGD("test", "Light output: %d", state); + +cover: + - platform: template + name: Garage Door + device_id: motion_sensor + optimistic: true + +fan: + - platform: template + name: Ceiling Fan + device_id: humidity_monitor + speed_count: 3 + has_oscillating: false + has_direction: false + +lock: + - platform: template + name: Front Door Lock + device_id: motion_sensor + optimistic: true + +number: + - platform: template + name: Target Temperature + device_id: temperature_monitor + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + +select: + - platform: template + name: Mode Select + device_id: humidity_monitor + optimistic: true + options: + - "Auto" + - "Manual" + +text: + - platform: template + name: Device Label + device_id: temperature_monitor + optimistic: true + mode: text + +valve: + - platform: template + name: Water Valve + device_id: humidity_monitor + optimistic: true + +globals: + - id: global_away + type: bool + initial_value: "false" + - id: global_is_on + type: bool + initial_value: "true" + +water_heater: + - platform: template + name: Test Boiler + device_id: temperature_monitor + optimistic: true + current_temperature: !lambda "return 45.0f;" + target_temperature: !lambda "return 60.0f;" + away: !lambda "return id(global_away);" + is_on: !lambda "return id(global_is_on);" + supported_modes: + - "off" + - electric + visual: + min_temperature: 30.0 + max_temperature: 85.0 + target_temperature_step: 0.5 + set_action: + - lambda: |- + ESP_LOGD("test", "Water heater set"); + +alarm_control_panel: + - platform: template + name: House Alarm + device_id: motion_sensor + codes: + - "1234" + restore_mode: ALWAYS_DISARMED + binary_sensors: + - input: motion_detected + +datetime: + - platform: template + name: Schedule Date + device_id: temperature_monitor + type: date + optimistic: true + - platform: template + name: Schedule Time + device_id: humidity_monitor + type: time + optimistic: true + - platform: template + name: Schedule DateTime + device_id: motion_sensor + type: datetime + optimistic: true + +event: + - platform: template + name: Doorbell + device_id: motion_sensor + event_types: + - "press" + - "double_press" diff --git a/tests/integration/test_device_id_in_state.py b/tests/integration/test_device_id_in_state.py index 51088bcbf7..48de94a85a 100644 --- a/tests/integration/test_device_id_in_state.py +++ b/tests/integration/test_device_id_in_state.py @@ -4,11 +4,80 @@ from __future__ import annotations import asyncio -from aioesphomeapi import BinarySensorState, EntityState, SensorState, TextSensorState +from aioesphomeapi import ( + AlarmControlPanelEntityState, + BinarySensorState, + CoverState, + DateState, + DateTimeState, + EntityState, + FanState, + LightState, + LockEntityState, + NumberState, + SelectState, + SensorState, + SwitchState, + TextSensorState, + TextState, + TimeState, + ValveState, + WaterHeaterState, +) import pytest from .types import APIClientConnectedFactory, RunCompiledFunction +# Mapping of entity name to device name for all entities with device_id +ENTITY_TO_DEVICE = { + # Original entities + "Temperature": "Temperature Monitor", + "Humidity": "Humidity Monitor", + "Motion Detected": "Motion Sensor", + "Temperature Monitor Power": "Temperature Monitor", + "Temperature Status": "Temperature Monitor", + "Motion Light": "Motion Sensor", + # New entity types + "Garage Door": "Motion Sensor", + "Ceiling Fan": "Humidity Monitor", + "Front Door Lock": "Motion Sensor", + "Target Temperature": "Temperature Monitor", + "Mode Select": "Humidity Monitor", + "Device Label": "Temperature Monitor", + "Water Valve": "Humidity Monitor", + "Test Boiler": "Temperature Monitor", + "House Alarm": "Motion Sensor", + "Schedule Date": "Temperature Monitor", + "Schedule Time": "Humidity Monitor", + "Schedule DateTime": "Motion Sensor", + "Doorbell": "Motion Sensor", +} + +# Entities without device_id (should have device_id 0) +NO_DEVICE_ENTITIES = {"No Device Sensor"} + +# State types that should have non-zero device_id, mapped by their aioesphomeapi class +EXPECTED_STATE_TYPES = [ + (SensorState, "sensor"), + (BinarySensorState, "binary_sensor"), + (SwitchState, "switch"), + (TextSensorState, "text_sensor"), + (LightState, "light"), + (CoverState, "cover"), + (FanState, "fan"), + (LockEntityState, "lock"), + (NumberState, "number"), + (SelectState, "select"), + (TextState, "text"), + (ValveState, "valve"), + (WaterHeaterState, "water_heater"), + (AlarmControlPanelEntityState, "alarm_control_panel"), + (DateState, "date"), + (TimeState, "time"), + (DateTimeState, "datetime"), + # Event is stateless (no initial state sent on subscribe) +] + @pytest.mark.asyncio async def test_device_id_in_state( @@ -40,34 +109,35 @@ async def test_device_id_in_state( entity_device_mapping: dict[int, int] = {} for entity in all_entities: - # All entities have name and key attributes - if entity.name == "Temperature": - entity_device_mapping[entity.key] = device_ids["Temperature Monitor"] - elif entity.name == "Humidity": - entity_device_mapping[entity.key] = device_ids["Humidity Monitor"] - elif entity.name == "Motion Detected": - entity_device_mapping[entity.key] = device_ids["Motion Sensor"] - elif entity.name in {"Temperature Monitor Power", "Temperature Status"}: - entity_device_mapping[entity.key] = device_ids["Temperature Monitor"] - elif entity.name == "Motion Light": - entity_device_mapping[entity.key] = device_ids["Motion Sensor"] - elif entity.name == "No Device Sensor": - # Entity without device_id should have device_id 0 + if entity.name in ENTITY_TO_DEVICE: + expected_device = ENTITY_TO_DEVICE[entity.name] + entity_device_mapping[entity.key] = device_ids[expected_device] + elif entity.name in NO_DEVICE_ENTITIES: entity_device_mapping[entity.key] = 0 - assert len(entity_device_mapping) >= 6, ( - f"Expected at least 6 mapped entities, got {len(entity_device_mapping)}" + expected_count = len(ENTITY_TO_DEVICE) + len(NO_DEVICE_ENTITIES) + assert len(entity_device_mapping) >= expected_count, ( + f"Expected at least {expected_count} mapped entities, " + f"got {len(entity_device_mapping)}. " + f"Missing: {set(ENTITY_TO_DEVICE) | NO_DEVICE_ENTITIES - {e.name for e in all_entities}}" + ) + + # Subscribe to states and wait for all mapped entities + # Event entities are stateless (no initial state on subscribe), + # so exclude them from the expected count + stateless_keys = {e.key for e in all_entities if e.name == "Doorbell"} + stateful_count = len(entity_device_mapping) - len( + stateless_keys & entity_device_mapping.keys() ) - # Subscribe to states loop = asyncio.get_running_loop() states: dict[int, EntityState] = {} states_future: asyncio.Future[bool] = loop.create_future() def on_state(state: EntityState) -> None: - states[state.key] = state - # Check if we have states for all mapped entities - if len(states) >= len(entity_device_mapping) and not states_future.done(): + if state.key in entity_device_mapping: + states[state.key] = state + if len(states) >= stateful_count and not states_future.done(): states_future.set_result(True) client.subscribe_states(on_state) @@ -76,9 +146,16 @@ async def test_device_id_in_state( try: await asyncio.wait_for(states_future, timeout=10.0) except TimeoutError: + received_names = {e.name for e in all_entities if e.key in states} + missing_names = ( + (set(ENTITY_TO_DEVICE) | NO_DEVICE_ENTITIES) + - received_names + - {"Doorbell"} + ) pytest.fail( f"Did not receive all entity states within 10 seconds. " - f"Received {len(states)} states, expected {len(entity_device_mapping)}" + f"Received {len(states)} states. " + f"Missing: {missing_names}" ) # Verify each state has the correct device_id @@ -86,51 +163,33 @@ async def test_device_id_in_state( for key, expected_device_id in entity_device_mapping.items(): if key in states: state = states[key] + entity_name = next( + (e.name for e in all_entities if e.key == key), f"key={key}" + ) assert state.device_id == expected_device_id, ( - f"State for key {key} has device_id {state.device_id}, " - f"expected {expected_device_id}" + f"State for '{entity_name}' (type={type(state).__name__}) " + f"has device_id {state.device_id}, expected {expected_device_id}" ) verified_count += 1 - assert verified_count >= 6, ( - f"Only verified {verified_count} states, expected at least 6" + # All stateful entities should be verified (everything except Doorbell event) + expected_verified = expected_count - 1 # exclude Doorbell + assert verified_count >= expected_verified, ( + f"Only verified {verified_count} states, expected at least {expected_verified}" ) - # Test specific state types to ensure device_id is present - # Find a sensor state with device_id - sensor_state = next( - ( + # Verify each expected state type has at least one instance with non-zero device_id + for state_type, type_name in EXPECTED_STATE_TYPES: + matching = [ s for s in states.values() - if isinstance(s, SensorState) - and isinstance(s.state, float) - and s.device_id != 0 - ), - None, - ) - assert sensor_state is not None, "No sensor state with device_id found" - assert sensor_state.device_id > 0, "Sensor state should have non-zero device_id" - - # Find a binary sensor state - binary_sensor_state = next( - (s for s in states.values() if isinstance(s, BinarySensorState)), - None, - ) - assert binary_sensor_state is not None, "No binary sensor state found" - assert binary_sensor_state.device_id > 0, ( - "Binary sensor state should have non-zero device_id" - ) - - # Find a text sensor state - text_sensor_state = next( - (s for s in states.values() if isinstance(s, TextSensorState)), - None, - ) - assert text_sensor_state is not None, "No text sensor state found" - assert text_sensor_state.device_id > 0, ( - "Text sensor state should have non-zero device_id" - ) + if isinstance(s, state_type) and s.device_id != 0 + ] + assert matching, ( + f"No {type_name} state (type={state_type.__name__}) " + f"with non-zero device_id found" + ) # Verify the "No Device Sensor" has device_id = 0 no_device_key = next( From ded457c2c1a5b27aba9af05b7b29a38dd832e7fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 18:52:46 -0600 Subject: [PATCH 148/609] [libretiny] Tune oversized lwIP defaults for ESPHome (#14186) Co-authored-by: Claude Opus 4.6 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/api/__init__.py | 4 +- esphome/components/captive_portal/__init__.py | 6 +- esphome/components/esp32/__init__.py | 59 +++---- .../esp32_camera_web_server/__init__.py | 6 +- esphome/components/esphome/ota/__init__.py | 5 +- esphome/components/libretiny/__init__.py | 153 +++++++++++++++++- esphome/components/mdns/__init__.py | 2 +- esphome/components/socket/__init__.py | 90 ++++++++++- esphome/components/udp/__init__.py | 2 +- esphome/components/web_server/__init__.py | 6 +- .../components/web_server_base/__init__.py | 25 ++- esphome/components/wifi/__init__.py | 20 +++ .../socket/test_wake_loop_threadsafe.py | 8 +- 13 files changed, 327 insertions(+), 59 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 9bff9f5635..3f7cafb485 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -233,8 +233,8 @@ def _consume_api_sockets(config: ConfigType) -> ConfigType: # API needs 1 listening socket + typically 3 concurrent client connections # (not max_connections, which is the upper limit rarely reached) - sockets_needed = 1 + 3 - socket.consume_sockets(sockets_needed, "api")(config) + socket.consume_sockets(3, "api")(config) + socket.consume_sockets(1, "api", socket.SocketType.TCP_LISTEN)(config) return config diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 049618219e..6c190814c0 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -76,13 +76,15 @@ def _final_validate(config: ConfigType) -> ConfigType: # Register socket needs for DNS server and additional HTTP connections # - 1 UDP socket for DNS server - # - 3 additional TCP sockets for captive portal detection probes + configuration requests + # - 3 TCP sockets for captive portal detection probes + configuration requests # OS captive portal detection makes multiple probe requests that stay in TIME_WAIT. # Need headroom for actual user configuration requests. # LRU purging will reclaim idle sockets to prevent exhaustion from repeated attempts. + # The listening socket is registered by web_server_base (shared HTTP server). from esphome.components import socket - socket.consume_sockets(4, "captive_portal")(config) + socket.consume_sockets(3, "captive_portal")(config) + socket.consume_sockets(1, "captive_portal", socket.SocketType.UDP)(config) return config diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 06677006ea..4c211b2f2a 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1258,21 +1258,15 @@ def _configure_lwip_max_sockets(conf: dict) -> None: This function runs in to_code() after all components have registered their socket needs. User-provided sdkconfig_options take precedence. """ - from esphome.components.socket import KEY_SOCKET_CONSUMERS + from esphome.components.socket import get_socket_counts # Check if user manually specified CONFIG_LWIP_MAX_SOCKETS user_max_sockets = conf[CONF_SDKCONFIG_OPTIONS].get("CONFIG_LWIP_MAX_SOCKETS") - socket_consumers: dict[str, int] = CORE.data.get(KEY_SOCKET_CONSUMERS, {}) - total_sockets = sum(socket_consumers.values()) - - # Early return if no sockets registered and no user override - if total_sockets == 0 and user_max_sockets is None: - return - - components_list = ", ".join( - f"{name}={count}" for name, count in sorted(socket_consumers.items()) - ) + # CONFIG_LWIP_MAX_SOCKETS is a single VFS socket pool shared by all socket + # types (TCP clients, TCP listeners, and UDP). Include all three counts. + sc = get_socket_counts() + total_sockets = sc.tcp + sc.udp + sc.tcp_listen # User specified their own value - respect it but warn if insufficient if user_max_sockets is not None: @@ -1281,22 +1275,23 @@ def _configure_lwip_max_sockets(conf: dict) -> None: user_max_sockets, ) - # Warn if user's value is less than what components need - if total_sockets > 0: - user_sockets_int = 0 - with contextlib.suppress(ValueError, TypeError): - user_sockets_int = int(user_max_sockets) + user_sockets_int = 0 + with contextlib.suppress(ValueError, TypeError): + user_sockets_int = int(user_max_sockets) - if user_sockets_int < total_sockets: - _LOGGER.warning( - "CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration " - "needs %d sockets (registered: %s). You may experience socket " - "exhaustion errors. Consider increasing to at least %d.", - user_sockets_int, - total_sockets, - components_list, - total_sockets, - ) + if user_sockets_int < total_sockets: + _LOGGER.warning( + "CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration " + "needs %d sockets (%d TCP + %d UDP + %d TCP_LISTEN). You may " + "experience socket exhaustion errors. Consider increasing to " + "at least %d.", + user_sockets_int, + total_sockets, + sc.tcp, + sc.udp, + sc.tcp_listen, + total_sockets, + ) # User's value already added via sdkconfig_options processing return @@ -1305,11 +1300,19 @@ def _configure_lwip_max_sockets(conf: dict) -> None: max_sockets = max(DEFAULT_MAX_SOCKETS, total_sockets) log_level = logging.INFO if max_sockets > DEFAULT_MAX_SOCKETS else logging.DEBUG + sock_min = " (min)" if max_sockets > total_sockets else "" _LOGGER.log( log_level, - "Setting CONFIG_LWIP_MAX_SOCKETS to %d (registered: %s)", + "Setting CONFIG_LWIP_MAX_SOCKETS to %d%s " + "(TCP=%d [%s], UDP=%d [%s], TCP_LISTEN=%d [%s])", max_sockets, - components_list, + sock_min, + sc.tcp, + sc.tcp_details, + sc.udp, + sc.udp_details, + sc.tcp_listen, + sc.tcp_listen_details, ) add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets) diff --git a/esphome/components/esp32_camera_web_server/__init__.py b/esphome/components/esp32_camera_web_server/__init__.py index ed1aaa2e07..da260ad7a1 100644 --- a/esphome/components/esp32_camera_web_server/__init__.py +++ b/esphome/components/esp32_camera_web_server/__init__.py @@ -20,8 +20,10 @@ def _consume_camera_web_server_sockets(config: ConfigType) -> ConfigType: from esphome.components import socket # Each camera web server instance needs 1 listening socket + 2 client connections - sockets_needed = 3 - socket.consume_sockets(sockets_needed, "esp32_camera_web_server")(config) + socket.consume_sockets(2, "esp32_camera_web_server")(config) + socket.consume_sockets(1, "esp32_camera_web_server", socket.SocketType.TCP_LISTEN)( + config + ) return config diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index 2f637d714d..337064dd27 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -97,8 +97,9 @@ def _consume_ota_sockets(config: ConfigType) -> ConfigType: """Register socket needs for OTA component.""" from esphome.components import socket - # OTA needs 1 listening socket (client connections are temporary during updates) - socket.consume_sockets(1, "ota")(config) + # OTA needs 1 listening socket. The active transfer connection during an update + # uses a TCP PCB from the general pool, covered by MIN_TCP_SOCKETS headroom. + socket.consume_sockets(1, "ota", socket.SocketType.TCP_LISTEN)(config) return config diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 01445da7ee..2291114d9a 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -275,6 +275,146 @@ BASE_SCHEMA.add_extra(_detect_variant) BASE_SCHEMA.add_extra(_update_core_data) +def _configure_lwip(config: dict) -> None: + """Configure lwIP options for LibreTiny platforms. + + The BK/RTL/LN SDKs each ship different lwIP defaults. BK72XX defaults are + wildly oversized for ESPHome's IoT use case, causing OOM on BK7231N. + RTL87XX and LN882H have more conservative defaults but still need tuning + for ESPHome's socket usage patterns. + + See https://github.com/esphome/esphome/issues/14095 + + Comparison of SDK defaults vs ESPHome targets (TCP_MSS=1460 on all LT): + + Setting ESP8266 ESP32 BK SDK RTL SDK LN SDK New + ──────────────────────────────────────────────────────────────────────────── + TCP_SND_BUF 2×MSS 4×MSS 10×MSS 5×MSS 7×MSS 4×MSS + TCP_WND 4×MSS 4×MSS 3/10×MSS 2×MSS 3×MSS 4×MSS + MEM_LIBC_MALLOC 1 1 0 0 1 1 + MEMP_MEM_MALLOC 1 1 0 0 0 1 + MEM_SIZE N/A* N/A* 16/32KB 5KB N/A* N/A* BK + PBUF_POOL_SIZE 10 16 3/10 20 20 10 BK + MAX_SOCKETS_TCP 5 16 12 —** —** dynamic + MAX_SOCKETS_UDP 4 16 22 —** —** dynamic + TCP_SND_QUEUELEN ~8 17 20 20 35 17 + MEMP_NUM_TCP_SEG 10 16 40 20 =qlen 17 + MEMP_NUM_TCP_PCB 5 16 12 10 8 =TCP + MEMP_NUM_TCP_PCB_LISTEN 4 16 4 5 3 dynamic + MEMP_NUM_UDP_PCB 4 16 25*** 7**** 7**** =UDP + MEMP_NUM_NETCONN 0 10 38 4***** =sum =sum + MEMP_NUM_NETBUF 0 2 16 2***** 8 4 + MEMP_NUM_TCPIP_MSG_INPKT 4 8 16 8***** 12 8 + + * ESP8266/ESP32/LN882H use MEM_LIBC_MALLOC=1 (system heap, no dedicated pool). + ESP8266/ESP32 also use MEMP_MEM_MALLOC=1 (MEMP pools from heap, not static). + ** RTL/LN SDKs don't define MAX_SOCKETS_TCP/UDP (LibreTiny-specific). + *** BK LT overlay: MAX_SOCKETS_UDP+2+1 = 25. + **** RTL/LN LT overlay overrides to flat 7. + ***** Not defined in RTL SDK — lwIP opt.h defaults shown. + "dynamic" = auto-calculated from component socket registrations via + socket.get_socket_counts() with minimums of 8 TCP / 6 UDP. + """ + from esphome.components.socket import ( + MIN_TCP_LISTEN_SOCKETS, + MIN_TCP_SOCKETS, + MIN_UDP_SOCKETS, + get_socket_counts, + ) + + sc = get_socket_counts() + # Apply platform minimums — ensure headroom for ESPHome's needs + tcp_sockets = max(MIN_TCP_SOCKETS, sc.tcp) + udp_sockets = max(MIN_UDP_SOCKETS, sc.udp) + # Listening sockets — registered by components (api, ota, web_server_base, etc.) + # Not all components register yet, so ensure a minimum for baseline operation. + listening_tcp = max(MIN_TCP_LISTEN_SOCKETS, sc.tcp_listen) + + # TCP_SND_BUF: ESPAsyncWebServer allocates malloc(tcp_sndbuf()) per + # response chunk. At 10×MSS=14.6KB (BK default) this causes OOM (#14095). + # 4×MSS=5,840 matches ESP32. RTL(5×) and LN(7×) are close already. + tcp_snd_buf = "(4*TCP_MSS)" # BK: 10×MSS, RTL: 5×MSS, LN: 7×MSS + + # TCP_WND: receive window. 4×MSS matches ESP32. + # RTL SDK uses only 2×MSS; increasing to 4× is safe and improves throughput. + tcp_wnd = "(4*TCP_MSS)" # BK: 10×MSS, RTL: 2×MSS, LN: 3×MSS + + # TCP_SND_QUEUELEN: max pbufs queued for send buffer + # ESP-IDF formula: (4 * TCP_SND_BUF + (TCP_MSS - 1)) / TCP_MSS + # With 4×MSS: (4*5840 + 1459) / 1460 = 17 — match ESP32 + tcp_snd_queuelen = 17 # BK: 20, RTL: 20, LN: 35 + # MEMP_NUM_TCP_SEG: segment pool, must be >= TCP_SND_QUEUELEN (lwIP sanity check) + memp_num_tcp_seg = tcp_snd_queuelen # BK: 40, RTL: 20, LN: =qlen + + lwip_opts: list[str] = [ + # Disable statistics — not needed for production, saves RAM + "LWIP_STATS=0", # BK: 1, RTL: 0 already, LN: 0 already + "MEM_STATS=0", + "MEMP_STATS=0", + # TCP send buffer — 4×MSS matches ESP32 + f"TCP_SND_BUF={tcp_snd_buf}", + # TCP receive window — 4×MSS matches ESP32 + f"TCP_WND={tcp_wnd}", + # Socket counts — auto-calculated from component registrations + f"MAX_SOCKETS_TCP={tcp_sockets}", + f"MAX_SOCKETS_UDP={udp_sockets}", + # Listening sockets — BK SDK uses this to derive MEMP_NUM_TCP_PCB_LISTEN; + # RTL/LN don't use it, but we set MEMP_NUM_TCP_PCB_LISTEN explicitly below. + f"MAX_LISTENING_SOCKETS_TCP={listening_tcp}", + # Queued segment limits — derived from 4×MSS buffer size + f"TCP_SND_QUEUELEN={tcp_snd_queuelen}", + f"MEMP_NUM_TCP_SEG={memp_num_tcp_seg}", # must be >= queuelen + # PCB pools — active connections + listening sockets + f"MEMP_NUM_TCP_PCB={tcp_sockets}", # BK: 12, RTL: 10, LN: 8 + f"MEMP_NUM_TCP_PCB_LISTEN={listening_tcp}", # BK: =MAX_LISTENING, RTL: 5, LN: 3 + # UDP PCB pool — includes wifi.lwip_internal (DHCP + DNS) + f"MEMP_NUM_UDP_PCB={udp_sockets}", # BK: 25, RTL/LN: 7 via LT + # Netconn pool — each socket (active + listening) needs a netconn + f"MEMP_NUM_NETCONN={tcp_sockets + udp_sockets + listening_tcp}", + # Netbuf pool + "MEMP_NUM_NETBUF=4", # BK: 16, RTL: 2 (opt.h), LN: 8 + # Inbound message pool + "MEMP_NUM_TCPIP_MSG_INPKT=8", # BK: 16, RTL: 8 (opt.h), LN: 12 + ] + + # Use system heap for all lwIP allocations on all LibreTiny platforms. + # - MEM_LIBC_MALLOC=1: Use system heap instead of dedicated lwIP heap. + # LN882H already ships with this. BK SDK defaults to a 16/32KB dedicated + # pool that fragments during OTA. RTL SDK defaults to a 5KB pool. + # All three SDKs wire malloc → pvPortMalloc (FreeRTOS thread-safe heap). + # - MEMP_MEM_MALLOC=1: Allocate MEMP pools from heap on demand instead + # of static arrays. Saves ~20KB RAM on BK72XX. Safe because WiFi + # receive paths run in task context, not ISR context. ESP32 and ESP8266 + # both ship with MEMP_MEM_MALLOC=1. + lwip_opts.append("MEM_LIBC_MALLOC=1") + lwip_opts.append("MEMP_MEM_MALLOC=1") + + # BK72XX-specific: PBUF_POOL_SIZE override + # BK SDK "reduced plan" sets this to only 3 — too few for multiple + # concurrent connections (API + web_server + OTA). BK default plan + # uses 10; match that. RTL(20) and LN(20) need no override. + # With MEMP_MEM_MALLOC=1, this is a max count (allocated on demand). + if CORE.is_bk72xx: + lwip_opts.append("PBUF_POOL_SIZE=10") + + tcp_min = " (min)" if tcp_sockets > sc.tcp else "" + udp_min = " (min)" if udp_sockets > sc.udp else "" + listen_min = " (min)" if listening_tcp > sc.tcp_listen else "" + _LOGGER.info( + "Configuring lwIP: TCP=%d%s [%s], UDP=%d%s [%s], TCP_LISTEN=%d%s [%s]", + tcp_sockets, + tcp_min, + sc.tcp_details, + udp_sockets, + udp_min, + sc.udp_details, + listening_tcp, + listen_min, + sc.tcp_listen_details, + ) + cg.add_platformio_option("custom_options.lwip", lwip_opts) + + # pylint: disable=use-dict-literal async def component_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) @@ -389,11 +529,12 @@ async def component_to_code(config): "custom_options.sys_config#h", _BK7231N_SYS_CONFIG_OPTIONS ) - # Disable LWIP statistics to save RAM - not needed in production - # Must explicitly disable all sub-stats to avoid redefinition warnings - cg.add_platformio_option( - "custom_options.lwip", - ["LWIP_STATS=0", "MEM_STATS=0", "MEMP_STATS=0"], - ) + # Tune lwIP for ESPHome's actual needs. + # The SDK defaults (TCP_SND_BUF=10*MSS, MAX_SOCKETS_TCP=12, MEM_SIZE=32KB) + # are wildly oversized for an IoT device. ESPAsyncWebServer allocates + # malloc(tcp_sndbuf()) per response chunk — at 14.6KB this causes silent + # OOM on memory-constrained chips like BK7231N. + # See https://github.com/esphome/esphome/issues/14095 + _configure_lwip(config) await cg.register_component(var, config) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index f87f929615..420e6a60e3 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -56,7 +56,7 @@ def _consume_mdns_sockets(config: ConfigType) -> ConfigType: from esphome.components import socket # mDNS needs 2 sockets (IPv4 + IPv6 multicast) - socket.consume_sockets(2, "mdns")(config) + socket.consume_sockets(2, "mdns", socket.SocketType.UDP)(config) return config diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index e364da78f8..d82f0c7aba 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -1,9 +1,14 @@ from collections.abc import Callable, MutableMapping +from dataclasses import dataclass +from enum import StrEnum +import logging import esphome.codegen as cg import esphome.config_validation as cv from esphome.core import CORE +_LOGGER = logging.getLogger(__name__) + CODEOWNERS = ["@esphome/core"] CONF_IMPLEMENTATION = "implementation" @@ -13,33 +18,110 @@ IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets" # Socket tracking infrastructure # Components register their socket needs and platforms read this to configure appropriately -KEY_SOCKET_CONSUMERS = "socket_consumers" +KEY_SOCKET_CONSUMERS_TCP = "socket_consumers_tcp" +KEY_SOCKET_CONSUMERS_UDP = "socket_consumers_udp" +KEY_SOCKET_CONSUMERS_TCP_LISTEN = "socket_consumers_tcp_listen" + +# Recommended minimum socket counts. +# Platforms should apply these (or their own) on top of get_socket_counts(). +# These cover minimal configs (e.g. api-only without web_server). +# When web_server is present, its 5 registered sockets push past the TCP minimum. +MIN_TCP_SOCKETS = 8 +MIN_UDP_SOCKETS = 6 +# Minimum listening sockets — at least api + ota baseline. +MIN_TCP_LISTEN_SOCKETS = 2 # Wake loop threadsafe support tracking KEY_WAKE_LOOP_THREADSAFE_REQUIRED = "wake_loop_threadsafe_required" +class SocketType(StrEnum): + TCP = "tcp" + UDP = "udp" + TCP_LISTEN = "tcp_listen" + + +_SOCKET_TYPE_KEYS = { + SocketType.TCP: KEY_SOCKET_CONSUMERS_TCP, + SocketType.UDP: KEY_SOCKET_CONSUMERS_UDP, + SocketType.TCP_LISTEN: KEY_SOCKET_CONSUMERS_TCP_LISTEN, +} + + def consume_sockets( - value: int, consumer: str + value: int, consumer: str, socket_type: SocketType = SocketType.TCP ) -> Callable[[MutableMapping], MutableMapping]: """Register socket usage for a component. Args: value: Number of sockets needed by the component consumer: Name of the component consuming the sockets + socket_type: Type of socket (SocketType.TCP, SocketType.UDP, or SocketType.TCP_LISTEN) Returns: A validator function that records the socket usage """ + typed_key = _SOCKET_TYPE_KEYS[socket_type] def _consume_sockets(config: MutableMapping) -> MutableMapping: - consumers: dict[str, int] = CORE.data.setdefault(KEY_SOCKET_CONSUMERS, {}) + consumers: dict[str, int] = CORE.data.setdefault(typed_key, {}) consumers[consumer] = consumers.get(consumer, 0) + value return config return _consume_sockets +def _format_consumers(consumers: dict[str, int]) -> str: + """Format consumer dict as 'name=count, ...' or 'none'.""" + if not consumers: + return "none" + return ", ".join(f"{name}={count}" for name, count in sorted(consumers.items())) + + +@dataclass(frozen=True) +class SocketCounts: + """Socket counts and component details for platform configuration.""" + + tcp: int + udp: int + tcp_listen: int + tcp_details: str + udp_details: str + tcp_listen_details: str + + +def get_socket_counts() -> SocketCounts: + """Return socket counts and component details for platform configuration. + + Platforms call this during code generation to configure lwIP socket limits. + All components will have registered their needs by then. + + Platforms should apply their own minimums on top of these values. + """ + tcp_consumers = CORE.data.get(KEY_SOCKET_CONSUMERS_TCP, {}) + udp_consumers = CORE.data.get(KEY_SOCKET_CONSUMERS_UDP, {}) + tcp_listen_consumers = CORE.data.get(KEY_SOCKET_CONSUMERS_TCP_LISTEN, {}) + tcp = sum(tcp_consumers.values()) + udp = sum(udp_consumers.values()) + tcp_listen = sum(tcp_listen_consumers.values()) + + tcp_details = _format_consumers(tcp_consumers) + udp_details = _format_consumers(udp_consumers) + tcp_listen_details = _format_consumers(tcp_listen_consumers) + _LOGGER.debug( + "Socket counts: TCP=%d (%s), UDP=%d (%s), TCP_LISTEN=%d (%s)", + tcp, + tcp_details, + udp, + udp_details, + tcp_listen, + tcp_listen_details, + ) + return SocketCounts( + tcp, udp, tcp_listen, tcp_details, udp_details, tcp_listen_details + ) + + def require_wake_loop_threadsafe() -> None: """Mark that wake_loop_threadsafe support is required by a component. @@ -66,7 +148,7 @@ def require_wake_loop_threadsafe() -> None: CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True cg.add_define("USE_WAKE_LOOP_THREADSAFE") # Consume 1 socket for the shared wake notification socket - consume_sockets(1, "socket.wake_loop_threadsafe")({}) + consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({}) CONFIG_SCHEMA = cv.Schema( diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index c9586d0b95..37dd871a6c 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -73,7 +73,7 @@ def _consume_udp_sockets(config: ConfigType) -> ConfigType: # UDP uses up to 2 sockets: 1 broadcast + 1 listen # Whether each is used depends on code generation, so register worst case - socket.consume_sockets(2, "udp")(config) + socket.consume_sockets(2, "udp", socket.SocketType.UDP)(config) return config diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 9305a2de61..84910b6f90 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -144,11 +144,11 @@ def _consume_web_server_sockets(config: ConfigType) -> ConfigType: """Register socket needs for web_server component.""" from esphome.components import socket - # Web server needs 1 listening socket + typically 5 concurrent client connections + # Web server needs typically 5 concurrent client connections # (browser opens connections for page resources, SSE event stream, and POST # requests for entity control which may linger before closing) - sockets_needed = 6 - socket.consume_sockets(sockets_needed, "web_server")(config) + # The listening socket is registered by web_server_base (shared with captive_portal) + socket.consume_sockets(5, "web_server")(config) return config diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 183b907ae6..b587841dfd 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -23,10 +23,27 @@ web_server_base_ns = cg.esphome_ns.namespace("web_server_base") WebServerBase = web_server_base_ns.class_("WebServerBase") CONF_WEB_SERVER_BASE_ID = "web_server_base_id" -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(WebServerBase), - } + + +def _consume_web_server_base_sockets(config): + """Register the shared listening socket for the HTTP server. + + web_server_base is the shared HTTP server used by web_server and captive_portal. + The listening socket is registered here rather than in each consumer. + """ + from esphome.components import socket + + socket.consume_sockets(1, "web_server_base", socket.SocketType.TCP_LISTEN)(config) + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(WebServerBase), + } + ), + _consume_web_server_base_sockets, ) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 8c1deb6217..b89245b10e 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -58,6 +58,7 @@ from esphome.const import ( ) from esphome.core import CORE, CoroPriority, HexInt, coroutine_with_priority import esphome.final_validate as fv +from esphome.types import ConfigType from . import wpa2_eap @@ -269,9 +270,28 @@ def final_validate(config): ) +def _consume_wifi_sockets(config: ConfigType) -> ConfigType: + """Register UDP PCBs used internally by lwIP for DHCP and DNS. + + Only needed on LibreTiny where we directly set MEMP_NUM_UDP_PCB (the raw + PCB pool shared by both application sockets and lwIP internals like DHCP/DNS). + On ESP32, CONFIG_LWIP_MAX_SOCKETS only controls the POSIX socket layer — + DHCP/DNS use raw udp_new() which bypasses it entirely. + """ + if not (CORE.is_bk72xx or CORE.is_rtl87xx or CORE.is_ln882x): + return config + from esphome.components import socket + + # lwIP allocates UDP PCBs for DHCP client and DNS resolver internally. + # These are not application sockets but consume MEMP_NUM_UDP_PCB pool entries. + socket.consume_sockets(2, "wifi.lwip_internal", socket.SocketType.UDP)(config) + return config + + FINAL_VALIDATE_SCHEMA = cv.All( final_validate, validate_variant, + _consume_wifi_sockets, ) diff --git a/tests/components/socket/test_wake_loop_threadsafe.py b/tests/components/socket/test_wake_loop_threadsafe.py index b4bc95176d..a40b6068a8 100644 --- a/tests/components/socket/test_wake_loop_threadsafe.py +++ b/tests/components/socket/test_wake_loop_threadsafe.py @@ -66,12 +66,12 @@ def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() - CORE.config = {"logger": {}} # Track initial socket consumer state - initial_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS, {}) + initial_udp = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) # Call require_wake_loop_threadsafe socket.require_wake_loop_threadsafe() # Verify no socket was consumed - consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS, {}) - assert "socket.wake_loop_threadsafe" not in consumers - assert consumers == initial_consumers + udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) + assert "socket.wake_loop_threadsafe" not in udp_consumers + assert udp_consumers == initial_udp From 263fff0ba2fc5ea7b9c849da88600f17852a86aa Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:35:00 +0100 Subject: [PATCH 149/609] Move CONF_OUTPUT_POWER into const.py (#14201) --- esphome/components/cc1101/__init__.py | 2 +- esphome/components/wifi/__init__.py | 2 +- esphome/const.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index 14b92a18a4..e2e5986daf 100644 --- a/esphome/components/cc1101/__init__.py +++ b/esphome/components/cc1101/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_DATA, CONF_FREQUENCY, CONF_ID, + CONF_OUTPUT_POWER, CONF_VALUE, CONF_WAIT_TIME, ) @@ -22,7 +23,6 @@ ns = cg.esphome_ns.namespace("cc1101") CC1101Component = ns.class_("CC1101Component", cg.Component, spi.SPIDevice) # Config keys -CONF_OUTPUT_POWER = "output_power" CONF_RX_ATTENUATION = "rx_attenuation" CONF_DC_BLOCKING_FILTER = "dc_blocking_filter" CONF_IF_FREQUENCY = "if_frequency" diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index b89245b10e..2aa63b87cc 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -42,6 +42,7 @@ from esphome.const import ( CONF_ON_CONNECT, CONF_ON_DISCONNECT, CONF_ON_ERROR, + CONF_OUTPUT_POWER, CONF_PASSWORD, CONF_POWER_SAVE_MODE, CONF_PRIORITY, @@ -344,7 +345,6 @@ def _validate(config): return config -CONF_OUTPUT_POWER = "output_power" CONF_PASSIVE_SCAN = "passive_scan" CONFIG_SCHEMA = cv.All( cv.Schema( diff --git a/esphome/const.py b/esphome/const.py index 0179aa129e..ccc9d56dbb 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -748,6 +748,7 @@ CONF_OTA = "ota" CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" CONF_OUTPUT = "output" CONF_OUTPUT_ID = "output_id" +CONF_OUTPUT_POWER = "output_power" CONF_OUTPUT_SPEAKER = "output_speaker" CONF_OUTPUTS = "outputs" CONF_OVERSAMPLING = "oversampling" From 6e70987451dfce381645e06ec61e87fab745b900 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 21:35:30 -0600 Subject: [PATCH 150/609] [binary_sensor] Conditionally compile filter infrastructure (#14215) --- esphome/components/binary_sensor/__init__.py | 1 + esphome/components/binary_sensor/binary_sensor.cpp | 6 ++++++ esphome/components/binary_sensor/binary_sensor.h | 6 ++++++ esphome/components/binary_sensor/filter.cpp | 5 +++++ esphome/components/binary_sensor/filter.h | 5 +++++ esphome/core/defines.h | 1 + 6 files changed, 24 insertions(+) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index c38d6b78d3..4500168cb1 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -562,6 +562,7 @@ async def setup_binary_sensor_core_(var, config): if inverted := config.get(CONF_INVERTED): cg.add(var.set_inverted(inverted)) if filters_config := config.get(CONF_FILTERS): + cg.add_define("USE_BINARY_SENSOR_FILTER") filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config) cg.add(var.add_filters(filters)) diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 7c3b06970d..c4d3a29a1e 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -18,11 +18,15 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi } void BinarySensor::publish_state(bool new_state) { +#ifdef USE_BINARY_SENSOR_FILTER if (this->filter_list_ == nullptr) { +#endif this->send_state_internal(new_state); +#ifdef USE_BINARY_SENSOR_FILTER } else { this->filter_list_->input(new_state); } +#endif } void BinarySensor::publish_initial_state(bool new_state) { this->invalidate_state(); @@ -47,6 +51,7 @@ bool BinarySensor::set_new_state(const optional &new_state) { return false; } +#ifdef USE_BINARY_SENSOR_FILTER void BinarySensor::add_filter(Filter *filter) { filter->parent_ = this; if (this->filter_list_ == nullptr) { @@ -63,6 +68,7 @@ void BinarySensor::add_filters(std::initializer_list filters) { this->add_filter(filter); } } +#endif // USE_BINARY_SENSOR_FILTER bool BinarySensor::is_status_binary_sensor() const { return false; } } // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 83c992bfed..4b655e1bd1 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -2,7 +2,9 @@ #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" +#ifdef USE_BINARY_SENSOR_FILTER #include "esphome/components/binary_sensor/filter.h" +#endif #include @@ -45,8 +47,10 @@ class BinarySensor : public StatefulEntityBase, public EntityBase_DeviceCl */ void publish_initial_state(bool new_state); +#ifdef USE_BINARY_SENSOR_FILTER void add_filter(Filter *filter); void add_filters(std::initializer_list filters); +#endif // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -60,7 +64,9 @@ class BinarySensor : public StatefulEntityBase, public EntityBase_DeviceCl bool state{}; protected: +#ifdef USE_BINARY_SENSOR_FILTER Filter *filter_list_{nullptr}; +#endif bool set_new_state(const optional &new_state) override; }; diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index d69671c5bf..25a69c413a 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -1,3 +1,6 @@ +#include "esphome/core/defines.h" +#ifdef USE_BINARY_SENSOR_FILTER + #include "filter.h" #include "binary_sensor.h" @@ -142,3 +145,5 @@ optional SettleFilter::new_value(bool value) { float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; } } // namespace esphome::binary_sensor + +#endif // USE_BINARY_SENSOR_FILTER diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 59bc43eeba..2735a32ab0 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -1,5 +1,8 @@ #pragma once +#include "esphome/core/defines.h" +#ifdef USE_BINARY_SENSOR_FILTER + #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -138,3 +141,5 @@ class SettleFilter : public Filter, public Component { }; } // namespace esphome::binary_sensor + +#endif // USE_BINARY_SENSOR_FILTER diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 1128df94f0..99e80785ec 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -29,6 +29,7 @@ #define USE_ALARM_CONTROL_PANEL #define USE_AREAS #define USE_BINARY_SENSOR +#define USE_BINARY_SENSOR_FILTER #define USE_BUTTON #define USE_CAMERA #define USE_CLIMATE From 93ce582ad30d1059cf59335882a6bd231a93127e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 21:35:51 -0600 Subject: [PATCH 151/609] [sensor] Conditionally compile filter infrastructure (#14214) Co-authored-by: Claude Opus 4.6 --- esphome/components/bme680_bsec/bme680_bsec.h | 1 + esphome/components/haier/hon_climate.h | 1 + esphome/components/kamstrup_kmp/kamstrup_kmp.h | 1 + esphome/components/sensor/__init__.py | 1 + esphome/components/sensor/filter.cpp | 5 +++++ esphome/components/sensor/filter.h | 6 +++++- esphome/components/sensor/sensor.cpp | 6 ++++++ esphome/components/sensor/sensor.h | 8 ++++++++ esphome/components/sound_level/sound_level.h | 1 + esphome/components/wireguard/wireguard.h | 1 + esphome/core/defines.h | 1 + 11 files changed, 31 insertions(+), 1 deletion(-) diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h index ec919f31df..22aa2789e6 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.h +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -7,6 +7,7 @@ #include "esphome/core/preferences.h" #include "esphome/core/defines.h" #include +#include #ifdef USE_BSEC #include diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index 608d5e7f21..4565ed2981 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -1,6 +1,7 @@ #pragma once #include +#include #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" #endif diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.h b/esphome/components/kamstrup_kmp/kamstrup_kmp.h index f84e360132..725cf20abf 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.h +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.h @@ -1,5 +1,6 @@ #pragma once +#include #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" #include "esphome/core/component.h" diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index ebbe0fbccc..b0e0c28bda 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -903,6 +903,7 @@ async def setup_sensor_core_(var, config): if config[CONF_FORCE_UPDATE]: cg.add(var.set_force_update(True)) if config.get(CONF_FILTERS): # must exist and not be empty + cg.add_define("USE_SENSOR_FILTER") filters = await build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index ea0e2f0d7c..cd4db98457 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -1,3 +1,6 @@ +#include "esphome/core/defines.h" +#ifdef USE_SENSOR_FILTER + #include "filter.h" #include #include "esphome/core/application.h" @@ -580,3 +583,5 @@ void StreamingMovingAverageFilter::reset_batch() { } } // namespace esphome::sensor + +#endif // USE_SENSOR_FILTER diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 573b916a5d..8bfcdb37cf 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -1,6 +1,8 @@ #pragma once -#include +#include "esphome/core/defines.h" +#ifdef USE_SENSOR_FILTER + #include #include #include "esphome/core/automation.h" @@ -638,3 +640,5 @@ class StreamingMovingAverageFilter : public StreamingFilter { }; } // namespace esphome::sensor + +#endif // USE_SENSOR_FILTER diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index ae2ee3e3d1..a7af6403ef 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -68,11 +68,15 @@ void Sensor::publish_state(float state) { ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state); +#ifdef USE_SENSOR_FILTER if (this->filter_list_ == nullptr) { +#endif this->internal_send_state_to_frontend(state); +#ifdef USE_SENSOR_FILTER } else { this->filter_list_->input(state); } +#endif } void Sensor::add_on_state_callback(std::function &&callback) { this->callback_.add(std::move(callback)); } @@ -80,6 +84,7 @@ void Sensor::add_on_raw_state_callback(std::function &&callback) { this->raw_callback_.add(std::move(callback)); } +#ifdef USE_SENSOR_FILTER void Sensor::add_filter(Filter *filter) { // inefficient, but only happens once on every sensor setup and nobody's going to have massive amounts of // filters @@ -109,6 +114,7 @@ void Sensor::clear_filters() { } this->filter_list_ = nullptr; } +#endif // USE_SENSOR_FILTER float Sensor::get_state() const { return this->state; } float Sensor::get_raw_state() const { return this->raw_state; } diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 80981b8e28..54e75ee2a1 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -4,13 +4,17 @@ #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#ifdef USE_SENSOR_FILTER #include "esphome/components/sensor/filter.h" +#endif #include #include namespace esphome::sensor { +class Sensor; + void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *obj); #define LOG_SENSOR(prefix, type, obj) log_sensor(TAG, prefix, LOG_STR_LITERAL(type), obj) @@ -67,6 +71,7 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa /// Set force update mode. void set_force_update(bool force_update) { sensor_flags_.force_update = force_update; } +#ifdef USE_SENSOR_FILTER /// Add a filter to the filter chain. Will be appended to the back. void add_filter(Filter *filter); @@ -87,6 +92,7 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa /// Clear the entire filter chain. void clear_filters(); +#endif /// Getter-syntax for .state. float get_state() const; @@ -130,7 +136,9 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa LazyCallbackManager raw_callback_; ///< Storage for raw state callbacks. LazyCallbackManager callback_; ///< Storage for filtered state callbacks. +#ifdef USE_SENSOR_FILTER Filter *filter_list_{nullptr}; ///< Store all active filters. +#endif // Group small members together to avoid padding int8_t accuracy_decimals_{-1}; ///< Accuracy in decimals (-1 = not set) diff --git a/esphome/components/sound_level/sound_level.h b/esphome/components/sound_level/sound_level.h index dc35f69fe2..a1021eb1e8 100644 --- a/esphome/components/sound_level/sound_level.h +++ b/esphome/components/sound_level/sound_level.h @@ -6,6 +6,7 @@ #include "esphome/components/microphone/microphone_source.h" #include "esphome/components/sensor/sensor.h" +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/ring_buffer.h" diff --git a/esphome/components/wireguard/wireguard.h b/esphome/components/wireguard/wireguard.h index e8470c75cd..c11d592cd1 100644 --- a/esphome/components/wireguard/wireguard.h +++ b/esphome/components/wireguard/wireguard.h @@ -4,6 +4,7 @@ #include #include +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/components/time/real_time_clock.h" diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 99e80785ec..ff32edff16 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -112,6 +112,7 @@ #define USE_SAFE_MODE_CALLBACK #define USE_SELECT #define USE_SENSOR +#define USE_SENSOR_FILTER #define USE_SETUP_PRIORITY_OVERRIDE #define USE_STATUS_LED #define USE_STATUS_SENSOR From d239a2400dddfb9854ea5e6c7f476002b6bf22c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 21:36:21 -0600 Subject: [PATCH 152/609] [text_sensor] Conditionally compile filter infrastructure (#14213) --- esphome/components/text_sensor/__init__.py | 1 + esphome/components/text_sensor/filter.cpp | 5 +++++ esphome/components/text_sensor/filter.h | 5 +++++ esphome/components/text_sensor/text_sensor.cpp | 18 +++++++++++++----- esphome/components/text_sensor/text_sensor.h | 8 ++++++++ esphome/core/defines.h | 1 + 6 files changed, 33 insertions(+), 5 deletions(-) diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 0d22400a8e..2e8edb43c9 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -204,6 +204,7 @@ async def setup_text_sensor_core_(var, config): cg.add(var.set_device_class(device_class)) if config.get(CONF_FILTERS): # must exist and not be empty + cg.add_define("USE_TEXT_SENSOR_FILTER") filters = await build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) diff --git a/esphome/components/text_sensor/filter.cpp b/esphome/components/text_sensor/filter.cpp index 4ee12e8602..f6552c7c66 100644 --- a/esphome/components/text_sensor/filter.cpp +++ b/esphome/components/text_sensor/filter.cpp @@ -1,3 +1,6 @@ +#include "esphome/core/defines.h" +#ifdef USE_TEXT_SENSOR_FILTER + #include "filter.h" #include "text_sensor.h" #include "esphome/core/log.h" @@ -106,3 +109,5 @@ bool MapFilter::new_value(std::string &value) { } // namespace text_sensor } // namespace esphome + +#endif // USE_TEXT_SENSOR_FILTER diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index 1922b503ca..f88e8645cc 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -1,5 +1,8 @@ #pragma once +#include "esphome/core/defines.h" +#ifdef USE_TEXT_SENSOR_FILTER + #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -164,3 +167,5 @@ class MapFilter : public Filter { } // namespace text_sensor } // namespace esphome + +#endif // USE_TEXT_SENSOR_FILTER diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index c48bdf4b82..c66d08ec40 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -24,7 +24,9 @@ void TextSensor::publish_state(const std::string &state) { this->publish_state(s void TextSensor::publish_state(const char *state) { this->publish_state(state, strlen(state)); } void TextSensor::publish_state(const char *state, size_t len) { +#ifdef USE_TEXT_SENSOR_FILTER if (this->filter_list_ == nullptr) { +#endif // No filters: raw_state == state, store once and use for both callbacks // Only assign if changed to avoid heap allocation if (len != this->state.size() || memcmp(state, this->state.data(), len) != 0) { @@ -33,6 +35,7 @@ void TextSensor::publish_state(const char *state, size_t len) { this->raw_callback_.call(this->state); ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), this->state.c_str()); this->notify_frontend_(); +#ifdef USE_TEXT_SENSOR_FILTER } else { // Has filters: need separate raw storage #pragma GCC diagnostic push @@ -46,8 +49,10 @@ void TextSensor::publish_state(const char *state, size_t len) { this->filter_list_->input(this->raw_state); #pragma GCC diagnostic pop } +#endif } +#ifdef USE_TEXT_SENSOR_FILTER void TextSensor::add_filter(Filter *filter) { // inefficient, but only happens once on every sensor setup and nobody's going to have massive amounts of // filters @@ -77,6 +82,7 @@ void TextSensor::clear_filters() { } this->filter_list_ = nullptr; } +#endif // USE_TEXT_SENSOR_FILTER void TextSensor::add_on_state_callback(std::function callback) { this->callback_.add(std::move(callback)); @@ -87,14 +93,16 @@ void TextSensor::add_on_raw_state_callback(std::functionstate; } const std::string &TextSensor::get_raw_state() const { - if (this->filter_list_ == nullptr) { - return this->state; // No filters, raw == filtered - } -// Suppress deprecation warning - get_raw_state() is the replacement API +#ifdef USE_TEXT_SENSOR_FILTER + if (this->filter_list_ != nullptr) { + // Suppress deprecation warning - get_raw_state() is the replacement API #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" - return this->raw_state; + return this->raw_state; #pragma GCC diagnostic pop + } +#endif + return this->state; // No filters, raw == filtered } void TextSensor::internal_send_state_to_frontend(const std::string &state) { this->internal_send_state_to_frontend(state.data(), state.size()); diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index 1352a8c1e4..97373dc716 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -3,7 +3,9 @@ #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" +#ifdef USE_TEXT_SENSOR_FILTER #include "esphome/components/text_sensor/filter.h" +#endif #include #include @@ -11,6 +13,8 @@ namespace esphome { namespace text_sensor { +class TextSensor; + void log_text_sensor(const char *tag, const char *prefix, const char *type, TextSensor *obj); #define LOG_TEXT_SENSOR(prefix, type, obj) log_text_sensor(TAG, prefix, LOG_STR_LITERAL(type), obj) @@ -45,6 +49,7 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { void publish_state(const char *state); void publish_state(const char *state, size_t len); +#ifdef USE_TEXT_SENSOR_FILTER /// Add a filter to the filter chain. Will be appended to the back. void add_filter(Filter *filter); @@ -56,6 +61,7 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { /// Clear the entire filter chain. void clear_filters(); +#endif void add_on_state_callback(std::function callback); /// Add a callback that will be called every time the sensor sends a raw value. @@ -73,7 +79,9 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { LazyCallbackManager raw_callback_; ///< Storage for raw state callbacks. LazyCallbackManager callback_; ///< Storage for filtered state callbacks. +#ifdef USE_TEXT_SENSOR_FILTER Filter *filter_list_{nullptr}; ///< Store all active filters. +#endif }; } // namespace text_sensor diff --git a/esphome/core/defines.h b/esphome/core/defines.h index ff32edff16..a1e3d1707f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -119,6 +119,7 @@ #define USE_SWITCH #define USE_TEXT #define USE_TEXT_SENSOR +#define USE_TEXT_SENSOR_FILTER #define USE_TIME #define USE_TOUCHSCREEN #define USE_UART_DEBUGGER From 5c388a5200b65a9e4470d4f4a681e41c49517915 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:39:36 +0100 Subject: [PATCH 153/609] [openthread_info] Optimize: Devirtualize/unify (#14208) --- .../openthread_info_text_sensor.h | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/esphome/components/openthread_info/openthread_info_text_sensor.h b/esphome/components/openthread_info/openthread_info_text_sensor.h index ac5623e0c1..10e83281f0 100644 --- a/esphome/components/openthread_info/openthread_info_text_sensor.h +++ b/esphome/components/openthread_info/openthread_info_text_sensor.h @@ -25,7 +25,7 @@ class OpenThreadInstancePollingComponent : public PollingComponent { virtual void update_instance(otInstance *instance) = 0; }; -class IPAddressOpenThreadInfo : public PollingComponent, public text_sensor::TextSensor { +class IPAddressOpenThreadInfo final : public PollingComponent, public text_sensor::TextSensor { public: void update() override { std::optional address = openthread::global_openthread_component->get_omr_address(); @@ -48,7 +48,7 @@ class IPAddressOpenThreadInfo : public PollingComponent, public text_sensor::Tex std::string last_ip_; }; -class RoleOpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { +class RoleOpenThreadInfo final : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { public: void update_instance(otInstance *instance) override { otDeviceRole role = otThreadGetDeviceRole(instance); @@ -64,7 +64,7 @@ class RoleOpenThreadInfo : public OpenThreadInstancePollingComponent, public tex otDeviceRole last_role_; }; -class Rloc16OpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { +class Rloc16OpenThreadInfo final : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { public: void update_instance(otInstance *instance) override { uint16_t rloc16 = otThreadGetRloc16(instance); @@ -75,14 +75,13 @@ class Rloc16OpenThreadInfo : public OpenThreadInstancePollingComponent, public t this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: uint16_t last_rloc16_; }; -class ExtAddrOpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { +class ExtAddrOpenThreadInfo final : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { public: void update_instance(otInstance *instance) override { const auto *extaddr = otLinkGetExtendedAddress(instance); @@ -93,14 +92,13 @@ class ExtAddrOpenThreadInfo : public OpenThreadInstancePollingComponent, public this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: std::array last_extaddr_{}; }; -class Eui64OpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { +class Eui64OpenThreadInfo final : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { public: void update_instance(otInstance *instance) override { otExtAddress addr; @@ -113,14 +111,13 @@ class Eui64OpenThreadInfo : public OpenThreadInstancePollingComponent, public te this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: std::array last_eui64_{}; }; -class ChannelOpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { +class ChannelOpenThreadInfo final : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { public: void update_instance(otInstance *instance) override { uint8_t channel = otLinkGetChannel(instance); @@ -131,7 +128,6 @@ class ChannelOpenThreadInfo : public OpenThreadInstancePollingComponent, public this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: @@ -153,7 +149,7 @@ class DatasetOpenThreadInfo : public OpenThreadInstancePollingComponent { virtual void update_dataset(otOperationalDataset *dataset) = 0; }; -class NetworkNameOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::TextSensor { +class NetworkNameOpenThreadInfo final : public DatasetOpenThreadInfo, public text_sensor::TextSensor { public: void update_dataset(otOperationalDataset *dataset) override { if (this->last_network_name_ != dataset->mNetworkName.m8) { @@ -161,14 +157,13 @@ class NetworkNameOpenThreadInfo : public DatasetOpenThreadInfo, public text_sens this->publish_state(this->last_network_name_); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: std::string last_network_name_; }; -class NetworkKeyOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::TextSensor { +class NetworkKeyOpenThreadInfo final : public DatasetOpenThreadInfo, public text_sensor::TextSensor { public: void update_dataset(otOperationalDataset *dataset) override { if (!std::equal(this->last_key_.begin(), this->last_key_.end(), dataset->mNetworkKey.m8)) { @@ -178,14 +173,13 @@ class NetworkKeyOpenThreadInfo : public DatasetOpenThreadInfo, public text_senso this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: std::array last_key_{}; }; -class PanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::TextSensor { +class PanIdOpenThreadInfo final : public DatasetOpenThreadInfo, public text_sensor::TextSensor { public: void update_dataset(otOperationalDataset *dataset) override { uint16_t panid = dataset->mPanId; @@ -196,14 +190,13 @@ class PanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::Te this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: uint16_t last_panid_; }; -class ExtPanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::TextSensor { +class ExtPanIdOpenThreadInfo final : public DatasetOpenThreadInfo, public text_sensor::TextSensor { public: void update_dataset(otOperationalDataset *dataset) override { if (!std::equal(this->last_extpanid_.begin(), this->last_extpanid_.end(), dataset->mExtendedPanId.m8)) { @@ -214,7 +207,6 @@ class ExtPanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor: } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: From 680160453355210018b073e0036c256d45a143ce Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:40:40 +0100 Subject: [PATCH 154/609] [openthread] Add Thread version DEBUG trace (#14196) --- esphome/components/openthread/openthread_esp.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index ec212d1f68..4f8db0634a 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -104,6 +104,8 @@ void OpenThreadComponent::ot_main() { esp_cli_custom_command_init(); #endif // CONFIG_OPENTHREAD_CLI_ESP_EXTENSION + ESP_LOGD(TAG, "Thread Version: %" PRIu16, otThreadGetVersion()); + otLinkModeConfig link_mode_config{}; #if CONFIG_OPENTHREAD_FTD link_mode_config.mRxOnWhenIdle = true; From ee94bc471541a460c989b51cf317530b7ffa4611 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:43:42 +0100 Subject: [PATCH 155/609] [openthread] Refactor to optimize and match code rules (#14156) --- esphome/components/openthread/openthread.cpp | 4 +-- esphome/components/openthread/openthread.h | 10 +++---- .../components/openthread/openthread_esp.cpp | 29 ++++++++++--------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 90da17e2d3..d22a14aeae 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -35,9 +35,9 @@ void OpenThreadComponent::dump_config() { #elif CONFIG_OPENTHREAD_MTD ESP_LOGCONFIG(TAG, " Device Type: MTD"); // TBD: Synchronized Sleepy End Device - if (this->poll_period > 0) { + if (this->poll_period_ > 0) { ESP_LOGCONFIG(TAG, " Device is configured as Sleepy End Device (SED)"); - uint32_t duration = this->poll_period / 1000; + uint32_t duration = this->poll_period_ / 1000; ESP_LOGCONFIG(TAG, " Poll Period: %" PRIu32 "s", duration); } else { ESP_LOGCONFIG(TAG, " Device is configured as Minimal End Device (MED)"); diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 3c60acaadd..9e429f289b 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -36,22 +36,22 @@ class OpenThreadComponent : public Component { const char *get_use_address() const; void set_use_address(const char *use_address); #if CONFIG_OPENTHREAD_MTD - void set_poll_period(uint32_t poll_period) { this->poll_period = poll_period; } + void set_poll_period(uint32_t poll_period) { this->poll_period_ = poll_period; } #endif protected: std::optional get_omr_address_(InstanceLock &lock); + std::function factory_reset_external_callback_; +#if CONFIG_OPENTHREAD_MTD + uint32_t poll_period_{0}; +#endif bool teardown_started_{false}; bool teardown_complete_{false}; - std::function factory_reset_external_callback_; private: // Stores a pointer to a string literal (static storage duration). // ONLY set from Python-generated code with string literals - never dynamic strings. const char *use_address_{""}; -#if CONFIG_OPENTHREAD_MTD - uint32_t poll_period{0}; -#endif }; extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 4f8db0634a..bb70701471 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -81,9 +81,11 @@ void OpenThreadComponent::ot_main() { // Initialize the OpenThread stack // otLoggingSetLevel(OT_LOG_LEVEL_DEBG); ESP_ERROR_CHECK(esp_openthread_init(&config)); + // Fetch OT instance once to avoid repeated call into OT stack + otInstance *instance = esp_openthread_get_instance(); #if CONFIG_OPENTHREAD_STATE_INDICATOR_ENABLE - ESP_ERROR_CHECK(esp_openthread_state_indicator_init(esp_openthread_get_instance())); + ESP_ERROR_CHECK(esp_openthread_state_indicator_init(instance)); #endif #if CONFIG_OPENTHREAD_LOG_LEVEL_DYNAMIC @@ -112,28 +114,29 @@ void OpenThreadComponent::ot_main() { link_mode_config.mDeviceType = true; link_mode_config.mNetworkData = true; #elif CONFIG_OPENTHREAD_MTD - if (this->poll_period > 0) { - if (otLinkSetPollPeriod(esp_openthread_get_instance(), this->poll_period) != OT_ERROR_NONE) { - ESP_LOGE(TAG, "Failed to set OpenThread pollperiod."); + if (this->poll_period_ > 0) { + if (otLinkSetPollPeriod(instance, this->poll_period_) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set pollperiod"); } - uint32_t link_polling_period = otLinkGetPollPeriod(esp_openthread_get_instance()); - ESP_LOGD(TAG, "Link Polling Period: %" PRIu32, link_polling_period); + ESP_LOGD(TAG, "Link Polling Period: %" PRIu32, otLinkGetPollPeriod(instance)); } - link_mode_config.mRxOnWhenIdle = this->poll_period == 0; + link_mode_config.mRxOnWhenIdle = this->poll_period_ == 0; link_mode_config.mDeviceType = false; link_mode_config.mNetworkData = false; #endif - if (otThreadSetLinkMode(esp_openthread_get_instance(), link_mode_config) != OT_ERROR_NONE) { - ESP_LOGE(TAG, "Failed to set OpenThread linkmode."); + if (otThreadSetLinkMode(instance, link_mode_config) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set linkmode"); } - link_mode_config = otThreadGetLinkMode(esp_openthread_get_instance()); +#ifdef ESPHOME_LOG_HAS_DEBUG // Fetch link mode from OT only when DEBUG + link_mode_config = otThreadGetLinkMode(instance); ESP_LOGD(TAG, "Link Mode Device Type: %s\n" "Link Mode Network Data: %s\n" "Link Mode RX On When Idle: %s", - link_mode_config.mDeviceType ? "true" : "false", link_mode_config.mNetworkData ? "true" : "false", - link_mode_config.mRxOnWhenIdle ? "true" : "false"); + TRUEFALSE(link_mode_config.mDeviceType), TRUEFALSE(link_mode_config.mNetworkData), + TRUEFALSE(link_mode_config.mRxOnWhenIdle)); +#endif // Run the main loop #if CONFIG_OPENTHREAD_CLI @@ -144,7 +147,7 @@ void OpenThreadComponent::ot_main() { #ifndef USE_OPENTHREAD_FORCE_DATASET // Check if openthread has a valid dataset from a previous execution - otError error = otDatasetGetActiveTlvs(esp_openthread_get_instance(), &dataset); + otError error = otDatasetGetActiveTlvs(instance, &dataset); if (error != OT_ERROR_NONE) { // Make sure the length is 0 so we fallback to the configuration dataset.mLength = 0; From 417f4535af983e90d5ac2407847a29f15cb4905e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 22:25:20 -0600 Subject: [PATCH 156/609] [logger] Use subtraction-based line number formatting to avoid division (#14219) --- esphome/components/logger/log_buffer.h | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/esphome/components/logger/log_buffer.h b/esphome/components/logger/log_buffer.h index 3d87278248..a56276f732 100644 --- a/esphome/components/logger/log_buffer.h +++ b/esphome/components/logger/log_buffer.h @@ -75,18 +75,13 @@ struct LogBuffer { *p++ = ':'; - // Format line number without modulo operations + // Format line number using subtraction loops (no division - important for ESP8266 which lacks hardware divider) if (line > 999) [[unlikely]] { - int thousands = line / 1000; - *p++ = '0' + thousands; - line -= thousands * 1000; + write_digit(p, line, 1000); } - int hundreds = line / 100; - int remainder = line - hundreds * 100; - int tens = remainder / 10; - *p++ = '0' + hundreds; - *p++ = '0' + tens; - *p++ = '0' + (remainder - tens * 10); + write_digit(p, line, 100); + write_digit(p, line, 10); + *p++ = '0' + line; *p++ = ']'; #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST) @@ -162,6 +157,15 @@ struct LogBuffer { this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args)); } #endif + // Extract one decimal digit via subtraction (no division - important for ESP8266) + static inline void ESPHOME_ALWAYS_INLINE write_digit(char *&p, int &value, int divisor) { + char d = '0'; + while (value >= divisor) { + d++; + value -= divisor; + } + *p++ = d; + } // Write ANSI color escape sequence to buffer, updates pointer in place // Caller is responsible for ensuring buffer has sufficient space void write_ansi_color_(char *&p, uint8_t level) { From fb6c7d81d5b3832c19eb1dd7b61c70a944742d8a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:08:40 -0500 Subject: [PATCH 157/609] [core] Fix multiline log continuations without leading whitespace (#14217) Co-authored-by: Claude Opus 4.6 --- .../absolute_humidity/absolute_humidity.cpp | 5 +- esphome/components/anova/anova.cpp | 6 +- esphome/components/as3935/as3935.cpp | 6 +- esphome/components/at581x/at581x.cpp | 16 ++--- .../components/binary_sensor/automation.cpp | 6 +- esphome/components/bl0940/bl0940.cpp | 5 +- .../current_based/current_based_cover.cpp | 8 +-- esphome/components/debug/debug_esp32.cpp | 60 ++++++++++--------- esphome/components/debug/debug_esp8266.cpp | 18 +++--- esphome/components/debug/debug_libretiny.cpp | 14 +++-- esphome/components/debug/debug_zephyr.cpp | 42 +++++++------ .../components/dfrobot_sen0395/commands.cpp | 16 ++--- esphome/components/emmeti/emmeti.cpp | 19 ++---- .../components/ens160_base/ens160_base.cpp | 13 ++-- esphome/components/es8388/es8388.cpp | 7 ++- .../esp32_ble_client/ble_client_base.cpp | 6 +- .../update/esp32_hosted_update.cpp | 11 ++-- .../packet_transport/espnow_transport.cpp | 8 +-- .../ethernet/ethernet_component.cpp | 5 +- esphome/components/ezo_pmp/ezo_pmp.cpp | 6 +- esphome/components/gcja5/gcja5.cpp | 8 +-- .../graphical_display_menu.cpp | 14 ++--- .../components/honeywellabp/honeywellabp.cpp | 5 +- .../http_request/ota/ota_http_request.cpp | 3 +- .../components/ina2xx_base/ina2xx_base.cpp | 6 +- esphome/components/ld2410/ld2410.cpp | 7 +-- esphome/components/ld2412/ld2412.cpp | 5 +- esphome/components/ledc/ledc_output.cpp | 6 +- esphome/components/max6956/max6956.cpp | 6 +- .../components/mqtt/mqtt_backend_esp32.cpp | 5 +- .../nextion/nextion_upload_arduino.cpp | 22 ++----- .../nextion/nextion_upload_esp32.cpp | 30 ++-------- .../components/openthread/openthread_esp.cpp | 8 +-- .../components/pi4ioe5v6408/pi4ioe5v6408.cpp | 7 ++- esphome/components/pipsolar/pipsolar.cpp | 3 +- esphome/components/pn532/pn532.cpp | 5 +- esphome/components/pn7150/pn7150.cpp | 15 +++-- esphome/components/pn7160/pn7160.cpp | 16 +++-- esphome/components/qmp6988/qmp6988.cpp | 26 ++++---- .../remote_base/pronto_protocol.cpp | 5 +- esphome/components/safe_mode/safe_mode.cpp | 10 ++-- esphome/components/sim800l/sim800l.cpp | 2 +- esphome/components/sonoff_d1/sonoff_d1.cpp | 10 +--- esphome/components/sun/sun.cpp | 29 ++++----- .../components/usb_host/usb_host_client.cpp | 44 +++++++------- esphome/components/usb_uart/cp210x.cpp | 6 +- .../voice_assistant/voice_assistant.cpp | 4 +- esphome/components/wl_134/wl_134.cpp | 13 ++-- esphome/components/xgzp68xx/xgzp68xx.cpp | 6 +- script/ci-custom.py | 33 ++++++++++ 50 files changed, 291 insertions(+), 345 deletions(-) diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index 9c66531d05..40676f8655 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -92,10 +92,7 @@ void AbsoluteHumidityComponent::loop() { // Calculate absolute humidity const float absolute_humidity = vapor_density(es, hr, temperature_k); - ESP_LOGD(TAG, - "Saturation vapor pressure %f kPa\n" - "Publishing absolute humidity %f g/m³", - es, absolute_humidity); + ESP_LOGD(TAG, "Saturation vapor pressure %f kPa, absolute humidity %f g/m³", es, absolute_humidity); // Publish absolute humidity this->status_clear_warning(); diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index 5054488089..2693224a97 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -67,10 +67,8 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ case ESP_GATTC_SEARCH_CMPL_EVT: { auto *chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID); if (chr == nullptr) { - ESP_LOGW(TAG, - "[%s] No control service found at device, not an Anova..?\n" - "[%s] Note, this component does not currently support Anova Nano.", - this->get_name().c_str(), this->get_name().c_str()); + ESP_LOGW(TAG, "[%s] No control service found at device, not an Anova..?", this->get_name().c_str()); + ESP_LOGW(TAG, "[%s] Note, this component does not currently support Anova Nano.", this->get_name().c_str()); break; } this->char_handle_ = chr->handle; diff --git a/esphome/components/as3935/as3935.cpp b/esphome/components/as3935/as3935.cpp index dd0ab714f7..c4dc0466a0 100644 --- a/esphome/components/as3935/as3935.cpp +++ b/esphome/components/as3935/as3935.cpp @@ -307,9 +307,9 @@ void AS3935Component::tune_antenna() { uint8_t tune_val = this->read_capacitance(); ESP_LOGI(TAG, "Starting antenna tuning\n" - "Division Ratio is set to: %d\n" - "Internal Capacitor is set to: %d\n" - "Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio", + " Division Ratio is set to: %d\n" + " Internal Capacitor is set to: %d\n" + " Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio", div_ratio, tune_val); this->display_oscillator(true, ANTFREQ); } diff --git a/esphome/components/at581x/at581x.cpp b/esphome/components/at581x/at581x.cpp index 6804a7f4b5..728fbe20c6 100644 --- a/esphome/components/at581x/at581x.cpp +++ b/esphome/components/at581x/at581x.cpp @@ -77,14 +77,14 @@ void AT581XComponent::dump_config() { LOG_I2C_DEVICE(this); } bool AT581XComponent::i2c_write_config() { ESP_LOGCONFIG(TAG, "Writing new config for AT581X\n" - "Frequency: %dMHz\n" - "Sensing distance: %d\n" - "Power: %dµA\n" - "Gain: %d\n" - "Trigger base time: %dms\n" - "Trigger keep time: %dms\n" - "Protect time: %dms\n" - "Self check time: %dms", + " Frequency: %dMHz\n" + " Sensing distance: %d\n" + " Power: %dµA\n" + " Gain: %d\n" + " Trigger base time: %dms\n" + " Trigger keep time: %dms\n" + " Protect time: %dms\n" + " Self check time: %dms", this->freq_, this->delta_, this->power_, this->gain_, this->trigger_base_time_ms_, this->trigger_keep_time_ms_, this->protect_time_ms_, this->self_check_time_ms_); diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index faebe7e88f..7e43d42357 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -29,10 +29,8 @@ void MultiClickTrigger::on_state_(bool state) { // Start matching MultiClickTriggerEvent evt = this->timing_[0]; if (evt.state == state) { - ESP_LOGV(TAG, - "START min=%" PRIu32 " max=%" PRIu32 "\n" - "Multi Click: Starting multi click action!", - evt.min_length, evt.max_length); + ESP_LOGV(TAG, "START min=%" PRIu32 " max=%" PRIu32, evt.min_length, evt.max_length); + ESP_LOGV(TAG, "Multi Click: Starting multi click action!"); this->at_index_ = 1; if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) { this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); }); diff --git a/esphome/components/bl0940/bl0940.cpp b/esphome/components/bl0940/bl0940.cpp index 42e20eb69b..31625ebf6d 100644 --- a/esphome/components/bl0940/bl0940.cpp +++ b/esphome/components/bl0940/bl0940.cpp @@ -182,7 +182,10 @@ void BL0940::recalibrate_() { ESP_LOGD(TAG, "Recalibrated reference values:\n" - "Voltage: %f\n, Current: %f\n, Power: %f\n, Energy: %f\n", + " Voltage: %f\n" + " Current: %f\n" + " Power: %f\n" + " Energy: %f", this->voltage_reference_cal_, this->current_reference_cal_, this->power_reference_cal_, this->energy_reference_cal_); } diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index 5dfaeeff39..58ae7cbc34 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -148,14 +148,14 @@ void CurrentBasedCover::dump_config() { } ESP_LOGCONFIG(TAG, " Close Duration: %.1fs\n" - "Obstacle Rollback: %.1f%%", + " Obstacle Rollback: %.1f%%", this->close_duration_ / 1e3f, this->obstacle_rollback_ * 100); if (this->max_duration_ != UINT32_MAX) { - ESP_LOGCONFIG(TAG, "Maximum duration: %.1fs", this->max_duration_ / 1e3f); + ESP_LOGCONFIG(TAG, " Maximum duration: %.1fs", this->max_duration_ / 1e3f); } ESP_LOGCONFIG(TAG, - "Start sensing delay: %.1fs\n" - "Malfunction detection: %s", + " Start sensing delay: %.1fs\n" + " Malfunction detection: %s", this->start_sensing_delay_ / 1e3f, YESNO(this->malfunction_detection_)); } diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index aad4c7426c..6898621dd0 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -79,7 +79,6 @@ const char *DebugComponent::get_reset_reason_(std::span } uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT - ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode); pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed, flash_mode); #endif @@ -194,39 +191,46 @@ size_t DebugComponent::get_device_info_(std::span if (info.features != 0) { pos = buf_append_printf(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features); } - ESP_LOGD(TAG, "Chip: Model=%s, Cores=%u, Revision=%u", model, info.cores, info.revision); pos = buf_append_printf(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision); uint32_t cpu_freq_mhz = arch_get_cpu_freq_hz() / 1000000; - ESP_LOGD(TAG, "CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz); pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz); - // Framework detection -#ifdef USE_ARDUINO - ESP_LOGD(TAG, "Framework: Arduino"); - pos = buf_append_printf(buf, size, pos, "|Framework: Arduino"); -#elif defined(USE_ESP32) - ESP_LOGD(TAG, "Framework: ESP-IDF"); - pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF"); -#else - ESP_LOGW(TAG, "Framework: UNKNOWN"); - pos = buf_append_printf(buf, size, pos, "|Framework: UNKNOWN"); -#endif - - ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version()); - pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version()); - - uint8_t mac[6]; - get_mac_address_raw(mac); - ESP_LOGD(TAG, "EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], - mac[4], mac[5]); - char reason_buffer[RESET_REASON_BUFFER_SIZE]; const char *reset_reason = get_reset_reason_(std::span(reason_buffer)); - pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason); - const char *wakeup_cause = get_wakeup_cause_(std::span(reason_buffer)); + + uint8_t mac[6]; + get_mac_address_raw(mac); + + ESP_LOGD(TAG, + "ESP32 debug info:\n" + " Chip: %s\n" + " Cores: %u\n" + " Revision: %u\n" + " CPU Frequency: %" PRIu32 " MHz\n" + " ESP-IDF Version: %s\n" + " EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X\n" + " Reset Reason: %s\n" + " Wakeup Cause: %s", + model, info.cores, info.revision, cpu_freq_mhz, esp_get_idf_version(), mac[0], mac[1], mac[2], mac[3], + mac[4], mac[5], reset_reason, wakeup_cause); +#if defined(USE_ARDUINO) + ESP_LOGD(TAG, " Flash: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode); +#endif + // Framework detection +#ifdef USE_ARDUINO + ESP_LOGD(TAG, " Framework: Arduino"); + pos = buf_append_printf(buf, size, pos, "|Framework: Arduino"); +#else + ESP_LOGD(TAG, " Framework: ESP-IDF"); + pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF"); +#endif + + pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version()); + pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], + mac[4], mac[5]); + pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason); pos = buf_append_printf(buf, size, pos, "|Wakeup: %s", wakeup_cause); return pos; diff --git a/esphome/components/debug/debug_esp8266.cpp b/esphome/components/debug/debug_esp8266.cpp index 1a07ec4f3a..4df4aaa851 100644 --- a/esphome/components/debug/debug_esp8266.cpp +++ b/esphome/components/debug/debug_esp8266.cpp @@ -128,14 +128,16 @@ size_t DebugComponent::get_device_info_(std::span // NOLINTEND(readability-static-accessed-through-instance) ESP_LOGD(TAG, - "Chip ID: 0x%08" PRIX32 "\n" - "SDK Version: %s\n" - "Core Version: %s\n" - "Boot Version=%u Mode=%u\n" - "CPU Frequency: %u\n" - "Flash Chip ID=0x%08" PRIX32 "\n" - "Reset Reason: %s\n" - "Reset Info: %s", + "ESP8266 debug info:\n" + " Chip ID: 0x%08" PRIX32 "\n" + " SDK Version: %s\n" + " Core Version: %s\n" + " Boot Version: %u\n" + " Boot Mode: %u\n" + " CPU Frequency: %u\n" + " Flash Chip ID: 0x%08" PRIX32 "\n" + " Reset Reason: %s\n" + " Reset Info: %s", chip_id, sdk_version, get_core_version_str(core_version_buffer), boot_version, boot_mode, cpu_freq, flash_chip_id, reset_reason, get_reset_info_str(reset_info_buffer, resetInfo.reason)); diff --git a/esphome/components/debug/debug_libretiny.cpp b/esphome/components/debug/debug_libretiny.cpp index 14bbdb945a..39269d6f2f 100644 --- a/esphome/components/debug/debug_libretiny.cpp +++ b/esphome/components/debug/debug_libretiny.cpp @@ -27,12 +27,14 @@ size_t DebugComponent::get_device_info_(std::span uint32_t mac_id = lt_cpu_get_mac_id(); ESP_LOGD(TAG, - "LibreTiny Version: %s\n" - "Chip: %s (%04x) @ %u MHz\n" - "Chip ID: 0x%06" PRIX32 "\n" - "Board: %s\n" - "Flash: %" PRIu32 " KiB / RAM: %" PRIu32 " KiB\n" - "Reset Reason: %s", + "LibreTiny debug info:\n" + " Version: %s\n" + " Chip: %s (%04x) @ %u MHz\n" + " Chip ID: 0x%06" PRIX32 "\n" + " Board: %s\n" + " Flash: %" PRIu32 " KiB\n" + " RAM: %" PRIu32 " KiB\n" + " Reset Reason: %s", lt_get_version(), lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz(), mac_id, lt_get_board_code(), flash_kib, ram_kib, reset_reason); diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index ecca7150bd..bd6432e949 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -79,13 +79,13 @@ static void fa_cb(const struct flash_area *fa, void *user_data) { void DebugComponent::log_partition_info_() { #if CONFIG_FLASH_MAP_LABELS ESP_LOGCONFIG(TAG, "ID | Device | Device Name " - "| Label | Offset | Size\n" - "--------------------------------------------" + "| Label | Offset | Size"); + ESP_LOGCONFIG(TAG, "--------------------------------------------" "-----------------------------------------------"); #else ESP_LOGCONFIG(TAG, "ID | Device | Device Name " - "| Offset | Size\n" - "-----------------------------------------" + "| Offset | Size"); + ESP_LOGCONFIG(TAG, "-----------------------------------------" "------------------------------"); #endif flash_area_foreach(fa_cb, nullptr); @@ -284,11 +284,12 @@ size_t DebugComponent::get_device_info_(std::span char mac_pretty[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; get_mac_address_pretty_into_buffer(mac_pretty); ESP_LOGD(TAG, - "Code page size: %u, code size: %u, device id: 0x%08x%08x\n" - "Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x\n" - "Device address type: %s, address: %s\n" - "Part code: nRF%x, version: %c%c%c%c, package: %s\n" - "RAM: %ukB, Flash: %ukB, production test: %sdone", + "nRF debug info:\n" + " Code page size: %u, code size: %u, device id: 0x%08x%08x\n" + " Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x\n" + " Device address type: %s, address: %s\n" + " Part code: nRF%x, version: %c%c%c%c, package: %s\n" + " RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->CODEPAGESIZE, NRF_FICR->CODESIZE, NRF_FICR->DEVICEID[1], NRF_FICR->DEVICEID[0], NRF_FICR->ER[0], NRF_FICR->ER[1], NRF_FICR->ER[2], NRF_FICR->ER[3], NRF_FICR->IR[0], NRF_FICR->IR[1], NRF_FICR->IR[2], NRF_FICR->IR[3], (NRF_FICR->DEVICEADDRTYPE & 0x1 ? "Random" : "Public"), mac_pretty, NRF_FICR->INFO.PART, @@ -299,23 +300,22 @@ size_t DebugComponent::get_device_info_(std::span (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) == UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos; ESP_LOGD( - TAG, "GPIO as NFC pins: %s, GPIO as nRESET pin: %s", + TAG, " GPIO as NFC pins: %s, GPIO as nRESET pin: %s", YESNO((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)), YESNO(n_reset_enabled)); if (n_reset_enabled) { uint8_t port = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PORT_Msk) >> UICR_PSELRESET_PORT_Pos; uint8_t pin = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PIN_Msk) >> UICR_PSELRESET_PIN_Pos; - ESP_LOGD(TAG, "nRESET port P%u.%02u", port, pin); + ESP_LOGD(TAG, " nRESET port P%u.%02u", port, pin); } #ifdef USE_BOOTLOADER_MCUBOOT - ESP_LOGD(TAG, "bootloader: mcuboot"); + ESP_LOGD(TAG, " Bootloader: mcuboot"); #else - ESP_LOGD(TAG, "bootloader: Adafruit, version %u.%u.%u", (BOOTLOADER_VERSION_REGISTER >> 16) & 0xFF, + ESP_LOGD(TAG, " Bootloader: Adafruit, version %u.%u.%u", (BOOTLOADER_VERSION_REGISTER >> 16) & 0xFF, (BOOTLOADER_VERSION_REGISTER >> 8) & 0xFF, BOOTLOADER_VERSION_REGISTER & 0xFF); - ESP_LOGD(TAG, - "MBR bootloader addr 0x%08x, UICR bootloader addr 0x%08x\n" - "MBR param page addr 0x%08x, UICR param page addr 0x%08x", - read_mem_u32(MBR_BOOTLOADER_ADDR), NRF_UICR->NRFFW[0], read_mem_u32(MBR_PARAM_PAGE_ADDR), + ESP_LOGD(TAG, " MBR bootloader addr 0x%08x, UICR bootloader addr 0x%08x", read_mem_u32(MBR_BOOTLOADER_ADDR), + NRF_UICR->NRFFW[0]); + ESP_LOGD(TAG, " MBR param page addr 0x%08x, UICR param page addr 0x%08x", read_mem_u32(MBR_PARAM_PAGE_ADDR), NRF_UICR->NRFFW[1]); if (is_sd_present()) { uint32_t const sd_id = sd_id_get(); @@ -326,7 +326,7 @@ size_t DebugComponent::get_device_info_(std::span ver[1] = (sd_version - ver[0] * 1000000) / 1000; ver[2] = (sd_version - ver[0] * 1000000 - ver[1] * 1000); - ESP_LOGD(TAG, "SoftDevice: S%u %u.%u.%u", sd_id, ver[0], ver[1], ver[2]); + ESP_LOGD(TAG, " SoftDevice: S%u %u.%u.%u", sd_id, ver[0], ver[1], ver[2]); #ifdef USE_SOFTDEVICE_ID #ifdef USE_SOFTDEVICE_VERSION if (USE_SOFTDEVICE_ID != sd_id || USE_SOFTDEVICE_VERSION != ver[0]) { @@ -352,10 +352,8 @@ size_t DebugComponent::get_device_info_(std::span } return res; }; - ESP_LOGD(TAG, - "NRFFW %s\n" - "NRFHW %s", - uicr(NRF_UICR->NRFFW, 13).c_str(), uicr(NRF_UICR->NRFHW, 12).c_str()); + ESP_LOGD(TAG, " NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str()); + ESP_LOGD(TAG, " NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str()); return pos; } diff --git a/esphome/components/dfrobot_sen0395/commands.cpp b/esphome/components/dfrobot_sen0395/commands.cpp index 2c44c6fba9..0f69c82f39 100644 --- a/esphome/components/dfrobot_sen0395/commands.cpp +++ b/esphome/components/dfrobot_sen0395/commands.cpp @@ -187,18 +187,18 @@ uint8_t DetRangeCfgCommand::on_message(std::string &message) { } else if (message == "Done") { ESP_LOGI(TAG, "Updated detection area config:\n" - "Detection area 1 from %.02fm to %.02fm.", + " Detection area 1 from %.02fm to %.02fm.", this->min1_, this->max1_); if (this->min2_ >= 0 && this->max2_ >= 0) { - ESP_LOGI(TAG, "Detection area 2 from %.02fm to %.02fm.", this->min2_, this->max2_); + ESP_LOGI(TAG, " Detection area 2 from %.02fm to %.02fm.", this->min2_, this->max2_); } if (this->min3_ >= 0 && this->max3_ >= 0) { - ESP_LOGI(TAG, "Detection area 3 from %.02fm to %.02fm.", this->min3_, this->max3_); + ESP_LOGI(TAG, " Detection area 3 from %.02fm to %.02fm.", this->min3_, this->max3_); } if (this->min4_ >= 0 && this->max4_ >= 0) { - ESP_LOGI(TAG, "Detection area 4 from %.02fm to %.02fm.", this->min4_, this->max4_); + ESP_LOGI(TAG, " Detection area 4 from %.02fm to %.02fm.", this->min4_, this->max4_); } - ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + ESP_LOGD(TAG, " Used command: %s", this->cmd_.c_str()); return 1; // Command done } return 0; // Command not done yet. @@ -222,10 +222,10 @@ uint8_t SetLatencyCommand::on_message(std::string &message) { } else if (message == "Done") { ESP_LOGI(TAG, "Updated output latency config:\n" - "Signal that someone was detected is delayed by %.03f s.\n" - "Signal that nobody is detected anymore is delayed by %.03f s.", + " Signal that someone was detected is delayed by %.03f s.\n" + " Signal that nobody is detected anymore is delayed by %.03f s.", this->delay_after_detection_, this->delay_after_disappear_); - ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + ESP_LOGD(TAG, " Used command: %s", this->cmd_.c_str()); return 1; // Command done } return 0; // Command not done yet diff --git a/esphome/components/emmeti/emmeti.cpp b/esphome/components/emmeti/emmeti.cpp index 5286f962b8..d3e923cbef 100644 --- a/esphome/components/emmeti/emmeti.cpp +++ b/esphome/components/emmeti/emmeti.cpp @@ -153,10 +153,7 @@ void EmmetiClimate::reverse_add_(T val, size_t len, esphome::remote_base::Remote bool EmmetiClimate::check_checksum_(uint8_t checksum) { uint8_t expected = this->gen_checksum_(); - ESP_LOGV(TAG, - "Expected checksum: %X\n" - "Checksum received: %X", - expected, checksum); + ESP_LOGV(TAG, "Expected checksum: %X, Checksum received: %X", expected, checksum); return checksum == expected; } @@ -266,10 +263,7 @@ bool EmmetiClimate::on_receive(remote_base::RemoteReceiveData data) { } } - ESP_LOGD(TAG, - "Swing: %d\n" - "Sleep: %d", - (curr_state.bitmap >> 1) & 0x01, (curr_state.bitmap >> 2) & 0x01); + ESP_LOGD(TAG, "Swing: %d, Sleep: %d", (curr_state.bitmap >> 1) & 0x01, (curr_state.bitmap >> 2) & 0x01); for (size_t pos = 0; pos < 4; pos++) { if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) { @@ -295,13 +289,8 @@ bool EmmetiClimate::on_receive(remote_base::RemoteReceiveData data) { } } - ESP_LOGD(TAG, - "Turbo: %d\n" - "Light: %d\n" - "Tree: %d\n" - "Blow: %d", - (curr_state.bitmap >> 3) & 0x01, (curr_state.bitmap >> 4) & 0x01, (curr_state.bitmap >> 5) & 0x01, - (curr_state.bitmap >> 6) & 0x01); + ESP_LOGD(TAG, "Turbo: %d, Light: %d, Tree: %d, Blow: %d", (curr_state.bitmap >> 3) & 0x01, + (curr_state.bitmap >> 4) & 0x01, (curr_state.bitmap >> 5) & 0x01, (curr_state.bitmap >> 6) & 0x01); uint16_t control_data = 0; for (size_t pos = 0; pos < 11; pos++) { diff --git a/esphome/components/ens160_base/ens160_base.cpp b/esphome/components/ens160_base/ens160_base.cpp index 785b053f04..e1cee5005c 100644 --- a/esphome/components/ens160_base/ens160_base.cpp +++ b/esphome/components/ens160_base/ens160_base.cpp @@ -152,12 +152,13 @@ void ENS160Component::update() { // verbose status logging ESP_LOGV(TAG, - "Status: ENS160 STATAS bit 0x%x\n" - "Status: ENS160 STATER bit 0x%x\n" - "Status: ENS160 VALIDITY FLAG 0x%02x\n" - "Status: ENS160 NEWDAT bit 0x%x\n" - "Status: ENS160 NEWGPR bit 0x%x", - (ENS160_DATA_STATUS_STATAS & (status_value)) == ENS160_DATA_STATUS_STATAS, + "ENS160 Status Register: 0x%02x\n" + " STATAS bit 0x%x\n" + " STATER bit 0x%x\n" + " VALIDITY FLAG 0x%02x\n" + " NEWDAT bit 0x%x\n" + " NEWGPR bit 0x%x", + status_value, (ENS160_DATA_STATUS_STATAS & (status_value)) == ENS160_DATA_STATUS_STATAS, (ENS160_DATA_STATUS_STATER & (status_value)) == ENS160_DATA_STATUS_STATER, (ENS160_DATA_STATUS_VALIDITY & status_value) >> 2, (ENS160_DATA_STATUS_NEWDAT & (status_value)) == ENS160_DATA_STATUS_NEWDAT, diff --git a/esphome/components/es8388/es8388.cpp b/esphome/components/es8388/es8388.cpp index 9deb29416f..72026a2a84 100644 --- a/esphome/components/es8388/es8388.cpp +++ b/esphome/components/es8388/es8388.cpp @@ -209,9 +209,10 @@ bool ES8388::set_dac_output(DacOutputLine line) { }; ESP_LOGV(TAG, - "Setting ES8388_DACPOWER to 0x%02X\n" - "Setting ES8388_DACCONTROL24 / ES8388_DACCONTROL25 to 0x%02X\n" - "Setting ES8388_DACCONTROL26 / ES8388_DACCONTROL27 to 0x%02X", + "DAC output config:\n" + " DACPOWER: 0x%02X\n" + " DACCONTROL24/25: 0x%02X\n" + " DACCONTROL26/27: 0x%02X", dac_power, reg_out1, reg_out2); ES8388_ERROR_CHECK(this->write_byte(ES8388_DACCONTROL24, reg_out1)); // LOUT1VOL diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 3f0eeeab4a..e6a85c784a 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -423,10 +423,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ for (auto &svc : this->services_) { char uuid_buf[espbt::UUID_STR_LEN]; svc->uuid.to_str(uuid_buf); - ESP_LOGV(TAG, - "[%d] [%s] Service UUID: %s\n" - "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", - this->connection_index_, this->address_str_, uuid_buf, this->connection_index_, this->address_str_, + ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_, uuid_buf); + ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->address_str_, svc->start_handle, svc->end_handle); } #endif diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index c8e2e879d4..dcd6e643c2 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -106,11 +106,12 @@ void Esp32HostedUpdate::setup() { esp_app_desc_t *app_desc = (esp_app_desc_t *) (this->firmware_data_ + app_desc_offset); if (app_desc->magic_word == ESP_APP_DESC_MAGIC_WORD) { ESP_LOGD(TAG, - "Firmware version: %s\n" - "Project name: %s\n" - "Build date: %s\n" - "Build time: %s\n" - "IDF version: %s", + "ESP32 Hosted firmware:\n" + " Firmware version: %s\n" + " Project name: %s\n" + " Build date: %s\n" + " Build time: %s\n" + " IDF version: %s", app_desc->version, app_desc->project_name, app_desc->date, app_desc->time, app_desc->idf_ver); this->update_info_.latest_version = app_desc->version; if (this->update_info_.latest_version != this->update_info_.current_version) { diff --git a/esphome/components/espnow/packet_transport/espnow_transport.cpp b/esphome/components/espnow/packet_transport/espnow_transport.cpp index 3d16f28c7d..6e4f606466 100644 --- a/esphome/components/espnow/packet_transport/espnow_transport.cpp +++ b/esphome/components/espnow/packet_transport/espnow_transport.cpp @@ -21,11 +21,9 @@ void ESPNowTransport::setup() { return; } - ESP_LOGI(TAG, - "Registering ESP-NOW handlers\n" - "Peer address: %02X:%02X:%02X:%02X:%02X:%02X", - this->peer_address_[0], this->peer_address_[1], this->peer_address_[2], this->peer_address_[3], - this->peer_address_[4], this->peer_address_[5]); + ESP_LOGI(TAG, "Registering ESP-NOW handlers, peer: %02X:%02X:%02X:%02X:%02X:%02X", this->peer_address_[0], + this->peer_address_[1], this->peer_address_[2], this->peer_address_[3], this->peer_address_[4], + this->peer_address_[5]); // Register received handler this->parent_->register_received_handler(this); diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index f9d98ad51b..fcd32223e4 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -866,10 +866,7 @@ void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister regi } #endif - ESP_LOGD(TAG, - "Writing to PHY Register Address: 0x%02" PRIX32 "\n" - "Writing to PHY Register Value: 0x%04" PRIX32, - register_data.address, register_data.value); + ESP_LOGD(TAG, "Writing PHY reg 0x%02" PRIX32 " = 0x%04" PRIX32, register_data.address, register_data.value); err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value); ESPHL_ERROR_CHECK(err, "Writing PHY Register failed"); diff --git a/esphome/components/ezo_pmp/ezo_pmp.cpp b/esphome/components/ezo_pmp/ezo_pmp.cpp index 9d2f4fc687..bf6e3926b8 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.cpp +++ b/esphome/components/ezo_pmp/ezo_pmp.cpp @@ -150,9 +150,9 @@ void EzoPMP::read_command_result_() { if (current_char == '\0') { ESP_LOGV(TAG, "Read Response from device: %s\n" - "First Component: %s\n" - "Second Component: %s\n" - "Third Component: %s", + " First Component: %s\n" + " Second Component: %s\n" + " Third Component: %s", (char *) response_buffer, (char *) first_parameter_buffer, (char *) second_parameter_buffer, (char *) third_parameter_buffer); diff --git a/esphome/components/gcja5/gcja5.cpp b/esphome/components/gcja5/gcja5.cpp index f7f7f8d02c..43b2fa20d3 100644 --- a/esphome/components/gcja5/gcja5.cpp +++ b/esphome/components/gcja5/gcja5.cpp @@ -97,10 +97,10 @@ void GCJA5Component::parse_data_() { ESP_LOGI(TAG, "GCJA5 Status\n" - "Overall Status : %i\n" - "PD Status : %i\n" - "LD Status : %i\n" - "Fan Status : %i", + " Overall Status : %i\n" + " PD Status : %i\n" + " LD Status : %i\n" + " Fan Status : %i", (status >> 6) & 0x03, (status >> 4) & 0x03, (status >> 2) & 0x03, (status >> 0) & 0x03); } } diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.cpp b/esphome/components/graphical_display_menu/graphical_display_menu.cpp index 2b120a746f..cf1672f217 100644 --- a/esphome/components/graphical_display_menu/graphical_display_menu.cpp +++ b/esphome/components/graphical_display_menu/graphical_display_menu.cpp @@ -38,13 +38,13 @@ void GraphicalDisplayMenu::setup() { void GraphicalDisplayMenu::dump_config() { ESP_LOGCONFIG(TAG, "Graphical Display Menu\n" - "Has Display: %s\n" - "Popup Mode: %s\n" - "Advanced Drawing Mode: %s\n" - "Has Font: %s\n" - "Mode: %s\n" - "Active: %s\n" - "Menu items:", + " Has Display: %s\n" + " Popup Mode: %s\n" + " Advanced Drawing Mode: %s\n" + " Has Font: %s\n" + " Mode: %s\n" + " Active: %s\n" + " Menu items:", YESNO(this->display_ != nullptr), YESNO(this->display_ != nullptr), YESNO(this->display_ == nullptr), YESNO(this->font_ != nullptr), this->mode_ == display_menu_base::MENU_MODE_ROTARY ? "Rotary" : "Joystick", YESNO(this->active_)); diff --git a/esphome/components/honeywellabp/honeywellabp.cpp b/esphome/components/honeywellabp/honeywellabp.cpp index c204325dfc..58c5df230f 100644 --- a/esphome/components/honeywellabp/honeywellabp.cpp +++ b/esphome/components/honeywellabp/honeywellabp.cpp @@ -35,10 +35,7 @@ uint8_t HONEYWELLABPSensor::readsensor_() { pressure_count_ = ((uint16_t) (buf_[0]) << 8 & 0x3F00) | ((uint16_t) (buf_[1]) & 0xFF); // 11 - bit temperature is all of byte 2 (lowest 8 bits) and the first three bits of byte 3 temperature_count_ = (((uint16_t) (buf_[2]) << 3) & 0x7F8) | (((uint16_t) (buf_[3]) >> 5) & 0x7); - ESP_LOGV(TAG, - "Sensor pressure_count_ %d\n" - "Sensor temperature_count_ %d", - pressure_count_, temperature_count_); + ESP_LOGV(TAG, "Sensor pressure_count_ %d, temperature_count_ %d", pressure_count_, temperature_count_); } return status_; } diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 882def4d7f..d77a768211 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -105,8 +105,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() { // we will compute MD5 on the fly for verification -- Arduino OTA seems to ignore it md5_receive.init(); - ESP_LOGV(TAG, "MD5Digest initialized\n" - "OTA backend begin"); + ESP_LOGV(TAG, "MD5Digest initialized, OTA backend begin"); auto backend = ota::make_ota_backend(); auto error_code = backend->begin(container->content_length); if (error_code != ota::OTA_RESPONSE_OK) { diff --git a/esphome/components/ina2xx_base/ina2xx_base.cpp b/esphome/components/ina2xx_base/ina2xx_base.cpp index de01c99a19..8a20192c1e 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.cpp +++ b/esphome/components/ina2xx_base/ina2xx_base.cpp @@ -362,10 +362,8 @@ bool INA2XX::configure_shunt_() { ESP_LOGW(TAG, "Shunt value too high"); } this->shunt_cal_ &= 0x7FFF; - ESP_LOGV(TAG, - "Given Rshunt=%f Ohm and Max_current=%.3f\n" - "New CURRENT_LSB=%f, SHUNT_CAL=%u", - this->shunt_resistance_ohm_, this->max_current_a_, this->current_lsb_, this->shunt_cal_); + ESP_LOGV(TAG, "Rshunt=%f Ohm, max current=%.3f A, current LSB=%f, shunt cal=%u", this->shunt_resistance_ohm_, + this->max_current_a_, this->current_lsb_, this->shunt_cal_); return this->write_unsigned_16_(RegisterMap::REG_SHUNT_CAL, this->shunt_cal_); } diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index f8f782f804..dd1d53857d 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -489,11 +489,8 @@ bool LD2410Component::handle_ack_data_() { this->out_pin_level_ = this->buffer_data_[12]; const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_); const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_); - ESP_LOGV(TAG, - "Light function: %s\n" - "Light threshold: %u\n" - "Out pin level: %s", - light_function_str, this->light_threshold_, out_pin_level_str); + ESP_LOGV(TAG, "Light function: %s, threshold: %u, out pin level: %s", light_function_str, this->light_threshold_, + out_pin_level_str); #ifdef USE_SELECT if (this->light_function_select_ != nullptr) { this->light_function_select_->publish_state(light_function_str); diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 95e19e0d5f..484d5bd281 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -530,10 +530,7 @@ bool LD2412Component::handle_ack_data_() { this->light_function_ = this->buffer_data_[10]; this->light_threshold_ = this->buffer_data_[11]; const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_); - ESP_LOGV(TAG, - "Light function: %s\n" - "Light threshold: %u", - light_function_str, this->light_threshold_); + ESP_LOGV(TAG, "Light function: %s, threshold: %u", light_function_str, this->light_threshold_); #ifdef USE_SELECT if (this->light_function_select_ != nullptr) { this->light_function_select_->publish_state(light_function_str); diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index a203dde115..a01d42ac8b 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -130,10 +130,8 @@ void LEDCOutput::setup() { } int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_); - ESP_LOGV(TAG, - "Configured frequency %f with a bit depth of %u bits\n" - "Angle of %.1f° results in hpoint %u", - this->frequency_, this->bit_depth_, this->phase_angle_, hpoint); + ESP_LOGV(TAG, "Configured frequency %f with bit depth %u, angle %.1f° hpoint %u", this->frequency_, this->bit_depth_, + this->phase_angle_, hpoint); ledc_channel_config_t chan_conf{}; chan_conf.gpio_num = this->pin_->get_pin(); diff --git a/esphome/components/max6956/max6956.cpp b/esphome/components/max6956/max6956.cpp index 6ba17f11d1..a350e66ee0 100644 --- a/esphome/components/max6956/max6956.cpp +++ b/esphome/components/max6956/max6956.cpp @@ -146,11 +146,11 @@ void MAX6956::dump_config() { if (brightness_mode_ == MAX6956CURRENTMODE::GLOBAL) { ESP_LOGCONFIG(TAG, - "current mode: global\n" - "global brightness: %u", + " Current mode: global\n" + " Brightness: %u", global_brightness_); } else { - ESP_LOGCONFIG(TAG, "current mode: segment"); + ESP_LOGCONFIG(TAG, " Current mode: segment"); } } diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index c12c79499f..8a7fb965e9 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -165,10 +165,7 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) { case MQTT_EVENT_ERROR: ESP_LOGE(TAG, "MQTT_EVENT_ERROR"); if (event.error_handle.error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { - ESP_LOGE(TAG, - "Last error code reported from esp-tls: 0x%x\n" - "Last tls stack error number: 0x%x\n" - "Last captured errno : %d (%s)", + ESP_LOGE(TAG, "Last esp-tls error: 0x%x, tls stack error: 0x%x, socket errno: %d (%s)", event.error_handle.esp_tls_last_esp_err, event.error_handle.esp_tls_stack_err, event.error_handle.esp_transport_sock_errno, strerror(event.error_handle.esp_transport_sock_errno)); } else if (event.error_handle.error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) { diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index a433eff883..46a04c1b2e 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -25,11 +25,7 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { uint32_t range_end = ((upload_first_chunk_sent_ or this->tft_size_ < 4096) ? this->tft_size_ : 4096) - 1; ESP_LOGD(TAG, "Range start: %" PRIu32, range_start); if (range_size <= 0 or range_end <= range_start) { - ESP_LOGE(TAG, "Invalid range"); - ESP_LOGD(TAG, - "Range end: %" PRIu32 "\n" - "Range size: %" PRIu32, - range_end, range_size); + ESP_LOGE(TAG, "Invalid range end: %" PRIu32 ", size: %" PRIu32, range_end, range_size); return -1; } @@ -138,11 +134,7 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { } bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { - ESP_LOGD(TAG, - "TFT upload requested\n" - "Exit reparse: %s\n" - "URL: %s", - YESNO(exit_reparse), this->tft_url_.c_str()); + ESP_LOGD(TAG, "TFT upload requested, exit reparse: %s, URL: %s", YESNO(exit_reparse), this->tft_url_.c_str()); if (this->connection_state_.is_updating_) { ESP_LOGW(TAG, "Upload in progress"); @@ -172,10 +164,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); // Define the configuration for the HTTP client - ESP_LOGV(TAG, - "Init HTTP client\n" - "Heap: %" PRIu32, - EspClass::getFreeHeap()); + ESP_LOGV(TAG, "Init HTTP client, heap: %" PRIu32, EspClass::getFreeHeap()); HTTPClient http_client; http_client.setTimeout(15000); // Yes 15 seconds.... Helps 8266s along @@ -262,10 +251,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { this->reset_(false); delay(250); // NOLINT - ESP_LOGV(TAG, - "Heap: %" PRIu32 "\n" - "Upload cmd: %s", - EspClass::getFreeHeap(), command); + ESP_LOGV(TAG, "Heap: %" PRIu32 ", upload cmd: %s", EspClass::getFreeHeap(), command); this->send_command_(command); if (baud_rate != this->original_baud_rate_) { diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index 46352afd75..43f59a8d4b 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -27,11 +27,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r uint32_t range_end = ((upload_first_chunk_sent_ or this->tft_size_ < 4096) ? this->tft_size_ : 4096) - 1; ESP_LOGD(TAG, "Range start: %" PRIu32, range_start); if (range_size <= 0 or range_end <= range_start) { - ESP_LOGD(TAG, - "Range end: %" PRIu32 "\n" - "Range size: %" PRIu32, - range_end, range_size); - ESP_LOGE(TAG, "Invalid range"); + ESP_LOGE(TAG, "Invalid range end: %" PRIu32 ", size: %" PRIu32, range_end, range_size); return -1; } @@ -159,11 +155,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r } bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { - ESP_LOGD(TAG, - "TFT upload requested\n" - "Exit reparse: %s\n" - "URL: %s", - YESNO(exit_reparse), this->tft_url_.c_str()); + ESP_LOGD(TAG, "TFT upload requested, exit reparse: %s, URL: %s", YESNO(exit_reparse), this->tft_url_.c_str()); if (this->connection_state_.is_updating_) { ESP_LOGW(TAG, "Upload in progress"); @@ -193,10 +185,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); // Define the configuration for the HTTP client - ESP_LOGV(TAG, - "Init HTTP client\n" - "Heap: %" PRIu32, - esp_get_free_heap_size()); + ESP_LOGV(TAG, "Init HTTP client, heap: %" PRIu32, esp_get_free_heap_size()); esp_http_client_config_t config = { .url = this->tft_url_.c_str(), .cert_pem = nullptr, @@ -220,10 +209,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { } // Perform the HTTP request - ESP_LOGV(TAG, - "Check connection\n" - "Heap: %" PRIu32, - esp_get_free_heap_size()); + ESP_LOGV(TAG, "Check connection, heap: %" PRIu32, esp_get_free_heap_size()); err = esp_http_client_perform(http_client); if (err != ESP_OK) { ESP_LOGE(TAG, "HTTP failed: %s", esp_err_to_name(err)); @@ -232,10 +218,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { } // Check the HTTP Status Code - ESP_LOGV(TAG, - "Check status\n" - "Heap: %" PRIu32, - esp_get_free_heap_size()); + ESP_LOGV(TAG, "Check status, heap: %" PRIu32, esp_get_free_heap_size()); int status_code = esp_http_client_get_status_code(http_client); if (status_code != 200 && status_code != 206) { ESP_LOGE(TAG, "HTTP request failed with status %d", status_code); @@ -344,8 +327,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ESP_LOGV(TAG, "Heap: %" PRIu32 " left: %" PRIu32, esp_get_free_heap_size(), this->content_length_); } - ESP_LOGD(TAG, "TFT upload complete\n" - "Close HTTP"); + ESP_LOGD(TAG, "TFT upload complete, closing HTTP"); esp_http_client_close(http_client); esp_http_client_cleanup(http_client); ESP_LOGV(TAG, "Connection closed"); diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index bb70701471..9dd68a1ccc 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -130,10 +130,7 @@ void OpenThreadComponent::ot_main() { } #ifdef ESPHOME_LOG_HAS_DEBUG // Fetch link mode from OT only when DEBUG link_mode_config = otThreadGetLinkMode(instance); - ESP_LOGD(TAG, - "Link Mode Device Type: %s\n" - "Link Mode Network Data: %s\n" - "Link Mode RX On When Idle: %s", + ESP_LOGD(TAG, "Link Mode Device Type: %s, Network Data: %s, RX On When Idle: %s", TRUEFALSE(link_mode_config.mDeviceType), TRUEFALSE(link_mode_config.mNetworkData), TRUEFALSE(link_mode_config.mRxOnWhenIdle)); #endif @@ -152,8 +149,7 @@ void OpenThreadComponent::ot_main() { // Make sure the length is 0 so we fallback to the configuration dataset.mLength = 0; } else { - ESP_LOGI(TAG, "Found OpenThread-managed dataset, ignoring esphome configuration\n" - "(set force_dataset: true to override)"); + ESP_LOGI(TAG, "Found existing dataset, ignoring config (force_dataset: true to override)"); } #endif diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index fdff11dedb..9247e114f0 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -144,9 +144,10 @@ bool PI4IOE5V6408Component::write_gpio_modes_() { } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGV(TAG, - "Wrote GPIO modes: 0b" BYTE_TO_BINARY_PATTERN "\n" - "Wrote GPIO pullup/pulldown: 0b" BYTE_TO_BINARY_PATTERN "\n" - "Wrote GPIO pull enable: 0b" BYTE_TO_BINARY_PATTERN, + "Wrote GPIO config:\n" + " modes: 0b" BYTE_TO_BINARY_PATTERN "\n" + " pullup/pulldown: 0b" BYTE_TO_BINARY_PATTERN "\n" + " pull enable: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(this->mode_mask_), BYTE_TO_BINARY(this->pull_up_down_mask_), BYTE_TO_BINARY(this->pull_enable_mask_)); #endif diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index e6831ad19e..f95bf4aedb 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -748,8 +748,7 @@ esphome::optional Pipsolar::get_bit_(std::string bits, uint8_t bit_pos) { } void Pipsolar::dump_config() { - ESP_LOGCONFIG(TAG, "Pipsolar:\n" - "enabled polling commands:"); + ESP_LOGCONFIG(TAG, "Pipsolar enabled polling commands:"); for (auto &enabled_polling_command : this->enabled_polling_commands_) { if (enabled_polling_command.length != 0) { ESP_LOGCONFIG(TAG, "%s", enabled_polling_command.command); diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 5366aab54e..1ab0da3df7 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -31,10 +31,7 @@ void PN532::setup() { this->mark_failed(); return; } - ESP_LOGD(TAG, - "Found chip PN5%02X\n" - "Firmware ver. %d.%d", - version_data[0], version_data[1], version_data[2]); + ESP_LOGD(TAG, "Found chip PN5%02X, Firmware v%d.%d", version_data[0], version_data[1], version_data[2]); if (!this->write_command_({ PN532_COMMAND_SAMCONFIGURATION, diff --git a/esphome/components/pn7150/pn7150.cpp b/esphome/components/pn7150/pn7150.cpp index 7bec1e08a9..8c76c8b88c 100644 --- a/esphome/components/pn7150/pn7150.cpp +++ b/esphome/components/pn7150/pn7150.cpp @@ -243,9 +243,7 @@ uint8_t PN7150::reset_core_(const bool reset_config, const bool power) { return nfc::STATUS_FAILED; } - ESP_LOGD(TAG, - "Configuration %s\n" - "NCI version: %s", + ESP_LOGD(TAG, "Configuration %s, NCI version: %s", rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 2] ? "reset" : "retained", rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] == 0x20 ? "2.0" : "1.0"); @@ -274,11 +272,12 @@ uint8_t PN7150::init_core_() { uint8_t flash_minor_version = rx.get_message()[19 + rx.get_message()[8]]; ESP_LOGD(TAG, - "Manufacturer ID: 0x%02X\n" - "Hardware version: 0x%02X\n" - "ROM code version: 0x%02X\n" - "FLASH major version: 0x%02X\n" - "FLASH minor version: 0x%02X", + "PN7150 chip info:\n" + " Manufacturer ID: 0x%02X\n" + " Hardware version: 0x%02X\n" + " ROM code version: 0x%02X\n" + " FLASH major version: 0x%02X\n" + " FLASH minor version: 0x%02X", manf_id, hw_version, rom_code_version, flash_major_version, flash_minor_version); return rx.get_simple_status_response(); diff --git a/esphome/components/pn7160/pn7160.cpp b/esphome/components/pn7160/pn7160.cpp index 28907b8e30..3fcd1221a7 100644 --- a/esphome/components/pn7160/pn7160.cpp +++ b/esphome/components/pn7160/pn7160.cpp @@ -265,10 +265,7 @@ uint8_t PN7160::reset_core_(const bool reset_config, const bool power) { return nfc::STATUS_FAILED; } - ESP_LOGD(TAG, - "Configuration %s\n" - "NCI version: %s\n" - "Manufacturer ID: 0x%02X", + ESP_LOGD(TAG, "Configuration %s, NCI version: %s, Manufacturer ID: 0x%02X", rx.get_message()[4] ? "reset" : "retained", rx.get_message()[5] == 0x20 ? "2.0" : "1.0", rx.get_message()[6]); rx.get_message().erase(rx.get_message().begin(), rx.get_message().begin() + 8); @@ -301,11 +298,12 @@ uint8_t PN7160::init_core_() { char feat_buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; ESP_LOGD(TAG, - "Hardware version: %u\n" - "ROM code version: %u\n" - "FLASH major version: %u\n" - "FLASH minor version: %u\n" - "Features: %s", + "PN7160 chip info:\n" + " Hardware version: %u\n" + " ROM code version: %u\n" + " FLASH major version: %u\n" + " FLASH minor version: %u\n" + " Features: %s", hw_version, rom_code_version, flash_major_version, flash_minor_version, nfc::format_bytes_to(feat_buf, features)); diff --git a/esphome/components/qmp6988/qmp6988.cpp b/esphome/components/qmp6988/qmp6988.cpp index 4e1ef27d5e..24fe34e785 100644 --- a/esphome/components/qmp6988/qmp6988.cpp +++ b/esphome/components/qmp6988/qmp6988.cpp @@ -128,15 +128,14 @@ bool QMP6988Component::get_calibration_data_() { qmp6988_data_.qmp6988_cali.COE_bp3 = (int16_t) encode_uint16(a_data_uint8_tr[16], a_data_uint8_tr[17]); ESP_LOGV(TAG, - "<-----------calibration data-------------->\n" - "COE_a0[%d] COE_a1[%d] COE_a2[%d] COE_b00[%d]", + "Calibration data:\n" + " COE_a0[%d] COE_a1[%d] COE_a2[%d] COE_b00[%d]\n" + " COE_bt1[%d] COE_bt2[%d] COE_bp1[%d] COE_b11[%d]\n" + " COE_bp2[%d] COE_b12[%d] COE_b21[%d] COE_bp3[%d]", qmp6988_data_.qmp6988_cali.COE_a0, qmp6988_data_.qmp6988_cali.COE_a1, qmp6988_data_.qmp6988_cali.COE_a2, - qmp6988_data_.qmp6988_cali.COE_b00); - ESP_LOGV(TAG, "COE_bt1[%d] COE_bt2[%d] COE_bp1[%d] COE_b11[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_bt1, - qmp6988_data_.qmp6988_cali.COE_bt2, qmp6988_data_.qmp6988_cali.COE_bp1, qmp6988_data_.qmp6988_cali.COE_b11); - ESP_LOGV(TAG, "COE_bp2[%d] COE_b12[%d] COE_b21[%d] COE_bp3[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_bp2, + qmp6988_data_.qmp6988_cali.COE_b00, qmp6988_data_.qmp6988_cali.COE_bt1, qmp6988_data_.qmp6988_cali.COE_bt2, + qmp6988_data_.qmp6988_cali.COE_bp1, qmp6988_data_.qmp6988_cali.COE_b11, qmp6988_data_.qmp6988_cali.COE_bp2, qmp6988_data_.qmp6988_cali.COE_b12, qmp6988_data_.qmp6988_cali.COE_b21, qmp6988_data_.qmp6988_cali.COE_bp3); - ESP_LOGV(TAG, "<-----------calibration data-------------->\r\n"); qmp6988_data_.ik.a0 = qmp6988_data_.qmp6988_cali.COE_a0; // 20Q4 qmp6988_data_.ik.b00 = qmp6988_data_.qmp6988_cali.COE_b00; // 20Q4 @@ -153,14 +152,13 @@ bool QMP6988Component::get_calibration_data_() { qmp6988_data_.ik.b21 = 13836L * (int64_t) qmp6988_data_.qmp6988_cali.COE_b21 + 79333336L; // 29Q60 qmp6988_data_.ik.bp3 = 2915L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bp3 + 157155561L; // 28Q65 ESP_LOGV(TAG, - "<----------- int calibration data -------------->\n" - "a0[%d] a1[%d] a2[%d] b00[%d]", - qmp6988_data_.ik.a0, qmp6988_data_.ik.a1, qmp6988_data_.ik.a2, qmp6988_data_.ik.b00); - ESP_LOGV(TAG, "bt1[%lld] bt2[%lld] bp1[%lld] b11[%lld]\r\n", qmp6988_data_.ik.bt1, qmp6988_data_.ik.bt2, - qmp6988_data_.ik.bp1, qmp6988_data_.ik.b11); - ESP_LOGV(TAG, "bp2[%lld] b12[%lld] b21[%lld] bp3[%lld]\r\n", qmp6988_data_.ik.bp2, qmp6988_data_.ik.b12, + "Int calibration data:\n" + " a0[%d] a1[%d] a2[%d] b00[%d]\n" + " bt1[%lld] bt2[%lld] bp1[%lld] b11[%lld]\n" + " bp2[%lld] b12[%lld] b21[%lld] bp3[%lld]", + qmp6988_data_.ik.a0, qmp6988_data_.ik.a1, qmp6988_data_.ik.a2, qmp6988_data_.ik.b00, qmp6988_data_.ik.bt1, + qmp6988_data_.ik.bt2, qmp6988_data_.ik.bp1, qmp6988_data_.ik.b11, qmp6988_data_.ik.bp2, qmp6988_data_.ik.b12, qmp6988_data_.ik.b21, qmp6988_data_.ik.bp3); - ESP_LOGV(TAG, "<----------- int calibration data -------------->\r\n"); return true; } diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index cff3145199..43029cbc2f 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -104,10 +104,7 @@ void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::vectorlabel); + ESP_LOGW(TAG, "OTA rollback detected! Rolled back from partition '%s'", last_invalid->label); + ESP_LOGW(TAG, "The device reset before the boot was marked successful"); if (esp_reset_reason() == ESP_RST_BROWNOUT) { - ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!\n" - "See https://esphome.io/guides/faq.html#brownout-detector-was-triggered"); + ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!"); + ESP_LOGW(TAG, "See https://esphome.io/guides/faq.html#brownout-detector-was-triggered"); } } #endif diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index 251e18648b..2115c72cef 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -326,7 +326,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { if (ok || message.compare(0, 6, "+CMGL:") == 0) { ESP_LOGD(TAG, "Received SMS from: %s\n" - "%s", + " %s", this->sender_.c_str(), this->message_.c_str()); this->sms_received_callback_.call(this->message_, this->sender_); this->state_ = STATE_RECEIVED_SMS; diff --git a/esphome/components/sonoff_d1/sonoff_d1.cpp b/esphome/components/sonoff_d1/sonoff_d1.cpp index 7b99086546..03586b6398 100644 --- a/esphome/components/sonoff_d1/sonoff_d1.cpp +++ b/esphome/components/sonoff_d1/sonoff_d1.cpp @@ -93,10 +93,7 @@ bool SonoffD1Output::read_command_(uint8_t *cmd, size_t &len) { if (this->read_array(cmd, 6)) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_pretty_size(6)]; - ESP_LOGV(TAG, - "[%04d] Reading from dimmer:\n" - "[%04d] %s", - this->write_count_, this->write_count_, format_hex_pretty_to(hex_buf, cmd, 6)); + ESP_LOGV(TAG, "[%04d] Reading from dimmer: %s", this->write_count_, format_hex_pretty_to(hex_buf, cmd, 6)); #endif if (cmd[0] != 0xAA || cmd[1] != 0x55) { @@ -190,10 +187,7 @@ bool SonoffD1Output::write_command_(uint8_t *cmd, const size_t len, bool needs_a do { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_pretty_size(SONOFF_D1_MAX_CMD_SIZE)]; - ESP_LOGV(TAG, - "[%04d] Writing to the dimmer:\n" - "[%04d] %s", - this->write_count_, this->write_count_, format_hex_pretty_to(hex_buf, cmd, len)); + ESP_LOGV(TAG, "[%04d] Writing to the dimmer: %s", this->write_count_, format_hex_pretty_to(hex_buf, cmd, len)); #endif this->write_array(cmd, len); this->write_count_++; diff --git a/esphome/components/sun/sun.cpp b/esphome/components/sun/sun.cpp index e8fc4e44d1..d55a14f192 100644 --- a/esphome/components/sun/sun.cpp +++ b/esphome/components/sun/sun.cpp @@ -174,20 +174,21 @@ struct SunAtTime { // debug output like in example 25.a, p. 165 auto eq = equatorial_coordinate(); ESP_LOGV(TAG, - "jde: %f\n" - "T: %f\n" - "L_0: %f\n" - "M: %f\n" - "e: %f\n" - "C: %f\n" - "Odot: %f\n" - "Omega: %f\n" - "lambda: %f\n" - "epsilon_0: %f\n" - "epsilon: %f\n" - "v: %f\n" - "right_ascension: %f\n" - "declination: %f", + "Sun position:\n" + " jde: %f\n" + " T: %f\n" + " L_0: %f\n" + " M: %f\n" + " e: %f\n" + " C: %f\n" + " Odot: %f\n" + " Omega: %f\n" + " lambda: %f\n" + " epsilon_0: %f\n" + " epsilon: %f\n" + " v: %f\n" + " right_ascension: %f\n" + " declination: %f", jde, t, mean_longitude(), mean_anomaly(), eccentricity(), equation_of_center(), true_longitude(), omega(), apparent_longitude(), mean_obliquity(), true_obliquity(), true_anomaly(), eq.right_ascension, eq.declination); diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index a9be38fb03..422d74095c 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -70,14 +70,14 @@ static void usbh_print_intf_desc(const usb_intf_desc_t *intf_desc) { static void usbh_print_cfg_desc(const usb_config_desc_t *cfg_desc) { ESP_LOGV(TAG, "*** Configuration descriptor ***\n" - "bLength %d\n" - "bDescriptorType %d\n" - "wTotalLength %d\n" - "bNumInterfaces %d\n" - "bConfigurationValue %d\n" - "iConfiguration %d\n" - "bmAttributes 0x%x\n" - "bMaxPower %dmA", + " bLength %d\n" + " bDescriptorType %d\n" + " wTotalLength %d\n" + " bNumInterfaces %d\n" + " bConfigurationValue %d\n" + " iConfiguration %d\n" + " bmAttributes 0x%x\n" + " bMaxPower %dmA", cfg_desc->bLength, cfg_desc->bDescriptorType, cfg_desc->wTotalLength, cfg_desc->bNumInterfaces, cfg_desc->bConfigurationValue, cfg_desc->iConfiguration, cfg_desc->bmAttributes, cfg_desc->bMaxPower * 2); } @@ -89,20 +89,20 @@ static void usb_client_print_device_descriptor(const usb_device_desc_t *devc_des ESP_LOGV(TAG, "*** Device descriptor ***\n" - "bLength %d\n" - "bDescriptorType %d\n" - "bcdUSB %d.%d0\n" - "bDeviceClass 0x%x\n" - "bDeviceSubClass 0x%x\n" - "bDeviceProtocol 0x%x\n" - "bMaxPacketSize0 %d\n" - "idVendor 0x%x\n" - "idProduct 0x%x\n" - "bcdDevice %d.%d0\n" - "iManufacturer %d\n" - "iProduct %d\n" - "iSerialNumber %d\n" - "bNumConfigurations %d", + " bLength %d\n" + " bDescriptorType %d\n" + " bcdUSB %d.%d0\n" + " bDeviceClass 0x%x\n" + " bDeviceSubClass 0x%x\n" + " bDeviceProtocol 0x%x\n" + " bMaxPacketSize0 %d\n" + " idVendor 0x%x\n" + " idProduct 0x%x\n" + " bcdDevice %d.%d0\n" + " iManufacturer %d\n" + " iProduct %d\n" + " iSerialNumber %d\n" + " bNumConfigurations %d", devc_desc->bLength, devc_desc->bDescriptorType, ((devc_desc->bcdUSB >> 8) & 0xF), ((devc_desc->bcdUSB >> 4) & 0xF), devc_desc->bDeviceClass, devc_desc->bDeviceSubClass, devc_desc->bDeviceProtocol, devc_desc->bMaxPacketSize0, devc_desc->idVendor, devc_desc->idProduct, diff --git a/esphome/components/usb_uart/cp210x.cpp b/esphome/components/usb_uart/cp210x.cpp index 483286560a..fa8c2331b2 100644 --- a/esphome/components/usb_uart/cp210x.cpp +++ b/esphome/components/usb_uart/cp210x.cpp @@ -58,10 +58,8 @@ std::vector USBUartTypeCP210X::parse_descriptors(usb_device_handle_t dev ESP_LOGE(TAG, "get_active_config_descriptor failed"); return {}; } - ESP_LOGD(TAG, - "bDeviceClass: %u, bDeviceSubClass: %u\n" - "bNumInterfaces: %u", - device_desc->bDeviceClass, device_desc->bDeviceSubClass, config_desc->bNumInterfaces); + ESP_LOGD(TAG, "bDeviceClass: %u, bDeviceSubClass: %u, bNumInterfaces: %u", device_desc->bDeviceClass, + device_desc->bDeviceSubClass, config_desc->bNumInterfaces); if (device_desc->bDeviceClass != 0) { ESP_LOGE(TAG, "bDeviceClass != 0"); return {}; diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 641d4d6ff8..d6cbfd4b21 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -434,8 +434,8 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr char new_peername[socket::SOCKADDR_STR_LEN]; ESP_LOGE(TAG, "Multiple API Clients attempting to connect to Voice Assistant\n" - "Current client: %s (%s)\n" - "New client: %s (%s)", + " Current client: %s (%s)\n" + " New client: %s (%s)", this->api_client_->get_name(), this->api_client_->get_peername_to(current_peername), client->get_name(), client->get_peername_to(new_peername)); return; diff --git a/esphome/components/wl_134/wl_134.cpp b/esphome/components/wl_134/wl_134.cpp index a589f71c84..a902adfddd 100644 --- a/esphome/components/wl_134/wl_134.cpp +++ b/esphome/components/wl_134/wl_134.cpp @@ -70,12 +70,13 @@ Wl134Component::Rfid134Error Wl134Component::read_packet_() { RFID134_PACKET_CHECKSUM - RFID134_PACKET_RESERVED1); ESP_LOGV(TAG, - "Tag id: %012lld\n" - "Country: %03d\n" - "isData: %s\n" - "isAnimal: %s\n" - "Reserved0: %d\n" - "Reserved1: %" PRId32, + "RFID134 Tag:\n" + " Tag id: %012lld\n" + " Country: %03d\n" + " isData: %s\n" + " isAnimal: %s\n" + " Reserved0: %d\n" + " Reserved1: %" PRId32, reading.id, reading.country, reading.isData ? "true" : "false", reading.isAnimal ? "true" : "false", reading.reserved0, reading.reserved1); diff --git a/esphome/components/xgzp68xx/xgzp68xx.cpp b/esphome/components/xgzp68xx/xgzp68xx.cpp index b5b786c105..5e816469ac 100644 --- a/esphome/components/xgzp68xx/xgzp68xx.cpp +++ b/esphome/components/xgzp68xx/xgzp68xx.cpp @@ -72,10 +72,8 @@ void XGZP68XXComponent::update() { temperature_raw = encode_uint16(data[3], data[4]); // Convert the pressure data to hPa - ESP_LOGV(TAG, - "Got raw pressure=%" PRIu32 ", raw temperature=%u\n" - "K value is %u", - pressure_raw, temperature_raw, this->k_value_); + ESP_LOGV(TAG, "Got raw pressure=%" PRIu32 ", raw temperature=%u, K value=%u", pressure_raw, temperature_raw, + this->k_value_); // Sign extend the pressure float pressure_in_pa = (float) (((int32_t) pressure_raw << 8) >> 8); diff --git a/script/ci-custom.py b/script/ci-custom.py index df819e0f04..85a446ba0d 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -841,6 +841,39 @@ def lint_no_scanf(fname, match): ) +LOG_MULTILINE_RE = re.compile(r"ESP_LOG\w+\s*\(.*?;", re.DOTALL) +LOG_BAD_CONTINUATION_RE = re.compile(r'\\n(?:[^ \\"\r\n\t]|"\s*\n\s*"[^ \\])') +LOG_PERCENT_S_CONTINUATION_RE = re.compile(r'\\n(?:%s|"\s*\n\s*"%s)') + + +@lint_content_check(include=cpp_include) +def lint_log_multiline_continuation(fname, content): + errs = [] + for log_match in LOG_MULTILINE_RE.finditer(content): + log_text = log_match.group(0) + for bad_match in LOG_BAD_CONTINUATION_RE.finditer(log_text): + # %s may expand to a whitespace prefix at runtime, skip those + if LOG_PERCENT_S_CONTINUATION_RE.match(log_text, bad_match.start()): + continue + # Calculate line number from position in full content + abs_pos = log_match.start() + bad_match.start() + lineno = content.count("\n", 0, abs_pos) + 1 + col = abs_pos - content.rfind("\n", 0, abs_pos) + errs.append( + ( + lineno, + col, + "Multi-line log message has a continuation line that does " + "not start with a space. The log viewer uses leading " + "whitespace to detect continuation lines and re-add the " + f"log tag prefix (e.g. {highlight('[C][component:042]:')}).\n" + "Either start the continuation with a space/indent, or " + "split into separate ESP_LOG* calls.", + ) + ) + return errs + + @lint_content_find_check( "ESP_LOG", include=["*.h", "*.tcc"], From 1f945a334a472420d3f3e62c453f3ab3c5246c27 Mon Sep 17 00:00:00 2001 From: Joshua Sing Date: Tue, 24 Feb 2026 04:01:23 +1100 Subject: [PATCH 158/609] [hdc302x] Add new component (#10160) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/hdc302x/__init__.py | 1 + esphome/components/hdc302x/hdc302x.cpp | 171 ++++++++++++++++++ esphome/components/hdc302x/hdc302x.h | 68 +++++++ esphome/components/hdc302x/sensor.py | 135 ++++++++++++++ tests/components/hdc302x/common.yaml | 19 ++ tests/components/hdc302x/test.esp32-idf.yaml | 4 + .../components/hdc302x/test.esp8266-ard.yaml | 4 + tests/components/hdc302x/test.rp2040-ard.yaml | 4 + 9 files changed, 407 insertions(+) create mode 100644 esphome/components/hdc302x/__init__.py create mode 100644 esphome/components/hdc302x/hdc302x.cpp create mode 100644 esphome/components/hdc302x/hdc302x.h create mode 100644 esphome/components/hdc302x/sensor.py create mode 100644 tests/components/hdc302x/common.yaml create mode 100644 tests/components/hdc302x/test.esp32-idf.yaml create mode 100644 tests/components/hdc302x/test.esp8266-ard.yaml create mode 100644 tests/components/hdc302x/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 2aa0656343..6728e76bba 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -213,6 +213,7 @@ esphome/components/hbridge/light/* @DotNetDann esphome/components/hbridge/switch/* @dwmw2 esphome/components/hc8/* @omartijn esphome/components/hdc2010/* @optimusprimespace @ssieb +esphome/components/hdc302x/* @joshuasing esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal diff --git a/esphome/components/hdc302x/__init__.py b/esphome/components/hdc302x/__init__.py new file mode 100644 index 0000000000..d8165281bf --- /dev/null +++ b/esphome/components/hdc302x/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@joshuasing"] diff --git a/esphome/components/hdc302x/hdc302x.cpp b/esphome/components/hdc302x/hdc302x.cpp new file mode 100644 index 0000000000..b50d34169a --- /dev/null +++ b/esphome/components/hdc302x/hdc302x.cpp @@ -0,0 +1,171 @@ +#include "hdc302x.h" + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::hdc302x { + +static const char *const TAG = "hdc302x.sensor"; + +// Commands (per datasheet Table 7-4) +static const uint8_t HDC302X_CMD_SOFT_RESET[2] = {0x30, 0xa2}; +static const uint8_t HDC302X_CMD_CLEAR_STATUS_REGISTER[2] = {0x30, 0x41}; + +static const uint8_t HDC302X_CMD_TRIGGER_MSB = 0x24; + +static const uint8_t HDC302X_CMD_HEATER_ENABLE[2] = {0x30, 0x6d}; +static const uint8_t HDC302X_CMD_HEATER_DISABLE[2] = {0x30, 0x66}; +static const uint8_t HDC302X_CMD_HEATER_CONFIGURE[2] = {0x30, 0x6e}; + +void HDC302XComponent::setup() { + // Soft reset the device + if (this->write(HDC302X_CMD_SOFT_RESET, 2) != i2c::ERROR_OK) { + this->mark_failed(LOG_STR("Soft reset failed")); + return; + } + // Delay SensorRR (reset ready), per datasheet, 6.5. + delay(3); + + // Clear status register + if (this->write(HDC302X_CMD_CLEAR_STATUS_REGISTER, 2) != i2c::ERROR_OK) { + this->mark_failed(LOG_STR("Clear status failed")); + return; + } +} + +void HDC302XComponent::dump_config() { + ESP_LOGCONFIG(TAG, + "HDC302x:\n" + " Heater: %s", + this->heater_active_ ? "active" : "inactive"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temp_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +void HDC302XComponent::update() { + uint8_t cmd[] = { + HDC302X_CMD_TRIGGER_MSB, + this->power_mode_, + }; + if (this->write(cmd, 2) != i2c::ERROR_OK) { + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + + // Read data after ADC conversion has completed + this->set_timeout(this->conversion_delay_ms_(), [this]() { this->read_data_(); }); +} + +void HDC302XComponent::start_heater(uint16_t power, uint32_t duration_ms) { + if (!this->disable_heater_()) { + ESP_LOGD(TAG, "Heater disable before start failed"); + } + if (!this->configure_heater_(power) || !this->enable_heater_()) { + ESP_LOGW(TAG, "Heater start failed"); + return; + } + this->heater_active_ = true; + this->cancel_timeout("heater_off"); + if (duration_ms > 0) { + this->set_timeout("heater_off", duration_ms, [this]() { this->stop_heater(); }); + } +} + +void HDC302XComponent::stop_heater() { + this->cancel_timeout("heater_off"); + if (!this->disable_heater_()) { + ESP_LOGW(TAG, "Heater stop failed"); + } + this->heater_active_ = false; +} + +bool HDC302XComponent::enable_heater_() { + if (this->write(HDC302X_CMD_HEATER_ENABLE, 2) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Enable heater failed"); + return false; + } + return true; +} + +bool HDC302XComponent::configure_heater_(uint16_t power_level) { + if (power_level > 0x3fff) { + ESP_LOGW(TAG, "Heater power 0x%04x exceeds max 0x3fff", power_level); + return false; + } + + // Heater current level config. + uint8_t config[] = { + static_cast((power_level >> 8) & 0xff), // MSB + static_cast(power_level & 0xff) // LSB + }; + + // Configure level of heater current (per datasheet 7.5.7.8). + uint8_t cmd[] = { + HDC302X_CMD_HEATER_CONFIGURE[0], HDC302X_CMD_HEATER_CONFIGURE[1], config[0], config[1], + crc8(config, 2, 0xff, 0x31, true), + }; + if (this->write(cmd, sizeof(cmd)) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Configure heater failed"); + return false; + } + + return true; +} + +bool HDC302XComponent::disable_heater_() { + if (this->write(HDC302X_CMD_HEATER_DISABLE, 2) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Disable heater failed"); + return false; + } + return true; +} + +void HDC302XComponent::read_data_() { + uint8_t buf[6]; + if (this->read(buf, 6) != i2c::ERROR_OK) { + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + + // Check checksums + if (crc8(buf, 2, 0xff, 0x31, true) != buf[2] || crc8(buf + 3, 2, 0xff, 0x31, true) != buf[5]) { + this->status_set_warning(LOG_STR("Read data: invalid CRC")); + return; + } + + this->status_clear_warning(); + + if (this->temp_sensor_ != nullptr) { + uint16_t raw_t = encode_uint16(buf[0], buf[1]); + // Calculate temperature in Celsius per datasheet section 7.3.3. + float temp = -45 + 175 * (float(raw_t) / 65535.0f); + this->temp_sensor_->publish_state(temp); + } + + if (this->humidity_sensor_ != nullptr) { + uint16_t raw_rh = encode_uint16(buf[3], buf[4]); + // Calculate RH% per datasheet section 7.3.3. + float humidity = 100 * (float(raw_rh) / 65535.0f); + this->humidity_sensor_->publish_state(humidity); + } +} + +uint32_t HDC302XComponent::conversion_delay_ms_() { + // ADC conversion delay per datasheet, Table 7-5. - Trigger on Demand + switch (this->power_mode_) { + case HDC302XPowerMode::BALANCED: + return 8; + case HDC302XPowerMode::LOW_POWER: + return 5; + case HDC302XPowerMode::ULTRA_LOW_POWER: + return 4; + case HDC302XPowerMode::HIGH_ACCURACY: + default: + return 13; + } +} + +} // namespace esphome::hdc302x diff --git a/esphome/components/hdc302x/hdc302x.h b/esphome/components/hdc302x/hdc302x.h new file mode 100644 index 0000000000..6afea0a8c0 --- /dev/null +++ b/esphome/components/hdc302x/hdc302x.h @@ -0,0 +1,68 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome::hdc302x { + +enum HDC302XPowerMode : uint8_t { + HIGH_ACCURACY = 0x00, + BALANCED = 0x0b, + LOW_POWER = 0x16, + ULTRA_LOW_POWER = 0xff, +}; + +/** + HDC302x Temperature and humidity sensor. + + Datasheet: + https://www.ti.com/lit/ds/symlink/hdc3020.pdf + */ +class HDC302XComponent : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + void update() override; + + void start_heater(uint16_t power, uint32_t duration_ms); + void stop_heater(); + + void set_temp_sensor(sensor::Sensor *temp_sensor) { this->temp_sensor_ = temp_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } + + void set_power_mode(HDC302XPowerMode power_mode) { this->power_mode_ = power_mode; } + + protected: + sensor::Sensor *temp_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + + HDC302XPowerMode power_mode_{HDC302XPowerMode::HIGH_ACCURACY}; + bool heater_active_{false}; + + bool enable_heater_(); + bool configure_heater_(uint16_t power_level); + bool disable_heater_(); + void read_data_(); + uint32_t conversion_delay_ms_(); +}; + +template class HeaterOnAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, power) + TEMPLATABLE_VALUE(uint32_t, duration) + + void play(const Ts &...x) override { + auto power_val = this->power_.value(x...); + auto duration_val = this->duration_.value(x...); + this->parent_->start_heater(power_val, duration_val); + } +}; + +template class HeaterOffAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->stop_heater(); } +}; + +} // namespace esphome::hdc302x diff --git a/esphome/components/hdc302x/sensor.py b/esphome/components/hdc302x/sensor.py new file mode 100644 index 0000000000..7215a4cfb7 --- /dev/null +++ b/esphome/components/hdc302x/sensor.py @@ -0,0 +1,135 @@ +from esphome import automation +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_DURATION, + CONF_HUMIDITY, + CONF_ID, + CONF_POWER, + CONF_POWER_MODE, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +DEPENDENCIES = ["i2c"] + +hdc302x_ns = cg.esphome_ns.namespace("hdc302x") +HDC302XComponent = hdc302x_ns.class_( + "HDC302XComponent", cg.PollingComponent, i2c.I2CDevice +) + +HDC302XPowerMode = hdc302x_ns.enum("HDC302XPowerMode") +POWER_MODE_OPTIONS = { + "HIGH_ACCURACY": HDC302XPowerMode.HIGH_ACCURACY, + "BALANCED": HDC302XPowerMode.BALANCED, + "LOW_POWER": HDC302XPowerMode.LOW_POWER, + "ULTRA_LOW_POWER": HDC302XPowerMode.ULTRA_LOW_POWER, +} + +# Actions +HeaterOnAction = hdc302x_ns.class_("HeaterOnAction", automation.Action) +HeaterOffAction = hdc302x_ns.class_("HeaterOffAction", automation.Action) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HDC302XComponent), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER_MODE, default="HIGH_ACCURACY"): cv.enum( + POWER_MODE_OPTIONS, upper=True + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x44)) # Default address per datasheet, Table 7-2. +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if temp_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temp_config) + cg.add(var.set_temp_sensor(sens)) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity_sensor(sens)) + + cg.add(var.set_power_mode(config[CONF_POWER_MODE])) + + +# HDC302x heater power configs, per datasheet Table 7-15. +HDC302X_HEATER_POWER_MAP = { + "QUARTER": 0x009F, + "HALF": 0x03FF, + "FULL": 0x3FFF, +} + + +def heater_power_value(value): + """Accept enum names or raw uint16 values""" + if isinstance(value, cv.Lambda): + return value + if isinstance(value, str): + upper = value.upper() + if upper in HDC302X_HEATER_POWER_MAP: + return HDC302X_HEATER_POWER_MAP[upper] + raise cv.Invalid( + f"Unknown heater power preset: {value}. Use QUARTER, HALF, FULL, or a raw value 0-16383" + ) + return cv.int_range(min=0, max=0x3FFF)(value) + + +HDC302X_ACTION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(HDC302XComponent)}) + +HDC302X_HEATER_ON_ACTION_SCHEMA = maybe_simple_id( + { + cv.GenerateID(): cv.use_id(HDC302XComponent), + cv.Optional(CONF_POWER, default="QUARTER"): cv.templatable(heater_power_value), + cv.Optional(CONF_DURATION, default="5s"): cv.templatable( + cv.positive_time_period_milliseconds + ), + } +) + + +@automation.register_action( + "hdc302x.heater_on", HeaterOnAction, HDC302X_HEATER_ON_ACTION_SCHEMA +) +async def hdc302x_heater_on_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_POWER], args, cg.uint16) + cg.add(var.set_power(template_)) + template_ = await cg.templatable(config[CONF_DURATION], args, cg.uint32) + cg.add(var.set_duration(template_)) + return var + + +@automation.register_action( + "hdc302x.heater_off", HeaterOffAction, HDC302X_ACTION_SCHEMA +) +async def hdc302x_heater_off_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/tests/components/hdc302x/common.yaml b/tests/components/hdc302x/common.yaml new file mode 100644 index 0000000000..56bb31effe --- /dev/null +++ b/tests/components/hdc302x/common.yaml @@ -0,0 +1,19 @@ +esphome: + on_boot: + then: + - hdc302x.heater_on: + id: hdc302x_sensor + power: QUARTER + duration: 5s + - hdc302x.heater_off: + id: hdc302x_sensor + +sensor: + - platform: hdc302x + id: hdc302x_sensor + i2c_id: i2c_bus + temperature: + name: Temperature + humidity: + name: Humidity + update_interval: 15s diff --git a/tests/components/hdc302x/test.esp32-idf.yaml b/tests/components/hdc302x/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/hdc302x/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/hdc302x/test.esp8266-ard.yaml b/tests/components/hdc302x/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/hdc302x/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/hdc302x/test.rp2040-ard.yaml b/tests/components/hdc302x/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/hdc302x/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml From e199145f1c3b168e8584c4613f1d814c1694a4f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 12:20:55 -0600 Subject: [PATCH 159/609] [core] Avoid expensive modulo in LockFreeQueue for non-power-of-2 sizes (#14221) --- esphome/core/lock_free_queue.h | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h index e96b739b58..522fbd36e1 100644 --- a/esphome/core/lock_free_queue.h +++ b/esphome/core/lock_free_queue.h @@ -38,13 +38,27 @@ template class LockFreeQueue { } protected: + // Advance ring buffer index by one, wrapping at SIZE. + // Power-of-2 sizes use modulo (compiler emits single mask instruction). + // Non-power-of-2 sizes use comparison to avoid expensive multiply-shift sequences. + static constexpr uint8_t next_index(uint8_t index) { + if constexpr ((SIZE & (SIZE - 1)) == 0) { + return (index + 1) % SIZE; + } else { + uint8_t next = index + 1; + if (next >= SIZE) [[unlikely]] + next = 0; + return next; + } + } + // Internal push that reports queue state - for use by derived classes bool push_internal_(T *element, bool &was_empty, uint8_t &old_tail) { if (element == nullptr) return false; uint8_t current_tail = tail_.load(std::memory_order_relaxed); - uint8_t next_tail = (current_tail + 1) % SIZE; + uint8_t next_tail = next_index(current_tail); // Read head before incrementing tail uint8_t head_before = head_.load(std::memory_order_acquire); @@ -73,14 +87,21 @@ template class LockFreeQueue { } T *element = buffer_[current_head]; - head_.store((current_head + 1) % SIZE, std::memory_order_release); + head_.store(next_index(current_head), std::memory_order_release); return element; } size_t size() const { uint8_t tail = tail_.load(std::memory_order_acquire); uint8_t head = head_.load(std::memory_order_acquire); - return (tail - head + SIZE) % SIZE; + if constexpr ((SIZE & (SIZE - 1)) == 0) { + return (tail - head + SIZE) % SIZE; + } else { + int diff = static_cast(tail) - static_cast(head); + if (diff < 0) + diff += SIZE; + return static_cast(diff); + } } uint16_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); } @@ -90,7 +111,7 @@ template class LockFreeQueue { bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); } bool full() const { - uint8_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE; + uint8_t next_tail = next_index(tail_.load(std::memory_order_relaxed)); return next_tail == head_.load(std::memory_order_acquire); } From 0d32a5321ca20943eea2d5c9c9d3b3aeac44b568 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:46:53 -0500 Subject: [PATCH 160/609] [remote_transmitter/remote_receiver] Rename _esp32.cpp to _rmt.cpp (#14226) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/remote_receiver/__init__.py | 2 +- .../{remote_receiver_esp32.cpp => remote_receiver_rmt.cpp} | 0 esphome/components/remote_transmitter/__init__.py | 2 +- ...{remote_transmitter_esp32.cpp => remote_transmitter_rmt.cpp} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename esphome/components/remote_receiver/{remote_receiver_esp32.cpp => remote_receiver_rmt.cpp} (100%) rename esphome/components/remote_transmitter/{remote_transmitter_esp32.cpp => remote_transmitter_rmt.cpp} (100%) diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index 362f6e99db..53a0f8fb77 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -237,7 +237,7 @@ async def to_code(config): FILTER_SOURCE_FILES = filter_source_files_from_platform( { - "remote_receiver_esp32.cpp": { + "remote_receiver_rmt.cpp": { PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_rmt.cpp similarity index 100% rename from esphome/components/remote_receiver/remote_receiver_esp32.cpp rename to esphome/components/remote_receiver/remote_receiver_rmt.cpp diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index fc772f88b2..371dbb685f 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -171,7 +171,7 @@ async def to_code(config): FILTER_SOURCE_FILES = filter_source_files_from_platform( { - "remote_transmitter_esp32.cpp": { + "remote_transmitter_rmt.cpp": { PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_rmt.cpp similarity index 100% rename from esphome/components/remote_transmitter/remote_transmitter_esp32.cpp rename to esphome/components/remote_transmitter/remote_transmitter_rmt.cpp From daee71a2c17871b1f23e782059ae9e906abeb9f2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:21:29 -0500 Subject: [PATCH 161/609] [http_request] Retry update check on startup until network is ready (#14228) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../update/http_request_update.cpp | 24 ++++++++++++++++++- .../http_request/update/http_request_update.h | 1 + 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 85609bd31f..1900f69a69 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -24,8 +24,29 @@ namespace http_request { static const char *const TAG = "http_request.update"; static const size_t MAX_READ_SIZE = 256; +static constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0; +static constexpr uint32_t INITIAL_CHECK_INTERVAL_MS = 10000; +static constexpr uint8_t INITIAL_CHECK_MAX_ATTEMPTS = 6; -void HttpRequestUpdate::setup() { this->ota_parent_->add_state_listener(this); } +void HttpRequestUpdate::setup() { + this->ota_parent_->add_state_listener(this); + + // Check periodically until network is ready + // Only if update interval is > total retry window to avoid redundant checks + if (this->get_update_interval() != SCHEDULER_DONT_RUN && + this->get_update_interval() > INITIAL_CHECK_INTERVAL_MS * INITIAL_CHECK_MAX_ATTEMPTS) { + this->initial_check_remaining_ = INITIAL_CHECK_MAX_ATTEMPTS; + this->set_interval(INITIAL_CHECK_INTERVAL_ID, INITIAL_CHECK_INTERVAL_MS, [this]() { + bool connected = network::is_connected(); + if (--this->initial_check_remaining_ == 0 || connected) { + this->cancel_interval(INITIAL_CHECK_INTERVAL_ID); + if (connected) { + this->update(); + } + } + }); + } +} void HttpRequestUpdate::on_ota_state(ota::OTAState state, float progress, uint8_t error) { if (state == ota::OTAState::OTA_IN_PROGRESS) { @@ -45,6 +66,7 @@ void HttpRequestUpdate::update() { ESP_LOGD(TAG, "Network not connected, skipping update check"); return; } + this->cancel_interval(INITIAL_CHECK_INTERVAL_ID); #ifdef USE_ESP32 xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_); #else diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h index cf34ace18e..b8350346f9 100644 --- a/esphome/components/http_request/update/http_request_update.h +++ b/esphome/components/http_request/update/http_request_update.h @@ -40,6 +40,7 @@ class HttpRequestUpdate final : public update::UpdateEntity, public PollingCompo #ifdef USE_ESP32 TaskHandle_t update_task_handle_{nullptr}; #endif + uint8_t initial_check_remaining_{0}; }; } // namespace http_request From 063c6a9e45576eb5b22c94977cddac1f1eab1aef Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Mon, 23 Feb 2026 21:06:20 +0100 Subject: [PATCH 162/609] [esp32,core] Move CONF_ENABLE_OTA_ROLLBACK to core (#14231) --- esphome/components/esp32/__init__.py | 2 +- esphome/const.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 4c211b2f2a..62367443da 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -14,6 +14,7 @@ from esphome.const import ( CONF_BOARD, CONF_COMPONENTS, CONF_DISABLED, + CONF_ENABLE_OTA_ROLLBACK, CONF_ESPHOME, CONF_FRAMEWORK, CONF_IGNORE_EFUSE_CUSTOM_MAC, @@ -90,7 +91,6 @@ CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" CONF_ENGINEERING_SAMPLE = "engineering_sample" CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" -CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision" CONF_RELEASE = "release" diff --git a/esphome/const.py b/esphome/const.py index ccc9d56dbb..0b1037d091 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -354,6 +354,7 @@ CONF_ELSE = "else" CONF_ENABLE_BTM = "enable_btm" CONF_ENABLE_IPV6 = "enable_ipv6" CONF_ENABLE_ON_BOOT = "enable_on_boot" +CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" CONF_ENABLE_PIN = "enable_pin" CONF_ENABLE_PRIVATE_NETWORK_ACCESS = "enable_private_network_access" CONF_ENABLE_RRM = "enable_rrm" From 918bbfb0d3c73be7616fc7e7ab57238f27bc35ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:22:32 +0000 Subject: [PATCH 163/609] Bump aioesphomeapi from 44.0.0 to 44.1.0 (#14232) 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 be3445dceb..d22097b3ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.1.7 esphome-dashboard==20260210.0 -aioesphomeapi==44.0.0 +aioesphomeapi==44.1.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 02c37bb6d63d9ccb457a13f35b6f2e3c80c35cc9 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Mon, 23 Feb 2026 21:23:40 +0100 Subject: [PATCH 164/609] [nrf52,logger] generate crash magic in python (#14173) --- esphome/components/logger/logger_zephyr.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index c2d24d6efc..6b46b93c61 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -20,8 +20,6 @@ __attribute__((weak)) void print_coredump() {} namespace esphome::logger { -static const uint32_t CRASH_MAGIC = 0xDEADBEEF; - __attribute__((section(".noinit"))) struct { uint32_t magic; uint32_t reason; @@ -152,7 +150,7 @@ static const char *reason_to_str(unsigned int reason, char *buf) { void Logger::dump_crash_() { ESP_LOGD(TAG, "Crash buffer address %p", &crash_buf); - if (crash_buf.magic == CRASH_MAGIC) { + if (crash_buf.magic == App.get_config_hash()) { char reason_buf[REASON_BUF_SIZE]; ESP_LOGE(TAG, "Last crash:"); ESP_LOGE(TAG, "Reason=%s PC=0x%08x LR=0x%08x", reason_to_str(crash_buf.reason, reason_buf), crash_buf.pc, @@ -164,7 +162,7 @@ void Logger::dump_crash_() { } void k_sys_fatal_error_handler(unsigned int reason, const z_arch_esf_t *esf) { - crash_buf.magic = CRASH_MAGIC; + crash_buf.magic = App.get_config_hash(); crash_buf.reason = reason; if (esf) { crash_buf.pc = esf->basic.pc; From 4a529003527b3d475742b29ae2fed82e55b0066c Mon Sep 17 00:00:00 2001 From: James Myatt Date: Mon, 23 Feb 2026 21:16:32 +0000 Subject: [PATCH 165/609] [nfc] Fix logging tag for nfc helpers (#14235) --- esphome/components/nfc/nfc_helpers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/nfc/nfc_helpers.cpp b/esphome/components/nfc/nfc_helpers.cpp index bfaed6e486..fb0954a833 100644 --- a/esphome/components/nfc/nfc_helpers.cpp +++ b/esphome/components/nfc/nfc_helpers.cpp @@ -39,7 +39,7 @@ std::string get_random_ha_tag_ndef() { for (int i = 0; i < 12; i++) { uri += ALPHANUM[random_uint32() % (sizeof(ALPHANUM) - 1)]; } - ESP_LOGD("pn7160", "Payload to be written: %s", uri.c_str()); + ESP_LOGD(TAG, "Payload to be written: %s", uri.c_str()); return uri; } From 869678953dfbe22f2b784e8ec927bfa394676671 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 17:03:24 -0600 Subject: [PATCH 166/609] [core] Add pow10_int helper, replace powf in normalize_accuracy and sensor filters (#14114) Co-authored-by: Claude Opus 4.6 --- esphome/components/sensor/filter.cpp | 5 +++-- esphome/core/helpers.cpp | 11 +++++++++-- esphome/core/helpers.h | 15 +++++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index cd4db98457..0fe1effe17 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -5,6 +5,7 @@ #include #include "esphome/core/application.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "sensor.h" @@ -240,7 +241,7 @@ ValueListFilter::ValueListFilter(std::initializer_list> bool ValueListFilter::value_matches_any_(float sensor_value) { int8_t accuracy = this->parent_->get_accuracy_decimals(); - float accuracy_mult = powf(10.0f, accuracy); + float accuracy_mult = pow10_int(accuracy); float rounded_sensor = roundf(accuracy_mult * sensor_value); for (auto &filter_value : this->values_) { @@ -472,7 +473,7 @@ optional ClampFilter::new_value(float value) { RoundFilter::RoundFilter(uint8_t precision) : precision_(precision) {} optional RoundFilter::new_value(float value) { if (std::isfinite(value)) { - float accuracy_mult = powf(10.0f, this->precision_); + float accuracy_mult = pow10_int(this->precision_); return roundf(accuracy_mult * value) / accuracy_mult; } return value; diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 9f850b5df8..6d801e7ebc 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -468,8 +468,15 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_decimals) { if (accuracy_decimals < 0) { - auto multiplier = powf(10.0f, accuracy_decimals); - value = roundf(value * multiplier) / multiplier; + float divisor; + if (accuracy_decimals == -1) { + divisor = 10.0f; + } else if (accuracy_decimals == -2) { + divisor = 100.0f; + } else { + divisor = pow10_int(-accuracy_decimals); + } + value = roundf(value / divisor) * divisor; accuracy_decimals = 0; } } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 298b93fbc4..b606e68df3 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -439,6 +439,21 @@ template class SmallBufferWithHeapFallb /// @name Mathematics ///@{ +/// Compute 10^exp using iterative multiplication/division. +/// Avoids pulling in powf/__ieee754_powf (~2.3KB flash) for small integer exponents. +/// Matches powf(10, exp) for the int8_t exponent range used by sensor accuracy_decimals. +inline float pow10_int(int8_t exp) { + float result = 1.0f; + if (exp >= 0) { + for (int8_t i = 0; i < exp; i++) + result *= 10.0f; + } else { + for (int8_t i = exp; i < 0; i++) + result /= 10.0f; + } + return result; +} + /// Remap \p value from the range (\p min, \p max) to (\p min_out, \p max_out). template T remap(U value, U min, U max, T min_out, T max_out) { return (value - min) * (max_out - min_out) / (max - min) + min_out; From ebf1047da79805e86ec7a7e1058a2c50c55e42b0 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:51:56 -0500 Subject: [PATCH 167/609] [core] Move build_info_data.h out of application.h to fix incremental rebuilds (#14230) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../components/version/version_text_sensor.cpp | 1 + esphome/components/web_server/web_server.cpp | 2 +- esphome/core/application.cpp | 11 +++++++++++ esphome/core/application.h | 18 ++++++++---------- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp index 2e5686008b..74bb4c76e8 100644 --- a/esphome/components/version/version_text_sensor.cpp +++ b/esphome/components/version/version_text_sensor.cpp @@ -1,5 +1,6 @@ #include "version_text_sensor.h" #include "esphome/core/application.h" +#include "esphome/core/build_info_data.h" #include "esphome/core/log.h" #include "esphome/core/version.h" #include "esphome/core/helpers.h" diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 4b572417c1..682008c40e 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -375,7 +375,7 @@ json::SerializationBuffer<> WebServer::get_config_json() { JsonObject root = builder.root(); root[ESPHOME_F("title")] = App.get_friendly_name().empty() ? App.get_name().c_str() : App.get_friendly_name().c_str(); - char comment_buffer[ESPHOME_COMMENT_SIZE]; + char comment_buffer[Application::ESPHOME_COMMENT_SIZE_MAX]; App.get_comment_string(comment_buffer); root[ESPHOME_F("comment")] = comment_buffer; #if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index c6597897dc..1cb7dc0075 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -749,4 +749,15 @@ void Application::get_build_time_string(std::span buf buffer[buffer.size() - 1] = '\0'; } +void Application::get_comment_string(std::span buffer) { + ESPHOME_strncpy_P(buffer.data(), ESPHOME_COMMENT_STR, ESPHOME_COMMENT_SIZE); + buffer[ESPHOME_COMMENT_SIZE - 1] = '\0'; +} + +uint32_t Application::get_config_hash() { return ESPHOME_CONFIG_HASH; } + +uint32_t Application::get_config_version_hash() { return fnv1a_hash_extend(ESPHOME_CONFIG_HASH, ESPHOME_VERSION); } + +time_t Application::get_build_time() { return ESPHOME_BUILD_TIME; } + } // namespace esphome diff --git a/esphome/core/application.h b/esphome/core/application.h index 5b3e3dfed6..cd275bb97f 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -6,7 +6,6 @@ #include #include #include -#include "esphome/core/build_info_data.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" @@ -274,16 +273,15 @@ class Application { return ""; } + /// Maximum size of the comment buffer (including null terminator) + static constexpr size_t ESPHOME_COMMENT_SIZE_MAX = 256; + /// Copy the comment string into the provided buffer - /// Buffer must be ESPHOME_COMMENT_SIZE bytes (compile-time enforced) - void get_comment_string(std::span buffer) { - ESPHOME_strncpy_P(buffer.data(), ESPHOME_COMMENT_STR, buffer.size()); - buffer[buffer.size() - 1] = '\0'; - } + void get_comment_string(std::span buffer); /// Get the comment of this Application as a string std::string get_comment() { - char buffer[ESPHOME_COMMENT_SIZE]; + char buffer[ESPHOME_COMMENT_SIZE_MAX]; this->get_comment_string(buffer); return std::string(buffer); } @@ -294,13 +292,13 @@ class Application { static constexpr size_t BUILD_TIME_STR_SIZE = 26; /// Get the config hash as a 32-bit integer - constexpr uint32_t get_config_hash() { return ESPHOME_CONFIG_HASH; } + uint32_t get_config_hash(); /// Get the config hash extended with ESPHome version - constexpr uint32_t get_config_version_hash() { return fnv1a_hash_extend(ESPHOME_CONFIG_HASH, ESPHOME_VERSION); } + uint32_t get_config_version_hash(); /// Get the build time as a Unix timestamp - constexpr time_t get_build_time() { return ESPHOME_BUILD_TIME; } + time_t get_build_time(); /// Copy the build time string into the provided buffer /// Buffer must be BUILD_TIME_STR_SIZE bytes (compile-time enforced) From 30cc51eac97948449aa87a4e741bab84bb79d6a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 19:49:00 -0600 Subject: [PATCH 168/609] [version] Use C++17 nested namespace syntax (#14240) --- esphome/components/version/version_text_sensor.cpp | 6 ++---- esphome/components/version/version_text_sensor.h | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp index 74bb4c76e8..4a08001cc4 100644 --- a/esphome/components/version/version_text_sensor.cpp +++ b/esphome/components/version/version_text_sensor.cpp @@ -6,8 +6,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/progmem.h" -namespace esphome { -namespace version { +namespace esphome::version { static const char *const TAG = "version.text_sensor"; @@ -36,5 +35,4 @@ void VersionTextSensor::setup() { void VersionTextSensor::set_hide_timestamp(bool hide_timestamp) { this->hide_timestamp_ = hide_timestamp; } void VersionTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Version Text Sensor", this); } -} // namespace version -} // namespace esphome +} // namespace esphome::version diff --git a/esphome/components/version/version_text_sensor.h b/esphome/components/version/version_text_sensor.h index b7d8001120..6153c5dd7c 100644 --- a/esphome/components/version/version_text_sensor.h +++ b/esphome/components/version/version_text_sensor.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/text_sensor/text_sensor.h" -namespace esphome { -namespace version { +namespace esphome::version { class VersionTextSensor : public text_sensor::TextSensor, public Component { public: @@ -16,5 +15,4 @@ class VersionTextSensor : public text_sensor::TextSensor, public Component { bool hide_timestamp_{false}; }; -} // namespace version -} // namespace esphome +} // namespace esphome::version From 843d06df3f5bf63e9447d1a10eaaa6987f0b5705 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 19:49:15 -0600 Subject: [PATCH 169/609] [switch] Use C++17 nested namespace syntax (#14241) --- esphome/components/switch/automation.cpp | 6 ++---- esphome/components/switch/automation.h | 6 ++---- .../switch/binary_sensor/switch_binary_sensor.cpp | 6 ++---- .../switch/binary_sensor/switch_binary_sensor.h | 6 ++---- esphome/components/switch/switch.cpp | 6 ++---- esphome/components/switch/switch.h | 14 ++++++-------- 6 files changed, 16 insertions(+), 28 deletions(-) diff --git a/esphome/components/switch/automation.cpp b/esphome/components/switch/automation.cpp index 5989ae9ce3..9a0221fe56 100644 --- a/esphome/components/switch/automation.cpp +++ b/esphome/components/switch/automation.cpp @@ -1,10 +1,8 @@ #include "automation.h" #include "esphome/core/log.h" -namespace esphome { -namespace switch_ { +namespace esphome::switch_ { static const char *const TAG = "switch.automation"; -} // namespace switch_ -} // namespace esphome +} // namespace esphome::switch_ diff --git a/esphome/components/switch/automation.h b/esphome/components/switch/automation.h index 27d3474c97..ed1f056c8b 100644 --- a/esphome/components/switch/automation.h +++ b/esphome/components/switch/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/components/switch/switch.h" -namespace esphome { -namespace switch_ { +namespace esphome::switch_ { template class TurnOnAction : public Action { public: @@ -104,5 +103,4 @@ template class SwitchPublishAction : public Action { Switch *switch_; }; -} // namespace switch_ -} // namespace esphome +} // namespace esphome::switch_ diff --git a/esphome/components/switch/binary_sensor/switch_binary_sensor.cpp b/esphome/components/switch/binary_sensor/switch_binary_sensor.cpp index ba57154446..19995fb1ae 100644 --- a/esphome/components/switch/binary_sensor/switch_binary_sensor.cpp +++ b/esphome/components/switch/binary_sensor/switch_binary_sensor.cpp @@ -1,8 +1,7 @@ #include "switch_binary_sensor.h" #include "esphome/core/log.h" -namespace esphome { -namespace switch_ { +namespace esphome::switch_ { static const char *const TAG = "switch.binary_sensor"; @@ -13,5 +12,4 @@ void SwitchBinarySensor::setup() { void SwitchBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Switch Binary Sensor", this); } -} // namespace switch_ -} // namespace esphome +} // namespace esphome::switch_ diff --git a/esphome/components/switch/binary_sensor/switch_binary_sensor.h b/esphome/components/switch/binary_sensor/switch_binary_sensor.h index 53b07da903..0b77cdd920 100644 --- a/esphome/components/switch/binary_sensor/switch_binary_sensor.h +++ b/esphome/components/switch/binary_sensor/switch_binary_sensor.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace switch_ { +namespace esphome::switch_ { class SwitchBinarySensor : public binary_sensor::BinarySensor, public Component { public: @@ -17,5 +16,4 @@ class SwitchBinarySensor : public binary_sensor::BinarySensor, public Component Switch *source_; }; -} // namespace switch_ -} // namespace esphome +} // namespace esphome::switch_ diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 61a273d25c..9e9af21368 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -3,8 +3,7 @@ #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" -namespace esphome { -namespace switch_ { +namespace esphome::switch_ { static const char *const TAG = "switch"; @@ -107,5 +106,4 @@ void log_switch(const char *tag, const char *prefix, const char *type, Switch *o } } -} // namespace switch_ -} // namespace esphome +} // namespace esphome::switch_ diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index 9319adf9ed..982c640cf9 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -5,8 +5,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" -namespace esphome { -namespace switch_ { +namespace esphome::switch_ { #define SUB_SWITCH(name) \ protected: \ @@ -16,10 +15,10 @@ namespace switch_ { void set_##name##_switch(switch_::Switch *s) { this->name##_switch_ = s; } // bit0: on/off. bit1: persistent. bit2: inverted. bit3: disabled -const int RESTORE_MODE_ON_MASK = 0x01; -const int RESTORE_MODE_PERSISTENT_MASK = 0x02; -const int RESTORE_MODE_INVERTED_MASK = 0x04; -const int RESTORE_MODE_DISABLED_MASK = 0x08; +constexpr int RESTORE_MODE_ON_MASK = 0x01; +constexpr int RESTORE_MODE_PERSISTENT_MASK = 0x02; +constexpr int RESTORE_MODE_INVERTED_MASK = 0x04; +constexpr int RESTORE_MODE_DISABLED_MASK = 0x08; enum SwitchRestoreMode : uint8_t { SWITCH_ALWAYS_OFF = !RESTORE_MODE_ON_MASK, @@ -146,5 +145,4 @@ class Switch : public EntityBase, public EntityBase_DeviceClass { #define LOG_SWITCH(prefix, type, obj) log_switch((TAG), (prefix), LOG_STR_LITERAL(type), (obj)) void log_switch(const char *tag, const char *prefix, const char *type, Switch *obj); -} // namespace switch_ -} // namespace esphome +} // namespace esphome::switch_ From 63c1496115befafbdf8565f48b34ab7a6ea57753 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 19:49:25 -0600 Subject: [PATCH 170/609] [text] Use C++17 nested namespace syntax (#14242) --- esphome/components/text/automation.h | 6 ++---- esphome/components/text/text.cpp | 6 ++---- esphome/components/text/text.h | 6 ++---- esphome/components/text/text_call.cpp | 6 ++---- esphome/components/text/text_call.h | 6 ++---- esphome/components/text/text_traits.h | 6 ++---- 6 files changed, 12 insertions(+), 24 deletions(-) diff --git a/esphome/components/text/automation.h b/esphome/components/text/automation.h index e7667fe491..ac8166d0be 100644 --- a/esphome/components/text/automation.h +++ b/esphome/components/text/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "text.h" -namespace esphome { -namespace text { +namespace esphome::text { class TextStateTrigger : public Trigger { public: @@ -29,5 +28,4 @@ template class TextSetAction : public Action { Text *text_; }; -} // namespace text -} // namespace esphome +} // namespace esphome::text diff --git a/esphome/components/text/text.cpp b/esphome/components/text/text.cpp index e3f74b685b..d8ab6b1b92 100644 --- a/esphome/components/text/text.cpp +++ b/esphome/components/text/text.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include -namespace esphome { -namespace text { +namespace esphome::text { static const char *const TAG = "text"; @@ -34,5 +33,4 @@ void Text::add_on_state_callback(std::function &&call this->state_callback_.add(std::move(callback)); } -} // namespace text -} // namespace esphome +} // namespace esphome::text diff --git a/esphome/components/text/text.h b/esphome/components/text/text.h index 3a1bea56cb..7d255e5688 100644 --- a/esphome/components/text/text.h +++ b/esphome/components/text/text.h @@ -6,8 +6,7 @@ #include "text_call.h" #include "text_traits.h" -namespace esphome { -namespace text { +namespace esphome::text { #define LOG_TEXT(prefix, type, obj) \ if ((obj) != nullptr) { \ @@ -47,5 +46,4 @@ class Text : public EntityBase { LazyCallbackManager state_callback_; }; -} // namespace text -} // namespace esphome +} // namespace esphome::text diff --git a/esphome/components/text/text_call.cpp b/esphome/components/text/text_call.cpp index 0d0a1d228d..8a1630c5ca 100644 --- a/esphome/components/text/text_call.cpp +++ b/esphome/components/text/text_call.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "text.h" -namespace esphome { -namespace text { +namespace esphome::text { static const char *const TAG = "text"; @@ -52,5 +51,4 @@ void TextCall::perform() { this->parent_->control(target_value); } -} // namespace text -} // namespace esphome +} // namespace esphome::text diff --git a/esphome/components/text/text_call.h b/esphome/components/text/text_call.h index 9f75a25c6b..532fae34b2 100644 --- a/esphome/components/text/text_call.h +++ b/esphome/components/text/text_call.h @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "text_traits.h" -namespace esphome { -namespace text { +namespace esphome::text { class Text; @@ -21,5 +20,4 @@ class TextCall { void validate_(); }; -} // namespace text -} // namespace esphome +} // namespace esphome::text diff --git a/esphome/components/text/text_traits.h b/esphome/components/text/text_traits.h index 473daafb8e..72e65b83ce 100644 --- a/esphome/components/text/text_traits.h +++ b/esphome/components/text/text_traits.h @@ -4,8 +4,7 @@ #include "esphome/core/string_ref.h" -namespace esphome { -namespace text { +namespace esphome::text { enum TextMode : uint8_t { TEXT_MODE_TEXT = 0, @@ -37,5 +36,4 @@ class TextTraits { TextMode mode_{TEXT_MODE_TEXT}; }; -} // namespace text -} // namespace esphome +} // namespace esphome::text From 500aa7bf1d27378ea94141babdad6c83b0176f4f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 19:49:35 -0600 Subject: [PATCH 171/609] [text_sensor] Use C++17 nested namespace syntax (#14243) --- esphome/components/text_sensor/automation.h | 6 ++---- esphome/components/text_sensor/filter.cpp | 6 ++---- esphome/components/text_sensor/filter.h | 6 ++---- esphome/components/text_sensor/text_sensor.cpp | 6 ++---- esphome/components/text_sensor/text_sensor.h | 6 ++---- 5 files changed, 10 insertions(+), 20 deletions(-) diff --git a/esphome/components/text_sensor/automation.h b/esphome/components/text_sensor/automation.h index 709c54c140..ab30362774 100644 --- a/esphome/components/text_sensor/automation.h +++ b/esphome/components/text_sensor/automation.h @@ -6,8 +6,7 @@ #include "esphome/core/automation.h" #include "esphome/components/text_sensor/text_sensor.h" -namespace esphome { -namespace text_sensor { +namespace esphome::text_sensor { class TextSensorStateTrigger : public Trigger { public: @@ -46,5 +45,4 @@ template class TextSensorPublishAction : public Action { TextSensor *sensor_; }; -} // namespace text_sensor -} // namespace esphome +} // namespace esphome::text_sensor diff --git a/esphome/components/text_sensor/filter.cpp b/esphome/components/text_sensor/filter.cpp index f6552c7c66..f7c6a695fb 100644 --- a/esphome/components/text_sensor/filter.cpp +++ b/esphome/components/text_sensor/filter.cpp @@ -6,8 +6,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace text_sensor { +namespace esphome::text_sensor { static const char *const TAG = "text_sensor.filter"; @@ -107,7 +106,6 @@ bool MapFilter::new_value(std::string &value) { return true; // Pass through if no match } -} // namespace text_sensor -} // namespace esphome +} // namespace esphome::text_sensor #endif // USE_TEXT_SENSOR_FILTER diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index f88e8645cc..8a8bc55c8e 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -6,8 +6,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace text_sensor { +namespace esphome::text_sensor { class TextSensor; @@ -165,7 +164,6 @@ class MapFilter : public Filter { FixedVector mappings_; }; -} // namespace text_sensor -} // namespace esphome +} // namespace esphome::text_sensor #endif // USE_TEXT_SENSOR_FILTER diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index c66d08ec40..91561c5f42 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include -namespace esphome { -namespace text_sensor { +namespace esphome::text_sensor { static const char *const TAG = "text_sensor"; @@ -125,5 +124,4 @@ void TextSensor::notify_frontend_() { #endif } -} // namespace text_sensor -} // namespace esphome +} // namespace esphome::text_sensor diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index 97373dc716..9916aa63b2 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -10,8 +10,7 @@ #include #include -namespace esphome { -namespace text_sensor { +namespace esphome::text_sensor { class TextSensor; @@ -84,5 +83,4 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { #endif }; -} // namespace text_sensor -} // namespace esphome +} // namespace esphome::text_sensor From a694003fe3f2aca98e930f9d1836d43ac2ee05d4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 19:49:48 -0600 Subject: [PATCH 172/609] [usb_host] Use C++17 nested namespace syntax (#14244) --- esphome/components/usb_host/usb_host.h | 26 +++++++++---------- .../components/usb_host/usb_host_client.cpp | 6 ++--- .../usb_host/usb_host_component.cpp | 6 ++--- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index a6a97d0bd7..2eec0c9699 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -12,8 +12,7 @@ #include "esphome/core/event_pool.h" #include -namespace esphome { -namespace usb_host { +namespace esphome::usb_host { // THREADING MODEL: // This component uses a dedicated USB task for event processing to prevent data loss. @@ -44,16 +43,16 @@ struct TransferRequest; class USBClient; // constants for setup packet type -static const uint8_t USB_RECIP_DEVICE = 0; -static const uint8_t USB_RECIP_INTERFACE = 1; -static const uint8_t USB_RECIP_ENDPOINT = 2; -static const uint8_t USB_TYPE_STANDARD = 0 << 5; -static const uint8_t USB_TYPE_CLASS = 1 << 5; -static const uint8_t USB_TYPE_VENDOR = 2 << 5; -static const uint8_t USB_DIR_MASK = 1 << 7; -static const uint8_t USB_DIR_IN = 1 << 7; -static const uint8_t USB_DIR_OUT = 0; -static const size_t SETUP_PACKET_SIZE = 8; +static constexpr uint8_t USB_RECIP_DEVICE = 0; +static constexpr uint8_t USB_RECIP_INTERFACE = 1; +static constexpr uint8_t USB_RECIP_ENDPOINT = 2; +static constexpr uint8_t USB_TYPE_STANDARD = 0 << 5; +static constexpr uint8_t USB_TYPE_CLASS = 1 << 5; +static constexpr uint8_t USB_TYPE_VENDOR = 2 << 5; +static constexpr uint8_t USB_DIR_MASK = 1 << 7; +static constexpr uint8_t USB_DIR_IN = 1 << 7; +static constexpr uint8_t USB_DIR_OUT = 0; +static constexpr size_t SETUP_PACKET_SIZE = 8; static constexpr size_t MAX_REQUESTS = USB_HOST_MAX_REQUESTS; // maximum number of outstanding requests possible. static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be between 1 and 32"); @@ -189,7 +188,6 @@ class USBHost : public Component { std::vector clients_{}; }; -} // namespace usb_host -} // namespace esphome +} // namespace esphome::usb_host #endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 422d74095c..5b0aed4c59 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -10,8 +10,7 @@ #include #include #include -namespace esphome { -namespace usb_host { +namespace esphome::usb_host { #pragma GCC diagnostic ignored "-Wparentheses" @@ -568,6 +567,5 @@ void USBClient::release_trq(TransferRequest *trq) { this->trq_in_use_.fetch_and(mask, std::memory_order_release); } -} // namespace usb_host -} // namespace esphome +} // namespace esphome::usb_host #endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_host/usb_host_component.cpp b/esphome/components/usb_host/usb_host_component.cpp index 790fe6713b..8ce0a70dc9 100644 --- a/esphome/components/usb_host/usb_host_component.cpp +++ b/esphome/components/usb_host/usb_host_component.cpp @@ -4,8 +4,7 @@ #include #include "esphome/core/log.h" -namespace esphome { -namespace usb_host { +namespace esphome::usb_host { void USBHost::setup() { usb_host_config_t config{}; @@ -28,7 +27,6 @@ void USBHost::loop() { } } -} // namespace usb_host -} // namespace esphome +} // namespace esphome::usb_host #endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 From 1614eb9c9cc3be784d22f262171b7978b21ebbb3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 19:50:00 -0600 Subject: [PATCH 173/609] [i2c] Use C++17 nested namespace syntax (#14245) --- esphome/components/i2c/i2c.cpp | 6 ++---- esphome/components/i2c/i2c.h | 6 ++---- esphome/components/i2c/i2c_bus.h | 6 ++---- esphome/components/i2c/i2c_bus_arduino.cpp | 6 ++---- esphome/components/i2c/i2c_bus_arduino.h | 6 ++---- esphome/components/i2c/i2c_bus_esp_idf.cpp | 6 ++---- esphome/components/i2c/i2c_bus_esp_idf.h | 6 ++---- 7 files changed, 14 insertions(+), 28 deletions(-) diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index b9b5d79428..76b3ec3b4d 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -5,8 +5,7 @@ #include "esphome/core/log.h" #include -namespace esphome { -namespace i2c { +namespace esphome::i2c { static const char *const TAG = "i2c"; @@ -109,5 +108,4 @@ uint8_t I2CRegister16::get() const { return value; } -} // namespace i2c -} // namespace esphome +} // namespace esphome::i2c diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index aab98d5f46..00929db620 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -6,8 +6,7 @@ #include "esphome/core/optional.h" #include "i2c_bus.h" -namespace esphome { -namespace i2c { +namespace esphome::i2c { #define LOG_I2C_DEVICE(this) ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); @@ -272,5 +271,4 @@ class I2CDevice { I2CBus *bus_{nullptr}; ///< pointer to I2CBus instance }; -} // namespace i2c -} // namespace esphome +} // namespace esphome::i2c diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index 2bc0dc1ef9..0c5e80bfe5 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -6,8 +6,7 @@ #include "esphome/core/helpers.h" -namespace esphome { -namespace i2c { +namespace esphome::i2c { /// @brief Error codes returned by I2CBus and I2CDevice methods enum ErrorCode { @@ -69,5 +68,4 @@ class InternalI2CBus : public I2CBus { virtual int get_port() const = 0; }; -} // namespace i2c -} // namespace esphome +} // namespace esphome::i2c diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index edd6b81588..5120eb4c00 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -7,8 +7,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace i2c { +namespace esphome::i2c { static const char *const TAG = "i2c.arduino"; @@ -262,7 +261,6 @@ void ArduinoI2CBus::recover_() { recovery_result_ = RECOVERY_COMPLETED; } -} // namespace i2c -} // namespace esphome +} // namespace esphome::i2c #endif // defined(USE_ARDUINO) && !defined(USE_ESP32) diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h index 2d69e7684c..edc14af7bc 100644 --- a/esphome/components/i2c/i2c_bus_arduino.h +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -6,8 +6,7 @@ #include "esphome/core/component.h" #include "i2c_bus.h" -namespace esphome { -namespace i2c { +namespace esphome::i2c { enum RecoveryCode { RECOVERY_FAILED_SCL_LOW, @@ -45,7 +44,6 @@ class ArduinoI2CBus : public InternalI2CBus, public Component { bool initialized_ = false; }; -} // namespace i2c -} // namespace esphome +} // namespace esphome::i2c #endif // defined(USE_ARDUINO) && !defined(USE_ESP32) diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index 7a965ce5ad..eaefabf75b 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -10,8 +10,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace i2c { +namespace esphome::i2c { static const char *const TAG = "i2c.idf"; @@ -312,6 +311,5 @@ void IDFI2CBus::recover_() { recovery_result_ = RECOVERY_COMPLETED; } -} // namespace i2c -} // namespace esphome +} // namespace esphome::i2c #endif // USE_ESP32 diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h index 84f4616967..c23f9f0c54 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.h +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -6,8 +6,7 @@ #include "i2c_bus.h" #include -namespace esphome { -namespace i2c { +namespace esphome::i2c { enum RecoveryCode { RECOVERY_FAILED_SCL_LOW, @@ -56,7 +55,6 @@ class IDFI2CBus : public InternalI2CBus, public Component { #endif }; -} // namespace i2c -} // namespace esphome +} // namespace esphome::i2c #endif // USE_ESP32 From 70e47f301d0dd936ce0d30241aa1427fbab02812 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 19:50:11 -0600 Subject: [PATCH 174/609] [ethernet] Use C++17 nested namespace syntax (#14246) --- esphome/components/ethernet/ethernet_component.cpp | 6 ++---- esphome/components/ethernet/ethernet_component.h | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index fcd32223e4..f855bc89cc 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -19,8 +19,7 @@ #include #endif -namespace esphome { -namespace ethernet { +namespace esphome::ethernet { #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) // work around IDF compile issue on P4 https://github.com/espressif/esp-idf/pull/15637 @@ -881,7 +880,6 @@ void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister regi #endif -} // namespace ethernet -} // namespace esphome +} // namespace esphome::ethernet #endif // USE_ESP32 diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index b4859c308d..1cd44d2b2c 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -15,8 +15,7 @@ #include "esp_mac.h" #include "esp_idf_version.h" -namespace esphome { -namespace ethernet { +namespace esphome::ethernet { #ifdef USE_ETHERNET_IP_STATE_LISTENERS /** Listener interface for Ethernet IP state changes. @@ -218,7 +217,6 @@ extern EthernetComponent *global_eth_component; extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config); #endif -} // namespace ethernet -} // namespace esphome +} // namespace esphome::ethernet #endif // USE_ESP32 From 7d9d90d3f8c4b431b74ad197f9f354f9a4795f59 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 19:50:22 -0600 Subject: [PATCH 175/609] [cse7766] Use C++17 nested namespace syntax (#14247) --- esphome/components/cse7766/cse7766.cpp | 6 ++---- esphome/components/cse7766/cse7766.h | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 7ffdf757a0..806b79e19e 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace cse7766 { +namespace esphome::cse7766 { static const char *const TAG = "cse7766"; @@ -258,5 +257,4 @@ void CSE7766Component::dump_config() { this->check_uart_settings(4800, 1, uart::UART_CONFIG_PARITY_EVEN); } -} // namespace cse7766 -} // namespace esphome +} // namespace esphome::cse7766 diff --git a/esphome/components/cse7766/cse7766.h b/esphome/components/cse7766/cse7766.h index 66a4e04633..77b80dd824 100644 --- a/esphome/components/cse7766/cse7766.h +++ b/esphome/components/cse7766/cse7766.h @@ -5,8 +5,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace cse7766 { +namespace esphome::cse7766 { static constexpr size_t CSE7766_RAW_DATA_SIZE = 24; @@ -49,5 +48,4 @@ class CSE7766Component : public Component, public uart::UARTDevice { uint16_t cf_pulses_last_{0}; }; -} // namespace cse7766 -} // namespace esphome +} // namespace esphome::cse7766 From ad2da0af52469c18b8741a60e6b4aa977415bf8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 20:00:21 -0600 Subject: [PATCH 176/609] [network] Use C++17 nested namespace syntax (#14248) --- esphome/components/network/ip_address.h | 6 ++---- esphome/components/network/util.cpp | 6 ++---- esphome/components/network/util.h | 6 ++---- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index b2a2c563e2..c0e7b2886c 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -37,8 +37,7 @@ using ip4_addr_t = in_addr; #include #endif -namespace esphome { -namespace network { +namespace esphome::network { /// Buffer size for IP address string (IPv6 max: 39 chars + null) static constexpr size_t IP_ADDRESS_BUFFER_SIZE = 40; @@ -187,6 +186,5 @@ struct IPAddress { using IPAddresses = std::array; -} // namespace network -} // namespace esphome +} // namespace esphome::network #endif diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index 5e741fd244..e397d77077 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -17,8 +17,7 @@ #include "esphome/components/modem/modem_component.h" #endif -namespace esphome { -namespace network { +namespace esphome::network { // The order of the components is important: WiFi should come after any possible main interfaces (it may be used as // an AP that use a previous interface for NAT). @@ -109,6 +108,5 @@ const char *get_use_address() { #endif } -} // namespace network -} // namespace esphome +} // namespace esphome::network #endif diff --git a/esphome/components/network/util.h b/esphome/components/network/util.h index 3dc12232aa..ae949ab0a8 100644 --- a/esphome/components/network/util.h +++ b/esphome/components/network/util.h @@ -4,8 +4,7 @@ #include #include "ip_address.h" -namespace esphome { -namespace network { +namespace esphome::network { /// Return whether the node is connected to the network (through wifi, eth, ...) bool is_connected(); @@ -15,6 +14,5 @@ bool is_disabled(); const char *get_use_address(); IPAddresses get_ip_addresses(); -} // namespace network -} // namespace esphome +} // namespace esphome::network #endif From abf7074518379503dec1a19dd46b8019c1d3a789 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Feb 2026 08:27:48 -0500 Subject: [PATCH 177/609] [esp32] Improve ESP32-P4 engineering sample warning message (#14252) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/esp32/__init__.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 62367443da..a34747a183 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -790,8 +790,15 @@ def _detect_variant(value): engineering_sample = value.get(CONF_ENGINEERING_SAMPLE) if engineering_sample is None: _LOGGER.warning( - "No board specified for ESP32-P4. Defaulting to production silicon (rev3). " - "If you have an early engineering sample (pre-rev3), set 'engineering_sample: true'." + "No board specified for ESP32-P4. Defaulting to production silicon (rev3).\n" + "If you have an early engineering sample (pre-rev3), add this to your config:\n" + "\n" + " esp32:\n" + " engineering_sample: true\n" + "\n" + "To check your chip revision, look for 'chip revision: vX.Y' in the boot log.\n" + "Engineering samples will show a revision below v3.0.\n" + "The 'debug:' component also reports the revision (e.g. Revision: 100 = v1.0, 300 = v3.0)." ) elif engineering_sample: value[CONF_BOARD] = "esp32-p4-evboard" From 72263eda855c7ffee631a5281fe4c5c9549f4deb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=2E=20=C3=81rkosi=20R=C3=B3bert?= Date: Tue, 24 Feb 2026 14:31:58 +0100 Subject: [PATCH 178/609] [version] text sensor add option `hide_hash` to restore the pre-2026.1 behavior (#14251) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/version/text_sensor.py | 11 +++++- .../version/version_text_sensor.cpp | 34 ++++++++++++++----- .../components/version/version_text_sensor.h | 2 ++ esphome/const.py | 1 + tests/components/version/common.yaml | 15 +++++++- 5 files changed, 52 insertions(+), 11 deletions(-) diff --git a/esphome/components/version/text_sensor.py b/esphome/components/version/text_sensor.py index ba8c493d4b..a55e18a5a7 100644 --- a/esphome/components/version/text_sensor.py +++ b/esphome/components/version/text_sensor.py @@ -1,7 +1,12 @@ import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv -from esphome.const import CONF_HIDE_TIMESTAMP, ENTITY_CATEGORY_DIAGNOSTIC, ICON_NEW_BOX +from esphome.const import ( + CONF_HIDE_HASH, + CONF_HIDE_TIMESTAMP, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_NEW_BOX, +) version_ns = cg.esphome_ns.namespace("version") VersionTextSensor = version_ns.class_( @@ -16,6 +21,9 @@ CONFIG_SCHEMA = ( .extend( { cv.GenerateID(): cv.declare_id(VersionTextSensor), + # Hide the config hash suffix and restore the pre-2026.1 + # version text format when set to true. + cv.Optional(CONF_HIDE_HASH, default=False): cv.boolean, cv.Optional(CONF_HIDE_TIMESTAMP, default=False): cv.boolean, } ) @@ -26,4 +34,5 @@ CONFIG_SCHEMA = ( async def to_code(config): var = await text_sensor.new_text_sensor(config) await cg.register_component(var, config) + cg.add(var.set_hide_hash(config[CONF_HIDE_HASH])) cg.add(var.set_hide_timestamp(config[CONF_HIDE_TIMESTAMP])) diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp index 4a08001cc4..8aec98d2da 100644 --- a/esphome/components/version/version_text_sensor.cpp +++ b/esphome/components/version/version_text_sensor.cpp @@ -1,37 +1,53 @@ #include "version_text_sensor.h" #include "esphome/core/application.h" #include "esphome/core/build_info_data.h" -#include "esphome/core/log.h" -#include "esphome/core/version.h" #include "esphome/core/helpers.h" +#include "esphome/core/log.h" #include "esphome/core/progmem.h" +#include "esphome/core/version.h" namespace esphome::version { static const char *const TAG = "version.text_sensor"; void VersionTextSensor::setup() { - static const char PREFIX[] PROGMEM = ESPHOME_VERSION " (config hash 0x"; + static const char HASH_PREFIX[] PROGMEM = ESPHOME_VERSION " (config hash 0x"; + static const char VERSION_PREFIX[] PROGMEM = ESPHOME_VERSION; static const char BUILT_STR[] PROGMEM = ", built "; - // Buffer size: PREFIX + 8 hex chars + BUILT_STR + BUILD_TIME_STR_SIZE + ")" + null - constexpr size_t buf_size = sizeof(PREFIX) + 8 + sizeof(BUILT_STR) + esphome::Application::BUILD_TIME_STR_SIZE + 2; + + // Buffer size: HASH_PREFIX + 8 hex chars + BUILT_STR + BUILD_TIME_STR_SIZE + ")" + null + constexpr size_t buf_size = + sizeof(HASH_PREFIX) + 8 + sizeof(BUILT_STR) + esphome::Application::BUILD_TIME_STR_SIZE + 2; char version_str[buf_size]; - ESPHOME_strncpy_P(version_str, PREFIX, sizeof(version_str)); + // hide_hash restores the pre-2026.1 base format by omitting + // the " (config hash 0x...)" suffix entirely. + if (this->hide_hash_) { + ESPHOME_strncpy_P(version_str, VERSION_PREFIX, sizeof(version_str)); + } else { + ESPHOME_strncpy_P(version_str, HASH_PREFIX, sizeof(version_str)); - size_t len = strlen(version_str); - snprintf(version_str + len, sizeof(version_str) - len, "%08" PRIx32, App.get_config_hash()); + size_t len = strlen(version_str); + snprintf(version_str + len, sizeof(version_str) - len, "%08" PRIx32, App.get_config_hash()); + } + // Keep hide_timestamp behavior independent from hide_hash so all + // combinations remain available to users. if (!this->hide_timestamp_) { size_t len = strlen(version_str); ESPHOME_strncat_P(version_str, BUILT_STR, sizeof(version_str) - len - 1); ESPHOME_strncat_P(version_str, ESPHOME_BUILD_TIME_STR, sizeof(version_str) - strlen(version_str) - 1); } - strncat(version_str, ")", sizeof(version_str) - strlen(version_str) - 1); + // The closing parenthesis is part of the config-hash suffix and must + // only be appended when that suffix is present. + if (!this->hide_hash_) { + strncat(version_str, ")", sizeof(version_str) - strlen(version_str) - 1); + } version_str[sizeof(version_str) - 1] = '\0'; this->publish_state(version_str); } +void VersionTextSensor::set_hide_hash(bool hide_hash) { this->hide_hash_ = hide_hash; } void VersionTextSensor::set_hide_timestamp(bool hide_timestamp) { this->hide_timestamp_ = hide_timestamp; } void VersionTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Version Text Sensor", this); } diff --git a/esphome/components/version/version_text_sensor.h b/esphome/components/version/version_text_sensor.h index 6153c5dd7c..fec898ae03 100644 --- a/esphome/components/version/version_text_sensor.h +++ b/esphome/components/version/version_text_sensor.h @@ -7,11 +7,13 @@ namespace esphome::version { class VersionTextSensor : public text_sensor::TextSensor, public Component { public: + void set_hide_hash(bool hide_hash); void set_hide_timestamp(bool hide_timestamp); void setup() override; void dump_config() override; protected: + bool hide_hash_{false}; bool hide_timestamp_{false}; }; diff --git a/esphome/const.py b/esphome/const.py index 0b1037d091..1611aeb101 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -463,6 +463,7 @@ CONF_HEAT_OVERRUN = "heat_overrun" CONF_HEATER = "heater" CONF_HEIGHT = "height" CONF_HIDDEN = "hidden" +CONF_HIDE_HASH = "hide_hash" CONF_HIDE_TIMESTAMP = "hide_timestamp" CONF_HIGH = "high" CONF_HIGH_VOLTAGE_REFERENCE = "high_voltage_reference" diff --git a/tests/components/version/common.yaml b/tests/components/version/common.yaml index 7713afc37c..f9ff778e14 100644 --- a/tests/components/version/common.yaml +++ b/tests/components/version/common.yaml @@ -1,3 +1,16 @@ text_sensor: - platform: version - name: "ESPHome Version" + name: "ESPHome Version Full" + + - platform: version + name: "ESPHome Version No Timestamp" + hide_timestamp: true + + - platform: version + name: "ESPHome Version No Hash" + hide_hash: true + + - platform: version + name: "ESPHome Version Shortest" + hide_timestamp: true + hide_hash: true From 4abbed0cd4ee94845cee7034a1d06d9a279167a3 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 25 Feb 2026 00:33:33 +1100 Subject: [PATCH 179/609] [mipi_dsi] Allow transform disable; fix warnings (#14216) --- esphome/components/mipi_dsi/display.py | 24 +++++++++++-------- esphome/components/mipi_dsi/mipi_dsi.cpp | 4 ++-- esphome/components/mipi_dsi/mipi_dsi.h | 10 ++++---- .../mipi_dsi/fixtures/mipi_dsi.yaml | 17 +++++++++++++ .../mipi_dsi/test_mipi_dsi_config.py | 3 ++- 5 files changed, 40 insertions(+), 18 deletions(-) diff --git a/esphome/components/mipi_dsi/display.py b/esphome/components/mipi_dsi/display.py index de3791b3a4..85bfad7f1a 100644 --- a/esphome/components/mipi_dsi/display.py +++ b/esphome/components/mipi_dsi/display.py @@ -39,6 +39,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_COLOR_ORDER, CONF_DIMENSIONS, + CONF_DISABLED, CONF_ENABLE_PIN, CONF_ID, CONF_INIT_SEQUENCE, @@ -88,14 +89,17 @@ COLOR_DEPTHS = { def model_schema(config): model = MODELS[config[CONF_MODEL].upper()] model.defaults[CONF_SWAP_XY] = cv.UNDEFINED - transform = cv.Schema( - { - cv.Required(CONF_MIRROR_X): cv.boolean, - cv.Required(CONF_MIRROR_Y): cv.boolean, - cv.Optional(CONF_SWAP_XY): cv.invalid( - "Axis swapping not supported by DSI displays" - ), - } + transform = cv.Any( + cv.Schema( + { + cv.Required(CONF_MIRROR_X): cv.boolean, + cv.Required(CONF_MIRROR_Y): cv.boolean, + cv.Optional(CONF_SWAP_XY): cv.invalid( + "Axis swapping not supported by DSI displays" + ), + } + ), + cv.one_of(CONF_DISABLED, lower=True), ) # CUSTOM model will need to provide a custom init sequence iseqconf = ( @@ -199,9 +203,9 @@ async def to_code(config): cg.add(var.set_vsync_pulse_width(config[CONF_VSYNC_PULSE_WIDTH])) cg.add(var.set_vsync_back_porch(config[CONF_VSYNC_BACK_PORCH])) cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH])) - cg.add(var.set_pclk_frequency(int(config[CONF_PCLK_FREQUENCY] / 1e6))) + cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY] / 1.0e6)) cg.add(var.set_lanes(int(config[CONF_LANES]))) - cg.add(var.set_lane_bit_rate(int(config[CONF_LANE_BIT_RATE] / 1e6))) + cg.add(var.set_lane_bit_rate(config[CONF_LANE_BIT_RATE] / 1.0e6)) if reset_pin := config.get(CONF_RESET_PIN): reset = await cg.gpio_pin_expression(reset_pin) cg.add(var.set_reset_pin(reset)) diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index 18cafab684..4d45cfb799 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -374,7 +374,7 @@ void MIPI_DSI::dump_config() { "\n Swap X/Y: %s" "\n Rotation: %d degrees" "\n DSI Lanes: %u" - "\n Lane Bit Rate: %uMbps" + "\n Lane Bit Rate: %.0fMbps" "\n HSync Pulse Width: %u" "\n HSync Back Porch: %u" "\n HSync Front Porch: %u" @@ -385,7 +385,7 @@ void MIPI_DSI::dump_config() { "\n Display Pixel Mode: %d bit" "\n Color Order: %s" "\n Invert Colors: %s" - "\n Pixel Clock: %dMHz", + "\n Pixel Clock: %.1fMHz", this->model_, this->width_, this->height_, YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)), YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY)), YESNO(this->madctl_ & MADCTL_MV), this->rotation_, this->lanes_, this->lane_bit_rate_, this->hsync_pulse_width_, this->hsync_back_porch_, diff --git a/esphome/components/mipi_dsi/mipi_dsi.h b/esphome/components/mipi_dsi/mipi_dsi.h index 1cffe3b178..6e27912aa5 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.h +++ b/esphome/components/mipi_dsi/mipi_dsi.h @@ -47,7 +47,7 @@ class MIPI_DSI : public display::Display { 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_pclk_frequency(uint32_t pclk_frequency) { this->pclk_frequency_ = pclk_frequency; } + void set_pclk_frequency(float pclk_frequency) { this->pclk_frequency_ = pclk_frequency; } int get_width_internal() override { return this->width_; } int get_height_internal() override { return this->height_; } void set_hsync_back_porch(uint16_t hsync_back_porch) { this->hsync_back_porch_ = hsync_back_porch; } @@ -58,7 +58,7 @@ class MIPI_DSI : public display::Display { void set_vsync_front_porch(uint16_t vsync_front_porch) { this->vsync_front_porch_ = vsync_front_porch; } void set_init_sequence(const std::vector &init_sequence) { this->init_sequence_ = init_sequence; } void set_model(const char *model) { this->model_ = model; } - void set_lane_bit_rate(uint16_t lane_bit_rate) { this->lane_bit_rate_ = lane_bit_rate; } + void set_lane_bit_rate(float lane_bit_rate) { this->lane_bit_rate_ = lane_bit_rate; } void set_lanes(uint8_t lanes) { this->lanes_ = lanes; } void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } @@ -95,9 +95,9 @@ class MIPI_DSI : public display::Display { uint16_t vsync_front_porch_ = 10; const char *model_{"Unknown"}; std::vector init_sequence_{}; - uint16_t pclk_frequency_ = 16; // in MHz - uint16_t lane_bit_rate_{1500}; // in Mbps - uint8_t lanes_{2}; // 1, 2, 3 or 4 lanes + float pclk_frequency_ = 16; // in MHz + float lane_bit_rate_{1500}; // in Mbps + uint8_t lanes_{2}; // 1, 2, 3 or 4 lanes bool invert_colors_{}; display::ColorOrder color_mode_{display::COLOR_ORDER_BGR}; diff --git a/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml b/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml index 6de2bd5a77..6f76dcb1d1 100644 --- a/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml +++ b/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml @@ -22,6 +22,23 @@ display: id: p4_86 model: "WAVESHARE-P4-86-PANEL" rotation: 180 + - platform: mipi_dsi + model: custom + id: custom_id + dimensions: + width: 400 + height: 1280 + hsync_back_porch: 40 + hsync_pulse_width: 30 + hsync_front_porch: 40 + vsync_back_porch: 20 + vsync_pulse_width: 10 + vsync_front_porch: 20 + pclk_frequency: 48Mhz + lane_bit_rate: 1.2Gbps + rotation: 180 + transform: disabled + init_sequence: i2c: sda: GPIO7 scl: GPIO8 diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py index d465a8c81b..92f56b5451 100644 --- a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -123,7 +123,8 @@ def test_code_generation( in main_cpp ) assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp - assert "p4_nano->set_lane_bit_rate(1500);" in main_cpp + assert "p4_nano->set_lane_bit_rate(1500.0f);" in main_cpp assert "p4_nano->set_rotation(display::DISPLAY_ROTATION_90_DEGREES);" in main_cpp assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" in main_cpp + assert "custom_id->set_rotation(display::DISPLAY_ROTATION_180_DEGREES);" in main_cpp # assert "backlight_id = new light::LightState(mipi_dsi_dsibacklight_id);" in main_cpp From 6554ad7c7ef933a094b31583bb2dd285831bc8b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Feb 2026 12:08:51 -0600 Subject: [PATCH 180/609] [core] Prevent inlining of mark_matching_items_removed_locked_ on Thumb-1 (#14256) --- esphome/core/scheduler.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 16b0ded312..ed457b87f6 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -496,9 +496,9 @@ class Scheduler { // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id // Returns the number of items marked for removal // IMPORTANT: Must be called with scheduler lock held - size_t mark_matching_items_removed_locked_(std::vector> &container, - Component *component, NameType name_type, const char *static_name, - uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry) { + __attribute__((noinline)) size_t mark_matching_items_removed_locked_( + std::vector> &container, Component *component, NameType name_type, + const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry) { size_t count = 0; for (auto &item : container) { // Skip nullptr items (can happen in defer_queue_ when items are being processed) From fe3c2ba5553f4d07328d1c5b1e927ca0cbea25e1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Feb 2026 14:15:22 -0500 Subject: [PATCH 181/609] [http_request.ota] Percent-encode credentials in URL (#14257) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../http_request/ota/ota_http_request.cpp | 22 +++++++++++++++++++ .../http_request/ota/ota_http_request.h | 4 ++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index d77a768211..0db3a50b47 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -1,5 +1,7 @@ #include "ota_http_request.h" +#include + #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/log.h" @@ -209,6 +211,26 @@ uint8_t OtaHttpRequestComponent::do_ota_() { return ota::OTA_RESPONSE_OK; } +// URL-encode characters that are not unreserved per RFC 3986 section 2.3. +// This is needed for embedding userinfo (username/password) in URLs safely. +static std::string url_encode(const std::string &str) { + std::string result; + result.reserve(str.size()); + for (char c : str) { + if (std::isalnum(static_cast(c)) || c == '-' || c == '_' || c == '.' || c == '~') { + result += c; + } else { + result += '%'; + result += format_hex_pretty_char((static_cast(c) >> 4) & 0x0F); + result += format_hex_pretty_char(static_cast(c) & 0x0F); + } + } + return result; +} + +void OtaHttpRequestComponent::set_password(const std::string &password) { this->password_ = url_encode(password); } +void OtaHttpRequestComponent::set_username(const std::string &username) { this->username_ = url_encode(username); } + std::string OtaHttpRequestComponent::get_url_with_auth_(const std::string &url) { if (this->username_.empty() || this->password_.empty()) { return url; diff --git a/esphome/components/http_request/ota/ota_http_request.h b/esphome/components/http_request/ota/ota_http_request.h index 8735189e99..e3f1a4aa90 100644 --- a/esphome/components/http_request/ota/ota_http_request.h +++ b/esphome/components/http_request/ota/ota_http_request.h @@ -29,9 +29,9 @@ class OtaHttpRequestComponent : public ota::OTAComponent, public Parentedmd5_expected_ = md5; } - void set_password(const std::string &password) { this->password_ = password; } + void set_password(const std::string &password); void set_url(const std::string &url); - void set_username(const std::string &username) { this->username_ = username; } + void set_username(const std::string &username); std::string md5_computed() { return this->md5_computed_; } std::string md5_expected() { return this->md5_expected_; } From af00d601be022c0ba3b5d9656c9906c95b4edb64 Mon Sep 17 00:00:00 2001 From: Andrew Rankin Date: Tue, 24 Feb 2026 16:19:13 -0500 Subject: [PATCH 182/609] [esp32_ble_server] add max_clients option for multi-client support (#14239) --- .../components/esp32_ble_server/__init__.py | 20 +++++++++++++++++++ .../esp32_ble_server/ble_server.cpp | 11 +++++++++- .../components/esp32_ble_server/ble_server.h | 4 ++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index cb494ed1bc..b08e791e7e 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -24,6 +24,7 @@ from esphome.const import ( __version__ as ESPHOME_VERSION, ) from esphome.core import CORE +import esphome.final_validate as fv from esphome.schema_extractors import SCHEMA_EXTRACT AUTO_LOAD = ["esp32_ble", "bytebuffer"] @@ -42,6 +43,7 @@ CONF_FIRMWARE_VERSION = "firmware_version" CONF_INDICATE = "indicate" CONF_MANUFACTURER = "manufacturer" CONF_MANUFACTURER_DATA = "manufacturer_data" +CONF_MAX_CLIENTS = "max_clients" CONF_ON_WRITE = "on_write" CONF_READ = "read" CONF_STRING = "string" @@ -287,6 +289,22 @@ def create_device_information_service(config): def final_validate_config(config): + # Validate max_clients does not exceed esp32_ble max_connections + max_clients = config[CONF_MAX_CLIENTS] + if max_clients > 1: + full_config = fv.full_config.get() + ble_config = full_config.get("esp32_ble", {}) + max_connections = ble_config.get( + "max_connections", esp32_ble.DEFAULT_MAX_CONNECTIONS + ) + if max_clients > max_connections: + raise cv.Invalid( + f"'max_clients' ({max_clients}) cannot exceed esp32_ble " + f"'max_connections' ({max_connections}). " + f"Please set 'max_connections: {max_clients}' in the " + f"'esp32_ble' component." + ) + # Check if all characteristics that require notifications have the notify property set for char_id in CORE.data.get(DOMAIN, {}).get(KEY_NOTIFY_REQUIRED, set()): # Look for the characteristic in the configuration @@ -428,6 +446,7 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_MODEL): value_schema("string", templatable=False), cv.Optional(CONF_FIRMWARE_VERSION): value_schema("string", templatable=False), cv.Optional(CONF_MANUFACTURER_DATA): cv.Schema([cv.uint8_t]), + cv.Optional(CONF_MAX_CLIENTS, default=1): cv.int_range(min=1, max=9), cv.Optional(CONF_SERVICES, default=[]): cv.ensure_list(SERVICE_SCHEMA), cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True), cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True), @@ -552,6 +571,7 @@ async def to_code(config): esp32_ble.register_ble_status_event_handler(parent, var) cg.add(var.set_parent(parent)) cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE])) + cg.add(var.set_max_clients(config[CONF_MAX_CLIENTS])) if CONF_MANUFACTURER_DATA in config: cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA])) for service_config in config[CONF_SERVICES]: diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index 2c13a8ac36..f292cf8722 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -175,6 +175,10 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga case ESP_GATTS_CONNECT_EVT: { ESP_LOGD(TAG, "BLE Client connected"); this->add_client_(param->connect.conn_id); + // Resume advertising so additional clients can discover and connect + if (this->client_count_ < this->max_clients_) { + this->parent_->advertising_start(); + } this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id); break; } @@ -241,7 +245,12 @@ void BLEServer::ble_before_disabled_event_handler() { float BLEServer::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH + 10; } -void BLEServer::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE Server:"); } +void BLEServer::dump_config() { + ESP_LOGCONFIG(TAG, + "ESP32 BLE Server:\n" + " Max clients: %u", + this->max_clients_); +} BLEServer *global_ble_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index 6fa86dd67f..ff7e0044e4 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -39,6 +39,9 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv this->restart_advertising_(); } + void set_max_clients(uint8_t max_clients) { this->max_clients_ = max_clients; } + uint8_t get_max_clients() const { return this->max_clients_; } + BLEService *create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15); void remove_service(ESPBTUUID uuid, uint8_t inst_id = 0); BLEService *get_service(ESPBTUUID uuid, uint8_t inst_id = 0); @@ -95,6 +98,7 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{}; uint8_t client_count_{0}; + uint8_t max_clients_{1}; std::vector services_{}; std::vector services_to_start_{}; BLEService *device_information_service_{}; From cca4777f6458c1bf41e2cdd79f91aafe699e9c08 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Feb 2026 17:51:01 -0600 Subject: [PATCH 183/609] [web_server_idf] Pass std::function by rvalue reference (#14262) --- esphome/components/web_server_idf/multipart.h | 4 ++-- esphome/components/web_server_idf/web_server_idf.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/web_server_idf/multipart.h b/esphome/components/web_server_idf/multipart.h index cb1e0ecd1d..8b94b0491e 100644 --- a/esphome/components/web_server_idf/multipart.h +++ b/esphome/components/web_server_idf/multipart.h @@ -33,8 +33,8 @@ class MultipartReader { ~MultipartReader(); // Set callbacks for handling data - void set_data_callback(DataCallback callback) { data_callback_ = std::move(callback); } - void set_part_complete_callback(PartCompleteCallback callback) { part_complete_callback_ = std::move(callback); } + void set_data_callback(DataCallback &&callback) { data_callback_ = std::move(callback); } + void set_part_complete_callback(PartCompleteCallback &&callback) { part_complete_callback_ = std::move(callback); } // Parse incoming data size_t parse(const char *data, size_t len); diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 76ddfa35fd..9d81d89ec7 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -204,7 +204,7 @@ class AsyncWebServer { ~AsyncWebServer() { this->end(); } // NOLINTNEXTLINE(readability-identifier-naming) - void onNotFound(std::function fn) { on_not_found_ = std::move(fn); } + void onNotFound(std::function &&fn) { on_not_found_ = std::move(fn); } void begin(); void end(); @@ -327,7 +327,7 @@ class AsyncEventSource : public AsyncWebHandler { // NOLINTNEXTLINE(readability-identifier-naming) void handleRequest(AsyncWebServerRequest *request) override; // NOLINTNEXTLINE(readability-identifier-naming) - void onConnect(connect_handler_t cb) { this->on_connect_ = std::move(cb); } + void onConnect(connect_handler_t &&cb) { this->on_connect_ = std::move(cb); } void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator); From 4dc6b12ec51d9e8610ca5be0fc77ebf9c3f39da3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Feb 2026 17:56:43 -0600 Subject: [PATCH 184/609] [api] Pass std::function by rvalue reference in state subscriptions (#14261) --- esphome/components/api/api_server.cpp | 20 ++++++++++---------- esphome/components/api/api_server.h | 20 ++++++++++---------- esphome/components/api/user_services.h | 2 +- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 5b096788f5..0352d7347b 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -433,8 +433,8 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef #ifdef USE_API_HOMEASSISTANT_STATES // Helper to add subscription (reduces duplication) -void APIServer::add_state_subscription_(const char *entity_id, const char *attribute, std::function f, - bool once) { +void APIServer::add_state_subscription_(const char *entity_id, const char *attribute, + std::function &&f, bool once) { this->state_subs_.push_back(HomeAssistantStateSubscription{ .entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once, // entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation) @@ -443,7 +443,7 @@ void APIServer::add_state_subscription_(const char *entity_id, const char *attri // Helper to add subscription with heap-allocated strings (reduces duplication) void APIServer::add_state_subscription_(std::string entity_id, optional attribute, - std::function f, bool once) { + std::function &&f, bool once) { HomeAssistantStateSubscription sub; // Allocate heap storage for the strings sub.entity_id_dynamic_storage = std::make_unique(std::move(entity_id)); @@ -463,29 +463,29 @@ void APIServer::add_state_subscription_(std::string entity_id, optional f) { + std::function &&f) { this->add_state_subscription_(entity_id, attribute, std::move(f), false); } void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute, - std::function f) { + std::function &&f) { this->add_state_subscription_(entity_id, attribute, std::move(f), true); } // std::string overload with StringRef callback (zero-allocation callback) void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, - std::function f) { + std::function &&f) { this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false); } void APIServer::get_home_assistant_state(std::string entity_id, optional attribute, - std::function f) { + std::function &&f) { this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true); } // Legacy helper: wraps std::string callback and delegates to StringRef version void APIServer::add_state_subscription_(std::string entity_id, optional attribute, - std::function f, bool once) { + std::function &&f, bool once) { // Wrap callback to convert StringRef -> std::string, then delegate this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::function([f = std::move(f)](StringRef state) { f(state.str()); }), @@ -494,12 +494,12 @@ void APIServer::add_state_subscription_(std::string entity_id, optional attribute, - std::function f) { + std::function &&f) { this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false); } void APIServer::get_home_assistant_state(std::string entity_id, optional attribute, - std::function f) { + std::function &&f) { this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true); } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index fed29016b3..3abf68358c 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -201,20 +201,20 @@ class APIServer : public Component, }; // New const char* overload (for internal components - zero allocation) - void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function f); - void get_home_assistant_state(const char *entity_id, const char *attribute, std::function f); + void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function &&f); + void get_home_assistant_state(const char *entity_id, const char *attribute, std::function &&f); // std::string overload with StringRef callback (for custom_api_device.h with zero-allocation callback) void subscribe_home_assistant_state(std::string entity_id, optional attribute, - std::function f); + std::function &&f); void get_home_assistant_state(std::string entity_id, optional attribute, - std::function f); + std::function &&f); // Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string for callback) void subscribe_home_assistant_state(std::string entity_id, optional attribute, - std::function f); + std::function &&f); void get_home_assistant_state(std::string entity_id, optional attribute, - std::function f); + std::function &&f); const std::vector &get_state_subs() const; #endif @@ -241,13 +241,13 @@ class APIServer : public Component, #endif // USE_API_NOISE #ifdef USE_API_HOMEASSISTANT_STATES // Helper methods to reduce code duplication - void add_state_subscription_(const char *entity_id, const char *attribute, std::function f, - bool once); - void add_state_subscription_(std::string entity_id, optional attribute, std::function f, + void add_state_subscription_(const char *entity_id, const char *attribute, std::function &&f, bool once); + void add_state_subscription_(std::string entity_id, optional attribute, + std::function &&f, bool once); // Legacy helper: wraps std::string callback and delegates to StringRef version void add_state_subscription_(std::string entity_id, optional attribute, - std::function f, bool once); + std::function &&f, bool once); #endif // USE_API_HOMEASSISTANT_STATES // No explicit close() needed — listen sockets have no active connections on // failure/shutdown. Destructor handles fd cleanup (close or abort per platform). diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 0fc529108c..d1b8a6ef0d 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -230,7 +230,7 @@ template class APIRespondAction : public Action { void set_is_optional_mode(bool is_optional) { this->is_optional_mode_ = is_optional; } #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON - void set_data(std::function func) { + void set_data(std::function &&func) { this->json_builder_ = std::move(func); this->has_data_ = true; } From 08dc487b5b668f1dbf4ec6573aceca2de0d1fdd8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Feb 2026 18:08:07 -0600 Subject: [PATCH 185/609] [core] Pass std::function by rvalue reference in scheduler (#14260) --- esphome/core/scheduler.cpp | 15 ++++++++------- esphome/core/scheduler.h | 18 +++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index e4e0751e10..674d70abcf 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -135,7 +135,7 @@ bool Scheduler::is_retry_cancelled_locked_(Component *component, NameType name_t // name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, NameType name_type, const char *static_name, uint32_t hash_or_id, uint32_t delay, - std::function func, bool is_retry, bool skip_cancel) { + std::function &&func, bool is_retry, bool skip_cancel) { if (delay == SCHEDULER_DONT_RUN) { // Still need to cancel existing timer if we have a name/id if (!skip_cancel) { @@ -216,17 +216,18 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type target->push_back(std::move(item)); } -void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, std::function func) { +void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, + std::function &&func) { this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::STATIC_STRING, name, 0, timeout, std::move(func)); } void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout, - std::function func) { + std::function &&func) { this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::HASHED_STRING, nullptr, fnv1a_hash(name), timeout, std::move(func)); } -void HOT Scheduler::set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function func) { +void HOT Scheduler::set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function &&func) { this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::NUMERIC_ID, nullptr, id, timeout, std::move(func)); } @@ -240,17 +241,17 @@ bool HOT Scheduler::cancel_timeout(Component *component, uint32_t id) { return this->cancel_item_(component, NameType::NUMERIC_ID, nullptr, id, SchedulerItem::TIMEOUT); } void HOT Scheduler::set_interval(Component *component, const std::string &name, uint32_t interval, - std::function func) { + std::function &&func) { this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::HASHED_STRING, nullptr, fnv1a_hash(name), interval, std::move(func)); } void HOT Scheduler::set_interval(Component *component, const char *name, uint32_t interval, - std::function func) { + std::function &&func) { this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::STATIC_STRING, name, 0, interval, std::move(func)); } -void HOT Scheduler::set_interval(Component *component, uint32_t id, uint32_t interval, std::function func) { +void HOT Scheduler::set_interval(Component *component, uint32_t id, uint32_t interval, std::function &&func) { this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::NUMERIC_ID, nullptr, id, interval, std::move(func)); } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index ed457b87f6..b6a8336606 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -33,7 +33,7 @@ class Scheduler { // std::string overload - deprecated, use const char* or uint32_t instead // Remove before 2026.7.0 ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") - void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function func); + void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function &&func); /** Set a timeout with a const char* name. * @@ -43,11 +43,11 @@ class Scheduler { * - A static const char* variable * - A pointer with lifetime >= the scheduled task */ - void set_timeout(Component *component, const char *name, uint32_t timeout, std::function func); + void set_timeout(Component *component, const char *name, uint32_t timeout, std::function &&func); /// Set a timeout with a numeric ID (zero heap allocation) - void set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function func); + void set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function &&func); /// Set a timeout with an internal scheduler ID (separate namespace from component NUMERIC_ID) - void set_timeout(Component *component, InternalSchedulerID id, uint32_t timeout, std::function func) { + void set_timeout(Component *component, InternalSchedulerID id, uint32_t timeout, std::function &&func) { this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::NUMERIC_ID_INTERNAL, nullptr, static_cast(id), timeout, std::move(func)); } @@ -62,7 +62,7 @@ class Scheduler { } ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") - void set_interval(Component *component, const std::string &name, uint32_t interval, std::function func); + void set_interval(Component *component, const std::string &name, uint32_t interval, std::function &&func); /** Set an interval with a const char* name. * @@ -72,11 +72,11 @@ class Scheduler { * - A static const char* variable * - A pointer with lifetime >= the scheduled task */ - void set_interval(Component *component, const char *name, uint32_t interval, std::function func); + void set_interval(Component *component, const char *name, uint32_t interval, std::function &&func); /// Set an interval with a numeric ID (zero heap allocation) - void set_interval(Component *component, uint32_t id, uint32_t interval, std::function func); + void set_interval(Component *component, uint32_t id, uint32_t interval, std::function &&func); /// Set an interval with an internal scheduler ID (separate namespace from component NUMERIC_ID) - void set_interval(Component *component, InternalSchedulerID id, uint32_t interval, std::function func) { + void set_interval(Component *component, InternalSchedulerID id, uint32_t interval, std::function &&func) { this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::NUMERIC_ID_INTERNAL, nullptr, static_cast(id), interval, std::move(func)); } @@ -255,7 +255,7 @@ class Scheduler { // Common implementation for both timeout and interval // name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id void set_timer_common_(Component *component, SchedulerItem::Type type, NameType name_type, const char *static_name, - uint32_t hash_or_id, uint32_t delay, std::function func, bool is_retry = false, + uint32_t hash_or_id, uint32_t delay, std::function &&func, bool is_retry = false, bool skip_cancel = false); // Common implementation for retry - Remove before 2026.8.0 From 2ff876c629a3181e1907fc52594d1257f5165139 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Feb 2026 18:18:44 -0600 Subject: [PATCH 186/609] [core] Use custom deleter for SchedulerItem unique_ptr to prevent destructor inlining (#14258) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/core/scheduler.cpp | 22 ++++++++++------- esphome/core/scheduler.h | 48 ++++++++++++++++++++++++-------------- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 674d70abcf..d9c66b2000 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -33,6 +33,11 @@ static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() // max delay to start an interval sequence static constexpr uint32_t MAX_INTERVAL_DELAY = 5000; +// Prevent inlining of SchedulerItem deletion. On BK7231N (Thumb-1), GCC inlines +// ~unique_ptr (~30 bytes each) at every destruction site. Defining +// the deleter in the .cpp file ensures a single copy of the destructor + operator delete. +void Scheduler::SchedulerItemDeleter::operator()(SchedulerItem *ptr) const noexcept { delete ptr; } + #if defined(ESPHOME_LOG_HAS_VERBOSE) || defined(ESPHOME_DEBUG_SCHEDULER) // Helper struct for formatting scheduler item names consistently in logs // Uses a stack buffer to avoid heap allocation @@ -468,7 +473,7 @@ void HOT Scheduler::call(uint32_t now) { if (now_64 - last_print > 2000) { last_print = now_64; - std::vector> old_items; + std::vector old_items; #ifdef ESPHOME_THREAD_MULTI_ATOMICS const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); @@ -481,7 +486,7 @@ void HOT Scheduler::call(uint32_t now) { // Cleanup before debug output this->cleanup_(); while (!this->items_.empty()) { - std::unique_ptr item; + SchedulerItemPtr item; { LockGuard guard{this->lock_}; item = this->pop_raw_locked_(); @@ -642,7 +647,7 @@ size_t HOT Scheduler::cleanup_() { } return this->items_.size(); } -std::unique_ptr HOT Scheduler::pop_raw_locked_() { +Scheduler::SchedulerItemPtr HOT Scheduler::pop_raw_locked_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); // Move the item out before popping - this is the item that was at the front of the heap @@ -865,8 +870,7 @@ uint64_t Scheduler::millis_64_(uint32_t now) { #endif } -bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr &a, - const std::unique_ptr &b) { +bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) { // High bits are almost always equal (change only on 32-bit rollover ~49 days) // Optimize for common case: check low bits first when high bits are equal return (a->next_execution_high_ == b->next_execution_high_) ? (a->next_execution_low_ > b->next_execution_low_) @@ -877,7 +881,7 @@ bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr &a, // IMPORTANT: Caller must hold the scheduler lock before calling this function. // This protects scheduler_item_pool_ from concurrent access by other threads // that may be acquiring items from the pool in set_timer_common_(). -void Scheduler::recycle_item_main_loop_(std::unique_ptr item) { +void Scheduler::recycle_item_main_loop_(SchedulerItemPtr item) { if (!item) return; @@ -920,8 +924,8 @@ void Scheduler::debug_log_timer_(const SchedulerItem *item, NameType name_type, // Helper to get or create a scheduler item from the pool // IMPORTANT: Caller must hold the scheduler lock before calling this function. -std::unique_ptr Scheduler::get_item_from_pool_locked_() { - std::unique_ptr item; +Scheduler::SchedulerItemPtr Scheduler::get_item_from_pool_locked_() { + SchedulerItemPtr item; if (!this->scheduler_item_pool_.empty()) { item = std::move(this->scheduler_item_pool_.back()); this->scheduler_item_pool_.pop_back(); @@ -929,7 +933,7 @@ std::unique_ptr Scheduler::get_item_from_pool_locked_( ESP_LOGD(TAG, "Reused item from pool (pool size now: %zu)", this->scheduler_item_pool_.size()); #endif } else { - item = make_unique(); + item = SchedulerItemPtr(new SchedulerItem()); #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Allocated new item (pool empty)"); #endif diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index b6a8336606..840ee7159a 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -142,6 +142,19 @@ class Scheduler { }; protected: + struct SchedulerItem; + + // Custom deleter for SchedulerItem unique_ptr that prevents the compiler from + // inlining the destructor at every destruction site. On BK7231N (Thumb-1), GCC + // inlines ~unique_ptr (~30 bytes: null check + ~std::function + + // operator delete) at every destruction site, while ESP32/ESP8266/RTL8720CF outline + // it into a single helper. This noinline deleter ensures only one copy exists. + // operator() is defined in scheduler.cpp to prevent inlining. + struct SchedulerItemDeleter { + void operator()(SchedulerItem *ptr) const noexcept; + }; + using SchedulerItemPtr = std::unique_ptr; + struct SchedulerItem { // Ordered by size to minimize padding Component *component; @@ -233,7 +246,7 @@ class Scheduler { name_type_ = type; } - static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b); + static bool cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b); // Note: We use 48 bits total (32 + 16), stored in a 64-bit value for API compatibility. // The upper 16 bits of the 64-bit value are always zero, which is fine since @@ -276,10 +289,10 @@ class Scheduler { size_t cleanup_(); // Remove and return the front item from the heap // IMPORTANT: Caller must hold the scheduler lock before calling this function. - std::unique_ptr pop_raw_locked_(); + SchedulerItemPtr pop_raw_locked_(); // Get or create a scheduler item from the pool // IMPORTANT: Caller must hold the scheduler lock before calling this function. - std::unique_ptr get_item_from_pool_locked_(); + SchedulerItemPtr get_item_from_pool_locked_(); private: // Helper to cancel items - must be called with lock held @@ -303,9 +316,9 @@ class Scheduler { // Helper function to check if item matches criteria for cancellation // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id // IMPORTANT: Must be called with scheduler lock held - inline bool HOT matches_item_locked_(const std::unique_ptr &item, Component *component, - NameType name_type, const char *static_name, uint32_t hash_or_id, - SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const { + inline bool HOT matches_item_locked_(const SchedulerItemPtr &item, Component *component, NameType name_type, + const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type, + bool match_retry, bool skip_removed = true) const { // THREAD SAFETY: Check for nullptr first to prevent LoadProhibited crashes. On multi-threaded // platforms, items can be moved out of defer_queue_ during processing, leaving nullptr entries. // PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_()), but this check @@ -340,7 +353,7 @@ class Scheduler { // IMPORTANT: Only call from main loop context! Recycling clears the callback, // so calling from another thread while the callback is executing causes use-after-free. // IMPORTANT: Caller must hold the scheduler lock before calling this function. - void recycle_item_main_loop_(std::unique_ptr item); + void recycle_item_main_loop_(SchedulerItemPtr item); // Helper to perform full cleanup when too many items are cancelled void full_cleanup_removed_items_(); @@ -396,7 +409,7 @@ class Scheduler { // Merge lock acquisitions: instead of separate locks for move-out and recycle (2N+1 total), // recycle each item after re-acquiring the lock for the next iteration (N+1 total). // The lock is held across: recycle → loop condition → move-out, then released for execution. - std::unique_ptr item; + SchedulerItemPtr item; this->lock_.lock(); while (this->defer_queue_front_ < defer_queue_end) { @@ -496,9 +509,10 @@ class Scheduler { // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id // Returns the number of items marked for removal // IMPORTANT: Must be called with scheduler lock held - __attribute__((noinline)) size_t mark_matching_items_removed_locked_( - std::vector> &container, Component *component, NameType name_type, - const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry) { + __attribute__((noinline)) size_t mark_matching_items_removed_locked_(std::vector &container, + Component *component, NameType name_type, + const char *static_name, uint32_t hash_or_id, + SchedulerItem::Type type, bool match_retry) { size_t count = 0; for (auto &item : container) { // Skip nullptr items (can happen in defer_queue_ when items are being processed) @@ -514,15 +528,15 @@ class Scheduler { } Mutex lock_; - std::vector> items_; - std::vector> to_add_; + std::vector items_; + std::vector to_add_; #ifndef ESPHOME_THREAD_SINGLE // Single-core platforms don't need the defer queue and save ~32 bytes of RAM // Using std::vector instead of std::deque avoids 512-byte chunked allocations // Index tracking avoids O(n) erase() calls when draining the queue each loop - std::vector> defer_queue_; // FIFO queue for defer() calls - size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items) -#endif /* ESPHOME_THREAD_SINGLE */ + std::vector defer_queue_; // FIFO queue for defer() calls + size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items) +#endif /* ESPHOME_THREAD_SINGLE */ uint32_t to_remove_{0}; // Memory pool for recycling SchedulerItem objects to reduce heap churn. @@ -533,7 +547,7 @@ class Scheduler { // - The pool significantly reduces heap fragmentation which is critical because heap allocation/deallocation // can stall the entire system, causing timing issues and dropped events for any components that need // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52) - std::vector> scheduler_item_pool_; + std::vector scheduler_item_pool_; #ifdef ESPHOME_THREAD_MULTI_ATOMICS /* From 3460a8c9225f504fdfdd9166d603fe2c3fb41474 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Feb 2026 18:44:50 -0600 Subject: [PATCH 187/609] [dlms_meter/kamstrup_kmp] Replace powf with pow10_int (#14125) Co-authored-by: Claude Opus 4.6 --- esphome/components/dlms_meter/dlms_meter.cpp | 4 +--- esphome/components/kamstrup_kmp/kamstrup_kmp.cpp | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/esphome/components/dlms_meter/dlms_meter.cpp b/esphome/components/dlms_meter/dlms_meter.cpp index 11d05b3a08..bd2150e8dd 100644 --- a/esphome/components/dlms_meter/dlms_meter.cpp +++ b/esphome/components/dlms_meter/dlms_meter.cpp @@ -1,7 +1,5 @@ #include "dlms_meter.h" -#include - #if defined(USE_ESP8266_FRAMEWORK_ARDUINO) #include #elif defined(USE_ESP32) @@ -410,7 +408,7 @@ void DlmsMeterComponent::decode_obis_(uint8_t *plaintext, uint16_t message_lengt if (current_position + 1 < message_length) { int8_t scaler = static_cast(plaintext[current_position + 1]); if (scaler != 0) { - value *= powf(10.0f, scaler); + value *= pow10_int(scaler); } } diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp index 534939f9af..00c65a1937 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp @@ -222,11 +222,11 @@ void KamstrupKMPComponent::parse_command_message_(uint16_t command, const uint8_ } // Calculate exponent - float exponent = msg[6] & 0x3F; + int8_t exp_val = msg[6] & 0x3F; if (msg[6] & 0x40) { - exponent = -exponent; + exp_val = -exp_val; } - exponent = powf(10, exponent); + float exponent = pow10_int(exp_val); if (msg[6] & 0x80) { exponent = -exponent; } From 905e81330ee3af27a82f2f6cf3675497bd6fcc70 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:28:19 +1300 Subject: [PATCH 188/609] Don't get stuck forever on a failed component can_proceed (#14267) --- esphome/core/application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 1cb7dc0075..6a7683a987 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -134,7 +134,7 @@ void Application::setup() { this->after_loop_tasks_(); this->app_state_ = new_app_state; yield(); - } while (!component->can_proceed()); + } while (!component->can_proceed() && !component->is_failed()); } ESP_LOGI(TAG, "setup() finished successfully!"); From 1dac501b049ffea5a1767df5dfbd429d768c0af2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Feb 2026 21:39:51 -0600 Subject: [PATCH 189/609] [light] Add additional light effect test cases (#14266) --- tests/components/light/common.yaml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/components/light/common.yaml b/tests/components/light/common.yaml index 55525fc67f..e5fab62a79 100644 --- a/tests/components/light/common.yaml +++ b/tests/components/light/common.yaml @@ -71,6 +71,32 @@ esphome: - light.control: id: test_monochromatic_light state: on + # Test static effect name resolution at codegen time + - light.turn_on: + id: test_monochromatic_light + effect: Strobe + - light.turn_on: + id: test_monochromatic_light + effect: none + # Test resolving a different effect on the same light + - light.control: + id: test_monochromatic_light + effect: My Flicker + # Test effect: None (capitalized) + - light.control: + id: test_monochromatic_light + effect: None + # Test effect lambda with no args (on_boot has empty Ts...) + - light.turn_on: + id: test_monochromatic_light + effect: !lambda 'return "Strobe";' + # Test effect lambda with non-empty args (repeat passes uint32_t iteration) + - repeat: + count: 3 + then: + - light.turn_on: + id: test_monochromatic_light + effect: !lambda 'return iteration > 1 ? "Strobe" : "none";' - light.dim_relative: id: test_monochromatic_light relative_brightness: 5% From 2e705a919fc8a4e9990e4fc5870e4677a05f6e46 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:26:00 -0500 Subject: [PATCH 190/609] [pid] Fix deadband threshold conversion for Fahrenheit (#14268) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/pid/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py index 5919d2cac8..5fa3166f9d 100644 --- a/esphome/components/pid/climate.py +++ b/esphome/components/pid/climate.py @@ -50,8 +50,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_HEAT_OUTPUT): cv.use_id(output.FloatOutput), cv.Optional(CONF_DEADBAND_PARAMETERS): cv.Schema( { - cv.Required(CONF_THRESHOLD_HIGH): cv.temperature, - cv.Required(CONF_THRESHOLD_LOW): cv.temperature, + cv.Required(CONF_THRESHOLD_HIGH): cv.temperature_delta, + cv.Required(CONF_THRESHOLD_LOW): cv.temperature_delta, cv.Optional(CONF_KP_MULTIPLIER, default=0.1): cv.float_, cv.Optional(CONF_KI_MULTIPLIER, default=0.0): cv.float_, cv.Optional(CONF_KD_MULTIPLIER, default=0.0): cv.float_, From b134c4679ca5f609633a2b97681a41867e62c12d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Feb 2026 22:33:57 -0600 Subject: [PATCH 191/609] [light] Replace std::lerp with lightweight lerp_fast in LightColorValues::lerp (#14238) --- .../components/light/light_color_values.cpp | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/esphome/components/light/light_color_values.cpp b/esphome/components/light/light_color_values.cpp index 2f22bb3c68..e54cc0a12a 100644 --- a/esphome/components/light/light_color_values.cpp +++ b/esphome/components/light/light_color_values.cpp @@ -1,27 +1,30 @@ #include "light_color_values.h" -#include - namespace esphome::light { +// Lightweight lerp: a + t * (b - a). +// Avoids std::lerp's NaN/infinity handling which Clang doesn't optimize out, +// adding ~200 bytes per call. Safe because all values are finite floats. +static float __attribute__((noinline)) lerp_fast(float a, float b, float t) { return a + t * (b - a); } + LightColorValues LightColorValues::lerp(const LightColorValues &start, const LightColorValues &end, float completion) { // Directly interpolate the raw values to avoid getter/setter overhead. // This is safe because: - // - All LightColorValues have their values clamped when set via the setters - // - std::lerp guarantees output is in the same range as inputs + // - All LightColorValues except color_temperature_ have their values clamped when set via the setters + // - lerp_fast output stays in range when inputs are in range and 0 <= completion <= 1 // - Therefore the output doesn't need clamping, so we can skip the setters LightColorValues v; v.color_mode_ = end.color_mode_; - v.state_ = std::lerp(start.state_, end.state_, completion); - v.brightness_ = std::lerp(start.brightness_, end.brightness_, completion); - v.color_brightness_ = std::lerp(start.color_brightness_, end.color_brightness_, completion); - v.red_ = std::lerp(start.red_, end.red_, completion); - v.green_ = std::lerp(start.green_, end.green_, completion); - v.blue_ = std::lerp(start.blue_, end.blue_, completion); - v.white_ = std::lerp(start.white_, end.white_, completion); - v.color_temperature_ = std::lerp(start.color_temperature_, end.color_temperature_, completion); - v.cold_white_ = std::lerp(start.cold_white_, end.cold_white_, completion); - v.warm_white_ = std::lerp(start.warm_white_, end.warm_white_, completion); + v.state_ = lerp_fast(start.state_, end.state_, completion); + v.brightness_ = lerp_fast(start.brightness_, end.brightness_, completion); + v.color_brightness_ = lerp_fast(start.color_brightness_, end.color_brightness_, completion); + v.red_ = lerp_fast(start.red_, end.red_, completion); + v.green_ = lerp_fast(start.green_, end.green_, completion); + v.blue_ = lerp_fast(start.blue_, end.blue_, completion); + v.white_ = lerp_fast(start.white_, end.white_, completion); + v.color_temperature_ = lerp_fast(start.color_temperature_, end.color_temperature_, completion); + v.cold_white_ = lerp_fast(start.cold_white_, end.cold_white_, completion); + v.warm_white_ = lerp_fast(start.warm_white_, end.warm_white_, completion); return v; } From bb05cfb7119102b877ad583d9d9091ff57663a44 Mon Sep 17 00:00:00 2001 From: Big Mike Date: Wed, 25 Feb 2026 06:34:58 -0600 Subject: [PATCH 192/609] [sensirion_common] Move sen5x's sensirion_convert_to_string_in_place() function to sensirion_common (#14269) --- esphome/components/sen5x/sen5x.cpp | 9 --------- esphome/components/sensirion_common/i2c_sensirion.h | 11 +++++++++++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index fc4932d867..09dda8bca4 100644 --- a/esphome/components/sen5x/sen5x.cpp +++ b/esphome/components/sen5x/sen5x.cpp @@ -56,15 +56,6 @@ static const LogString *rht_accel_mode_to_string(RhtAccelerationMode mode) { } } -// This function performs an in-place conversion of the provided buffer -// from uint16_t values to big endianness -static inline const char *sensirion_convert_to_string_in_place(uint16_t *array, size_t length) { - for (size_t i = 0; i < length; i++) { - array[i] = convert_big_endian(array[i]); - } - return reinterpret_cast(array); -} - void SEN5XComponent::setup() { // the sensor needs 1000 ms to enter the idle state this->set_timeout(1000, [this]() { diff --git a/esphome/components/sensirion_common/i2c_sensirion.h b/esphome/components/sensirion_common/i2c_sensirion.h index f3eb3761f6..3c2c14ccb8 100644 --- a/esphome/components/sensirion_common/i2c_sensirion.h +++ b/esphome/components/sensirion_common/i2c_sensirion.h @@ -21,6 +21,17 @@ class SensirionI2CDevice : public i2c::I2CDevice { public: enum CommandLen : uint8_t { ADDR_8_BIT = 1, ADDR_16_BIT = 2 }; + /** + * This function performs an in-place conversion of the provided buffer + * from uint16_t values to big endianness. Useful for Sensirion strings in SEN5X and SEN6X + */ + static inline const char *sensirion_convert_to_string_in_place(uint16_t *array, size_t length) { + for (size_t i = 0; i < length; i++) { + array[i] = convert_big_endian(array[i]); + } + return reinterpret_cast(array); + } + /** Read data words from I2C device. * handles CRC check used by Sensirion sensors * @param data pointer to raw result From 228874a52b268070199dc37f4622cca1361e729b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:45:50 -0500 Subject: [PATCH 193/609] [config] Improve dimensions validation and fix online_image resize aspect ratio (#14274) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/runtime_image/runtime_image.cpp | 9 +++++++++ esphome/config_validation.py | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/esphome/components/runtime_image/runtime_image.cpp b/esphome/components/runtime_image/runtime_image.cpp index 603ea76f01..5a4a2ea7d6 100644 --- a/esphome/components/runtime_image/runtime_image.cpp +++ b/esphome/components/runtime_image/runtime_image.cpp @@ -2,6 +2,7 @@ #include "image_decoder.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include #include #ifdef USE_RUNTIME_IMAGE_BMP @@ -43,6 +44,14 @@ int RuntimeImage::resize(int width, int height) { int target_width = this->fixed_width_ ? this->fixed_width_ : width; int target_height = this->fixed_height_ ? this->fixed_height_ : height; + // When both fixed dimensions are set, scale uniformly to preserve aspect ratio + if (this->fixed_width_ && this->fixed_height_ && width > 0 && height > 0) { + float scale = + std::min(static_cast(this->fixed_width_) / width, static_cast(this->fixed_height_) / height); + target_width = static_cast(width * scale); + target_height = static_cast(height * scale); + } + size_t result = this->resize_buffer_(target_width, target_height); if (result > 0 && this->progressive_display_) { // Update display dimensions for progressive display diff --git a/esphome/config_validation.py b/esphome/config_validation.py index ef1c66a20e..3b0e4da298 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1640,7 +1640,10 @@ def dimensions(value): if width <= 0 or height <= 0: raise Invalid("Width and height must at least be 1") return [width, height] - value = string(value) + if not isinstance(value, str): + raise Invalid( + "Dimensions must be a string (WIDTHxHEIGHT). Got a number instead, try quoting the value." + ) match = re.match(r"\s*([0-9]+)\s*[xX]\s*([0-9]+)\s*", value) if not match: raise Invalid( From 1beeb9ab5c34b1b28ba3c8dc8e78cc12be511dc2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Feb 2026 09:54:32 -0600 Subject: [PATCH 194/609] [web_server] Fix uptime display overflow after ~24.8 days (#13739) --- esphome/components/web_server/web_server.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 682008c40e..e2f9c21331 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -385,6 +385,7 @@ json::SerializationBuffer<> WebServer::get_config_json() { #endif root[ESPHOME_F("log")] = this->expose_log_; root[ESPHOME_F("lang")] = "en"; + root[ESPHOME_F("uptime")] = static_cast(App.scheduler.millis_64() / 1000); return builder.serialize(); } @@ -411,7 +412,12 @@ void WebServer::setup() { // doesn't need defer functionality - if the queue is full, the client JS knows it's alive because it's clearly // getting a lot of events - this->set_interval(10000, [this]() { this->events_.try_send_nodefer("", "ping", millis(), 30000); }); + this->set_interval(10000, [this]() { + char buf[32]; + auto uptime = static_cast(App.scheduler.millis_64() / 1000); + buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%u}", uptime); + this->events_.try_send_nodefer(buf, "ping", millis(), 30000); + }); } void WebServer::loop() { this->events_.loop(); } From 78ab63581b48575d04448e49d1ecc298627bce1a Mon Sep 17 00:00:00 2001 From: esphomebot Date: Thu, 26 Feb 2026 05:09:45 +1300 Subject: [PATCH 195/609] Update webserver local assets to 20260225-155043 (#14275) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../components/captive_portal/captive_index.h | 4 +- .../components/web_server/server_index_v2.h | 2286 ++--- .../components/web_server/server_index_v3.h | 8037 +++++++++-------- 3 files changed, 5166 insertions(+), 5161 deletions(-) diff --git a/esphome/components/captive_portal/captive_index.h b/esphome/components/captive_portal/captive_index.h index a81edc1900..645ebb7a2f 100644 --- a/esphome/components/captive_portal/captive_index.h +++ b/esphome/components/captive_portal/captive_index.h @@ -6,7 +6,7 @@ namespace esphome::captive_portal { #ifdef USE_CAPTIVE_PORTAL_GZIP -constexpr uint8_t INDEX_GZ[] PROGMEM = { +const uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7e, 0x05, 0x8f, 0x49, 0xbb, 0x52, 0xb3, 0x7a, 0x7a, 0xed, 0x6c, 0x24, 0x51, 0x45, 0x9a, 0xbb, 0xa2, 0x05, 0x9a, 0x36, 0xc0, 0x6e, 0x73, 0x1f, 0x82, 0x00, 0x4b, 0x53, 0x23, 0x8b, 0x31, 0x45, 0xea, 0x48, 0xca, 0x8f, 0x18, 0xbe, 0xdf, @@ -86,7 +86,7 @@ constexpr uint8_t INDEX_GZ[] PROGMEM = { 0xfc, 0xda, 0xd1, 0xf8, 0xe9, 0xa3, 0xe1, 0xa6, 0xfb, 0x1f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00}; #else // Brotli (default, smaller) -constexpr uint8_t INDEX_BR[] PROGMEM = { +const uint8_t INDEX_BR[] PROGMEM = { 0x1b, 0xf8, 0x0a, 0x00, 0x64, 0x5a, 0xd3, 0xfa, 0xe7, 0xf3, 0x62, 0xd8, 0x06, 0x1b, 0xe9, 0x6a, 0x8a, 0x81, 0x2b, 0xb5, 0x49, 0x14, 0x37, 0xdc, 0x9e, 0x1a, 0xcb, 0x56, 0x87, 0xfb, 0xff, 0xf7, 0x73, 0x75, 0x12, 0x0a, 0xd6, 0x48, 0x84, 0xc6, 0x21, 0xa4, 0x6d, 0xb5, 0x71, 0xef, 0x13, 0xbe, 0x4e, 0x54, 0xf1, 0x64, 0x8f, 0x3f, 0xcc, 0x9a, 0x78, diff --git a/esphome/components/web_server/server_index_v2.h b/esphome/components/web_server/server_index_v2.h index b5dac9ae4c..ffa9c87b3a 100644 --- a/esphome/components/web_server/server_index_v2.h +++ b/esphome/components/web_server/server_index_v2.h @@ -9,7 +9,7 @@ namespace esphome::web_server { #ifdef USE_WEBSERVER_GZIP -constexpr uint8_t INDEX_GZ[] PROGMEM = { +const uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xed, 0x7d, 0xd9, 0x72, 0xdb, 0x48, 0xb6, 0xe0, 0xf3, 0xd4, 0x57, 0x40, 0x28, 0xb5, 0x8c, 0x2c, 0x26, 0xc1, 0x45, 0x92, 0x2d, 0x83, 0x4a, 0xb2, 0x65, 0xd9, 0xd5, 0x76, 0x97, 0xb7, 0xb6, 0xec, 0xda, 0x58, 0x6c, 0x09, 0x02, 0x92, 0x44, 0x96, 0x41, 0x80, 0x05, 0x24, 0xb5, 0x14, 0x89, @@ -41,10 +41,10 @@ constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x90, 0xe0, 0xbe, 0xe3, 0x24, 0x21, 0x7d, 0x18, 0xf1, 0x84, 0x3b, 0xb0, 0x8a, 0xe9, 0xd8, 0x4a, 0x08, 0xb1, 0x73, 0xd1, 0xd6, 0x1e, 0x24, 0x5e, 0xd2, 0xb0, 0x6d, 0x2c, 0xa1, 0xc4, 0x09, 0x47, 0xf8, 0x23, 0x71, 0x12, 0xec, 0xba, 0x2e, 0x47, 0xa4, 0xbf, 0xd0, 0x58, 0x49, 0x8c, 0x79, 0x0e, 0x92, 0x61, 0x7b, 0xe4, 0x71, 0x37, 0xa3, 0xe1, 0x3c, - 0xa0, 0x8e, 0xc3, 0x70, 0x8e, 0x53, 0x44, 0xfa, 0xac, 0xe1, 0x64, 0xa4, 0x0f, 0xcb, 0x9d, 0xd5, 0xd7, 0x9a, 0x90, - 0x9d, 0x36, 0x52, 0x30, 0x66, 0x1a, 0x40, 0xc0, 0xb0, 0x82, 0x27, 0x23, 0xc4, 0x4e, 0xe6, 0xd3, 0x4b, 0x9a, 0xd9, + 0xa0, 0x8e, 0xc3, 0x70, 0x8e, 0x33, 0x44, 0xfa, 0xac, 0xe1, 0xa4, 0xa4, 0x0f, 0xcb, 0x9d, 0xd6, 0xd7, 0x9a, 0x90, + 0x9d, 0x36, 0x52, 0x30, 0xa6, 0x1a, 0x40, 0xc0, 0xb0, 0x82, 0x27, 0x25, 0xc4, 0x4e, 0xe6, 0xd3, 0x4b, 0x9a, 0xd9, 0x65, 0xb5, 0x5e, 0x8d, 0x2c, 0xe6, 0x39, 0xb5, 0x82, 0x3c, 0xb7, 0xc6, 0xf3, 0x24, 0xe0, 0x2c, 0x4d, 0x2c, 0xbb, - 0x91, 0x35, 0x6c, 0x49, 0x0e, 0x25, 0x35, 0xd8, 0xa8, 0x40, 0x4e, 0x8e, 0x1a, 0xc9, 0x30, 0x6d, 0x74, 0x46, 0x18, + 0x91, 0x36, 0x6c, 0x49, 0x0e, 0x25, 0x35, 0xd8, 0xa8, 0x40, 0x4e, 0x8e, 0x1a, 0xc9, 0x30, 0x6b, 0x74, 0x46, 0x18, 0xa0, 0x44, 0x3d, 0xd5, 0x9f, 0x42, 0x00, 0xc5, 0x09, 0xcc, 0xb1, 0xc0, 0x4f, 0x38, 0xcc, 0x52, 0x4c, 0x31, 0xe7, 0x83, 0xc4, 0x5d, 0xdf, 0x28, 0x84, 0xbb, 0x53, 0x7f, 0xe6, 0x50, 0xd2, 0xa7, 0x82, 0xb8, 0xfc, 0x24, 0x00, 0x58, 0x6b, 0xeb, 0x36, 0xa0, 0x1e, 0x75, 0x2b, 0x92, 0x42, 0x1e, 0x77, 0xc7, 0x69, 0xf6, 0xcc, 0x0f, 0x22, 0x68, 0x57, @@ -85,8 +85,8 @@ constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x12, 0x0d, 0x1c, 0x44, 0x95, 0xd0, 0x39, 0xec, 0x81, 0xd6, 0x3d, 0x3c, 0xfb, 0xfc, 0xdc, 0x6e, 0x70, 0xac, 0xd0, 0x30, 0xa1, 0x7a, 0xe8, 0xdb, 0xa7, 0x54, 0x6a, 0x57, 0x42, 0xf7, 0x58, 0xc3, 0x8c, 0xdc, 0x41, 0x6e, 0x48, 0xc7, 0x2c, 0x31, 0xa6, 0x5d, 0x03, 0x09, 0x73, 0x9c, 0xa3, 0xc2, 0x58, 0xd0, 0x8d, 0x5d, 0x0b, 0xb5, 0x46, 0xae, 0xdc, - 0x62, 0x22, 0x54, 0x09, 0x63, 0x19, 0x87, 0x74, 0x54, 0x60, 0x81, 0x7a, 0x3d, 0x9b, 0x54, 0x00, 0x3a, 0xe4, 0xa3, - 0x9e, 0x7a, 0x4f, 0x72, 0x89, 0xb9, 0x8c, 0xfe, 0x32, 0xa7, 0x39, 0x97, 0x74, 0xec, 0x70, 0x9c, 0x62, 0x06, 0xfc, + 0x62, 0x22, 0x54, 0x09, 0x63, 0x19, 0x87, 0x74, 0x54, 0x60, 0x81, 0x7a, 0x3d, 0x9b, 0x4c, 0x00, 0x3a, 0xe4, 0xa3, + 0x9e, 0x7a, 0x4f, 0x72, 0x89, 0xb9, 0x8c, 0xfe, 0x32, 0xa7, 0x39, 0x97, 0x74, 0xec, 0x70, 0x9c, 0x61, 0x06, 0xfc, 0x3a, 0x4d, 0xc6, 0x6c, 0x32, 0xcf, 0x40, 0xe3, 0x81, 0xcd, 0x48, 0x93, 0xf9, 0x94, 0xea, 0xa7, 0x4d, 0xb0, 0xbd, 0x99, 0x81, 0x4c, 0xcc, 0x81, 0xa6, 0xef, 0x26, 0x27, 0x80, 0x95, 0xa3, 0xe5, 0xf2, 0xaf, 0xba, 0x93, 0x6a, 0x29, 0x4b, 0x2d, 0x6d, 0x65, 0x4d, 0x28, 0x47, 0x4a, 0x26, 0xef, 0x74, 0x14, 0xf8, 0x7c, 0x44, 0x76, 0xda, 0x25, 0x0d, @@ -120,15 +120,15 @@ constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x11, 0x53, 0x33, 0x73, 0x80, 0x0b, 0x7c, 0x92, 0xe2, 0x34, 0x3f, 0x50, 0x74, 0x07, 0x06, 0x47, 0xb1, 0x02, 0x08, 0x47, 0x8b, 0x22, 0x64, 0xf9, 0x76, 0x0c, 0xfc, 0x21, 0x50, 0x3e, 0x35, 0x46, 0xb8, 0x2f, 0xa0, 0x25, 0x8f, 0x53, 0x5a, 0x73, 0x09, 0x99, 0xd2, 0x27, 0x34, 0xa3, 0xf9, 0x06, 0x74, 0x17, 0x41, 0xef, 0x6f, 0xe4, 0x2b, 0xd0, 0xca, - 0x00, 0x8a, 0xbc, 0x67, 0xaa, 0x13, 0x35, 0x0a, 0x50, 0x3c, 0x95, 0x09, 0x91, 0x9b, 0xd6, 0x2c, 0x48, 0xa5, 0xb1, - 0x4b, 0x23, 0x5c, 0xb1, 0xdc, 0x8c, 0x38, 0x8e, 0x93, 0x83, 0x11, 0xa7, 0x75, 0xfb, 0x6a, 0x12, 0xf9, 0xda, 0x24, + 0x00, 0x8a, 0xbc, 0x67, 0xaa, 0x13, 0x35, 0x0a, 0x50, 0x3c, 0x95, 0x09, 0x91, 0x9b, 0xd5, 0x2c, 0x48, 0xa5, 0xb1, + 0x4b, 0x23, 0x5c, 0xb1, 0xdc, 0x94, 0x38, 0x8e, 0x93, 0x83, 0x11, 0xa7, 0x75, 0xfb, 0x6a, 0x12, 0xf9, 0xda, 0x24, 0x72, 0xd7, 0x30, 0xb4, 0x50, 0x45, 0xcb, 0x46, 0x73, 0x8f, 0x73, 0x64, 0xd6, 0x02, 0x7d, 0xd5, 0x05, 0x06, 0x8d, - 0x4a, 0x7e, 0x1b, 0x13, 0x8e, 0x33, 0x65, 0xe5, 0x28, 0x52, 0x03, 0x8e, 0x51, 0x35, 0x49, 0x91, 0xdc, 0x1b, 0x35, - 0x93, 0x37, 0xc5, 0x19, 0x5a, 0x51, 0xee, 0x8b, 0x42, 0x21, 0x89, 0x22, 0xb5, 0x38, 0x35, 0xad, 0xd8, 0x40, 0x0b, - 0x4e, 0x89, 0xd2, 0x84, 0xa5, 0xe2, 0xb3, 0x8a, 0x9c, 0xb2, 0xdf, 0x1d, 0x42, 0xd2, 0x0a, 0x37, 0x35, 0x95, 0x52, - 0xeb, 0x56, 0x29, 0xc2, 0x91, 0x56, 0x4a, 0xb3, 0x6a, 0xe2, 0x84, 0xd8, 0xda, 0x27, 0x61, 0x0f, 0x16, 0x35, 0xbb, - 0xd0, 0x33, 0xaa, 0x15, 0x1e, 0xf0, 0xd4, 0x6c, 0x13, 0xbe, 0x37, 0x11, 0x4d, 0xad, 0x1f, 0x03, 0xe3, 0x59, 0x0d, - 0xe3, 0x06, 0x6a, 0x53, 0xc9, 0xbb, 0xd2, 0x11, 0x89, 0xea, 0x8d, 0x1d, 0x8a, 0x33, 0xb9, 0x10, 0x6b, 0x58, 0x5c, + 0x4a, 0x7e, 0x1b, 0x13, 0x8e, 0x53, 0x65, 0xe5, 0x28, 0x52, 0x03, 0x8e, 0x51, 0x35, 0xc9, 0x90, 0xdc, 0x1b, 0x35, + 0x93, 0x37, 0xc3, 0x29, 0x5a, 0x51, 0xee, 0x8b, 0x42, 0x21, 0x89, 0x22, 0xb5, 0x38, 0x35, 0xad, 0xd8, 0x40, 0x0b, + 0xce, 0x88, 0xd2, 0x84, 0xa5, 0xe2, 0xb3, 0x8a, 0x9c, 0xb2, 0xdf, 0x1d, 0x42, 0xb2, 0x0a, 0x37, 0x35, 0x95, 0x52, + 0xeb, 0x56, 0x19, 0xc2, 0x91, 0x56, 0x4a, 0xd3, 0x6a, 0xe2, 0x84, 0xd8, 0xda, 0x27, 0x61, 0x0f, 0x16, 0x35, 0xbb, + 0xd0, 0x33, 0xaa, 0x15, 0x1e, 0xf0, 0xd4, 0x74, 0x13, 0xbe, 0x37, 0x11, 0x4d, 0xad, 0x1f, 0x03, 0xe3, 0x69, 0x0d, + 0xe3, 0x06, 0x6a, 0x33, 0xc9, 0xbb, 0xb2, 0x11, 0x89, 0xea, 0x8d, 0x1d, 0x8a, 0x53, 0xb9, 0x10, 0x6b, 0x58, 0x5c, 0x55, 0x3e, 0x05, 0x11, 0x82, 0x19, 0x9b, 0x83, 0x7a, 0x67, 0x4a, 0x08, 0x07, 0x80, 0x67, 0xcb, 0xe5, 0x1a, 0xd9, 0x6d, 0xd4, 0x41, 0x91, 0x5b, 0x59, 0x86, 0xcb, 0xe5, 0x33, 0x8e, 0x1c, 0xa5, 0xfd, 0x62, 0x8a, 0x06, 0x9a, 0xe7, 0x9e, 0xbc, 0x84, 0x5a, 0x42, 0x19, 0xad, 0x4a, 0x4a, 0xb3, 0xa1, 0x4e, 0xb5, 0xf5, 0x85, 0xe2, 0x06, 0xe3, 0x3e, @@ -141,7 +141,7 @@ constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x65, 0x01, 0xf4, 0x56, 0x0c, 0xef, 0xca, 0x64, 0xaf, 0x09, 0xd3, 0x52, 0xfe, 0x4a, 0xb7, 0xa0, 0xa6, 0xcc, 0xdc, 0x34, 0xf1, 0x95, 0x4f, 0xb5, 0x27, 0xdd, 0x26, 0x3b, 0x9d, 0x5e, 0x69, 0xf7, 0x69, 0x6a, 0xe8, 0x49, 0xf7, 0x86, 0x12, 0xaa, 0xe9, 0x3c, 0x0e, 0x15, 0xb0, 0x0c, 0x61, 0xaa, 0xe8, 0xe8, 0x9a, 0xc5, 0x71, 0x55, 0xfa, 0x39, 0x9c, - 0x3d, 0x57, 0x9c, 0x3d, 0xd5, 0x9c, 0x1d, 0x58, 0x05, 0x70, 0x76, 0xd9, 0x5d, 0xd5, 0x3c, 0x5d, 0xdb, 0x9e, 0xa9, + 0x3d, 0x57, 0x9c, 0x3d, 0xd3, 0x9c, 0x1d, 0x58, 0x05, 0x70, 0x76, 0xd9, 0x5d, 0xd5, 0x3c, 0x5b, 0xdb, 0x9e, 0x99, 0xe4, 0xe9, 0xb9, 0xb0, 0xa5, 0x61, 0xbc, 0xb9, 0x86, 0x00, 0x95, 0xba, 0xd7, 0x47, 0x47, 0xb9, 0x62, 0xc0, 0x08, 0x94, 0x9e, 0x4c, 0x6a, 0xba, 0x29, 0x3e, 0x3a, 0x08, 0xe7, 0x05, 0x2d, 0x29, 0xfb, 0xe4, 0x19, 0xf8, 0xea, 0x8c, 0xe9, 0x80, 0x18, 0x13, 0xc5, 0x9f, 0xa5, 0x46, 0xe9, 0xd9, 0x31, 0x35, 0xbb, 0x5c, 0xcf, 0x0e, 0x78, 0x7d, 0x35, @@ -181,1135 +181,1137 @@ constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x19, 0xb3, 0x7a, 0xcb, 0x65, 0xa8, 0x00, 0xa4, 0x6c, 0xdd, 0x1d, 0x03, 0x2b, 0xbe, 0x93, 0xac, 0xf9, 0xac, 0x32, 0xff, 0xda, 0x46, 0xf5, 0xf8, 0x28, 0x4b, 0xae, 0xfc, 0x98, 0x85, 0x16, 0xa7, 0xd3, 0x59, 0xec, 0x73, 0x6a, 0xa9, 0xf9, 0x5a, 0x3e, 0x74, 0x64, 0x97, 0x3a, 0xc3, 0xcc, 0xb0, 0x39, 0x67, 0x3a, 0xf0, 0x04, 0x7b, 0xc5, 0x81, 0x28, - 0x95, 0xd2, 0x3b, 0x9e, 0x56, 0x41, 0xb0, 0xd5, 0x38, 0x5f, 0xb3, 0x03, 0xbe, 0xb0, 0x91, 0x90, 0xcf, 0x39, 0x86, - 0xa0, 0x25, 0x21, 0xdd, 0x81, 0x7d, 0x9c, 0x5f, 0x4d, 0xfa, 0x36, 0xc4, 0x68, 0x32, 0xf2, 0x41, 0xb8, 0x86, 0xa0, - 0x42, 0x44, 0xda, 0xbd, 0xe8, 0x98, 0xf6, 0xa2, 0x46, 0x43, 0x6b, 0xd1, 0x3e, 0x49, 0x86, 0x91, 0x6c, 0x1e, 0xe0, - 0x10, 0xcf, 0x49, 0xb3, 0x83, 0x67, 0xa4, 0x2d, 0x9a, 0xf4, 0x66, 0xc7, 0xbe, 0x1a, 0x66, 0x6f, 0xcf, 0xc9, 0xdc, - 0xd8, 0xcf, 0xf9, 0x0b, 0xb0, 0xf7, 0xc9, 0x0c, 0x87, 0x24, 0x73, 0xe9, 0x0d, 0x0d, 0x1c, 0x1f, 0xe1, 0x50, 0x71, - 0x1a, 0xd4, 0x43, 0x33, 0x62, 0x54, 0x03, 0x33, 0x82, 0x7c, 0x18, 0x84, 0xc3, 0xce, 0x88, 0x10, 0x62, 0xef, 0x34, - 0x9b, 0xf6, 0x20, 0x23, 0x13, 0xee, 0x41, 0x89, 0xa1, 0x2c, 0x93, 0x29, 0x14, 0x75, 0x8d, 0x22, 0xe7, 0x0d, 0x77, - 0x39, 0xcd, 0xb9, 0x03, 0xc5, 0xe0, 0x01, 0xc8, 0x35, 0x61, 0xdb, 0xc7, 0x2d, 0xbb, 0x01, 0xa5, 0x82, 0x38, 0x11, - 0xce, 0xc8, 0x35, 0xf2, 0xc2, 0xe1, 0xfe, 0xc8, 0x14, 0x00, 0xa2, 0x10, 0x06, 0xbf, 0x1e, 0x84, 0xc3, 0xb6, 0x18, - 0xbc, 0x6f, 0x0f, 0x9c, 0x8c, 0xe4, 0x52, 0x43, 0x1b, 0xe4, 0xde, 0x07, 0x31, 0x55, 0xe4, 0x29, 0xe0, 0xd4, 0xb8, - 0x73, 0xd2, 0xec, 0x7a, 0xce, 0xdc, 0x9c, 0x44, 0x13, 0x06, 0x53, 0x58, 0xc0, 0x01, 0x81, 0xfa, 0x38, 0x23, 0x30, - 0x62, 0xd5, 0xec, 0xda, 0x53, 0xcf, 0x0f, 0xec, 0x07, 0x83, 0x73, 0xee, 0x8d, 0xb9, 0x1c, 0xfe, 0x9c, 0x2f, 0x97, - 0xf0, 0xef, 0x98, 0x0f, 0x32, 0x72, 0x2d, 0x8a, 0x26, 0xaa, 0x68, 0x0a, 0x45, 0x1f, 0x3c, 0x00, 0x15, 0xe7, 0xa5, - 0x96, 0x25, 0xd7, 0x64, 0x4a, 0x04, 0xec, 0x7b, 0x7b, 0xc9, 0x30, 0x6a, 0x74, 0x46, 0xe0, 0xe4, 0xcf, 0x78, 0xfe, - 0x1d, 0xe3, 0x91, 0x63, 0xb7, 0xfa, 0x36, 0x1a, 0xd8, 0x16, 0x2c, 0x6d, 0x2f, 0x6d, 0x10, 0x89, 0x61, 0xbf, 0xf1, - 0x8a, 0x7b, 0xf3, 0x3e, 0x69, 0x0f, 0x1c, 0xa6, 0x5c, 0x7a, 0x08, 0xfb, 0x8a, 0x71, 0xb6, 0xf1, 0x1c, 0x35, 0x18, - 0x6f, 0xe8, 0xe7, 0x39, 0x6a, 0xdc, 0x36, 0xa6, 0xc8, 0xf3, 0x1b, 0xb7, 0x0d, 0x67, 0x4e, 0x08, 0x69, 0x76, 0xcb, - 0x66, 0x5a, 0xfc, 0x45, 0xc8, 0x9b, 0x6a, 0x7f, 0xe7, 0x50, 0x6c, 0x87, 0xb4, 0xe1, 0x24, 0x43, 0x3a, 0x5a, 0x2e, - 0xed, 0xe3, 0x41, 0xdf, 0x46, 0x0d, 0x47, 0x13, 0x5a, 0x4b, 0x53, 0x1a, 0x42, 0x98, 0x8d, 0x0a, 0x15, 0x4f, 0x7a, - 0x52, 0x8b, 0x1d, 0x2d, 0xaa, 0xcd, 0x6e, 0xf0, 0x00, 0x5a, 0x94, 0x86, 0x8c, 0x54, 0x58, 0x67, 0x30, 0x4d, 0x4d, - 0xcc, 0x29, 0x69, 0xe3, 0x8c, 0x68, 0xf7, 0x75, 0x44, 0x78, 0x45, 0xf0, 0x3e, 0xa9, 0xaa, 0xe3, 0x61, 0x80, 0xc3, - 0x11, 0x79, 0x2a, 0x0d, 0x92, 0x9e, 0x76, 0x8e, 0xd3, 0x98, 0x3c, 0x59, 0x89, 0xe2, 0x06, 0x10, 0x60, 0xb9, 0x71, - 0x83, 0x79, 0x96, 0xd1, 0x84, 0xbf, 0x4e, 0x43, 0xa5, 0xa7, 0xd1, 0x18, 0x4c, 0x25, 0x08, 0xcf, 0x62, 0x50, 0xd2, - 0xba, 0x7a, 0x67, 0xcc, 0xd7, 0x5e, 0xcf, 0xc8, 0x5c, 0xea, 0x4f, 0x22, 0x68, 0xdb, 0x9b, 0x29, 0xcb, 0xd8, 0x41, - 0x78, 0xae, 0xa2, 0xb9, 0x8e, 0xeb, 0xba, 0x33, 0x37, 0x80, 0xd7, 0x30, 0x40, 0x8e, 0x0a, 0xb1, 0x8f, 0x9c, 0x9c, - 0xdc, 0xb8, 0x09, 0xbd, 0x11, 0xa3, 0x3a, 0xa8, 0x92, 0xcc, 0x7a, 0x7b, 0x1d, 0x47, 0x3d, 0xc1, 0x6e, 0x72, 0x37, - 0x49, 0x43, 0x0a, 0xe8, 0x81, 0xf8, 0xbd, 0x2a, 0x8a, 0xfc, 0xdc, 0x0c, 0x52, 0x55, 0xf0, 0x0d, 0x4d, 0xff, 0xf5, - 0x0c, 0x9c, 0xbe, 0x42, 0xd9, 0x2a, 0x2b, 0x4b, 0x4f, 0x38, 0x42, 0x6c, 0xec, 0xcc, 0x5c, 0x08, 0xee, 0x09, 0x12, - 0x62, 0x60, 0xcb, 0xcd, 0x4c, 0xa2, 0xba, 0x2d, 0xfb, 0x9c, 0x92, 0x70, 0x98, 0x35, 0x1a, 0xc2, 0x11, 0x3d, 0x97, - 0x24, 0x31, 0x43, 0x78, 0x5a, 0xee, 0x2d, 0x5d, 0xef, 0x2d, 0xa9, 0x8f, 0xe4, 0x4c, 0xeb, 0x0e, 0xdd, 0x06, 0xe3, - 0x48, 0xf8, 0x0a, 0xb9, 0x73, 0x8b, 0xf0, 0x98, 0xb4, 0x9c, 0xa1, 0x3b, 0xf8, 0xf3, 0x08, 0x0d, 0x1c, 0xf7, 0x2b, - 0xd4, 0x92, 0x8c, 0x63, 0x8a, 0x7a, 0xbe, 0x1c, 0x62, 0x21, 0xa2, 0x98, 0x1d, 0x2c, 0x7c, 0x89, 0x5e, 0x8a, 0x13, - 0x7f, 0x4a, 0xbd, 0x31, 0xec, 0x71, 0x4d, 0x37, 0x6f, 0x31, 0xd0, 0x91, 0x37, 0x56, 0x9c, 0xc4, 0xb5, 0x07, 0xbf, - 0xf0, 0xf2, 0x69, 0x60, 0x0f, 0xbe, 0xae, 0x9e, 0xfe, 0x6c, 0x0f, 0xbe, 0xe5, 0xde, 0xb7, 0x85, 0x72, 0x77, 0xd7, - 0x86, 0x78, 0xa8, 0x87, 0x28, 0xe4, 0xc2, 0x18, 0x98, 0x9b, 0xa3, 0x75, 0x47, 0xc7, 0x0c, 0x15, 0x6c, 0x5c, 0xb2, - 0xa2, 0xdc, 0xe5, 0xfe, 0x04, 0x50, 0x6a, 0xac, 0x40, 0x6e, 0x46, 0xf7, 0xab, 0x09, 0x03, 0xa1, 0x68, 0x6a, 0x05, - 0x54, 0xce, 0xfa, 0x6d, 0xb4, 0xa8, 0xd5, 0x15, 0x1a, 0x53, 0x3d, 0x9a, 0x5e, 0x72, 0xe9, 0x29, 0x69, 0xf7, 0xa6, - 0xc7, 0xb3, 0xde, 0xb4, 0xd1, 0x40, 0xb9, 0x26, 0xac, 0xf9, 0x70, 0x3a, 0xc2, 0xaf, 0xc1, 0xab, 0x67, 0x52, 0x12, - 0xae, 0x4d, 0xaf, 0xab, 0xa6, 0xd7, 0x68, 0xa4, 0x05, 0xea, 0x19, 0x4d, 0x67, 0xb2, 0x69, 0x51, 0x48, 0x9c, 0xac, - 0x12, 0xda, 0x11, 0x12, 0x25, 0x90, 0x12, 0x45, 0x08, 0x39, 0xe3, 0x68, 0x63, 0xaf, 0xd0, 0x27, 0x34, 0x17, 0x3b, - 0x16, 0x98, 0xa7, 0x94, 0x11, 0x0e, 0x60, 0x01, 0x9a, 0x96, 0xae, 0xe0, 0x5b, 0x3c, 0x6f, 0x74, 0x04, 0x91, 0x37, - 0x3b, 0xbd, 0x7a, 0x5f, 0x8f, 0xaa, 0xbe, 0xf0, 0xbc, 0x41, 0x6e, 0x4b, 0x2c, 0x15, 0x69, 0xa3, 0x51, 0xd4, 0xe3, - 0x9d, 0x7a, 0xdf, 0xd6, 0x22, 0x10, 0x27, 0xab, 0xa9, 0x19, 0x5a, 0xbe, 0x56, 0x12, 0x95, 0xb9, 0x2c, 0x49, 0x68, - 0x06, 0x32, 0x94, 0x70, 0xcc, 0x8a, 0xa2, 0x94, 0xeb, 0x6f, 0x40, 0x88, 0x62, 0x4a, 0x12, 0xe0, 0x3b, 0xc2, 0xec, - 0xc2, 0x29, 0xce, 0x70, 0x24, 0xb8, 0x06, 0x21, 0xe4, 0x54, 0x27, 0xb5, 0x70, 0xc1, 0x81, 0x7c, 0xc2, 0x0c, 0x89, - 0x94, 0x13, 0xea, 0x9e, 0xef, 0x9e, 0xa6, 0x77, 0x9a, 0x64, 0x43, 0x36, 0xf2, 0x44, 0xb5, 0x58, 0xf1, 0xad, 0x80, - 0xbc, 0x73, 0x38, 0x2a, 0xc3, 0x23, 0xae, 0x60, 0x7f, 0x4f, 0x59, 0x46, 0x85, 0x06, 0xbe, 0xab, 0xcd, 0x3e, 0xbf, - 0xae, 0x3e, 0xfa, 0xa6, 0xf3, 0x06, 0x10, 0x19, 0x80, 0x6f, 0x27, 0x25, 0x6b, 0xd5, 0xce, 0x77, 0x4f, 0xde, 0x6c, - 0x32, 0x81, 0x97, 0x4b, 0x65, 0xfc, 0xfa, 0xa0, 0xd9, 0xe0, 0xa0, 0x82, 0xd4, 0x57, 0x3f, 0x3c, 0xc7, 0x17, 0x0a, - 0x52, 0xe0, 0x24, 0x40, 0x45, 0xe7, 0xbb, 0x27, 0xef, 0x9d, 0x44, 0xb8, 0x96, 0x10, 0x36, 0xa7, 0xed, 0x64, 0xc4, - 0x89, 0x08, 0x45, 0x72, 0xee, 0x25, 0xe3, 0xca, 0x0c, 0xf1, 0xed, 0x45, 0xe2, 0x25, 0xd8, 0x0f, 0x43, 0x36, 0x22, - 0xbe, 0xc2, 0x00, 0xf1, 0x11, 0xf6, 0x6b, 0x66, 0x19, 0x81, 0x05, 0x10, 0x63, 0x9d, 0xc1, 0x4a, 0xb8, 0x52, 0xf1, - 0x43, 0xd8, 0x17, 0xa3, 0xf2, 0x42, 0x8a, 0x8e, 0x9f, 0xd7, 0x72, 0xd3, 0x2a, 0x6b, 0xf4, 0x5b, 0xb0, 0x9c, 0xf4, - 0xc3, 0x6b, 0xd5, 0x75, 0x59, 0xf0, 0x54, 0x27, 0x91, 0x9d, 0xef, 0x9e, 0xbc, 0x52, 0x79, 0x64, 0x33, 0x5f, 0x73, - 0xfb, 0x35, 0x0b, 0xf3, 0xe4, 0x95, 0x5b, 0xbd, 0x15, 0x95, 0xcf, 0x77, 0x4f, 0x3e, 0x6c, 0xaa, 0x06, 0xe5, 0xc5, - 0xbc, 0x32, 0xf1, 0x05, 0x7c, 0x0b, 0x1a, 0x7b, 0x0b, 0x25, 0x1a, 0x3c, 0x56, 0x60, 0x21, 0x8e, 0xbc, 0xbc, 0x28, - 0x3d, 0x23, 0x4f, 0x71, 0x4a, 0x44, 0x1c, 0xa8, 0xbe, 0x6a, 0x4a, 0xc9, 0x63, 0x69, 0x72, 0x16, 0xa4, 0x33, 0xba, - 0x25, 0x38, 0x74, 0x82, 0x5c, 0x36, 0x85, 0x04, 0x1a, 0x01, 0x3a, 0xc3, 0x3b, 0x6d, 0xd4, 0xab, 0x0b, 0xaf, 0x54, - 0x10, 0x69, 0x56, 0x93, 0x2c, 0x38, 0x22, 0x6d, 0xec, 0x93, 0x36, 0x0e, 0x48, 0x3e, 0x6c, 0x4b, 0xf1, 0xd0, 0x0b, - 0xca, 0x7e, 0xa5, 0x90, 0x81, 0xdc, 0xb0, 0x40, 0xee, 0x56, 0x29, 0x7e, 0xc3, 0x5e, 0x20, 0x5c, 0x8f, 0x42, 0xa2, - 0x87, 0xd2, 0x68, 0x75, 0x32, 0x9c, 0x89, 0x8e, 0xcf, 0xd8, 0x65, 0x0c, 0xd9, 0x25, 0x30, 0x2b, 0xcc, 0x91, 0x57, - 0x56, 0xed, 0xa8, 0xaa, 0x81, 0x2b, 0xd6, 0x29, 0xc3, 0x81, 0x0b, 0x8c, 0x1b, 0x07, 0x2a, 0x19, 0x27, 0x5f, 0x6f, - 0xf2, 0x70, 0x6f, 0xcf, 0x91, 0x8d, 0xbe, 0xe3, 0x4e, 0xa6, 0xdf, 0x57, 0xa1, 0xbb, 0x6f, 0x25, 0xaf, 0x08, 0x91, - 0x80, 0xbf, 0xd1, 0xf0, 0x47, 0x05, 0xc4, 0xa1, 0x9d, 0xa0, 0x8e, 0x41, 0x0d, 0xbc, 0xd0, 0xf4, 0xea, 0xd3, 0x6f, - 0x34, 0xca, 0x30, 0x6d, 0x1d, 0x5b, 0x27, 0x38, 0x2d, 0xae, 0x9c, 0x32, 0xff, 0xa7, 0xbd, 0x96, 0x35, 0xa5, 0x41, - 0x40, 0xcc, 0xa4, 0x59, 0xa6, 0x27, 0x63, 0x6c, 0x09, 0x06, 0xf5, 0x5e, 0xa8, 0xc4, 0x05, 0x2c, 0x72, 0xac, 0x54, - 0x25, 0xcd, 0xce, 0xba, 0xc8, 0xd3, 0x95, 0x20, 0x2c, 0x05, 0x95, 0x1a, 0x85, 0x22, 0xef, 0x57, 0xeb, 0x99, 0x97, - 0x38, 0x47, 0xca, 0xc7, 0x25, 0xa0, 0x10, 0xc8, 0xea, 0x96, 0x48, 0x79, 0x4e, 0x26, 0xdb, 0x49, 0xfe, 0xc4, 0x20, - 0xf9, 0x27, 0x84, 0x1a, 0xe4, 0x2f, 0x3d, 0x1c, 0x6e, 0xaa, 0x5c, 0x0b, 0xb9, 0x7e, 0x75, 0x3a, 0x23, 0xe0, 0x43, - 0xab, 0x63, 0xb4, 0x16, 0x57, 0xdc, 0xc2, 0x50, 0xcc, 0x1d, 0x22, 0xbc, 0x90, 0x58, 0x07, 0x81, 0x9d, 0x2a, 0xaa, - 0x06, 0x43, 0x6f, 0x72, 0xe9, 0x99, 0x1c, 0xf0, 0xe4, 0xc3, 0xdd, 0x01, 0xd1, 0xd3, 0xd9, 0xfa, 0xce, 0x35, 0x32, - 0x40, 0x61, 0xd6, 0xc6, 0xc6, 0xad, 0xe7, 0x83, 0xc2, 0xf8, 0x65, 0x20, 0xbb, 0xce, 0x7c, 0x56, 0x36, 0xa1, 0x96, - 0x7f, 0x00, 0x6d, 0xa7, 0x23, 0x6a, 0x50, 0xa3, 0x5b, 0xe0, 0x47, 0x32, 0x0f, 0xd5, 0xcf, 0xb6, 0xb0, 0x8f, 0x13, - 0x51, 0x81, 0x26, 0xe1, 0xe6, 0xd7, 0x4f, 0x0a, 0x45, 0x26, 0x12, 0x34, 0xb4, 0x00, 0xfe, 0x27, 0x49, 0x1e, 0xe8, - 0x46, 0xc8, 0x05, 0x40, 0xd0, 0x44, 0xe0, 0xa9, 0x42, 0x98, 0x6d, 0x57, 0xce, 0xf7, 0xe7, 0x3b, 0x84, 0x4c, 0x2a, - 0xe7, 0xe3, 0xbb, 0x2a, 0xfb, 0x0a, 0xc8, 0x02, 0x79, 0x60, 0x3c, 0x96, 0x05, 0x32, 0x7e, 0x79, 0xaa, 0xab, 0x0b, - 0x03, 0xd2, 0xad, 0xf4, 0x6d, 0x23, 0xb6, 0x29, 0xbc, 0x72, 0xf2, 0xbd, 0x46, 0xc3, 0xca, 0xdb, 0x5d, 0x78, 0xfb, - 0x92, 0x0b, 0x18, 0xe1, 0xf9, 0xbd, 0xa8, 0xad, 0xfb, 0x2d, 0x3e, 0xae, 0xa6, 0xb0, 0xac, 0x2c, 0x8a, 0xcb, 0x92, - 0x9c, 0x66, 0xfc, 0x09, 0x1d, 0xa7, 0x19, 0x84, 0x2c, 0x4a, 0x9c, 0xa0, 0x62, 0xd7, 0x70, 0xdb, 0x89, 0xf9, 0x19, - 0x71, 0x82, 0x95, 0x09, 0x8a, 0x5f, 0x1f, 0x45, 0xd4, 0xfa, 0x7c, 0xb5, 0xd5, 0x64, 0x6f, 0xef, 0x5d, 0x85, 0x26, - 0x05, 0xa5, 0x80, 0xc2, 0x60, 0x5a, 0x52, 0xa5, 0x51, 0xa1, 0xdc, 0x5d, 0xa7, 0x74, 0x01, 0x68, 0x86, 0x61, 0xf2, - 0x9e, 0xe7, 0x84, 0x17, 0x93, 0x55, 0x16, 0xaf, 0x5c, 0x13, 0xcc, 0x34, 0x5b, 0x80, 0xc3, 0x83, 0xa1, 0x2d, 0x7d, - 0x45, 0x79, 0x95, 0x12, 0x5b, 0xc2, 0x70, 0x0a, 0xc8, 0x72, 0x84, 0x11, 0x62, 0x50, 0xe0, 0x46, 0xa3, 0xe4, 0x2d, - 0xe8, 0x95, 0x11, 0xce, 0xdd, 0x08, 0x92, 0x60, 0x6b, 0x5b, 0x16, 0x21, 0x2c, 0x33, 0x73, 0x8c, 0x5c, 0x82, 0x93, - 0xe7, 0x9b, 0x3c, 0xca, 0x9a, 0xa8, 0xa9, 0x90, 0x3a, 0x50, 0x23, 0x45, 0x65, 0x03, 0xf7, 0xca, 0x61, 0x4a, 0x71, - 0xd3, 0x71, 0x33, 0x60, 0xc0, 0x3f, 0x73, 0x47, 0xc6, 0xa2, 0x40, 0x66, 0x64, 0xee, 0xdc, 0xa9, 0x0d, 0xdd, 0xcb, - 0x44, 0x33, 0xac, 0x10, 0x17, 0x99, 0x68, 0xca, 0x44, 0x5c, 0xef, 0xb4, 0xe2, 0xa5, 0x57, 0x32, 0x8f, 0x9a, 0x6b, - 0x2e, 0x58, 0x65, 0x92, 0x18, 0xd3, 0xbf, 0x92, 0xa9, 0xd1, 0x65, 0x25, 0x50, 0xc3, 0xe8, 0xb5, 0xf5, 0x44, 0xac, - 0x01, 0x2d, 0x80, 0xbe, 0x16, 0xa7, 0xdc, 0x58, 0x51, 0xed, 0xc3, 0x16, 0x63, 0x1a, 0x52, 0xff, 0x1d, 0xe4, 0xba, - 0xac, 0xee, 0xf9, 0xe7, 0x42, 0x16, 0x32, 0x9c, 0xd7, 0x18, 0x7b, 0x2a, 0x18, 0x3b, 0x02, 0x3d, 0x4d, 0xa7, 0x7f, - 0x0f, 0x54, 0xca, 0x8b, 0xca, 0x5d, 0x74, 0x14, 0x89, 0xbd, 0x2e, 0xc3, 0xe5, 0xc6, 0xef, 0x95, 0xd5, 0xf0, 0x18, - 0x81, 0x34, 0x20, 0xac, 0x38, 0x7b, 0x8a, 0x70, 0xde, 0x68, 0xf4, 0xf2, 0x63, 0x5a, 0xb9, 0x48, 0x2a, 0x18, 0x19, - 0x44, 0x74, 0x81, 0xe0, 0x6b, 0x32, 0x14, 0x62, 0xfe, 0x3a, 0x3f, 0x3b, 0x07, 0x57, 0xfb, 0xc9, 0x3b, 0xc7, 0xe4, - 0x6a, 0x66, 0xdd, 0x32, 0x68, 0x0a, 0xf3, 0x71, 0xaa, 0x78, 0xcb, 0xdb, 0xbb, 0x33, 0x3c, 0x00, 0xee, 0x9d, 0x0e, - 0x86, 0x6c, 0x34, 0xd4, 0xe3, 0x92, 0x25, 0x94, 0xbb, 0xaf, 0x87, 0xaa, 0xc4, 0x44, 0x73, 0xb0, 0x1e, 0xaf, 0x4c, - 0x59, 0x4e, 0xf2, 0xa2, 0xc8, 0x69, 0x15, 0xdf, 0x5f, 0xc9, 0xc0, 0x14, 0xc2, 0x65, 0xdd, 0xd9, 0x7e, 0x3a, 0x23, - 0x1c, 0x1b, 0x84, 0xfa, 0x76, 0x5b, 0xe8, 0xa3, 0x02, 0x13, 0xf6, 0xb5, 0x12, 0x8a, 0xdf, 0x6e, 0x12, 0x8a, 0x38, - 0x55, 0x5b, 0x5e, 0x08, 0xc4, 0xce, 0x3d, 0x04, 0xa2, 0x72, 0xb2, 0x6b, 0x99, 0x08, 0xea, 0x48, 0x4d, 0x26, 0xd6, - 0x97, 0x94, 0xa4, 0x98, 0xa9, 0xd5, 0xe8, 0x77, 0x97, 0x4b, 0x36, 0x6c, 0x83, 0x13, 0xc9, 0xb6, 0xe1, 0x67, 0x47, - 0xfe, 0x34, 0x38, 0xb1, 0x74, 0x02, 0x3b, 0xac, 0x34, 0x59, 0x90, 0x0b, 0x69, 0xce, 0x8e, 0xc8, 0xca, 0x12, 0x34, - 0xad, 0x28, 0x48, 0x11, 0x38, 0x61, 0x65, 0x94, 0x09, 0x20, 0x16, 0xb2, 0x42, 0x19, 0x90, 0xce, 0xc6, 0xf4, 0x3f, - 0x6d, 0x5e, 0x7e, 0x5a, 0x13, 0xad, 0xc9, 0x15, 0xa9, 0x3e, 0xd4, 0x12, 0x0e, 0x14, 0x04, 0x4a, 0x3f, 0xdc, 0x11, - 0x26, 0x68, 0x25, 0xca, 0x91, 0x29, 0x87, 0x70, 0x1b, 0x5c, 0x68, 0x3b, 0xef, 0x64, 0x80, 0x77, 0x83, 0x34, 0xc1, - 0x99, 0x41, 0xd7, 0xcf, 0x09, 0xaf, 0xb1, 0x92, 0x88, 0x28, 0x4b, 0x09, 0x07, 0x82, 0x4c, 0x39, 0x49, 0x87, 0xed, - 0x11, 0x28, 0xa0, 0x3d, 0xff, 0x38, 0xad, 0x4c, 0x60, 0xbf, 0xd1, 0x40, 0x81, 0x1e, 0x35, 0x1a, 0xb2, 0x86, 0x3f, - 0xc2, 0x14, 0xfb, 0xd2, 0x30, 0x39, 0xdd, 0xdb, 0x73, 0x82, 0x6a, 0xdc, 0xa1, 0x3f, 0x42, 0x38, 0x5b, 0x2e, 0x1d, - 0x01, 0x56, 0x80, 0x96, 0xcb, 0xc0, 0x04, 0x4b, 0xbc, 0x86, 0x66, 0x93, 0x01, 0x27, 0x13, 0x21, 0x00, 0x27, 0x00, - 0x61, 0x83, 0x38, 0x81, 0x72, 0xee, 0x05, 0xe0, 0x8c, 0x6a, 0xa4, 0x43, 0xbf, 0xd1, 0x19, 0x19, 0x8c, 0x6b, 0xe8, - 0x8f, 0x48, 0x50, 0x40, 0x72, 0x6b, 0xae, 0x44, 0xe4, 0xcf, 0x20, 0xca, 0x7e, 0x16, 0x92, 0x45, 0x76, 0x68, 0xae, - 0xc6, 0xaa, 0x33, 0xa0, 0xa4, 0x28, 0xb5, 0xac, 0xba, 0x5e, 0x2d, 0x0b, 0xa2, 0xac, 0x84, 0x55, 0x2c, 0x78, 0x00, - 0x96, 0x7d, 0x49, 0xe6, 0xbf, 0xf0, 0x32, 0xcd, 0xfa, 0xdb, 0x8d, 0xc9, 0xd5, 0xae, 0xeb, 0xfa, 0xd9, 0x44, 0x44, - 0x32, 0x74, 0x14, 0x56, 0x10, 0xff, 0xbe, 0x02, 0xd3, 0x18, 0x78, 0x58, 0x8e, 0x35, 0x22, 0x12, 0x7c, 0xad, 0xda, - 0xe8, 0x13, 0x25, 0xbf, 0x6e, 0xf4, 0x32, 0x48, 0x48, 0xbe, 0xfe, 0xad, 0x90, 0x1c, 0x28, 0x48, 0x24, 0x79, 0xac, - 0xe0, 0x6c, 0x0b, 0x2e, 0x7e, 0xe5, 0x2b, 0x38, 0xdb, 0x8e, 0xdb, 0x92, 0x21, 0x6c, 0x83, 0xcf, 0xe0, 0x0d, 0x12, - 0xd0, 0xaa, 0xc0, 0x80, 0xf2, 0x70, 0x55, 0xf7, 0x92, 0xac, 0x14, 0x84, 0x29, 0x27, 0x0e, 0xab, 0x6f, 0x80, 0x4a, - 0x1b, 0x35, 0x0c, 0x5f, 0xe6, 0x4d, 0x90, 0xe1, 0x12, 0xa8, 0xa7, 0xae, 0x00, 0x39, 0x29, 0x5f, 0x3b, 0xa4, 0x22, - 0xec, 0x48, 0x25, 0xce, 0x0d, 0xfc, 0x19, 0x9f, 0x67, 0xa0, 0x4a, 0xe5, 0xfa, 0x37, 0x14, 0xc3, 0x59, 0x10, 0x51, - 0x06, 0x3f, 0xa0, 0x60, 0xe6, 0xe7, 0x39, 0xbb, 0x92, 0x65, 0xea, 0x37, 0xce, 0x88, 0x26, 0xe5, 0x5c, 0xea, 0x84, - 0x29, 0xea, 0xa5, 0x8a, 0x4e, 0xeb, 0x68, 0x7b, 0x76, 0x45, 0x13, 0xfe, 0x92, 0xe5, 0x9c, 0x26, 0x30, 0xfd, 0x8a, - 0xe2, 0x60, 0x46, 0x39, 0x82, 0x0d, 0x5b, 0x6b, 0xe5, 0x87, 0xe1, 0x9d, 0x4d, 0x78, 0x5d, 0x07, 0x8a, 0xfc, 0x24, - 0x8c, 0xe5, 0x20, 0x66, 0x42, 0xa3, 0x4e, 0xe2, 0x2c, 0x6b, 0x9a, 0xf9, 0x34, 0x95, 0xb2, 0x21, 0xb8, 0xbb, 0xc3, - 0x88, 0x96, 0x04, 0x5a, 0x7a, 0xde, 0xa9, 0xb5, 0x40, 0xc0, 0x7b, 0xcb, 0x22, 0x98, 0x33, 0xc1, 0xdc, 0xe0, 0xa8, - 0x6e, 0x1d, 0x4e, 0x4d, 0x37, 0xdf, 0x6d, 0x3c, 0xd8, 0xb6, 0x49, 0x38, 0x08, 0x3a, 0x79, 0xb8, 0xdd, 0xb2, 0x7a, - 0xa5, 0x25, 0x87, 0x96, 0x16, 0xec, 0xbe, 0x8c, 0x19, 0x2d, 0x34, 0x79, 0x21, 0xbd, 0x15, 0x77, 0x39, 0xf9, 0x05, - 0x4e, 0x0e, 0x3d, 0xe7, 0xd3, 0x78, 0xe5, 0x80, 0x4c, 0x6f, 0xb7, 0xd4, 0xfe, 0x77, 0xb9, 0xf3, 0x04, 0xbf, 0x82, - 0xb0, 0xee, 0x37, 0x55, 0xf5, 0xf5, 0x70, 0xee, 0x37, 0x15, 0x82, 0xbe, 0xf1, 0xd6, 0xea, 0x19, 0x61, 0xdc, 0xae, - 0x7b, 0xe4, 0xb6, 0x6d, 0xad, 0x2d, 0xfd, 0x28, 0x83, 0x48, 0x32, 0xd5, 0x52, 0xec, 0x07, 0x5c, 0x25, 0xaa, 0x41, - 0xc2, 0x5c, 0xdd, 0x42, 0xa2, 0x2a, 0xc5, 0x50, 0xea, 0xf0, 0xdb, 0x96, 0x47, 0xc9, 0x98, 0x54, 0xda, 0x19, 0x6f, - 0xfd, 0x8c, 0xef, 0xc2, 0x2e, 0xcb, 0xd6, 0x4e, 0xe3, 0x45, 0x04, 0x3c, 0x68, 0xf7, 0x1b, 0xc2, 0x30, 0xb6, 0x73, - 0x79, 0x18, 0xc8, 0xec, 0x9f, 0x64, 0x5a, 0x77, 0xab, 0x5b, 0x19, 0xaf, 0xc1, 0xfe, 0x47, 0x38, 0xd2, 0x47, 0xe4, - 0xa8, 0xe2, 0xc0, 0xd4, 0x5b, 0x14, 0xa5, 0x53, 0x20, 0x93, 0xca, 0x5b, 0x82, 0x70, 0x56, 0x88, 0xf0, 0xf6, 0xf7, - 0xf8, 0x07, 0xc5, 0x12, 0xcf, 0x4b, 0x8e, 0xf3, 0xec, 0xbe, 0x1c, 0x51, 0x82, 0x5f, 0x46, 0xef, 0x81, 0x8e, 0x05, - 0x85, 0x16, 0x9a, 0x8a, 0x9e, 0xa6, 0x6a, 0x22, 0x5b, 0xf3, 0x52, 0x31, 0x2d, 0x33, 0x6a, 0xc4, 0x30, 0x1b, 0x12, - 0x39, 0xb5, 0x95, 0xcd, 0xcb, 0x5d, 0x55, 0x1b, 0x17, 0x6d, 0xc1, 0x62, 0x15, 0x58, 0x5c, 0x2e, 0x9d, 0x3a, 0xaa, - 0x09, 0x33, 0xe2, 0x18, 0x08, 0x33, 0x23, 0xa1, 0xa2, 0xa6, 0x59, 0xcb, 0x36, 0x0e, 0x5a, 0xcd, 0x27, 0xd2, 0xba, - 0x79, 0x0d, 0x0e, 0xd3, 0x85, 0x20, 0x9b, 0x9b, 0x3e, 0x05, 0x2c, 0x67, 0x57, 0x0e, 0x64, 0x60, 0xe8, 0xc7, 0x32, - 0x57, 0xb6, 0x4a, 0x6a, 0xdd, 0x80, 0x5f, 0x74, 0x47, 0xb6, 0xac, 0x42, 0xdd, 0xfa, 0x7b, 0x23, 0xd7, 0xe8, 0x69, - 0xba, 0x2d, 0xd7, 0xa8, 0xa6, 0xed, 0xee, 0xb4, 0xd1, 0xdd, 0x79, 0xa9, 0x72, 0xac, 0xcd, 0x55, 0x7e, 0xc3, 0x70, - 0x1d, 0xa0, 0x4d, 0x89, 0x66, 0xcd, 0x55, 0x4e, 0x8b, 0xe2, 0xbc, 0x3c, 0x4d, 0x20, 0x52, 0x77, 0xce, 0x25, 0xfd, - 0x2b, 0xab, 0x51, 0x1c, 0xca, 0x75, 0xbe, 0x27, 0x93, 0x38, 0xbd, 0xf4, 0xe3, 0xf7, 0x30, 0x5e, 0xf5, 0xf2, 0xf9, - 0x6d, 0x98, 0xf9, 0x9c, 0x2a, 0xee, 0x52, 0xc1, 0xf0, 0xbd, 0x01, 0xc3, 0xf7, 0x92, 0x4f, 0x57, 0xed, 0xf1, 0xe2, - 0x65, 0xd9, 0x81, 0x77, 0x5e, 0x68, 0x96, 0x71, 0xcb, 0x37, 0x8f, 0xb1, 0xca, 0xc2, 0x6e, 0x4b, 0x16, 0x76, 0xcb, - 0x9d, 0xd5, 0xae, 0x1c, 0xe7, 0x87, 0xcd, 0xbd, 0xac, 0x73, 0xb6, 0x1f, 0xaa, 0x8d, 0xff, 0x83, 0x77, 0x67, 0x1b, - 0x83, 0xcb, 0xed, 0xbb, 0xfb, 0x22, 0x59, 0x45, 0x82, 0xfc, 0x12, 0x92, 0x0e, 0x38, 0xe9, 0x1b, 0x87, 0x0e, 0x2a, - 0x39, 0xa5, 0xf3, 0x80, 0x9c, 0x60, 0x9e, 0xf3, 0x74, 0xaa, 0xfa, 0xcc, 0xd5, 0x49, 0x23, 0xf1, 0x12, 0x5c, 0xd1, - 0x22, 0xd6, 0xee, 0xd5, 0xcf, 0x72, 0x2d, 0x3e, 0xb2, 0x24, 0xf4, 0x72, 0xac, 0xa4, 0x48, 0xee, 0xa5, 0x05, 0xd1, - 0xd9, 0xc6, 0xeb, 0xef, 0xf0, 0x98, 0x25, 0x2c, 0x8f, 0x68, 0xe6, 0x64, 0x68, 0xb1, 0x6d, 0xb0, 0x0c, 0x02, 0x32, - 0x72, 0x30, 0xfc, 0xd7, 0xea, 0xd4, 0x9f, 0x0b, 0xbd, 0x81, 0x1f, 0x68, 0x4a, 0x79, 0x94, 0x86, 0x90, 0x96, 0xe2, - 0x86, 0xe5, 0xa1, 0xa6, 0xbd, 0xbd, 0x1d, 0xc7, 0x16, 0x6e, 0x09, 0x38, 0x00, 0x6e, 0xbe, 0x41, 0x83, 0x05, 0x9c, - 0xcf, 0xa9, 0x86, 0xa6, 0x68, 0x41, 0x57, 0x8f, 0xb2, 0x70, 0xf7, 0x23, 0xbd, 0xc5, 0x09, 0x2a, 0x0a, 0x4f, 0x42, - 0x6d, 0x8f, 0x19, 0x8d, 0x43, 0x1b, 0x7f, 0xa4, 0xb7, 0x5e, 0x79, 0x66, 0x5c, 0x1c, 0x71, 0x16, 0x0b, 0x68, 0xa7, - 0xd7, 0x89, 0x8d, 0xab, 0x41, 0xbc, 0x45, 0x81, 0xd3, 0x8c, 0x4d, 0x80, 0x38, 0xbf, 0xa1, 0xb7, 0x9e, 0xec, 0x8f, - 0x19, 0xe7, 0xf5, 0xd0, 0x42, 0xa3, 0xde, 0x35, 0x8a, 0xcd, 0x65, 0x50, 0x06, 0xc5, 0x50, 0xb4, 0x1d, 0x91, 0x5a, - 0xbd, 0xca, 0x3c, 0x44, 0xa8, 0xb8, 0xef, 0x54, 0xf0, 0x37, 0xa6, 0x68, 0xe3, 0xb5, 0xcc, 0xd7, 0x95, 0x46, 0x14, - 0x1a, 0x54, 0x99, 0x1e, 0xbb, 0x4e, 0xa2, 0x77, 0x9d, 0x3a, 0x84, 0x60, 0x38, 0xc2, 0xbe, 0xe1, 0xaa, 0x53, 0xef, - 0xaf, 0x32, 0x21, 0xa4, 0x8a, 0x24, 0xbd, 0xa8, 0xda, 0x59, 0xbb, 0x0e, 0xe0, 0x1d, 0x12, 0x5a, 0x7c, 0x71, 0x26, - 0xb3, 0xd0, 0xd9, 0xa2, 0x7f, 0xe3, 0xc4, 0x59, 0xe8, 0x29, 0x78, 0x89, 0x89, 0x45, 0x5e, 0x00, 0x15, 0x2a, 0xfa, - 0x92, 0x09, 0x80, 0x6c, 0xec, 0xb0, 0x35, 0xa9, 0x99, 0x0a, 0xa9, 0xe9, 0x1a, 0x18, 0xdf, 0x22, 0x25, 0xa9, 0x40, - 0x86, 0x50, 0x22, 0x85, 0xd0, 0x53, 0x8b, 0xab, 0x48, 0xc8, 0x5c, 0xd0, 0xf2, 0x04, 0x9d, 0x5c, 0xf3, 0xb4, 0x06, - 0x96, 0x23, 0xfa, 0x41, 0x85, 0x07, 0x53, 0xa2, 0xb2, 0x42, 0x51, 0x1e, 0xcd, 0xd6, 0xe9, 0xad, 0x4e, 0xe6, 0xea, - 0x69, 0x11, 0x8d, 0x12, 0x27, 0x42, 0x8b, 0xc4, 0x89, 0x70, 0x0a, 0xe9, 0x88, 0x59, 0x51, 0xc2, 0x4f, 0xcd, 0xd5, - 0xa8, 0x25, 0x2b, 0x6f, 0x3e, 0xe5, 0x07, 0xca, 0x3c, 0x87, 0x14, 0x4d, 0x9c, 0x68, 0x9e, 0x92, 0x38, 0xe2, 0xb8, - 0x9d, 0xb1, 0x6c, 0xdf, 0xab, 0x04, 0x1d, 0x05, 0xd8, 0xdf, 0xb8, 0xb3, 0x30, 0x66, 0x61, 0x9e, 0xe8, 0x56, 0xa7, - 0xfe, 0x54, 0xb0, 0xaf, 0xca, 0x21, 0x75, 0x72, 0xb2, 0x22, 0x71, 0xee, 0x4e, 0xb5, 0xfc, 0x65, 0x4e, 0xb3, 0xdb, - 0x33, 0x0a, 0xa9, 0xce, 0x29, 0x1c, 0xf8, 0xad, 0x96, 0xa1, 0xca, 0x53, 0x1f, 0xa4, 0x42, 0x59, 0x29, 0xea, 0xe7, - 0x00, 0x57, 0x4f, 0x09, 0x16, 0x22, 0xda, 0x68, 0x38, 0x62, 0xe4, 0x6e, 0xa1, 0x5b, 0xcf, 0x4f, 0xd2, 0x1e, 0x03, - 0xff, 0x5a, 0x85, 0x69, 0x15, 0x2c, 0xc0, 0x99, 0x79, 0x26, 0x75, 0x98, 0x8f, 0x56, 0xbd, 0x32, 0x50, 0x04, 0xe1, - 0xbb, 0x74, 0xfb, 0x54, 0x37, 0x25, 0xcd, 0x6e, 0x9f, 0x6a, 0x2d, 0xe8, 0x27, 0x12, 0x7e, 0xb0, 0x1a, 0xa7, 0x3c, - 0xc1, 0xcc, 0x8a, 0x02, 0x15, 0x00, 0xde, 0x5f, 0x7a, 0x8e, 0xf3, 0x17, 0x95, 0x32, 0xe8, 0x42, 0x2c, 0xf6, 0x2c, - 0x4e, 0x35, 0x13, 0xaf, 0xc6, 0xff, 0xcb, 0xda, 0xf8, 0x7f, 0x31, 0x4e, 0x9d, 0x82, 0x69, 0x34, 0x49, 0x68, 0xa8, - 0x59, 0x27, 0x92, 0x04, 0x28, 0xf4, 0xb6, 0x8c, 0x93, 0x8f, 0x17, 0x1e, 0x68, 0x5c, 0x8b, 0x71, 0x9a, 0xf0, 0xe6, - 0xd8, 0x9f, 0xb2, 0xf8, 0xd6, 0x9b, 0xb3, 0xe6, 0x34, 0x4d, 0xd2, 0x7c, 0xe6, 0x07, 0x14, 0xe7, 0xb7, 0x39, 0xa7, - 0xd3, 0xe6, 0x9c, 0xe1, 0xe7, 0x34, 0xbe, 0xa2, 0x9c, 0x05, 0x3e, 0xb6, 0x4f, 0x32, 0xe6, 0xc7, 0xd6, 0x6b, 0x3f, - 0xcb, 0xd2, 0x6b, 0x1b, 0xbf, 0x4b, 0x2f, 0x53, 0x9e, 0xe2, 0x37, 0x37, 0xb7, 0x13, 0x9a, 0xe0, 0x0f, 0x97, 0xf3, - 0x84, 0xcf, 0x71, 0xee, 0x27, 0x79, 0x33, 0xa7, 0x19, 0x1b, 0xf7, 0x82, 0x34, 0x4e, 0xb3, 0x26, 0x64, 0x6c, 0x4f, - 0xa9, 0x17, 0xb3, 0x49, 0xc4, 0xad, 0xd0, 0xcf, 0x3e, 0xf6, 0x9a, 0xcd, 0x59, 0xc6, 0xa6, 0x7e, 0x76, 0xdb, 0x14, - 0x35, 0xbc, 0x2f, 0xdb, 0xfb, 0xfe, 0xe3, 0xf1, 0x41, 0x8f, 0x67, 0x7e, 0x92, 0x33, 0x58, 0x26, 0xcf, 0x8f, 0x63, - 0x6b, 0xff, 0xb0, 0x3d, 0xcd, 0x77, 0x64, 0x20, 0xcf, 0x4f, 0x78, 0x71, 0x81, 0xdf, 0x03, 0xdc, 0xee, 0x25, 0x4f, - 0xf0, 0xe5, 0x9c, 0xf3, 0x34, 0x59, 0x04, 0xf3, 0x2c, 0x4f, 0x33, 0x6f, 0x96, 0xb2, 0x84, 0xd3, 0xac, 0x77, 0x99, - 0x66, 0x21, 0xcd, 0x9a, 0x99, 0x1f, 0xb2, 0x79, 0xee, 0x1d, 0xcc, 0x6e, 0x7a, 0xa0, 0x59, 0x4c, 0xb2, 0x74, 0x9e, - 0x84, 0x6a, 0x2c, 0x96, 0x44, 0x34, 0x63, 0xdc, 0x7c, 0x21, 0x2e, 0x32, 0xf1, 0x62, 0x96, 0x50, 0x3f, 0x6b, 0x4e, - 0xa0, 0x31, 0x98, 0x45, 0xed, 0x90, 0x4e, 0x70, 0x36, 0xb9, 0xf4, 0x9d, 0x4e, 0xf7, 0x11, 0xd6, 0xff, 0xbb, 0x87, - 0xc8, 0x6a, 0x6f, 0x2e, 0xee, 0xb4, 0xdb, 0x7f, 0x42, 0xbd, 0x95, 0x51, 0x04, 0x40, 0x5e, 0x67, 0x76, 0x63, 0xe5, - 0x29, 0x64, 0xb4, 0x6d, 0x6a, 0xd9, 0x9b, 0xf9, 0x21, 0xe4, 0x03, 0x7b, 0xdd, 0xd9, 0x4d, 0x01, 0xb3, 0xf3, 0x64, - 0x8a, 0xa9, 0x9a, 0xa4, 0x7a, 0x5a, 0xfc, 0x56, 0x88, 0x8f, 0x36, 0x43, 0xdc, 0xd5, 0x10, 0x57, 0x58, 0x6f, 0x86, - 0xf3, 0x4c, 0xc4, 0x56, 0xbd, 0x4e, 0x2e, 0x01, 0x89, 0xd2, 0x2b, 0x9a, 0x69, 0x38, 0xc4, 0xc3, 0x6f, 0x06, 0xa3, - 0xbb, 0x19, 0x8c, 0xa3, 0x4f, 0x81, 0x91, 0x25, 0xe1, 0xa2, 0xbe, 0xae, 0x9d, 0x8c, 0x4e, 0x7b, 0x11, 0x05, 0x7a, - 0xf2, 0xba, 0xf0, 0xfb, 0x9a, 0x85, 0x3c, 0x92, 0x3f, 0x05, 0x39, 0x5f, 0xcb, 0x77, 0x87, 0xed, 0xb6, 0x7c, 0xce, - 0xd9, 0xaf, 0xd4, 0xeb, 0xb8, 0x50, 0xa1, 0xb8, 0xc0, 0x3f, 0x94, 0xa7, 0x79, 0xeb, 0xdc, 0x13, 0xff, 0xc5, 0x3c, - 0xe6, 0x6b, 0xa4, 0x28, 0x56, 0x87, 0xa2, 0x71, 0xaa, 0x65, 0xa5, 0x14, 0x3e, 0xe0, 0xb6, 0x13, 0xdc, 0x91, 0xb0, - 0x7e, 0x79, 0x8c, 0x93, 0x0d, 0xfe, 0x22, 0xf3, 0x2e, 0x3c, 0x88, 0x74, 0x18, 0xa9, 0x86, 0x59, 0x2f, 0xed, 0x93, - 0x76, 0x2f, 0x6d, 0x36, 0x91, 0x93, 0x91, 0x64, 0x98, 0xaa, 0xe4, 0x3c, 0x87, 0x0d, 0xa4, 0xb1, 0x9d, 0x23, 0x2f, - 0x83, 0xb3, 0xa6, 0xcb, 0x65, 0x15, 0x06, 0x60, 0xe2, 0xb4, 0xc6, 0x0f, 0x5c, 0x55, 0xc0, 0xb9, 0xc1, 0xc9, 0x7d, - 0x7d, 0xbd, 0x4b, 0xa2, 0x79, 0x45, 0x9c, 0x06, 0x02, 0x73, 0xee, 0xcc, 0xe7, 0x11, 0x78, 0x29, 0x4a, 0xf1, 0x53, - 0xa5, 0x30, 0xd9, 0x2d, 0x1b, 0x0d, 0x92, 0x32, 0xbf, 0x0d, 0xf2, 0xf8, 0x92, 0x02, 0x7a, 0xb9, 0xe4, 0x04, 0x7a, - 0xac, 0xfa, 0xff, 0xc0, 0x0d, 0x49, 0x9d, 0xb8, 0x2c, 0x09, 0xe2, 0x79, 0x48, 0x73, 0xd1, 0x43, 0x25, 0xce, 0xe1, - 0x6e, 0x88, 0xb2, 0x96, 0x68, 0x02, 0xbd, 0x8b, 0x6c, 0x1e, 0xa8, 0x08, 0xb7, 0xa8, 0x94, 0xcf, 0x4d, 0xf1, 0x5c, - 0xb5, 0x7d, 0x5d, 0x25, 0x8b, 0x42, 0x4b, 0x77, 0x9e, 0xb0, 0x5f, 0xe6, 0xf4, 0x9c, 0x85, 0xc6, 0xc9, 0x5d, 0x9a, - 0x04, 0x69, 0x48, 0x3f, 0xbc, 0x7b, 0x01, 0xd9, 0xee, 0x69, 0x02, 0x24, 0x96, 0x48, 0x7f, 0x17, 0xce, 0x49, 0xe2, - 0x86, 0xf4, 0x8a, 0x05, 0x74, 0x70, 0xb1, 0xbb, 0xd8, 0x58, 0x51, 0xbe, 0x46, 0x45, 0xeb, 0x02, 0xfc, 0x77, 0x12, - 0xca, 0x8b, 0xdd, 0xc5, 0x25, 0x2f, 0x5a, 0xbb, 0x8b, 0xc4, 0x0d, 0xd3, 0xa9, 0xcf, 0x12, 0xf8, 0x9d, 0x17, 0xbb, - 0x0b, 0x06, 0x3f, 0x78, 0x71, 0x51, 0x54, 0x89, 0xa2, 0x25, 0x44, 0xc6, 0x14, 0x14, 0xee, 0x3a, 0xc8, 0xfd, 0x39, - 0x65, 0x89, 0x28, 0xba, 0xab, 0x67, 0xaa, 0x7b, 0x05, 0x24, 0xff, 0x4a, 0xa4, 0xc1, 0xac, 0xcd, 0xe5, 0xf3, 0xfb, - 0x9a, 0xcb, 0x34, 0xe1, 0x4c, 0xa4, 0xc5, 0xeb, 0x70, 0x4e, 0xe4, 0xe7, 0xe7, 0x81, 0x3c, 0x89, 0x9a, 0x57, 0xa7, - 0x2e, 0x7c, 0x81, 0x58, 0x69, 0x01, 0x53, 0x69, 0xec, 0xd3, 0xed, 0x47, 0x25, 0x93, 0xbb, 0x8c, 0xbf, 0x92, 0xaa, - 0xf2, 0x74, 0x9e, 0x05, 0x10, 0xeb, 0x55, 0x2a, 0xc5, 0xba, 0x57, 0xcc, 0x16, 0xfa, 0x9b, 0x8d, 0xb9, 0x91, 0x64, - 0xcb, 0xe1, 0x4c, 0x5f, 0x75, 0x6d, 0x07, 0x15, 0xf1, 0x44, 0x58, 0x33, 0x26, 0x56, 0xef, 0x9c, 0x85, 0x10, 0x78, - 0x61, 0xa1, 0x4a, 0x58, 0xac, 0x4d, 0x12, 0x54, 0xa4, 0x50, 0x64, 0x90, 0xc2, 0x65, 0x3b, 0x59, 0xb5, 0x0a, 0x84, - 0x10, 0x19, 0xd7, 0x03, 0xe1, 0xdb, 0xec, 0xec, 0xed, 0xe5, 0xd5, 0x89, 0x36, 0xa6, 0x70, 0xbe, 0x5c, 0x72, 0xea, - 0xe4, 0xf2, 0xd4, 0x4d, 0x44, 0x40, 0x19, 0x63, 0x58, 0xbe, 0xf1, 0x32, 0x5c, 0xf6, 0xe4, 0xe5, 0x45, 0x2f, 0x12, - 0x48, 0x94, 0x28, 0x23, 0x1a, 0xa9, 0x27, 0x5a, 0x25, 0xc3, 0xe6, 0xeb, 0xf2, 0x20, 0x7f, 0x0d, 0xeb, 0xed, 0x95, - 0xc5, 0x91, 0x56, 0x55, 0xb4, 0x5a, 0x9a, 0xa7, 0x19, 0x77, 0x1c, 0x1f, 0x07, 0x88, 0xf4, 0x7d, 0x31, 0xfb, 0x63, - 0x99, 0xef, 0x31, 0x68, 0x76, 0xbc, 0x4e, 0xe9, 0x0f, 0xa9, 0x9d, 0xaf, 0x96, 0xd9, 0x66, 0xea, 0x8c, 0x2e, 0xe0, - 0x09, 0x97, 0xbf, 0x15, 0xfa, 0xaa, 0x02, 0x39, 0xbb, 0xea, 0xb9, 0x9c, 0x24, 0x56, 0x0c, 0x4d, 0x2a, 0x03, 0x4e, - 0x0d, 0xaa, 0x61, 0x3a, 0xc2, 0x6c, 0xcb, 0xd8, 0xa8, 0xa8, 0x10, 0x51, 0x6e, 0xee, 0x0b, 0xa9, 0x04, 0x9d, 0x1b, - 0xd4, 0x7d, 0xc1, 0xb4, 0x1b, 0xaf, 0x4e, 0x77, 0x85, 0x42, 0x91, 0xc1, 0x19, 0x36, 0x55, 0x93, 0xb0, 0xdc, 0x92, - 0x64, 0x23, 0xf1, 0xba, 0xf2, 0x91, 0x66, 0xa4, 0x0a, 0x28, 0xae, 0x75, 0x00, 0xc9, 0x90, 0x9b, 0x00, 0x03, 0xc7, - 0x40, 0xce, 0xf5, 0x14, 0x80, 0xc7, 0x8c, 0x29, 0x9c, 0x54, 0x52, 0x1c, 0x07, 0x2f, 0xa4, 0x76, 0xef, 0xd9, 0x6f, - 0xdf, 0x9c, 0xbd, 0xb7, 0x31, 0x5c, 0x75, 0x46, 0xb3, 0xdc, 0x5b, 0xd8, 0x2a, 0xc7, 0xb0, 0x09, 0xf1, 0x6a, 0xdb, - 0xb3, 0xfd, 0x19, 0x1c, 0xda, 0x16, 0x4c, 0xb5, 0x75, 0xd3, 0xbc, 0xbe, 0xbe, 0x6e, 0xc2, 0x89, 0xb2, 0xe6, 0x3c, - 0x8b, 0x25, 0xbb, 0x09, 0xed, 0xa2, 0x40, 0x2e, 0x8f, 0x68, 0x52, 0x5e, 0x86, 0x94, 0xc6, 0xd4, 0x8d, 0xd3, 0x89, - 0x3c, 0x0f, 0xbb, 0xea, 0x9e, 0x88, 0x2f, 0x8e, 0xc5, 0x25, 0x5f, 0xfd, 0x63, 0x2e, 0xaf, 0x57, 0xe3, 0x19, 0xfc, - 0xec, 0x43, 0xf0, 0xea, 0xb8, 0xc5, 0x23, 0xf1, 0x70, 0x06, 0xbb, 0x49, 0x3c, 0xed, 0x2e, 0xd6, 0xa8, 0x6e, 0x00, - 0x5d, 0x44, 0x7d, 0x39, 0xb5, 0x5c, 0xd4, 0xba, 0xf0, 0xe2, 0x8b, 0x8b, 0xe2, 0xb8, 0x05, 0x7d, 0xb5, 0x74, 0xbf, - 0x97, 0x69, 0x78, 0xab, 0xdb, 0x97, 0x94, 0x08, 0x97, 0x3d, 0x25, 0xa4, 0x0f, 0x5d, 0xc0, 0xb8, 0x61, 0x5f, 0xe0, - 0x4c, 0xb1, 0xd0, 0x61, 0xf5, 0x50, 0x8c, 0x2c, 0x60, 0x98, 0x05, 0x94, 0x00, 0xb9, 0x41, 0xe7, 0x61, 0xd9, 0x40, - 0xec, 0x76, 0x59, 0xb4, 0x0d, 0x40, 0x59, 0xb1, 0xda, 0x3f, 0xd2, 0xcd, 0x5d, 0x91, 0x85, 0x86, 0x38, 0x34, 0x81, - 0xbf, 0x40, 0xf0, 0xaf, 0x00, 0xfc, 0xb8, 0x25, 0xd1, 0x74, 0x61, 0x5e, 0x3b, 0x23, 0x2f, 0x84, 0x28, 0x91, 0x39, - 0xcc, 0x38, 0x7e, 0xcf, 0xf1, 0xc7, 0x0b, 0x51, 0x55, 0x6b, 0x09, 0xa0, 0xbe, 0x82, 0x36, 0xd5, 0xd6, 0xea, 0x60, - 0x90, 0xc6, 0xb1, 0x3f, 0xcb, 0xa9, 0xa7, 0x7f, 0x28, 0x85, 0x01, 0xf4, 0x8e, 0x75, 0x0d, 0x4d, 0xe5, 0x3d, 0x9d, - 0x82, 0x1e, 0xb7, 0xae, 0x3e, 0x5e, 0xf9, 0x99, 0xd3, 0x6c, 0x06, 0xcd, 0xcb, 0x09, 0x2a, 0x78, 0xb4, 0x30, 0xd5, - 0x8d, 0x87, 0xed, 0x76, 0x0f, 0x92, 0x54, 0x9b, 0x7e, 0xcc, 0x26, 0x89, 0x17, 0xd3, 0x31, 0x2f, 0x38, 0x9c, 0x1e, - 0x5c, 0x68, 0xfd, 0xce, 0xed, 0x1e, 0x66, 0x74, 0x6a, 0xb9, 0xf0, 0xf7, 0xee, 0x81, 0x0b, 0x1e, 0x7a, 0x09, 0x8f, - 0x9a, 0x22, 0x19, 0x1a, 0x8e, 0x72, 0xf0, 0xa8, 0xf6, 0xbc, 0x30, 0x06, 0x0a, 0x28, 0xe8, 0xbe, 0x05, 0xcf, 0x2c, - 0x1e, 0x61, 0x9e, 0x99, 0xf5, 0x12, 0xb4, 0x58, 0x9b, 0xc1, 0xba, 0x0a, 0xb6, 0x8f, 0x8a, 0x5c, 0x58, 0x2c, 0x8b, - 0x35, 0xbc, 0x18, 0xaa, 0x74, 0xc1, 0x92, 0xd9, 0x9c, 0x0f, 0x85, 0xe7, 0x3f, 0x83, 0x33, 0x24, 0x23, 0x6c, 0x94, - 0x00, 0x3c, 0x23, 0xd5, 0x3e, 0xf0, 0xe3, 0xc0, 0x81, 0x4e, 0xac, 0xa6, 0x75, 0x94, 0xd1, 0x29, 0xea, 0x4d, 0x59, - 0xd2, 0x94, 0xef, 0x0e, 0x0d, 0xdd, 0xcd, 0x7d, 0x04, 0x4f, 0x85, 0x2b, 0x7a, 0xc3, 0x22, 0xc1, 0x77, 0xc3, 0xbc, - 0x2e, 0x46, 0x45, 0xd1, 0x4b, 0xb9, 0x33, 0x7c, 0xe1, 0xa0, 0x11, 0xfe, 0xd5, 0xb8, 0xc4, 0xc6, 0xd6, 0x54, 0x6d, - 0xe3, 0x2e, 0xda, 0x52, 0xc5, 0xa4, 0x4b, 0x51, 0xed, 0x57, 0x02, 0x15, 0x5f, 0x3a, 0x36, 0xcd, 0x67, 0x4d, 0xc9, - 0x7e, 0x9a, 0x82, 0x7c, 0x6c, 0x68, 0x8a, 0x94, 0x3b, 0x9b, 0xd2, 0x85, 0xe0, 0x2c, 0xea, 0x1c, 0x8b, 0xf4, 0xb8, - 0x8c, 0xca, 0x73, 0x4f, 0xea, 0xd9, 0x3c, 0xe9, 0x84, 0x6a, 0x5b, 0xff, 0xe2, 0xa4, 0xce, 0xa6, 0x40, 0xfe, 0x97, - 0x77, 0xfd, 0xf9, 0x71, 0x0c, 0x03, 0x5e, 0x68, 0xa5, 0xc1, 0xbc, 0x1a, 0x65, 0xc8, 0x47, 0x0e, 0x2a, 0xd4, 0x9e, - 0x79, 0x22, 0xf4, 0x6e, 0xe3, 0x82, 0xc1, 0x1d, 0xae, 0x23, 0x6a, 0xf2, 0x04, 0x33, 0x83, 0x9c, 0x80, 0x5a, 0xee, - 0x78, 0xaf, 0x62, 0x33, 0x52, 0x6b, 0xb7, 0xc4, 0x84, 0x88, 0x9d, 0x25, 0xa1, 0x6d, 0xfd, 0x39, 0x88, 0x59, 0xf0, - 0x91, 0xd8, 0xbb, 0x0b, 0x07, 0xad, 0x1f, 0x0d, 0x15, 0x3b, 0x54, 0xf3, 0x5c, 0x54, 0x8f, 0x36, 0xa4, 0xae, 0xc1, - 0x4e, 0xe5, 0xed, 0x41, 0x76, 0x1f, 0x54, 0x9b, 0xe3, 0x96, 0x1c, 0xa7, 0x7f, 0x51, 0x9c, 0x57, 0xb7, 0x82, 0x55, - 0x50, 0x00, 0x9a, 0x65, 0xb9, 0x25, 0xe8, 0x8f, 0xd8, 0x72, 0x0b, 0xd5, 0x2c, 0x40, 0x6c, 0xd2, 0x3e, 0xb2, 0x2d, - 0xc9, 0x60, 0x00, 0x4e, 0xae, 0x78, 0x8d, 0x6d, 0xfd, 0xb9, 0x2c, 0xa3, 0xa5, 0xdb, 0x47, 0xe4, 0xad, 0x10, 0x1b, - 0xc6, 0x02, 0x5b, 0xdf, 0x0d, 0x29, 0xf7, 0x59, 0x2c, 0x9b, 0xf4, 0xb4, 0x97, 0x62, 0x65, 0x46, 0xcb, 0x65, 0x5e, - 0x9f, 0x0b, 0xab, 0x63, 0x50, 0xcc, 0xec, 0xb8, 0x55, 0xc1, 0x2d, 0x66, 0x26, 0xf6, 0x87, 0x19, 0x3f, 0xad, 0x66, - 0x28, 0xdf, 0x59, 0x7f, 0x0e, 0xc4, 0xc9, 0x2a, 0x00, 0x30, 0x53, 0x00, 0x42, 0x64, 0x5f, 0x2a, 0x21, 0x8e, 0x4f, - 0x32, 0x97, 0xfb, 0xd9, 0x84, 0xf2, 0x15, 0xc4, 0xfa, 0x32, 0x91, 0xb7, 0xa7, 0xa3, 0xf8, 0x6b, 0xd0, 0x06, 0x75, - 0x68, 0x41, 0xcf, 0x2d, 0x06, 0xa0, 0xaa, 0x92, 0x8d, 0x1a, 0x6f, 0x84, 0x40, 0xf6, 0x89, 0xc5, 0x91, 0xdc, 0x3e, - 0x13, 0xdc, 0x5e, 0xc6, 0xe1, 0x2c, 0x31, 0x96, 0x00, 0xb1, 0xb0, 0xad, 0x81, 0x84, 0x9c, 0x86, 0x12, 0x66, 0x92, - 0x8a, 0x56, 0x59, 0x71, 0xdc, 0x92, 0xb5, 0x25, 0x3b, 0x96, 0x95, 0x00, 0x09, 0x62, 0x9f, 0x56, 0x38, 0x80, 0xe4, - 0x6f, 0x13, 0x0f, 0x21, 0xbb, 0x2a, 0x89, 0x4d, 0x9c, 0x31, 0xeb, 0x1f, 0xc7, 0xfe, 0x25, 0x8d, 0xfb, 0xbb, 0x8b, - 0x74, 0xb9, 0x6c, 0x17, 0xc7, 0x2d, 0xf9, 0x68, 0x1d, 0x0b, 0xbe, 0x21, 0xef, 0x06, 0x15, 0x4b, 0x0c, 0x07, 0x37, - 0x21, 0x25, 0x56, 0xe7, 0x82, 0x79, 0xaa, 0x83, 0xc2, 0xb6, 0x44, 0x16, 0x8a, 0xa8, 0x54, 0xea, 0x34, 0x85, 0x6d, - 0xb1, 0x70, 0xbd, 0x2c, 0xe7, 0x74, 0x06, 0xa5, 0xd1, 0x72, 0xd9, 0x29, 0x6c, 0x6b, 0xca, 0x12, 0x78, 0x4a, 0x97, - 0x4b, 0x71, 0x26, 0x72, 0xca, 0x12, 0xa7, 0x0d, 0x64, 0x6b, 0x5b, 0x53, 0xff, 0x46, 0x4c, 0x58, 0xbf, 0xf1, 0x6f, - 0x9c, 0x8e, 0x7a, 0xe5, 0x96, 0xf8, 0xc9, 0x81, 0xe2, 0xaa, 0x15, 0xf5, 0xd5, 0x8a, 0x86, 0x78, 0x2e, 0x4f, 0x7b, - 0x11, 0x27, 0x24, 0xfe, 0xe6, 0x15, 0x0d, 0xf5, 0x8a, 0xce, 0xb7, 0xac, 0xe8, 0xfc, 0x8e, 0x15, 0x0d, 0xd4, 0xea, - 0x59, 0x25, 0xee, 0xb2, 0xe5, 0xb2, 0xd3, 0xae, 0xb0, 0x77, 0xdc, 0x0a, 0xd9, 0x15, 0xac, 0x06, 0x68, 0x6a, 0x9c, - 0x4d, 0xe9, 0x66, 0xa2, 0xac, 0xa3, 0x98, 0x7e, 0x16, 0x26, 0x2b, 0x2c, 0xa4, 0x75, 0x2c, 0x98, 0x74, 0x5d, 0x06, - 0x26, 0xff, 0x48, 0xca, 0x66, 0x80, 0x87, 0x1c, 0xf0, 0x10, 0xe9, 0xbb, 0x42, 0x1d, 0xfb, 0xbd, 0x8d, 0x6d, 0xcb, - 0xd6, 0x64, 0x7d, 0x51, 0x9c, 0x83, 0x8c, 0x10, 0xf3, 0xbb, 0x17, 0x2d, 0x42, 0x6d, 0xbb, 0xbf, 0x9d, 0xe6, 0x20, - 0x87, 0xe0, 0x3a, 0xcd, 0x42, 0xdb, 0x93, 0x55, 0x3f, 0x0b, 0x55, 0x53, 0x96, 0xa8, 0x8c, 0xb4, 0xad, 0xb4, 0x56, - 0xbd, 0x37, 0x29, 0xae, 0x7b, 0x78, 0x28, 0x6b, 0xcc, 0x7c, 0xce, 0x69, 0x96, 0x28, 0xca, 0xb5, 0xed, 0xff, 0x10, - 0x54, 0xb8, 0x81, 0xaf, 0x04, 0x7a, 0x01, 0x34, 0x01, 0x2a, 0x9d, 0x5b, 0xf1, 0x7c, 0x29, 0x9e, 0x76, 0x2a, 0x65, - 0xf3, 0x16, 0x99, 0x7a, 0xbf, 0x2c, 0x02, 0x33, 0x64, 0x3e, 0xa5, 0xe1, 0xb9, 0x60, 0xd0, 0x83, 0xf8, 0x42, 0x29, - 0x8f, 0x2b, 0xe2, 0xae, 0x6a, 0x80, 0xed, 0x9f, 0xe6, 0xdd, 0x47, 0x07, 0xa7, 0x36, 0x96, 0x3c, 0x3e, 0x1d, 0x8f, - 0x6d, 0x54, 0x58, 0xf7, 0x6b, 0xd6, 0x39, 0xf8, 0x69, 0xfe, 0xf5, 0xb3, 0xf6, 0xd7, 0x65, 0xe3, 0x04, 0x88, 0x48, - 0x25, 0x41, 0x68, 0x51, 0x65, 0xc0, 0xab, 0x67, 0x34, 0xf6, 0x93, 0xed, 0xd3, 0x19, 0x9a, 0xd3, 0xc9, 0x67, 0x94, - 0x86, 0x40, 0x9c, 0x78, 0xad, 0xf4, 0x3c, 0xa6, 0x57, 0x54, 0xdf, 0xd0, 0xb8, 0x61, 0xb0, 0x0d, 0x2d, 0x82, 0x74, - 0x9e, 0x70, 0x95, 0x0d, 0xa2, 0x58, 0xad, 0x31, 0xa5, 0x0b, 0x31, 0x07, 0x53, 0x9d, 0xbf, 0x95, 0x72, 0xae, 0x2e, - 0xbd, 0x8a, 0x0b, 0x6c, 0x1b, 0x00, 0x6c, 0x85, 0x6c, 0xb0, 0xa5, 0xdc, 0x6b, 0xe3, 0xf6, 0x36, 0xd8, 0x70, 0x07, - 0x79, 0xb6, 0x3d, 0xd2, 0x78, 0x12, 0x0e, 0xdd, 0xda, 0xa5, 0x1a, 0x5b, 0xf1, 0xf5, 0x49, 0x0c, 0x5c, 0x66, 0xd0, - 0x59, 0x42, 0xf3, 0x7c, 0x2b, 0x02, 0xca, 0x45, 0xc4, 0x76, 0x55, 0xdb, 0xde, 0xd2, 0x0b, 0x6e, 0x63, 0xd8, 0x61, - 0x02, 0xe0, 0x32, 0xac, 0xac, 0x6a, 0xd1, 0xf1, 0x98, 0x06, 0xa5, 0x3f, 0x1c, 0x02, 0x84, 0x63, 0x16, 0x73, 0x88, - 0x93, 0x89, 0x00, 0x96, 0xfd, 0x3a, 0x4d, 0xa8, 0x8d, 0x74, 0xca, 0xab, 0x82, 0x5f, 0xc9, 0xff, 0xcd, 0xf0, 0xc8, - 0x1e, 0xeb, 0xb0, 0xa8, 0x51, 0x96, 0x4b, 0xed, 0xae, 0xa9, 0x95, 0xd7, 0x11, 0x99, 0x0a, 0x7f, 0xcc, 0xb6, 0x0d, - 0x74, 0xbf, 0x6d, 0xb2, 0xe8, 0x7c, 0x7d, 0xd8, 0x69, 0x17, 0x36, 0xb6, 0xa1, 0xbb, 0xfb, 0xee, 0x12, 0xd1, 0x6a, - 0x1f, 0x5a, 0xcd, 0x93, 0xcf, 0x69, 0xd7, 0xed, 0x3c, 0xee, 0xd8, 0x58, 0xde, 0xb5, 0x80, 0x8a, 0x92, 0x19, 0x04, - 0xe0, 0x21, 0xfe, 0xdd, 0x53, 0xa9, 0x77, 0x7e, 0x3f, 0x78, 0x1e, 0x76, 0xda, 0x36, 0xb6, 0x73, 0x9e, 0xce, 0x3e, - 0x63, 0x0a, 0xfb, 0x36, 0xb6, 0x83, 0x38, 0xcd, 0xa9, 0x39, 0x07, 0xa9, 0xce, 0xfe, 0xfe, 0x49, 0x48, 0x88, 0x66, - 0x19, 0xcd, 0x73, 0xcb, 0xec, 0x5f, 0x91, 0xd2, 0x27, 0x18, 0xe6, 0x46, 0x8a, 0xcb, 0x29, 0x17, 0x78, 0x91, 0xd7, - 0x20, 0x98, 0x54, 0x25, 0xcb, 0xd6, 0x88, 0x4d, 0x88, 0x80, 0x92, 0xb1, 0x49, 0xed, 0xea, 0x93, 0x23, 0x6f, 0xd8, - 0x7a, 0x72, 0x60, 0x19, 0x38, 0x5f, 0x1f, 0xa0, 0x56, 0x32, 0x65, 0xc9, 0xf9, 0x86, 0x52, 0xff, 0x66, 0x43, 0x29, - 0xa8, 0x6c, 0x25, 0x74, 0xea, 0x8a, 0x9e, 0x4f, 0x63, 0xbd, 0x52, 0x7c, 0x4c, 0x10, 0x43, 0xe1, 0x7f, 0xfc, 0x04, - 0xa4, 0xc6, 0x32, 0x88, 0x1e, 0x7e, 0xfb, 0x70, 0x50, 0xf2, 0x39, 0xc3, 0x95, 0xbd, 0xfc, 0xbe, 0x19, 0x42, 0x69, - 0x13, 0x9c, 0xfc, 0xf1, 0x67, 0xcd, 0x95, 0xde, 0x7c, 0x9a, 0xe0, 0x0c, 0xad, 0xea, 0x77, 0x2c, 0xbd, 0x3a, 0xea, - 0xbf, 0xba, 0xf6, 0x1b, 0x8a, 0x95, 0xe2, 0x53, 0xae, 0x7f, 0x10, 0xb3, 0x69, 0x45, 0x02, 0xeb, 0x60, 0x0a, 0x8d, - 0x07, 0x32, 0xbe, 0xcc, 0x4e, 0xa4, 0xea, 0x73, 0x0e, 0xe7, 0x58, 0xe1, 0xaa, 0x90, 0x79, 0x46, 0xcf, 0xe3, 0xf4, - 0x7a, 0xf5, 0xf2, 0xb3, 0xed, 0x95, 0x23, 0x36, 0x89, 0x8c, 0xc3, 0x69, 0x94, 0x94, 0x8b, 0x70, 0xe7, 0x00, 0xc5, - 0xbf, 0xfc, 0xb3, 0xeb, 0xfe, 0xcb, 0x3f, 0x7f, 0xb2, 0x2a, 0x74, 0x5f, 0x5c, 0x60, 0x5e, 0x75, 0xbb, 0x7d, 0x77, - 0x6d, 0x1e, 0xa9, 0x8e, 0xf3, 0xcd, 0x75, 0xd6, 0x16, 0x01, 0xde, 0xaf, 0x2d, 0xc1, 0x5a, 0xa1, 0xdc, 0x7d, 0xd6, - 0x6f, 0x01, 0x0c, 0xe6, 0xf5, 0x49, 0xc8, 0xa0, 0xd2, 0xef, 0x02, 0xed, 0x02, 0x79, 0xf7, 0x5a, 0x91, 0xdf, 0x8e, - 0xe1, 0x4f, 0xcd, 0xe1, 0x77, 0x82, 0xaf, 0xfc, 0x13, 0xf1, 0xc5, 0x45, 0x99, 0x85, 0x68, 0x36, 0x85, 0x3b, 0x0e, - 0x06, 0x6b, 0x25, 0x4a, 0xf1, 0xf0, 0xda, 0xa8, 0x2f, 0xce, 0x50, 0x92, 0xf8, 0xe2, 0x15, 0x5c, 0x6c, 0x74, 0x7c, - 0x99, 0x69, 0x67, 0xeb, 0x1d, 0xc2, 0x01, 0xba, 0xa8, 0xcf, 0x4a, 0x74, 0xba, 0x26, 0x19, 0xa0, 0x14, 0xcc, 0x0d, - 0x00, 0x13, 0xc7, 0x17, 0xca, 0xda, 0x3c, 0x95, 0x6e, 0x18, 0x6f, 0x95, 0xb4, 0x95, 0x7b, 0xa6, 0x86, 0x74, 0x6c, - 0xbd, 0x17, 0xf8, 0x12, 0x95, 0x69, 0x65, 0xdd, 0x0b, 0x57, 0x17, 0xd8, 0x11, 0x25, 0xfb, 0xb9, 0xf2, 0xe3, 0xab, - 0xfb, 0x31, 0xbe, 0xed, 0x02, 0x75, 0x69, 0x2d, 0xff, 0xd1, 0x2a, 0xc1, 0xb2, 0xb9, 0xdc, 0xa4, 0x0f, 0x5c, 0xfb, - 0x9c, 0x66, 0xe7, 0x11, 0x24, 0x42, 0x65, 0x9f, 0x60, 0x4e, 0xb0, 0xd2, 0x98, 0x8a, 0xbf, 0x8c, 0xa8, 0x8b, 0xa4, - 0xff, 0x41, 0x9c, 0x8a, 0x41, 0x16, 0x23, 0x0c, 0x65, 0x2c, 0xc2, 0xff, 0xe7, 0x5b, 0xff, 0x61, 0xf8, 0xd6, 0xdd, - 0x43, 0xd4, 0xce, 0x48, 0x7f, 0xf6, 0x42, 0xfe, 0xc7, 0x66, 0x77, 0xb9, 0x60, 0x77, 0xbf, 0x81, 0xd1, 0xe5, 0xff, - 0x18, 0x46, 0x27, 0x6c, 0x64, 0xcd, 0xe9, 0xd6, 0x42, 0xcd, 0xb7, 0xae, 0x7f, 0xed, 0xdf, 0x56, 0xfb, 0x2a, 0xbe, - 0x38, 0xb9, 0xf6, 0x6f, 0xab, 0x45, 0xd8, 0xce, 0x2e, 0x56, 0xfb, 0x18, 0xd8, 0x6f, 0x5e, 0xdb, 0x9e, 0xfd, 0xe6, - 0xeb, 0xaf, 0x6d, 0x7c, 0x91, 0x53, 0x3e, 0x80, 0x42, 0xb2, 0xbb, 0xd8, 0x59, 0xad, 0x08, 0x6e, 0x14, 0x98, 0xa2, - 0x08, 0x7b, 0xe1, 0xac, 0x06, 0x0c, 0xcb, 0xcf, 0xd3, 0xc4, 0x84, 0xe6, 0x2d, 0x58, 0xf6, 0x9f, 0x0b, 0x8e, 0xe8, - 0x65, 0x0d, 0x1e, 0x51, 0xba, 0x0a, 0x90, 0x28, 0xac, 0x41, 0x54, 0x5d, 0x19, 0x74, 0x37, 0xff, 0xaf, 0xae, 0x45, - 0x90, 0xb7, 0x7d, 0x44, 0x83, 0xf8, 0xe2, 0x73, 0xc4, 0x87, 0x1c, 0xac, 0xf2, 0xd8, 0x69, 0x77, 0xa7, 0x5f, 0xec, - 0x2e, 0xa2, 0xbd, 0x3d, 0x36, 0xb0, 0xb1, 0xb8, 0xa7, 0xa9, 0xd8, 0x24, 0x5c, 0x72, 0xf8, 0x93, 0xc2, 0x9f, 0xac, - 0x62, 0xd4, 0x2c, 0x19, 0x67, 0x7e, 0x46, 0xc3, 0xed, 0x4c, 0xba, 0xbc, 0xdf, 0x48, 0x91, 0x86, 0x4c, 0xc0, 0xce, - 0xcf, 0x45, 0xea, 0xd1, 0x94, 0x81, 0x3e, 0xba, 0x63, 0x7e, 0xc5, 0x47, 0x5d, 0x88, 0x56, 0x7e, 0x04, 0xc0, 0x44, - 0x38, 0x25, 0x79, 0x99, 0xeb, 0x00, 0xb7, 0x6a, 0xaa, 0xec, 0x10, 0x6c, 0x23, 0xe1, 0x75, 0x0f, 0x49, 0x5f, 0xa4, - 0x3d, 0xbc, 0x48, 0xb8, 0x13, 0xba, 0x3c, 0x63, 0x53, 0x07, 0xe1, 0x4e, 0x1b, 0x21, 0xed, 0x6c, 0x08, 0x49, 0x7f, - 0x87, 0xe5, 0xaf, 0xfd, 0xd7, 0x4e, 0x28, 0x2e, 0xe2, 0x12, 0x9f, 0xee, 0x81, 0x43, 0x92, 0x4f, 0xe6, 0xe3, 0x31, - 0xcd, 0x1c, 0x7d, 0x00, 0xf0, 0xab, 0x03, 0x38, 0x63, 0x0c, 0x6f, 0x9f, 0xfa, 0xdc, 0xff, 0x96, 0xd1, 0x6b, 0x27, - 0x43, 0xbd, 0xb4, 0xba, 0x9c, 0x31, 0xc4, 0x73, 0x44, 0xfa, 0x11, 0x24, 0xc6, 0xbf, 0x48, 0xf8, 0x7e, 0xd7, 0x99, - 0x7f, 0x75, 0x80, 0x43, 0xb8, 0xf2, 0x42, 0x67, 0x75, 0xcb, 0xbb, 0x4a, 0x3e, 0xb0, 0x84, 0x1f, 0xc9, 0x63, 0x98, - 0x19, 0x52, 0xee, 0xc3, 0x32, 0x23, 0xc6, 0xf2, 0xcb, 0x0e, 0x43, 0xd2, 0x0f, 0x1a, 0x44, 0x1e, 0xca, 0x14, 0xb7, - 0xec, 0x9e, 0x46, 0x7e, 0x76, 0x0a, 0x07, 0xbe, 0x01, 0xd0, 0x4b, 0x9e, 0xfa, 0x4e, 0x50, 0x7e, 0xc9, 0xc9, 0x69, - 0xfd, 0xd4, 0x68, 0x4d, 0xb0, 0x48, 0x8a, 0xa9, 0x8a, 0x5a, 0x50, 0x74, 0x6e, 0x16, 0x91, 0xc6, 0x6e, 0x0b, 0xc3, - 0x1e, 0xec, 0x6d, 0xf4, 0xd1, 0xea, 0xa5, 0x6b, 0x5e, 0x67, 0xfe, 0xac, 0x8c, 0x1b, 0x9c, 0xfa, 0x59, 0xc6, 0x68, - 0x66, 0x39, 0xcf, 0x7f, 0x45, 0xde, 0xbf, 0xfc, 0xf3, 0xe6, 0xf8, 0x81, 0x0a, 0x19, 0x58, 0x90, 0x5c, 0xd2, 0x14, - 0xe9, 0xd8, 0xc4, 0x0e, 0x64, 0x43, 0x5b, 0x87, 0x3b, 0xf6, 0x8f, 0xda, 0xed, 0xb6, 0x0a, 0x09, 0x74, 0xe4, 0x4f, - 0x88, 0x01, 0xc0, 0x4f, 0x78, 0x10, 0x51, 0x65, 0x62, 0xcb, 0x00, 0xe5, 0x51, 0x7b, 0x76, 0x63, 0xf7, 0x61, 0x3b, - 0x28, 0x28, 0xde, 0xd1, 0x19, 0xf5, 0xf9, 0x67, 0x8d, 0x9f, 0x89, 0x26, 0xe5, 0xf0, 0x1d, 0x3d, 0x74, 0x35, 0xee, - 0xca, 0xa0, 0x87, 0xab, 0x83, 0xbe, 0x67, 0x53, 0x71, 0x75, 0xd3, 0xb6, 0x51, 0x85, 0xa7, 0xba, 0x36, 0x26, 0x97, - 0x2d, 0x6c, 0x4b, 0x60, 0x3c, 0x4a, 0xe3, 0x90, 0x66, 0xc4, 0xa6, 0xee, 0xc4, 0xb5, 0x1e, 0xb7, 0xdb, 0x6d, 0xdc, - 0x3c, 0x38, 0x6c, 0xb7, 0xf1, 0xe1, 0xc3, 0x36, 0x6e, 0xc2, 0x1f, 0xd7, 0x75, 0x57, 0x60, 0xb8, 0x2b, 0x6a, 0xdb, - 0x69, 0x67, 0x74, 0xaa, 0x00, 0xbc, 0x33, 0xac, 0x58, 0xed, 0x09, 0xb8, 0x60, 0x5a, 0xed, 0x7b, 0x29, 0xd9, 0xd4, - 0x05, 0x07, 0x2a, 0x1d, 0x55, 0xf8, 0x0b, 0xb3, 0x2a, 0x68, 0x4a, 0xe5, 0xc5, 0x7f, 0x2f, 0x14, 0x21, 0x78, 0xd6, - 0x29, 0xdc, 0x5e, 0x2a, 0xe2, 0xa5, 0x90, 0x0a, 0x04, 0x1f, 0x48, 0xe3, 0x3e, 0x4b, 0xe0, 0xdb, 0x59, 0x3a, 0x6a, - 0xaa, 0x19, 0x55, 0xb6, 0x92, 0x74, 0xfb, 0x40, 0x86, 0xa5, 0x37, 0x11, 0xc4, 0xe8, 0x01, 0xc2, 0xfe, 0x7d, 0x1a, - 0xa8, 0x15, 0x84, 0xfa, 0xc1, 0x7d, 0xea, 0x6b, 0xec, 0x8f, 0x1e, 0x88, 0xe4, 0xa4, 0x9d, 0x68, 0xb9, 0xdc, 0xf1, - 0x97, 0xcb, 0x9d, 0xe0, 0xfe, 0x33, 0x94, 0xcb, 0xab, 0x4f, 0x41, 0xc0, 0xcd, 0x9f, 0x12, 0xe8, 0x17, 0x50, 0xee, - 0x45, 0x58, 0x82, 0x24, 0x9f, 0x7c, 0xac, 0x06, 0x94, 0x8f, 0x41, 0xb1, 0x82, 0x94, 0x90, 0x44, 0xd2, 0x3e, 0x5f, - 0x2e, 0x15, 0xf1, 0xe3, 0x39, 0xf1, 0xcb, 0xa2, 0x8e, 0x8d, 0x67, 0x24, 0x28, 0x1f, 0x6d, 0x01, 0xf2, 0x4c, 0x71, - 0xa9, 0x0a, 0xe2, 0x6b, 0x3f, 0x4b, 0x4c, 0x80, 0x5f, 0xa7, 0x96, 0x1a, 0xd6, 0x9a, 0x65, 0xe9, 0x15, 0x83, 0xe4, - 0x97, 0x95, 0x81, 0xa7, 0x04, 0x2e, 0xfe, 0xea, 0x99, 0xa1, 0x70, 0xa3, 0x83, 0xf7, 0x9a, 0xcf, 0xc2, 0x2d, 0x93, - 0xe5, 0x04, 0xbd, 0x50, 0xcd, 0xcd, 0x9b, 0xeb, 0x69, 0xbd, 0xf3, 0xaf, 0xbd, 0x99, 0x7e, 0x78, 0x26, 0xf3, 0x6c, - 0xbc, 0x69, 0x79, 0xb2, 0xe6, 0x2d, 0x79, 0x0d, 0xb1, 0x1f, 0x5b, 0xf3, 0x6d, 0xb8, 0x67, 0x53, 0xf2, 0xb8, 0x77, - 0x2f, 0xcf, 0xa8, 0x9f, 0x05, 0xd1, 0x5b, 0x3f, 0xf3, 0xa7, 0x79, 0x6f, 0xac, 0x6f, 0xf1, 0xd2, 0x14, 0x70, 0x3e, - 0x16, 0x99, 0x4e, 0x49, 0x70, 0x6b, 0xe3, 0x10, 0xe1, 0xea, 0xbd, 0x84, 0x40, 0xfa, 0xb9, 0x6d, 0x3c, 0x37, 0x5f, - 0xc1, 0x3a, 0xdb, 0x78, 0x8a, 0xb0, 0x4c, 0x20, 0x7a, 0xfb, 0x47, 0xa6, 0x0e, 0x61, 0xc8, 0x75, 0xf1, 0xc6, 0x6e, - 0xf5, 0x95, 0x3b, 0x9d, 0x4c, 0xf4, 0x7e, 0x25, 0x99, 0x68, 0x03, 0x1a, 0xad, 0x8c, 0xe6, 0xb3, 0x34, 0xc9, 0xa9, - 0x8d, 0xdf, 0x43, 0x3b, 0x79, 0x15, 0xb3, 0xd9, 0x70, 0x8d, 0xe6, 0xca, 0xa6, 0xe2, 0x8d, 0x6c, 0x07, 0x41, 0x9d, - 0xf7, 0xdf, 0x97, 0x71, 0x7c, 0x1d, 0xdf, 0x11, 0x89, 0xe8, 0x8c, 0x6e, 0xc9, 0x95, 0xcd, 0xe9, 0x27, 0x73, 0x65, - 0xe3, 0x7b, 0xe5, 0xca, 0xe6, 0xf4, 0x8f, 0xce, 0x95, 0x65, 0xd4, 0xc8, 0x95, 0x05, 0x39, 0xf7, 0xf5, 0xbd, 0x52, - 0x2e, 0x75, 0x26, 0x5c, 0x7a, 0x9d, 0x93, 0x8e, 0x8a, 0x81, 0xc4, 0xe9, 0x04, 0xf2, 0x2d, 0xff, 0xf1, 0xe9, 0x93, - 0x71, 0x3a, 0x31, 0x93, 0x27, 0xe1, 0xc3, 0x24, 0x40, 0x76, 0x38, 0x25, 0x0b, 0xfb, 0xa7, 0x9b, 0xce, 0x93, 0x61, - 0xa7, 0xb7, 0xdf, 0x99, 0xda, 0x9e, 0x0d, 0x4e, 0x47, 0x51, 0xd0, 0xee, 0xed, 0xef, 0x43, 0xc1, 0xb5, 0x51, 0xd0, - 0x85, 0x02, 0x66, 0x14, 0x1c, 0x42, 0x41, 0x60, 0x14, 0x3c, 0x84, 0x82, 0xd0, 0x28, 0x78, 0x04, 0x05, 0x57, 0x76, - 0x31, 0x64, 0x65, 0x42, 0xf0, 0x23, 0x24, 0x6e, 0x30, 0xdc, 0x49, 0xeb, 0xa7, 0xb7, 0x23, 0xa2, 0xab, 0x3c, 0x2a, - 0x6f, 0x7e, 0x68, 0x1e, 0xe8, 0x8b, 0x0a, 0x2f, 0xbe, 0xb8, 0x00, 0xd6, 0x0a, 0x17, 0xb1, 0x60, 0x88, 0x49, 0xca, - 0x9a, 0xfb, 0xfa, 0xb5, 0xed, 0x95, 0x59, 0xb3, 0x6d, 0xdc, 0xd5, 0x79, 0xb3, 0x9e, 0x8d, 0x04, 0x5f, 0x92, 0x2f, - 0x0e, 0x1b, 0xa1, 0xea, 0x16, 0xee, 0x00, 0xac, 0x2e, 0xe0, 0xdc, 0x47, 0x78, 0xaa, 0x15, 0x20, 0xea, 0xc0, 0x07, - 0x18, 0xde, 0xb3, 0x29, 0xd5, 0xfb, 0x45, 0x0f, 0x60, 0x89, 0xcc, 0xe2, 0x5e, 0x54, 0x29, 0x46, 0x6f, 0xf1, 0xb8, - 0xba, 0xf3, 0xf5, 0x3d, 0x91, 0x77, 0xe8, 0xa5, 0x58, 0x86, 0xb9, 0x66, 0x98, 0xfb, 0x13, 0x0f, 0x52, 0x28, 0x21, - 0x63, 0xc4, 0x1b, 0x13, 0x42, 0xda, 0x83, 0xb9, 0xf7, 0x16, 0x5f, 0x47, 0x34, 0xf1, 0xa6, 0x45, 0xaf, 0x5c, 0x7f, - 0x99, 0xd2, 0xf9, 0xbe, 0xbc, 0x28, 0x5c, 0xd0, 0x44, 0xf5, 0x56, 0x42, 0xd9, 0x2c, 0x69, 0x67, 0x4b, 0xce, 0x9f, - 0xa1, 0xec, 0x8c, 0xe3, 0xf4, 0xba, 0x09, 0xe2, 0x7e, 0x63, 0x1e, 0x20, 0xcc, 0xad, 0xcc, 0x03, 0x7c, 0x09, 0xb0, - 0x96, 0x4f, 0xef, 0xfd, 0x49, 0xf9, 0xfb, 0x15, 0xcd, 0x73, 0x7f, 0xa2, 0x6a, 0x6e, 0xcf, 0xfb, 0x13, 0x20, 0x9a, - 0x39, 0x7f, 0x1a, 0x08, 0x48, 0xce, 0x03, 0x84, 0x40, 0x40, 0x57, 0xe5, 0xea, 0xc1, 0xcc, 0xeb, 0x69, 0x7e, 0x02, - 0x55, 0xf5, 0x22, 0xee, 0x4f, 0xaa, 0x82, 0xe3, 0x59, 0x46, 0x55, 0x02, 0x21, 0x60, 0xb1, 0x38, 0x6e, 0x41, 0x81, - 0x7c, 0xbd, 0x25, 0x9d, 0x4f, 0x73, 0x97, 0xed, 0x49, 0x7d, 0x96, 0x4e, 0xe7, 0x33, 0x4f, 0xa6, 0x94, 0xc7, 0x52, - 0xd6, 0x33, 0xf2, 0xbe, 0xec, 0x04, 0xf0, 0x9f, 0x3a, 0x78, 0xf1, 0xe5, 0x78, 0x3c, 0xbe, 0x33, 0xbd, 0xef, 0xcb, - 0x70, 0x4c, 0xbb, 0xf4, 0xb0, 0x07, 0xa7, 0x16, 0x9a, 0x2a, 0x11, 0xad, 0x53, 0x08, 0xdc, 0x2d, 0xee, 0x57, 0x19, - 0x72, 0xd6, 0x78, 0xb4, 0xb8, 0x7f, 0xaa, 0x5f, 0x31, 0xcb, 0xe8, 0x62, 0xea, 0x67, 0x13, 0x96, 0x78, 0xed, 0xc2, - 0xbd, 0x5a, 0x28, 0x50, 0x8f, 0x8e, 0x8e, 0x0a, 0x37, 0xd4, 0x4f, 0xed, 0x30, 0x2c, 0xdc, 0x60, 0x51, 0x4e, 0xa3, - 0xdd, 0x1e, 0x8f, 0x0b, 0x97, 0xe9, 0x82, 0xfd, 0x6e, 0x10, 0xee, 0x77, 0x0b, 0xf7, 0xda, 0xa8, 0x51, 0xb8, 0x54, - 0x3d, 0x65, 0x34, 0xac, 0x1d, 0x7d, 0x78, 0xd4, 0x6e, 0x17, 0xae, 0x24, 0xb4, 0x05, 0xc4, 0xe4, 0xe4, 0x4f, 0xcf, - 0x9f, 0xf3, 0xb4, 0xb8, 0x28, 0x8a, 0x5e, 0xcc, 0x9d, 0xe1, 0xae, 0xba, 0x56, 0x52, 0x7e, 0x87, 0xb1, 0x40, 0x23, - 0xfc, 0xb5, 0x99, 0x39, 0x07, 0xc4, 0x2c, 0x32, 0xe6, 0x62, 0x9d, 0x58, 0x57, 0x7b, 0x0d, 0x94, 0x25, 0x5e, 0x7f, - 0x4d, 0xe2, 0x2a, 0xa1, 0x0e, 0xf8, 0x18, 0xd4, 0x94, 0xb7, 0x9f, 0x27, 0xdb, 0xa4, 0x47, 0xf6, 0x69, 0xe9, 0x71, - 0x79, 0x1f, 0xe1, 0x91, 0xfd, 0xe1, 0xc2, 0x23, 0x31, 0x85, 0x87, 0x64, 0x1d, 0xd7, 0x9c, 0xd8, 0x41, 0x44, 0x83, - 0x8f, 0x97, 0xe9, 0x4d, 0x13, 0xb6, 0x44, 0x66, 0x0b, 0xb1, 0x72, 0xf5, 0x5b, 0x33, 0xf9, 0x75, 0x67, 0xc6, 0x47, - 0x1c, 0x85, 0x8e, 0xff, 0x26, 0x21, 0xf6, 0x1b, 0x1d, 0xd8, 0x93, 0x25, 0xe3, 0x31, 0xb1, 0xdf, 0x8c, 0xc7, 0xb6, - 0xbe, 0x1c, 0xc7, 0xe7, 0x54, 0xd4, 0x7a, 0x5d, 0x2b, 0x11, 0xb5, 0xc0, 0xd0, 0xaf, 0xca, 0xcc, 0x02, 0x95, 0x77, - 0x67, 0xe6, 0xd8, 0xa9, 0x37, 0x21, 0xcb, 0x61, 0xab, 0xc1, 0xb7, 0x25, 0xeb, 0x97, 0xf3, 0x27, 0xb5, 0x2f, 0x29, - 0x95, 0x00, 0x6f, 0xf8, 0xfc, 0xd3, 0xea, 0xcd, 0x70, 0x13, 0xaa, 0x55, 0xfc, 0x27, 0xb7, 0x2f, 0x42, 0xe7, 0x9a, - 0xa3, 0x82, 0xe5, 0x6f, 0x92, 0x95, 0x5b, 0x1f, 0x24, 0x8c, 0x84, 0x98, 0xd3, 0x2a, 0x78, 0x3a, 0x99, 0xc4, 0xe2, - 0x30, 0x49, 0xcd, 0xe0, 0x96, 0xcd, 0x07, 0xb5, 0xf9, 0x7a, 0x66, 0x43, 0xf5, 0x79, 0x0d, 0xf1, 0xbd, 0x61, 0x79, - 0x5a, 0xf8, 0x4a, 0x7d, 0x78, 0x56, 0xc4, 0x04, 0x17, 0x8a, 0xc7, 0x2f, 0xe4, 0x19, 0x53, 0x8e, 0x59, 0x28, 0x9b, - 0xb3, 0xb0, 0x28, 0xd4, 0xe9, 0xfc, 0x90, 0xe5, 0x33, 0xd0, 0x9e, 0x64, 0x4b, 0xfa, 0x29, 0x16, 0x9e, 0x5f, 0x1b, - 0xc9, 0x6d, 0xb5, 0xe5, 0x2a, 0xb4, 0x9d, 0x26, 0xb3, 0x85, 0xae, 0x79, 0x61, 0x2b, 0x93, 0x4d, 0x23, 0xd1, 0xb6, - 0x24, 0x3e, 0x65, 0xda, 0x9d, 0x31, 0x43, 0xc8, 0xfc, 0x29, 0x17, 0x44, 0xbf, 0xd2, 0x05, 0x85, 0x69, 0x65, 0x89, - 0x37, 0x12, 0x5b, 0x22, 0x55, 0x2c, 0x9f, 0xf9, 0x89, 0x36, 0xe6, 0x24, 0x3f, 0xd8, 0x5d, 0x54, 0x2b, 0x5f, 0xd8, - 0x1a, 0x6c, 0x49, 0xbc, 0xfd, 0xe3, 0x16, 0x34, 0xe8, 0x5b, 0x35, 0xd0, 0x93, 0xb5, 0x0c, 0xb3, 0xbb, 0xf3, 0xae, - 0x3f, 0x5e, 0xb8, 0xf9, 0x35, 0x76, 0xf3, 0x6b, 0xeb, 0xab, 0x45, 0xf3, 0x9a, 0x5e, 0x7e, 0x64, 0xbc, 0xc9, 0xfd, - 0x59, 0x13, 0xbc, 0xa7, 0x22, 0x33, 0x44, 0xb1, 0x67, 0xa1, 0xa3, 0x4b, 0xd3, 0xaf, 0x37, 0xcf, 0x21, 0x3d, 0x5b, - 0x98, 0x51, 0x5e, 0x92, 0x26, 0xb4, 0x57, 0x3f, 0xbe, 0x67, 0x66, 0x18, 0x6b, 0x6c, 0x8d, 0x16, 0x29, 0xa4, 0x73, - 0xf3, 0x5b, 0xaf, 0xad, 0xd8, 0x7a, 0x5b, 0xa7, 0x0f, 0xb7, 0x37, 0xd6, 0xf7, 0x14, 0x72, 0x1b, 0x42, 0x7a, 0x65, - 0xeb, 0xf9, 0xcf, 0xdb, 0xf2, 0xbb, 0x3f, 0x75, 0x98, 0x0d, 0xf2, 0x49, 0xf4, 0xff, 0xc6, 0x29, 0xc0, 0xd5, 0x62, - 0x71, 0x98, 0xed, 0x3e, 0x90, 0x79, 0xfe, 0x98, 0xd3, 0x0c, 0xdf, 0xa7, 0xe6, 0xa5, 0xb8, 0x77, 0x62, 0x01, 0x62, - 0xc6, 0xeb, 0x1c, 0xd5, 0x53, 0xb1, 0xef, 0xee, 0xfe, 0xee, 0xe9, 0x17, 0x0a, 0x47, 0xfa, 0x1e, 0x56, 0xdb, 0xee, - 0xc1, 0x46, 0x88, 0xfd, 0x5b, 0x8f, 0x25, 0x42, 0xe6, 0x5d, 0x42, 0x52, 0x48, 0x6f, 0x96, 0xaa, 0x53, 0x99, 0x19, - 0x8d, 0xc5, 0xa7, 0xd7, 0xd5, 0x52, 0xec, 0x3f, 0x9c, 0xdd, 0xe8, 0xd5, 0xe8, 0xac, 0x9c, 0xb6, 0xfc, 0x43, 0x0f, - 0x55, 0x6e, 0x3f, 0xc5, 0x59, 0x3f, 0x18, 0x78, 0x38, 0xbb, 0xe9, 0x49, 0x41, 0xdb, 0xcc, 0x24, 0x54, 0xed, 0xd9, - 0x8d, 0x79, 0xac, 0xb4, 0xea, 0xc8, 0x72, 0xf7, 0x73, 0x8b, 0xfa, 0x39, 0xed, 0xc1, 0x97, 0xa6, 0x58, 0xe0, 0xc7, - 0x4a, 0x98, 0x4f, 0x59, 0x18, 0xc6, 0xb4, 0xa7, 0xe5, 0xb5, 0xd5, 0x79, 0x08, 0xa7, 0x32, 0xcd, 0x25, 0xab, 0xaf, - 0x8a, 0x81, 0xbc, 0x12, 0x4f, 0xfe, 0x65, 0x9e, 0xc6, 0xf0, 0x9d, 0xc7, 0x8d, 0xe8, 0x54, 0xc7, 0x15, 0xdb, 0x15, - 0xf2, 0xc4, 0xef, 0xfa, 0x5c, 0x0e, 0xdb, 0x7f, 0xea, 0x89, 0x05, 0x6f, 0xf7, 0x78, 0x3a, 0xf3, 0x9a, 0xfb, 0xf5, - 0x89, 0xc0, 0xab, 0x72, 0x0a, 0x78, 0xc3, 0xb4, 0x30, 0x48, 0x2b, 0xc9, 0xa7, 0x2d, 0xb7, 0xa3, 0xca, 0x44, 0x07, - 0x60, 0x84, 0x96, 0x45, 0x45, 0x7d, 0x32, 0xff, 0x98, 0xdd, 0xf2, 0x78, 0xf3, 0x6e, 0x79, 0xac, 0x77, 0xcb, 0xdd, - 0x14, 0xfb, 0xe5, 0xb8, 0x03, 0xff, 0xf5, 0xaa, 0x09, 0x79, 0x6d, 0x6b, 0x7f, 0x76, 0x63, 0x81, 0x9e, 0xd6, 0xec, - 0xce, 0x6e, 0xe4, 0xa1, 0x5a, 0x48, 0x5c, 0x6b, 0xc3, 0x31, 0x53, 0xdc, 0xb6, 0xa0, 0x10, 0xfe, 0x6f, 0xd7, 0x5e, - 0x75, 0x0e, 0xe0, 0x1d, 0xb4, 0x3a, 0x5c, 0x7f, 0xd7, 0xbd, 0x7b, 0xd3, 0x7a, 0x49, 0xca, 0x1d, 0x4f, 0x73, 0x63, - 0xe4, 0x72, 0xff, 0xf2, 0x92, 0x86, 0xde, 0x38, 0x0d, 0xe6, 0xf9, 0x3f, 0x29, 0xf8, 0x15, 0x12, 0xef, 0xdc, 0xd2, - 0x2b, 0xfd, 0xe8, 0xa6, 0xf2, 0x88, 0xaf, 0xee, 0x61, 0x51, 0xae, 0x93, 0x97, 0x07, 0x7e, 0x4c, 0x9d, 0xae, 0x7b, - 0xb0, 0x61, 0x13, 0xfc, 0x9b, 0xac, 0xcd, 0xc6, 0xc9, 0xfc, 0x5e, 0x64, 0xdc, 0x89, 0x84, 0xcf, 0xc2, 0x81, 0xb9, - 0x86, 0xed, 0xa3, 0xcd, 0xe0, 0x0e, 0xf5, 0x48, 0x23, 0x2d, 0x14, 0x94, 0xdc, 0x09, 0xe9, 0xd8, 0x9f, 0xc7, 0xfc, - 0xee, 0x5e, 0xb7, 0x51, 0xc6, 0x5a, 0xaf, 0x77, 0x30, 0xf4, 0xaa, 0xee, 0x3d, 0xb9, 0xf4, 0x97, 0x8f, 0x0f, 0xe0, - 0x3f, 0x79, 0xf8, 0xe5, 0xb2, 0xd2, 0xd5, 0xa5, 0xd5, 0x0b, 0xba, 0xfa, 0x55, 0x4d, 0x19, 0x97, 0x22, 0x5c, 0xe8, - 0xe3, 0xf7, 0xad, 0x0d, 0x5a, 0xe5, 0xbd, 0xaa, 0x2b, 0x2d, 0xeb, 0xb3, 0x6a, 0x7f, 0x5e, 0xe7, 0xf7, 0xac, 0x1b, - 0x48, 0xcd, 0xb5, 0x5e, 0x57, 0x7d, 0x7a, 0x7e, 0xad, 0xb2, 0xc6, 0xb8, 0xa8, 0x7f, 0x45, 0x2e, 0x4b, 0x13, 0x45, - 0xa6, 0xa2, 0x82, 0x95, 0x72, 0x25, 0xad, 0x94, 0x94, 0x92, 0x8b, 0xe3, 0xc1, 0xcd, 0x34, 0xb6, 0xae, 0xe4, 0xfd, - 0x38, 0xc4, 0xee, 0xb8, 0x6d, 0xdb, 0x12, 0x4e, 0x3a, 0xf8, 0x4c, 0x97, 0xfd, 0xe1, 0xfd, 0xd7, 0xcd, 0x23, 0x7b, - 0x00, 0x9a, 0xd6, 0xd5, 0x44, 0x68, 0x76, 0x2f, 0xfd, 0x5b, 0x9a, 0x9d, 0x77, 0x95, 0x0b, 0x5e, 0xe6, 0x8b, 0x8b, - 0x32, 0xab, 0x6b, 0x5b, 0x37, 0xd3, 0x38, 0xc9, 0x89, 0x1d, 0x71, 0x3e, 0xf3, 0x5a, 0xad, 0xeb, 0xeb, 0x6b, 0xf7, - 0x7a, 0xdf, 0x4d, 0xb3, 0x49, 0xab, 0xdb, 0x6e, 0xb7, 0xe1, 0x8b, 0x1f, 0xb6, 0x75, 0xc5, 0xe8, 0xf5, 0x93, 0xf4, - 0x86, 0xd8, 0x6d, 0xab, 0x6d, 0x75, 0xba, 0x47, 0x56, 0xa7, 0x7b, 0xe0, 0x3e, 0x3c, 0xb2, 0xfb, 0x5f, 0x58, 0xd6, - 0x71, 0x48, 0xc7, 0x39, 0xfc, 0xb0, 0xac, 0x63, 0xa1, 0x78, 0xc9, 0xdf, 0x96, 0xe5, 0x06, 0x71, 0xde, 0xec, 0x58, - 0x0b, 0xf5, 0x68, 0x59, 0x70, 0x8b, 0x90, 0x67, 0x7d, 0x39, 0xee, 0x8e, 0x0f, 0xc6, 0x8f, 0x7b, 0xaa, 0xb8, 0xf8, - 0xa2, 0x56, 0x1d, 0xcb, 0x7f, 0xbb, 0x46, 0xb3, 0x9c, 0x67, 0xe9, 0x47, 0xaa, 0x5c, 0xfb, 0x16, 0x88, 0x9e, 0x8d, - 0x4d, 0xbb, 0xeb, 0x23, 0x75, 0x8e, 0x2e, 0x83, 0x71, 0xb7, 0xaa, 0x2e, 0x60, 0x6c, 0x95, 0x40, 0x1e, 0xb7, 0x34, - 0xe8, 0xc7, 0x26, 0x9a, 0x3a, 0xcd, 0x4d, 0x88, 0xea, 0xd8, 0x6a, 0x8e, 0x13, 0x3d, 0xbf, 0x63, 0x38, 0xb4, 0xae, - 0x75, 0x55, 0x01, 0x81, 0x6d, 0x85, 0xc4, 0x7e, 0xd5, 0xe9, 0x1e, 0xe1, 0x4e, 0xe7, 0xa1, 0xfb, 0xf0, 0x28, 0x68, - 0xe3, 0x03, 0xf7, 0xa0, 0xb9, 0xef, 0x3e, 0xc4, 0x47, 0xcd, 0x23, 0x7c, 0xf4, 0xfc, 0x28, 0x68, 0x1e, 0xb8, 0x07, - 0xb8, 0xdd, 0x3c, 0x82, 0xc2, 0xe6, 0x51, 0xf3, 0xe8, 0xaa, 0x79, 0x70, 0x14, 0xb4, 0x45, 0x69, 0xd7, 0x3d, 0x3c, - 0x6c, 0x76, 0xda, 0xee, 0xe1, 0x21, 0x3e, 0x74, 0x1f, 0x3e, 0x6c, 0x76, 0xf6, 0xdd, 0x87, 0x0f, 0x5f, 0x1e, 0x1e, - 0xb9, 0xfb, 0xf0, 0x6e, 0x7f, 0x3f, 0xd8, 0x77, 0x3b, 0x9d, 0x26, 0xfc, 0xc1, 0x47, 0x6e, 0x57, 0xfe, 0xe8, 0x74, - 0xdc, 0xfd, 0x0e, 0x6e, 0xc7, 0x87, 0x5d, 0xf7, 0xe1, 0x63, 0x2c, 0xfe, 0x8a, 0x6a, 0x58, 0xfc, 0x81, 0x6e, 0xf0, - 0x63, 0xb7, 0xfb, 0x50, 0xfe, 0x12, 0x1d, 0x5e, 0x1d, 0x1c, 0xfd, 0x68, 0xb7, 0xb6, 0xce, 0xa1, 0x23, 0xe7, 0x70, - 0x74, 0xe8, 0xee, 0xef, 0xe3, 0x83, 0x8e, 0x7b, 0xb4, 0x1f, 0x35, 0x0f, 0xba, 0xee, 0xc3, 0x47, 0x41, 0xb3, 0xe3, - 0x3e, 0x7a, 0x84, 0xdb, 0xcd, 0x7d, 0xb7, 0x8b, 0x3b, 0xee, 0xc1, 0xbe, 0xf8, 0xb1, 0xef, 0x76, 0xaf, 0x1e, 0x3d, - 0x76, 0x1f, 0x1e, 0x46, 0x0f, 0xdd, 0x83, 0x6f, 0x0f, 0x8e, 0xdc, 0xee, 0x7e, 0xb4, 0xff, 0xd0, 0xed, 0x3e, 0xba, - 0x7a, 0xe8, 0x1e, 0x44, 0xcd, 0xee, 0xc3, 0x3b, 0x5b, 0x76, 0xba, 0x2e, 0xe0, 0x48, 0xbc, 0x86, 0x17, 0x58, 0xbd, - 0x80, 0xff, 0x23, 0xd1, 0xf6, 0xdf, 0xb0, 0x9b, 0x7c, 0xbd, 0xe9, 0x63, 0xf7, 0xe8, 0x51, 0x20, 0xab, 0x43, 0x41, - 0x53, 0xd7, 0x80, 0x26, 0x57, 0x4d, 0x39, 0xac, 0xe8, 0xae, 0xa9, 0x3b, 0xd2, 0xff, 0xab, 0xc1, 0xae, 0x9a, 0x30, - 0xb0, 0x1c, 0xf7, 0xdf, 0xb5, 0x9f, 0x72, 0xc9, 0x8f, 0x5b, 0x13, 0x49, 0xfa, 0x93, 0xfe, 0x17, 0xf2, 0x73, 0x3e, - 0x5f, 0x5c, 0x60, 0x7f, 0x9b, 0xe3, 0x23, 0xfe, 0xb4, 0xe3, 0x23, 0xa2, 0xf7, 0xf1, 0x7c, 0xc4, 0x7f, 0xb8, 0xe7, - 0xc3, 0x5f, 0x75, 0x9b, 0xdf, 0xf0, 0x35, 0x07, 0xc7, 0xaa, 0x55, 0xfc, 0x82, 0x3b, 0xc3, 0x14, 0x3e, 0x1d, 0x5d, - 0xf4, 0x6e, 0x38, 0x89, 0xa8, 0xe9, 0x07, 0x4a, 0x81, 0xc5, 0xde, 0x70, 0xc9, 0x63, 0x83, 0x6d, 0x08, 0x09, 0x3f, - 0x8d, 0x90, 0xef, 0xee, 0x83, 0x8f, 0xf0, 0x0f, 0xc7, 0x47, 0x60, 0xe2, 0xa3, 0xe6, 0xc9, 0x17, 0x9e, 0x06, 0xe1, - 0x29, 0x38, 0x13, 0xcf, 0x0e, 0xdc, 0x9a, 0xd1, 0xb0, 0x5b, 0xf4, 0x4a, 0x44, 0xee, 0x64, 0x70, 0xfd, 0xf9, 0xe7, - 0x04, 0x1d, 0xe4, 0x15, 0x39, 0xc4, 0x56, 0x6e, 0x99, 0x99, 0x90, 0x3a, 0xea, 0xa1, 0x14, 0x4a, 0x5d, 0xb7, 0xed, - 0xb6, 0x4b, 0x97, 0x0e, 0x5c, 0x8b, 0x44, 0x16, 0x29, 0xf7, 0xbd, 0x9d, 0x0e, 0x8e, 0xd3, 0x09, 0x5c, 0x96, 0x24, - 0x3e, 0x1f, 0x07, 0x27, 0x1e, 0x02, 0xf9, 0xe5, 0x3e, 0x48, 0x9f, 0x50, 0x8e, 0x1e, 0x3f, 0xfb, 0xf8, 0x37, 0x08, - 0x62, 0xea, 0x98, 0xc4, 0x14, 0xbc, 0x1d, 0xaf, 0x68, 0xc8, 0x7c, 0xc7, 0x76, 0x66, 0x19, 0x1d, 0xd3, 0x2c, 0x6f, - 0xd6, 0xee, 0xeb, 0x11, 0x57, 0xf5, 0x20, 0x5b, 0x41, 0x38, 0xce, 0xe0, 0x73, 0x48, 0x64, 0xa8, 0xfc, 0x8d, 0xb6, - 0x32, 0xc0, 0xec, 0x02, 0xeb, 0x92, 0x0c, 0x64, 0x6d, 0xa5, 0xb4, 0xd9, 0x52, 0x6b, 0xeb, 0xb8, 0xdd, 0x43, 0x64, - 0x89, 0x62, 0xf8, 0xd0, 0xcc, 0x0f, 0x4e, 0x73, 0xbf, 0xfd, 0x27, 0x64, 0x34, 0x2b, 0x3b, 0x1a, 0x29, 0x77, 0x5b, - 0x52, 0x7e, 0x8e, 0x70, 0x25, 0xec, 0x6a, 0x4b, 0x8a, 0xf8, 0x52, 0xce, 0xdd, 0x46, 0xbd, 0x44, 0x25, 0xcd, 0xc9, - 0x2b, 0x01, 0xc7, 0x6c, 0xe2, 0x18, 0xd7, 0x4d, 0x24, 0xf2, 0x43, 0x36, 0x70, 0x5b, 0x3d, 0x42, 0x45, 0x55, 0x25, - 0x41, 0x0b, 0x11, 0x6d, 0x61, 0x89, 0x95, 0x2c, 0x97, 0x4e, 0x02, 0x2e, 0x72, 0x62, 0xe0, 0x14, 0x9e, 0x51, 0x0d, - 0xc9, 0x09, 0x2e, 0x01, 0x12, 0x08, 0x26, 0x89, 0xfc, 0xb7, 0x2a, 0xd6, 0x3f, 0x94, 0xe3, 0xcb, 0x8d, 0xfd, 0x64, - 0x02, 0x54, 0xe8, 0x27, 0x93, 0x35, 0xb7, 0x9a, 0x0c, 0x18, 0xad, 0x94, 0x56, 0x5d, 0x55, 0xee, 0xb3, 0xfc, 0xc9, - 0xed, 0x7b, 0x75, 0xe3, 0xb5, 0x0d, 0xde, 0x69, 0x11, 0xdf, 0xa8, 0xbe, 0xce, 0xd3, 0x20, 0x0f, 0x8e, 0xa7, 0x94, - 0xfb, 0xf2, 0xb0, 0x1a, 0xe8, 0x13, 0x90, 0xcb, 0x62, 0x29, 0x6b, 0x54, 0x05, 0xf5, 0x89, 0x3c, 0xcc, 0x2f, 0x45, - 0x3d, 0xb6, 0xd4, 0x55, 0x71, 0x4d, 0xb1, 0x34, 0xa4, 0x83, 0xa5, 0x3f, 0x26, 0xf0, 0xc5, 0x71, 0x64, 0x92, 0xa4, - 0x76, 0xff, 0x41, 0x99, 0xeb, 0xb2, 0x6d, 0x11, 0x62, 0x96, 0x7c, 0x1c, 0x66, 0x34, 0xfe, 0x27, 0xf2, 0x80, 0x05, - 0x69, 0xf2, 0x60, 0x64, 0xa3, 0x1e, 0x77, 0xa3, 0x8c, 0x8e, 0xc9, 0x03, 0x90, 0xf1, 0x9e, 0xb0, 0x3e, 0x80, 0x11, - 0x36, 0x6e, 0xa6, 0x31, 0x16, 0x1a, 0xd3, 0x3d, 0x14, 0x22, 0x09, 0xae, 0xdd, 0x3d, 0xb4, 0x2d, 0x69, 0x13, 0x8b, - 0xdf, 0x7d, 0x29, 0x4e, 0x85, 0x12, 0x60, 0x75, 0xba, 0xee, 0x61, 0xd4, 0x75, 0x1f, 0x5f, 0x3d, 0x72, 0x8f, 0xa2, - 0xce, 0xa3, 0xab, 0x26, 0xfc, 0xdb, 0x75, 0x1f, 0xc7, 0xcd, 0xae, 0xfb, 0x18, 0xfe, 0xff, 0xf6, 0xc0, 0x3d, 0x8c, - 0x9a, 0x1d, 0xf7, 0xe8, 0x6a, 0xdf, 0xdd, 0x7f, 0xd9, 0xe9, 0xba, 0xfb, 0x56, 0xc7, 0x92, 0xed, 0x80, 0x5d, 0x4b, - 0xee, 0xfc, 0x60, 0x65, 0x43, 0x6c, 0x08, 0xc6, 0xc9, 0x03, 0x77, 0x36, 0x16, 0x67, 0xa4, 0xcd, 0xfd, 0xa9, 0x9c, - 0x75, 0x4f, 0xfd, 0x0c, 0xbe, 0x6c, 0x5a, 0xdf, 0xbb, 0xb5, 0x77, 0xb8, 0xc6, 0x2f, 0x36, 0x0c, 0x31, 0x13, 0x11, - 0x70, 0xf3, 0xae, 0x35, 0xb8, 0xa8, 0x4c, 0x7c, 0x08, 0x4a, 0xdf, 0x87, 0xbf, 0x9d, 0xb4, 0x65, 0x45, 0x7d, 0xe7, - 0xbe, 0x60, 0x16, 0x4c, 0x7c, 0x72, 0x4f, 0x0c, 0xf2, 0x22, 0x2c, 0x56, 0xc7, 0x87, 0x73, 0x7f, 0x59, 0x6a, 0x5c, - 0x37, 0x47, 0xab, 0x20, 0x7f, 0xc8, 0xe0, 0x82, 0xc0, 0xa2, 0xd0, 0xa0, 0xd7, 0xdc, 0xb4, 0x15, 0x96, 0x04, 0xbf, - 0xa0, 0xf9, 0xc0, 0x86, 0x22, 0xdb, 0xb3, 0x85, 0x8b, 0xcf, 0x2e, 0xbf, 0xee, 0x5a, 0x12, 0x76, 0x55, 0x80, 0xc5, - 0xed, 0x0f, 0xb0, 0x6b, 0x01, 0x3f, 0x36, 0xda, 0xdb, 0xdb, 0xba, 0x5f, 0x84, 0x02, 0x09, 0x73, 0xad, 0x3e, 0x0a, - 0x69, 0xb2, 0x22, 0xdb, 0x44, 0x74, 0xd9, 0xaf, 0x40, 0x21, 0x52, 0x78, 0xba, 0xa4, 0x3e, 0x77, 0xfd, 0x44, 0x9e, - 0x20, 0x30, 0x18, 0x16, 0xee, 0xd0, 0x7d, 0x54, 0xa4, 0xdc, 0x97, 0x49, 0x61, 0xe6, 0x3e, 0x4f, 0xb9, 0xaf, 0x2f, - 0x4f, 0xf2, 0x79, 0xed, 0xe0, 0x7c, 0xd4, 0xed, 0xbf, 0x79, 0x7f, 0x62, 0xc9, 0xed, 0x79, 0xdc, 0x8a, 0xba, 0xfd, - 0x63, 0xe1, 0x33, 0x91, 0x61, 0x7f, 0x22, 0xc3, 0xfe, 0x96, 0xba, 0x35, 0x06, 0x22, 0x69, 0x45, 0x4b, 0x4e, 0x5b, - 0xd8, 0x0c, 0xd2, 0xdb, 0x3b, 0x9d, 0xc7, 0x9c, 0xc1, 0x47, 0x8d, 0x5a, 0x22, 0xe6, 0x2f, 0x72, 0x08, 0xf4, 0x21, - 0x54, 0xa5, 0x1d, 0x5e, 0xf2, 0x44, 0xfb, 0x86, 0xc7, 0x2c, 0xa6, 0xfa, 0xd8, 0xa9, 0xea, 0xaa, 0x4c, 0xf8, 0x59, - 0xaf, 0x9d, 0xcf, 0x2f, 0x21, 0xe9, 0x41, 0xa7, 0x17, 0x7d, 0x50, 0x0d, 0x8e, 0xc5, 0x50, 0x10, 0xb9, 0x97, 0x62, - 0x5a, 0x7f, 0xbe, 0xb5, 0xbe, 0xa4, 0x6a, 0xf6, 0x4a, 0x42, 0xc0, 0x55, 0x1d, 0xd1, 0x7e, 0xbf, 0x74, 0x17, 0x9b, - 0xef, 0x8a, 0xe3, 0x56, 0xb4, 0xdf, 0xbf, 0xf0, 0x26, 0xaa, 0xbf, 0x97, 0xe9, 0x64, 0x73, 0x5f, 0x71, 0x3a, 0x19, - 0x88, 0x73, 0xf0, 0xf2, 0x4e, 0x27, 0xad, 0xfc, 0xa6, 0xb1, 0xdd, 0x3f, 0x56, 0xca, 0x80, 0x25, 0xc2, 0xea, 0xf6, - 0x61, 0x5b, 0x1f, 0xad, 0x8f, 0xd3, 0x09, 0x6c, 0x48, 0xd9, 0xc4, 0x18, 0xa4, 0xe6, 0x71, 0x8f, 0x3a, 0xfd, 0x63, - 0xdf, 0x12, 0xbc, 0x45, 0x30, 0x8f, 0xdc, 0x6b, 0x41, 0xe3, 0x28, 0x9d, 0x52, 0x97, 0xa5, 0xad, 0x6b, 0x7a, 0xd9, - 0xf4, 0x67, 0xac, 0x72, 0x6f, 0x83, 0xd2, 0x51, 0x0e, 0x99, 0xae, 0xa4, 0x58, 0x75, 0x2b, 0x77, 0xdb, 0x01, 0xd8, - 0x3c, 0xda, 0x35, 0x27, 0x7c, 0x72, 0x06, 0x58, 0x69, 0xff, 0xb8, 0xe5, 0xaf, 0x60, 0x44, 0xf0, 0xfb, 0x42, 0x39, - 0xda, 0xc1, 0xb0, 0xb9, 0x14, 0xf9, 0x5d, 0x52, 0x1c, 0x68, 0x87, 0xbc, 0x12, 0xd4, 0x85, 0xdd, 0xff, 0xd7, 0xff, - 0xf1, 0xbf, 0x94, 0x8f, 0xfd, 0xb8, 0x15, 0x75, 0x74, 0x5f, 0x2b, 0xab, 0x52, 0x1c, 0xc3, 0x45, 0x35, 0x55, 0x50, - 0x98, 0xde, 0x34, 0x27, 0x19, 0x0b, 0x9b, 0x91, 0x1f, 0x8f, 0xed, 0xfe, 0x76, 0x6c, 0xca, 0xfc, 0xc3, 0xa6, 0x0e, - 0xa7, 0xae, 0x17, 0x01, 0xbd, 0xfe, 0xa6, 0x5b, 0x17, 0x74, 0x4a, 0x97, 0xd8, 0xda, 0xe6, 0x1d, 0x0c, 0xd5, 0xee, - 0xab, 0xdd, 0xc3, 0x90, 0xa8, 0x6f, 0x42, 0x2b, 0x0e, 0x98, 0xd4, 0xae, 0x5f, 0x28, 0x6c, 0xab, 0x0c, 0x6a, 0xfd, - 0xdf, 0xff, 0xf9, 0x5f, 0xfe, 0x9b, 0x7e, 0x84, 0x58, 0xd5, 0xbf, 0xfe, 0xf7, 0xff, 0xfc, 0x7f, 0xfe, 0xf7, 0x7f, - 0x85, 0xfc, 0x33, 0x15, 0xcf, 0x12, 0x4c, 0xc5, 0xaa, 0x82, 0x59, 0x92, 0xbb, 0x58, 0x70, 0xaa, 0x6d, 0xca, 0x72, - 0xce, 0x82, 0xfa, 0x85, 0x0c, 0x67, 0x62, 0x40, 0xb1, 0x33, 0x15, 0x74, 0x62, 0x87, 0x17, 0x15, 0x41, 0xd5, 0x50, - 0x2e, 0x08, 0xb7, 0x38, 0x6e, 0x01, 0xbe, 0xef, 0x77, 0x9f, 0x8c, 0x5b, 0x2e, 0xc7, 0x42, 0x93, 0x09, 0x94, 0x14, - 0x55, 0xb9, 0x05, 0xb1, 0x95, 0x05, 0x3c, 0x7a, 0x5d, 0xa3, 0x58, 0xac, 0x5e, 0xad, 0x4d, 0xef, 0xe7, 0x79, 0xce, - 0xd9, 0x18, 0x50, 0x2e, 0xfd, 0xc4, 0x22, 0x8c, 0xdd, 0x04, 0x5d, 0x31, 0xbe, 0x2d, 0x44, 0x2f, 0x92, 0x40, 0x0f, - 0x8e, 0xfe, 0x54, 0xfc, 0x79, 0x0a, 0x1a, 0x99, 0xe5, 0x4c, 0xfd, 0x1b, 0x65, 0x9e, 0x3f, 0x6c, 0xb7, 0x67, 0x37, - 0x68, 0x51, 0x8d, 0x80, 0xb7, 0x0d, 0x26, 0xe8, 0xd8, 0xec, 0x50, 0xc4, 0xbf, 0x4b, 0x37, 0x76, 0xdb, 0x02, 0x5f, - 0xb8, 0xd5, 0x2e, 0x8a, 0xaf, 0x16, 0xc2, 0x93, 0xca, 0x7e, 0x85, 0x38, 0xb5, 0x72, 0x3a, 0x5f, 0xa6, 0xe6, 0xe4, - 0x16, 0x46, 0xab, 0xae, 0x6c, 0x15, 0x75, 0xd6, 0xaf, 0x66, 0x31, 0xe3, 0xec, 0x66, 0x84, 0xfc, 0x00, 0x62, 0xde, - 0x51, 0x07, 0x47, 0xdd, 0x45, 0xd9, 0x3d, 0xe7, 0xe9, 0xd4, 0x0c, 0xac, 0x53, 0x9f, 0x06, 0x74, 0xac, 0x9d, 0xf5, - 0xea, 0xbd, 0x0c, 0x9a, 0x17, 0xd1, 0xfe, 0x86, 0xb1, 0x14, 0x48, 0x22, 0xa0, 0x6e, 0xb5, 0x8b, 0x2f, 0x61, 0x07, - 0x2e, 0xc6, 0x71, 0xea, 0x73, 0x4f, 0x10, 0x6c, 0xcf, 0x0c, 0xcf, 0xfb, 0xc0, 0x93, 0xd2, 0x85, 0x01, 0x4f, 0x4f, - 0x56, 0x05, 0xb7, 0x79, 0xfd, 0x8a, 0xc6, 0xc2, 0x15, 0xcd, 0xcd, 0xae, 0xa4, 0xd7, 0xed, 0x3b, 0x15, 0xf5, 0x7e, - 0x5e, 0x73, 0x57, 0x29, 0x81, 0xd4, 0x45, 0x9b, 0xdf, 0x4b, 0xb9, 0x2e, 0xdf, 0x7e, 0xcf, 0x1d, 0x5b, 0x80, 0x69, - 0xaf, 0xd6, 0x12, 0x85, 0x50, 0xeb, 0x39, 0xf9, 0xae, 0x34, 0x99, 0xfc, 0xd9, 0x4c, 0x54, 0x44, 0xbd, 0xe3, 0x96, - 0xd4, 0x74, 0x81, 0x7b, 0x88, 0x94, 0x0e, 0x99, 0x41, 0xa1, 0x2a, 0xa9, 0xad, 0x20, 0x7f, 0xa9, 0xdc, 0x0a, 0xf8, - 0x56, 0x78, 0xff, 0xff, 0x01, 0xa2, 0x89, 0x8c, 0x0d, 0xc4, 0x97, 0x00, 0x00}; + 0x95, 0xd2, 0x3b, 0x9e, 0x56, 0x41, 0xb0, 0xd5, 0x38, 0x5f, 0xb3, 0x03, 0xbe, 0xb0, 0x91, 0x90, 0xcf, 0x39, 0xce, + 0x08, 0x48, 0xd1, 0xee, 0xc0, 0x3e, 0xce, 0xaf, 0x26, 0x7d, 0x1b, 0x62, 0x34, 0x29, 0xf9, 0x20, 0x5c, 0x43, 0x50, + 0x21, 0x22, 0xed, 0x5e, 0x74, 0x4c, 0x7b, 0x51, 0xa3, 0xa1, 0xb5, 0x68, 0x9f, 0x24, 0xc3, 0x48, 0x36, 0x0f, 0x70, + 0x88, 0xe7, 0xa4, 0xd9, 0xc1, 0x33, 0xd2, 0x16, 0x4d, 0x7a, 0xb3, 0x63, 0x5f, 0x0d, 0xb3, 0xb7, 0xe7, 0xa4, 0x6e, + 0xec, 0xe7, 0xfc, 0x05, 0xd8, 0xfb, 0x64, 0x86, 0x43, 0x92, 0xba, 0xf4, 0x86, 0x06, 0x8e, 0x8f, 0x70, 0xa8, 0x38, + 0x0d, 0xea, 0xa1, 0x19, 0x31, 0xaa, 0x81, 0x19, 0x41, 0x3e, 0x0c, 0xc2, 0x61, 0x67, 0x44, 0x08, 0xb1, 0x77, 0x9a, + 0x4d, 0x7b, 0x90, 0x92, 0x09, 0xf7, 0xa0, 0xc4, 0x50, 0x96, 0xc9, 0x14, 0x8a, 0xba, 0x46, 0x91, 0xf3, 0x86, 0xbb, + 0x9c, 0xe6, 0xdc, 0x81, 0x62, 0xf0, 0x00, 0xe4, 0x9a, 0xb0, 0xed, 0xe3, 0x96, 0xdd, 0x80, 0x52, 0x41, 0x9c, 0x08, + 0xa7, 0xe4, 0x1a, 0x79, 0xe1, 0x70, 0x7f, 0x64, 0x0a, 0x00, 0x51, 0x08, 0x83, 0x5f, 0x0f, 0xc2, 0x61, 0x5b, 0x0c, + 0xde, 0xb7, 0x07, 0x4e, 0x4a, 0x72, 0xa9, 0xa1, 0x0d, 0x72, 0xef, 0x83, 0x98, 0x2a, 0xf2, 0x14, 0x70, 0x6a, 0xdc, + 0x39, 0x69, 0x76, 0x3d, 0x67, 0x6e, 0x4e, 0xa2, 0x09, 0x83, 0x29, 0x2c, 0xe0, 0x80, 0x40, 0x7d, 0x9c, 0x12, 0x18, + 0xb1, 0x6a, 0x76, 0xed, 0xa9, 0xe7, 0x07, 0xf6, 0x83, 0xc1, 0x39, 0xf7, 0xc6, 0x5c, 0x0e, 0x7f, 0xce, 0x97, 0x4b, + 0xf8, 0x77, 0xcc, 0x07, 0x29, 0xb9, 0x16, 0x45, 0x13, 0x55, 0x34, 0x85, 0xa2, 0x0f, 0x1e, 0x80, 0x8a, 0xf3, 0x52, + 0xcb, 0x92, 0x6b, 0x32, 0x25, 0x02, 0xf6, 0xbd, 0xbd, 0x64, 0x18, 0x35, 0x3a, 0x23, 0x70, 0xf2, 0x67, 0x3c, 0xff, + 0x8e, 0xf1, 0xc8, 0xb1, 0x5b, 0x7d, 0x1b, 0x0d, 0x6c, 0x0b, 0x96, 0xb6, 0x97, 0x35, 0x88, 0xc4, 0xb0, 0xdf, 0x78, + 0xc5, 0xbd, 0x79, 0x9f, 0xb4, 0x07, 0x0e, 0x53, 0x2e, 0x3d, 0x84, 0x7d, 0xc5, 0x38, 0xdb, 0x78, 0x8e, 0x1a, 0x8c, + 0x37, 0xf4, 0xf3, 0x1c, 0x35, 0x6e, 0x1b, 0x53, 0xe4, 0xf9, 0x8d, 0xdb, 0x86, 0x33, 0x27, 0x84, 0x34, 0xbb, 0x65, + 0x33, 0x2d, 0xfe, 0x22, 0xe4, 0x4d, 0xb5, 0xbf, 0x73, 0x28, 0xb6, 0x43, 0xd6, 0x70, 0x92, 0x21, 0x1d, 0x2d, 0x97, + 0xf6, 0xf1, 0xa0, 0x6f, 0xa3, 0x86, 0xa3, 0x09, 0xad, 0xa5, 0x29, 0x0d, 0x21, 0xcc, 0x46, 0x85, 0x8a, 0x27, 0x3d, + 0xa9, 0xc5, 0x8e, 0x16, 0xd5, 0x66, 0x37, 0x78, 0x00, 0x2d, 0x4a, 0x43, 0x46, 0x2a, 0xac, 0x33, 0x98, 0xa6, 0x26, + 0xe6, 0x8c, 0xb4, 0x71, 0x4a, 0xb4, 0xfb, 0x3a, 0x22, 0xbc, 0x22, 0x78, 0x9f, 0x54, 0xd5, 0xf1, 0x30, 0xc0, 0xe1, + 0x88, 0x3c, 0x95, 0x06, 0x49, 0x4f, 0x3b, 0xc7, 0x69, 0x4c, 0x9e, 0xac, 0x44, 0x71, 0x03, 0x08, 0xb0, 0xdc, 0xb8, + 0xc1, 0x3c, 0xcb, 0x68, 0xc2, 0x5f, 0xa7, 0xa1, 0xd2, 0xd3, 0x68, 0x0c, 0xa6, 0x12, 0x84, 0x67, 0x31, 0x28, 0x69, + 0x5d, 0xbd, 0x33, 0xe6, 0x6b, 0xaf, 0x67, 0x64, 0x2e, 0xf5, 0x27, 0x11, 0xb4, 0xed, 0xcd, 0x94, 0x65, 0xec, 0x20, + 0x3c, 0x57, 0xd1, 0x5c, 0xc7, 0x75, 0xdd, 0x99, 0x1b, 0xc0, 0x6b, 0x18, 0x20, 0x47, 0x85, 0xd8, 0x47, 0x4e, 0x4e, + 0x6e, 0xdc, 0x84, 0xde, 0x88, 0x51, 0x1d, 0x54, 0x49, 0x66, 0xbd, 0xbd, 0x8e, 0xa3, 0x9e, 0x60, 0x37, 0xb9, 0x9b, + 0xa4, 0x21, 0x05, 0xf4, 0x40, 0xfc, 0x5e, 0x15, 0x45, 0x7e, 0x6e, 0x06, 0xa9, 0x2a, 0xf8, 0x86, 0xa6, 0xff, 0x7a, + 0x06, 0x4e, 0x5f, 0xa1, 0x6c, 0x95, 0x95, 0xa5, 0x27, 0x1c, 0x21, 0x36, 0x76, 0x66, 0x2e, 0x04, 0xf7, 0x04, 0x09, + 0x31, 0xb0, 0xe5, 0x66, 0x26, 0x51, 0xdd, 0x96, 0x7d, 0x4e, 0x49, 0x38, 0x4c, 0x1b, 0x0d, 0xe1, 0x88, 0x9e, 0x4b, + 0x92, 0x98, 0x21, 0x3c, 0x2d, 0xf7, 0x96, 0xae, 0xf7, 0x96, 0xd4, 0x47, 0x72, 0xa6, 0x75, 0x87, 0x6e, 0x83, 0x71, + 0x24, 0x7c, 0x85, 0xdc, 0xb9, 0x45, 0x78, 0x4c, 0x5a, 0xce, 0xd0, 0x1d, 0xfc, 0x79, 0x84, 0x06, 0x8e, 0xfb, 0x15, + 0x6a, 0x49, 0xc6, 0x31, 0x45, 0x3d, 0x5f, 0x0e, 0xb1, 0x10, 0x51, 0xcc, 0x0e, 0x16, 0xbe, 0x44, 0x2f, 0xc3, 0x89, + 0x3f, 0xa5, 0xde, 0x18, 0xf6, 0xb8, 0xa6, 0x9b, 0xb7, 0x18, 0xe8, 0xc8, 0x1b, 0x2b, 0x4e, 0xe2, 0xda, 0x83, 0x5f, + 0x78, 0xf9, 0x34, 0xb0, 0x07, 0x5f, 0x57, 0x4f, 0x7f, 0xb6, 0x07, 0xdf, 0x72, 0xef, 0xdb, 0x42, 0xb9, 0xbb, 0x6b, + 0x43, 0x3c, 0xd4, 0x43, 0x14, 0x72, 0x61, 0x0c, 0xcc, 0xcd, 0xd1, 0xba, 0xa3, 0x63, 0x86, 0x0a, 0x36, 0x2e, 0x59, + 0x51, 0xee, 0x72, 0x7f, 0x02, 0x28, 0x35, 0x56, 0x20, 0x37, 0xa3, 0xfb, 0xd5, 0x84, 0x81, 0x50, 0x34, 0xb5, 0x02, + 0x2a, 0x67, 0xfd, 0x36, 0x5a, 0xd4, 0xea, 0x0a, 0x8d, 0xa9, 0x1e, 0x4d, 0x2f, 0xb9, 0xf4, 0x94, 0xb4, 0x7b, 0xd3, + 0xe3, 0x59, 0x6f, 0xda, 0x68, 0xa0, 0x5c, 0x13, 0xd6, 0x7c, 0x38, 0x1d, 0xe1, 0xd7, 0xe0, 0xd5, 0x33, 0x29, 0x09, + 0xd7, 0xa6, 0xd7, 0x55, 0xd3, 0x6b, 0x34, 0xb2, 0x02, 0xf5, 0x8c, 0xa6, 0x33, 0xd9, 0xb4, 0x28, 0x24, 0x4e, 0x56, + 0x09, 0xed, 0x08, 0x89, 0x12, 0x48, 0x89, 0x22, 0x84, 0x9c, 0x71, 0xb4, 0xb1, 0x57, 0xe8, 0x13, 0x9a, 0x8b, 0x1d, + 0x0b, 0xcc, 0x53, 0xca, 0x08, 0x07, 0xb0, 0x00, 0x4d, 0x4b, 0x57, 0xf0, 0x2d, 0x9e, 0x37, 0x3a, 0x82, 0xc8, 0x9b, + 0x9d, 0x5e, 0xbd, 0xaf, 0x47, 0x55, 0x5f, 0x78, 0xde, 0x20, 0xb7, 0x25, 0x96, 0x8a, 0xac, 0xd1, 0x28, 0xea, 0xf1, + 0x4e, 0xbd, 0x6f, 0x6b, 0x11, 0x88, 0x93, 0xd5, 0xd4, 0x0c, 0x2d, 0x5f, 0x2b, 0x89, 0xca, 0x5c, 0x96, 0x24, 0x34, + 0x03, 0x19, 0x4a, 0x38, 0x66, 0x45, 0x51, 0xca, 0xf5, 0x37, 0x20, 0x44, 0x31, 0x25, 0x09, 0xf0, 0x1d, 0x61, 0x76, + 0xe1, 0x0c, 0xa7, 0x38, 0x12, 0x5c, 0x83, 0x10, 0x72, 0xaa, 0x93, 0x5a, 0xb8, 0xe0, 0x40, 0x3e, 0x61, 0x86, 0x44, + 0xca, 0x09, 0x75, 0xcf, 0x77, 0x4f, 0xd3, 0x3b, 0x4d, 0xb2, 0x21, 0x1b, 0x79, 0xa2, 0x5a, 0xac, 0xf8, 0x56, 0x40, + 0xde, 0x39, 0x1c, 0x95, 0xe1, 0x11, 0x57, 0xb0, 0xbf, 0xa7, 0x2c, 0xa3, 0x42, 0x03, 0xdf, 0xd5, 0x66, 0x9f, 0x5f, + 0x57, 0x1f, 0x7d, 0xd3, 0x79, 0x03, 0x88, 0x0c, 0xc0, 0xb7, 0x93, 0x91, 0xb5, 0x6a, 0xe7, 0xbb, 0x27, 0x6f, 0x36, + 0x99, 0xc0, 0xcb, 0xa5, 0x32, 0x7e, 0x7d, 0xd0, 0x6c, 0x70, 0x50, 0x41, 0xea, 0xab, 0x1f, 0x9e, 0xe3, 0x0b, 0x05, + 0x29, 0x70, 0x12, 0xa0, 0xa2, 0xf3, 0xdd, 0x93, 0xf7, 0x4e, 0x22, 0x5c, 0x4b, 0x08, 0x9b, 0xd3, 0x76, 0x52, 0xe2, + 0x44, 0x84, 0x22, 0x39, 0xf7, 0x92, 0x71, 0xa5, 0x86, 0xf8, 0xf6, 0x22, 0xf1, 0x12, 0xec, 0x87, 0x21, 0x1b, 0x11, + 0x5f, 0x61, 0x80, 0xf8, 0x08, 0xfb, 0x35, 0xb3, 0x8c, 0xc0, 0x02, 0x88, 0xb1, 0xce, 0x60, 0x25, 0x5c, 0xa9, 0xf8, + 0x21, 0xec, 0x8b, 0x51, 0x79, 0x21, 0x45, 0xc7, 0xcf, 0x6b, 0xb9, 0x69, 0x95, 0x35, 0xfa, 0x2d, 0x58, 0x4e, 0xfa, + 0xe1, 0xb5, 0xea, 0xba, 0x2c, 0x78, 0xaa, 0x93, 0xc8, 0xce, 0x77, 0x4f, 0x5e, 0xa9, 0x3c, 0xb2, 0x99, 0xaf, 0xb9, + 0xfd, 0x9a, 0x85, 0x79, 0xf2, 0xca, 0xad, 0xde, 0x8a, 0xca, 0xe7, 0xbb, 0x27, 0x1f, 0x36, 0x55, 0x83, 0xf2, 0x62, + 0x5e, 0x99, 0xf8, 0x02, 0xbe, 0x05, 0x8d, 0xbd, 0x85, 0x12, 0x0d, 0x1e, 0x2b, 0xb0, 0x10, 0x47, 0x5e, 0x5e, 0x94, + 0x9e, 0x91, 0xa7, 0x38, 0x23, 0x22, 0x0e, 0x54, 0x5f, 0x35, 0xa5, 0xe4, 0xb1, 0x34, 0x39, 0x0b, 0xd2, 0x19, 0xdd, + 0x12, 0x1c, 0x3a, 0x41, 0x2e, 0x9b, 0x42, 0x02, 0x8d, 0x00, 0x9d, 0xe1, 0x9d, 0x36, 0xea, 0xd5, 0x85, 0x57, 0x26, + 0x88, 0x34, 0xad, 0x49, 0x16, 0x1c, 0x91, 0x36, 0xf6, 0x49, 0x1b, 0x07, 0x24, 0x1f, 0xb6, 0xa5, 0x78, 0xe8, 0x05, + 0x65, 0xbf, 0x52, 0xc8, 0x40, 0x6e, 0x58, 0x20, 0x77, 0xab, 0x14, 0xbf, 0x61, 0x2f, 0x10, 0xae, 0x47, 0x21, 0xd1, + 0x43, 0x69, 0xb4, 0x3a, 0x29, 0x4e, 0x45, 0xc7, 0x67, 0xec, 0x32, 0x86, 0xec, 0x12, 0x98, 0x15, 0xe6, 0xc8, 0x2b, + 0xab, 0x76, 0x54, 0xd5, 0xc0, 0x15, 0xeb, 0x94, 0xe2, 0xc0, 0x05, 0xc6, 0x8d, 0x03, 0x95, 0x8c, 0x93, 0xaf, 0x37, + 0x79, 0xb8, 0xb7, 0xe7, 0xc8, 0x46, 0xdf, 0x71, 0x27, 0xd5, 0xef, 0xab, 0xd0, 0xdd, 0xb7, 0x92, 0x57, 0x84, 0x48, + 0xc0, 0xdf, 0x68, 0xf8, 0xa3, 0x02, 0xe2, 0xd0, 0x4e, 0x50, 0xc7, 0xa0, 0x06, 0x5e, 0x68, 0x7a, 0xf5, 0xe9, 0x37, + 0x1a, 0x65, 0x98, 0xb6, 0x8e, 0xad, 0x13, 0x9c, 0x15, 0x57, 0x4e, 0x99, 0xff, 0xd3, 0x5e, 0xcb, 0x9a, 0xd2, 0x20, + 0x20, 0x66, 0xd2, 0x2c, 0xd3, 0x93, 0x31, 0xb6, 0x04, 0x83, 0x7a, 0x2f, 0x54, 0xe2, 0x02, 0x16, 0x39, 0x56, 0xaa, + 0x92, 0x66, 0x67, 0x5d, 0xe4, 0xe9, 0x4a, 0x10, 0x96, 0x82, 0x4a, 0x8d, 0x42, 0x91, 0xf7, 0xab, 0xf5, 0xcc, 0x4b, + 0x9c, 0x23, 0xe5, 0xe3, 0x12, 0x50, 0x08, 0x64, 0x75, 0x4b, 0xa4, 0x3c, 0x27, 0x93, 0xed, 0x24, 0x7f, 0x62, 0x90, + 0xfc, 0x13, 0x42, 0x0d, 0xf2, 0x97, 0x1e, 0x0e, 0x37, 0x55, 0xae, 0x85, 0x5c, 0xbf, 0x3a, 0x9d, 0x11, 0xf0, 0xa1, + 0xd5, 0x31, 0x5a, 0x8b, 0x2b, 0x6e, 0x61, 0x28, 0xe6, 0x0e, 0x11, 0x5e, 0x48, 0xac, 0x83, 0xc0, 0x4e, 0x15, 0x55, + 0x83, 0xa1, 0x37, 0xb9, 0xf4, 0x4c, 0x0e, 0x78, 0xf2, 0xe1, 0xee, 0x80, 0xe8, 0xe9, 0x6c, 0x7d, 0xe7, 0x1a, 0x19, + 0xa0, 0x30, 0x6b, 0x63, 0xe3, 0xd6, 0xf3, 0x41, 0x61, 0xfc, 0x32, 0x90, 0x5d, 0x67, 0x3e, 0x2b, 0x9b, 0x50, 0xcb, + 0x3f, 0x80, 0xb6, 0xd3, 0x11, 0x35, 0xa8, 0xd1, 0x2d, 0xf0, 0x23, 0x99, 0x87, 0xea, 0x67, 0x5b, 0xd8, 0xc7, 0x89, + 0xa8, 0x40, 0x93, 0x70, 0xf3, 0xeb, 0x27, 0x85, 0x22, 0x13, 0x09, 0x1a, 0x5a, 0x00, 0xff, 0x93, 0x24, 0x0f, 0x74, + 0x23, 0xe4, 0x02, 0x20, 0x68, 0x22, 0xf0, 0x54, 0x21, 0xcc, 0xb6, 0x2b, 0xe7, 0xfb, 0xf3, 0x1d, 0x42, 0x26, 0x95, + 0xf3, 0xf1, 0x5d, 0x95, 0x7d, 0x05, 0x64, 0x81, 0x3c, 0x30, 0x1e, 0xcb, 0x02, 0x19, 0xbf, 0x3c, 0xd5, 0xd5, 0x85, + 0x01, 0xe9, 0x56, 0xfa, 0xb6, 0x11, 0xdb, 0x14, 0x5e, 0x39, 0xf9, 0x5e, 0xa3, 0x61, 0xe5, 0xed, 0x2e, 0xbc, 0x7d, + 0xc9, 0x05, 0x8c, 0xf0, 0xfc, 0x5e, 0xd4, 0xd6, 0xfd, 0x16, 0x1f, 0x57, 0x53, 0x58, 0x56, 0x16, 0xc5, 0x65, 0x49, + 0x4e, 0x33, 0xfe, 0x84, 0x8e, 0xd3, 0x0c, 0x42, 0x16, 0x25, 0x4e, 0x50, 0xb1, 0x6b, 0xb8, 0xed, 0xc4, 0xfc, 0x8c, + 0x38, 0xc1, 0xca, 0x04, 0xc5, 0xaf, 0x8f, 0x22, 0x6a, 0x7d, 0xbe, 0xda, 0x6a, 0xb2, 0xb7, 0xf7, 0xae, 0x42, 0x93, + 0x82, 0x52, 0x40, 0x61, 0x30, 0x2d, 0xa9, 0xd2, 0xa8, 0x50, 0xee, 0xae, 0x53, 0xba, 0x00, 0x34, 0xc3, 0x30, 0x79, + 0xcf, 0x73, 0xc2, 0x8b, 0xc9, 0x2a, 0x8b, 0x57, 0xae, 0x09, 0x66, 0x9a, 0x2d, 0xc0, 0xe1, 0xc1, 0xd0, 0x96, 0xbe, + 0xa2, 0xbc, 0x4a, 0x89, 0x2d, 0x61, 0x38, 0x05, 0x64, 0x39, 0xc2, 0x08, 0x31, 0x28, 0x70, 0xa3, 0x51, 0xf2, 0x16, + 0xf4, 0xca, 0x08, 0xe7, 0x6e, 0x04, 0x49, 0xb0, 0xb5, 0x2d, 0x8b, 0x10, 0x96, 0x99, 0x39, 0x46, 0x2e, 0xc1, 0xc9, + 0xf3, 0x4d, 0x1e, 0x65, 0x4d, 0xd4, 0x54, 0x48, 0x1d, 0xa8, 0x91, 0xa1, 0xb2, 0x81, 0x7b, 0xe5, 0x30, 0xa5, 0xb8, + 0xe9, 0xb8, 0x19, 0x30, 0xe0, 0x9f, 0xb9, 0x23, 0x63, 0x51, 0x20, 0x33, 0x52, 0x77, 0xee, 0xd4, 0x86, 0xee, 0xa5, + 0xa2, 0x19, 0x56, 0x88, 0x8b, 0x4c, 0x34, 0xa5, 0x22, 0xae, 0x77, 0x5a, 0xf1, 0xd2, 0x2b, 0x99, 0x47, 0xcd, 0x35, + 0x17, 0xac, 0x32, 0x49, 0x8c, 0xe9, 0x5f, 0xc9, 0xd4, 0xe8, 0xb2, 0x12, 0xa8, 0x61, 0xf4, 0xda, 0x7a, 0x22, 0xd6, + 0x80, 0x16, 0x40, 0x5f, 0x8b, 0x53, 0x6e, 0xac, 0xa8, 0xf6, 0x61, 0x8b, 0x31, 0x0d, 0xa9, 0xff, 0x0e, 0x72, 0x5d, + 0x56, 0xf7, 0xfc, 0x73, 0x21, 0x0b, 0x19, 0xce, 0x6b, 0x8c, 0x3d, 0x13, 0x8c, 0x1d, 0x81, 0x9e, 0xa6, 0xd3, 0xbf, + 0x07, 0x2a, 0xe5, 0x45, 0xe5, 0x2e, 0x3a, 0x8a, 0xc4, 0x5e, 0x97, 0xe1, 0x72, 0xe3, 0xf7, 0xca, 0x6a, 0x78, 0x8c, + 0x40, 0x1a, 0x10, 0x56, 0x9c, 0x3d, 0x43, 0x38, 0x6f, 0x34, 0x7a, 0xf9, 0x31, 0xad, 0x5c, 0x24, 0x15, 0x8c, 0x0c, + 0x22, 0xba, 0x40, 0xf0, 0x35, 0x19, 0x0a, 0x31, 0x7f, 0x9d, 0x9f, 0x9d, 0x83, 0xab, 0xfd, 0xe4, 0x9d, 0x63, 0x72, + 0x35, 0xb3, 0x6e, 0x19, 0x34, 0x85, 0xf9, 0x38, 0x55, 0xbc, 0xe5, 0xed, 0xdd, 0x19, 0x1e, 0x00, 0xf7, 0x4e, 0x07, + 0x43, 0x36, 0x1a, 0xea, 0x71, 0xc9, 0x12, 0xca, 0xdd, 0xd7, 0x43, 0x55, 0x62, 0xa2, 0x39, 0x58, 0x8f, 0x57, 0xa6, + 0x2c, 0x27, 0x79, 0x51, 0xe4, 0xb4, 0x8a, 0xef, 0xaf, 0x64, 0x60, 0x0a, 0xe1, 0xb2, 0xee, 0x6c, 0x3f, 0x9d, 0x11, + 0x8e, 0x0d, 0x42, 0x7d, 0xbb, 0x2d, 0xf4, 0x51, 0x81, 0x09, 0xfb, 0x5a, 0x09, 0xc5, 0x6f, 0x37, 0x09, 0x45, 0x9c, + 0xa9, 0x2d, 0x2f, 0x04, 0x62, 0xe7, 0x1e, 0x02, 0x51, 0x39, 0xd9, 0xb5, 0x4c, 0x04, 0x75, 0xa4, 0x26, 0x13, 0xeb, + 0x4b, 0x4a, 0x32, 0xcc, 0xd4, 0x6a, 0xf4, 0xbb, 0xcb, 0x25, 0x1b, 0xb6, 0xc1, 0x89, 0x64, 0xdb, 0xf0, 0xb3, 0x23, + 0x7f, 0x1a, 0x9c, 0x58, 0x3a, 0x81, 0x1d, 0x56, 0x9a, 0x2c, 0xc8, 0x85, 0x34, 0x67, 0x47, 0x64, 0x65, 0x09, 0x9a, + 0x56, 0x14, 0xa4, 0x08, 0x9c, 0xb0, 0x32, 0xca, 0x04, 0x10, 0x0b, 0x59, 0xa1, 0x0c, 0x48, 0x67, 0x63, 0xfa, 0x9f, + 0x36, 0x2f, 0x3f, 0xad, 0x89, 0xd6, 0xe4, 0x8a, 0x54, 0x1f, 0x6a, 0x09, 0x07, 0x0a, 0x02, 0xa5, 0x1f, 0xee, 0x08, + 0x13, 0xb4, 0x12, 0xe5, 0xc8, 0x94, 0x43, 0xb8, 0x0d, 0x2e, 0xb4, 0x9d, 0x77, 0x32, 0xc0, 0xbb, 0x41, 0x9a, 0xe0, + 0xd4, 0xa0, 0xeb, 0xe7, 0x84, 0xd7, 0x58, 0x49, 0x44, 0x94, 0xa5, 0x84, 0x03, 0x41, 0xa6, 0x9c, 0x64, 0xc3, 0xf6, + 0x08, 0x14, 0xd0, 0x9e, 0x7f, 0x9c, 0x55, 0x26, 0xb0, 0xdf, 0x68, 0xa0, 0x40, 0x8f, 0x1a, 0x0d, 0x59, 0xc3, 0x1f, + 0x61, 0x8a, 0x7d, 0x69, 0x98, 0x9c, 0xee, 0xed, 0x39, 0x41, 0x35, 0xee, 0xd0, 0x1f, 0x21, 0x9c, 0x2e, 0x97, 0x8e, + 0x00, 0x2b, 0x40, 0xcb, 0x65, 0x60, 0x82, 0x25, 0x5e, 0x43, 0xb3, 0xc9, 0x80, 0x93, 0x89, 0x10, 0x80, 0x13, 0x80, + 0xb0, 0x41, 0x9c, 0x40, 0x39, 0xf7, 0x02, 0x70, 0x46, 0x35, 0xb2, 0xa1, 0xdf, 0xe8, 0x8c, 0x0c, 0xc6, 0x35, 0xf4, + 0x47, 0x24, 0x28, 0xd2, 0xbd, 0xbd, 0x9d, 0x5c, 0x89, 0xc8, 0x9f, 0x41, 0x94, 0xfd, 0x2c, 0x24, 0x8b, 0xec, 0xd0, + 0x5c, 0x8d, 0x55, 0x67, 0x40, 0x49, 0x51, 0x6a, 0x59, 0x75, 0xbd, 0x5a, 0x16, 0x44, 0x59, 0x09, 0xab, 0x58, 0xf0, + 0x00, 0x2c, 0xfb, 0x92, 0xcc, 0x7f, 0xe1, 0x65, 0x9a, 0xf5, 0xb7, 0x1b, 0x93, 0xab, 0x5d, 0xd7, 0xf5, 0xb3, 0x89, + 0x88, 0x64, 0xe8, 0x28, 0xac, 0x20, 0xfe, 0x7d, 0x05, 0xa6, 0x31, 0xf0, 0xb0, 0x1c, 0x6b, 0x44, 0x24, 0xf8, 0x5a, + 0xb5, 0xd1, 0x27, 0x4a, 0x7e, 0xdd, 0xe8, 0x65, 0x90, 0x90, 0x7c, 0xfd, 0x5b, 0x21, 0x39, 0x50, 0x90, 0x48, 0xf2, + 0x58, 0xc1, 0xd9, 0x16, 0x5c, 0xfc, 0xca, 0x57, 0x70, 0xb6, 0x1d, 0xb7, 0x25, 0x43, 0xd8, 0x06, 0x9f, 0xc1, 0x1b, + 0x24, 0xa0, 0x55, 0x81, 0x01, 0xe5, 0xe1, 0xaa, 0xee, 0x25, 0x59, 0x29, 0x08, 0x53, 0x4e, 0x1c, 0x56, 0xdf, 0x00, + 0x95, 0x36, 0x6a, 0x18, 0xbe, 0xcc, 0x9b, 0x20, 0xc3, 0x25, 0x50, 0x4f, 0x5d, 0x01, 0x72, 0x52, 0xbe, 0x76, 0x48, + 0x45, 0xd8, 0x91, 0x4a, 0x9c, 0x1b, 0xf8, 0x33, 0x3e, 0xcf, 0x40, 0x95, 0xca, 0xf5, 0x6f, 0x28, 0x86, 0xb3, 0x20, + 0xa2, 0x0c, 0x7e, 0x40, 0xc1, 0xcc, 0xcf, 0x73, 0x76, 0x25, 0xcb, 0xd4, 0x6f, 0x9c, 0x12, 0x4d, 0xca, 0xb9, 0xd4, + 0x09, 0x33, 0xd4, 0xcb, 0x14, 0x9d, 0xd6, 0xd1, 0xf6, 0xec, 0x8a, 0x26, 0xfc, 0x25, 0xcb, 0x39, 0x4d, 0x60, 0xfa, + 0x15, 0xc5, 0xc1, 0x8c, 0x72, 0x04, 0x1b, 0xb6, 0xd6, 0xca, 0x0f, 0xc3, 0x3b, 0x9b, 0xf0, 0xba, 0x0e, 0x14, 0xf9, + 0x49, 0x18, 0xcb, 0x41, 0xcc, 0x84, 0x46, 0x9d, 0xc4, 0x59, 0xd6, 0x34, 0xf3, 0x69, 0x2a, 0x65, 0x43, 0x70, 0x77, + 0x87, 0x11, 0x2d, 0x09, 0xb4, 0xf4, 0xbc, 0x53, 0x6b, 0x81, 0x80, 0xf7, 0x96, 0x45, 0x30, 0x67, 0x82, 0xb9, 0xc1, + 0x51, 0xdd, 0x3a, 0x9c, 0x9a, 0x6e, 0xbe, 0xdb, 0x78, 0xb0, 0x6d, 0x93, 0x70, 0x10, 0x74, 0xf2, 0x70, 0xbb, 0x65, + 0xf5, 0x4a, 0x4b, 0x0e, 0x2d, 0x2d, 0xd8, 0x7d, 0x19, 0x33, 0x5a, 0x68, 0xf2, 0x42, 0x7a, 0x2b, 0xee, 0x72, 0xf2, + 0x0b, 0x9c, 0x1c, 0x7a, 0xce, 0xa7, 0xf1, 0xca, 0x01, 0x99, 0xde, 0x6e, 0xa9, 0xfd, 0xef, 0x72, 0xe7, 0x09, 0x7e, + 0x05, 0x61, 0xdd, 0x6f, 0xaa, 0xea, 0xeb, 0xe1, 0xdc, 0x6f, 0x2a, 0x04, 0x7d, 0xe3, 0xad, 0xd5, 0x33, 0xc2, 0xb8, + 0x5d, 0xf7, 0xc8, 0x6d, 0xdb, 0x5a, 0x5b, 0xfa, 0x51, 0x06, 0x91, 0x64, 0xaa, 0xa5, 0xd8, 0x0f, 0xb8, 0x4a, 0x54, + 0x83, 0x84, 0xb9, 0xba, 0x85, 0x44, 0x55, 0x8a, 0xa1, 0xd4, 0xe1, 0xb7, 0x2d, 0x8f, 0x92, 0x31, 0x99, 0xb4, 0x33, + 0xde, 0xfa, 0x19, 0xdf, 0x85, 0x5d, 0x96, 0xae, 0x9d, 0xc6, 0x8b, 0x08, 0x78, 0xd0, 0xee, 0x37, 0x84, 0x61, 0x6c, + 0xe7, 0xf2, 0x30, 0x90, 0xd9, 0x3f, 0x49, 0xb5, 0xee, 0x56, 0xb7, 0x32, 0x5e, 0x83, 0xfd, 0x8f, 0x70, 0xa4, 0x8f, + 0xc8, 0x51, 0xc5, 0x81, 0xa9, 0xb7, 0x28, 0x4a, 0xa7, 0x40, 0x2a, 0x95, 0xb7, 0x04, 0xe1, 0xb4, 0x10, 0xe1, 0xed, + 0xef, 0xf1, 0x0f, 0x8a, 0x25, 0x9e, 0x97, 0x1c, 0xe7, 0xd9, 0x7d, 0x39, 0xa2, 0x04, 0xbf, 0x8c, 0xde, 0x03, 0x1d, + 0x0b, 0x0a, 0x2d, 0x34, 0x15, 0x3d, 0x4d, 0xd5, 0x44, 0xb6, 0xe6, 0xa5, 0x62, 0x5a, 0x66, 0xd4, 0x88, 0x61, 0x36, + 0x24, 0x72, 0x6a, 0x2b, 0x9b, 0x97, 0xbb, 0xaa, 0x36, 0x2e, 0xda, 0x82, 0xc5, 0x2a, 0xb0, 0xb8, 0x5c, 0x3a, 0x75, + 0x54, 0x13, 0x66, 0xc4, 0x31, 0x10, 0x66, 0x46, 0x42, 0x45, 0x4d, 0xb3, 0x96, 0x6d, 0x1c, 0xb4, 0x9a, 0x4f, 0xa4, + 0x75, 0xf3, 0x1a, 0x1c, 0xa6, 0x0b, 0x41, 0x36, 0x37, 0x7d, 0x0a, 0x58, 0xce, 0xae, 0x1c, 0xc8, 0xc0, 0xd0, 0x8f, + 0x65, 0xae, 0x6c, 0x95, 0xd4, 0xba, 0x01, 0xbf, 0xe8, 0x8e, 0x6c, 0x59, 0x85, 0xba, 0xf5, 0xf7, 0x46, 0xae, 0xd1, + 0xd3, 0x74, 0x5b, 0xae, 0x51, 0x4d, 0xdb, 0xdd, 0x69, 0xa3, 0xbb, 0xf3, 0x52, 0xe5, 0x58, 0x9b, 0xab, 0xfc, 0x86, + 0xe1, 0x3a, 0x40, 0x9b, 0x12, 0xcd, 0x9a, 0xab, 0x9c, 0x16, 0xc5, 0x79, 0x79, 0x9a, 0x40, 0xa4, 0xee, 0x9c, 0x4b, + 0xfa, 0x57, 0x56, 0xa3, 0x38, 0x94, 0xeb, 0x7c, 0x4f, 0x26, 0x71, 0x7a, 0xe9, 0xc7, 0xef, 0x61, 0xbc, 0xea, 0xe5, + 0xf3, 0xdb, 0x30, 0xf3, 0x39, 0x55, 0xdc, 0xa5, 0x82, 0xe1, 0x7b, 0x03, 0x86, 0xef, 0x25, 0x9f, 0xae, 0xda, 0xe3, + 0xc5, 0xcb, 0xb2, 0x03, 0xef, 0xbc, 0xd0, 0x2c, 0xe3, 0x96, 0x6f, 0x1e, 0x63, 0x95, 0x85, 0xdd, 0x96, 0x2c, 0xec, + 0x96, 0x3b, 0xab, 0x5d, 0x39, 0xce, 0x0f, 0x9b, 0x7b, 0x59, 0xe7, 0x6c, 0x3f, 0x54, 0x1b, 0xff, 0x07, 0xef, 0xce, + 0x36, 0x06, 0x97, 0xdb, 0x77, 0xf7, 0x45, 0xb2, 0x8a, 0x04, 0xf9, 0x25, 0x24, 0x1d, 0x70, 0xd2, 0x37, 0x0e, 0x1d, + 0x54, 0x72, 0x4a, 0xe7, 0x01, 0x39, 0xc1, 0x3c, 0xe7, 0xe9, 0x54, 0xf5, 0x99, 0xab, 0x93, 0x46, 0xe2, 0x25, 0xb8, + 0xa2, 0x45, 0xac, 0xdd, 0xab, 0x9f, 0xe5, 0x5a, 0x7c, 0x64, 0x49, 0xe8, 0xe5, 0x58, 0x49, 0x91, 0xdc, 0xcb, 0x0a, + 0xa2, 0xb3, 0x8d, 0xd7, 0xdf, 0xe1, 0x31, 0x4b, 0x58, 0x1e, 0xd1, 0xcc, 0x49, 0xd1, 0x62, 0xdb, 0x60, 0x29, 0x04, + 0x64, 0xe4, 0x60, 0xf8, 0xaf, 0xd5, 0xa9, 0x3f, 0x17, 0x7a, 0x03, 0x3f, 0xd0, 0x94, 0xf2, 0x28, 0x0d, 0x21, 0x2d, + 0xc5, 0x0d, 0xcb, 0x43, 0x4d, 0x7b, 0x7b, 0x3b, 0x8e, 0x2d, 0xdc, 0x12, 0x70, 0x00, 0xdc, 0x7c, 0x83, 0x06, 0x0b, + 0x38, 0x9f, 0x53, 0x0d, 0x4d, 0xd1, 0x82, 0xae, 0x1e, 0x65, 0xe1, 0xee, 0x47, 0x7a, 0x8b, 0x13, 0x54, 0x14, 0x9e, + 0x84, 0xda, 0x1e, 0x33, 0x1a, 0x87, 0x36, 0xfe, 0x48, 0x6f, 0xbd, 0xf2, 0xcc, 0xb8, 0x38, 0xe2, 0x2c, 0x16, 0xd0, + 0x4e, 0xaf, 0x13, 0x1b, 0x57, 0x83, 0x78, 0x8b, 0x02, 0xa7, 0x19, 0x9b, 0x00, 0x71, 0x7e, 0x43, 0x6f, 0x3d, 0xd9, + 0x1f, 0x33, 0xce, 0xeb, 0xa1, 0x85, 0x46, 0xbd, 0x6b, 0x14, 0x9b, 0xcb, 0xa0, 0x0c, 0x8a, 0xa1, 0x68, 0x3b, 0x22, + 0xb5, 0x7a, 0x95, 0x79, 0x88, 0x50, 0x71, 0xdf, 0xa9, 0xe0, 0x6f, 0x4c, 0xd1, 0xc6, 0x6b, 0x99, 0xaf, 0x2b, 0x8d, + 0x28, 0x34, 0xa8, 0x32, 0x3d, 0x76, 0x9d, 0x44, 0xef, 0x3a, 0x75, 0x08, 0xc1, 0x70, 0x84, 0x7d, 0xc3, 0x55, 0xa7, + 0xde, 0x5f, 0x65, 0x42, 0x48, 0x15, 0x49, 0x7a, 0x51, 0xb5, 0xb3, 0x76, 0x1d, 0xc0, 0x3b, 0x24, 0xb4, 0xf8, 0xe2, + 0x4c, 0x66, 0xa1, 0xb3, 0x45, 0xff, 0xc6, 0x89, 0xb3, 0xd0, 0x53, 0xf0, 0x12, 0x13, 0x8b, 0xbc, 0x00, 0x2a, 0x54, + 0xf4, 0x25, 0x13, 0x00, 0xd9, 0xd8, 0x61, 0x6b, 0x52, 0x33, 0x13, 0x52, 0xd3, 0x35, 0x30, 0xbe, 0x45, 0x4a, 0x52, + 0x81, 0x0c, 0xa1, 0x44, 0x0a, 0xa1, 0xa7, 0x16, 0x57, 0x91, 0x90, 0xb9, 0xa0, 0xe5, 0x09, 0x3a, 0xb9, 0xe6, 0x59, + 0x0d, 0x2c, 0x47, 0xf4, 0x83, 0x0a, 0x0f, 0xa6, 0x44, 0x65, 0x85, 0xa2, 0x3c, 0x9a, 0xad, 0xd3, 0x5b, 0x9d, 0xd4, + 0xd5, 0xd3, 0x22, 0x1a, 0x25, 0x4e, 0x84, 0x16, 0x89, 0x13, 0xe1, 0x0c, 0xd2, 0x11, 0xd3, 0xa2, 0x84, 0x9f, 0x9a, + 0xab, 0x51, 0x4b, 0x56, 0xde, 0x7c, 0xca, 0x0f, 0x94, 0x79, 0x0e, 0x29, 0x9a, 0x38, 0xd1, 0x3c, 0x25, 0x71, 0xc4, + 0x71, 0x3b, 0x63, 0xd9, 0xbe, 0x57, 0x09, 0x3a, 0x0a, 0xb0, 0xbf, 0x71, 0x67, 0x61, 0xcc, 0xc2, 0x3c, 0xd1, 0xad, + 0x4e, 0xfd, 0xa9, 0x60, 0x5f, 0x95, 0x43, 0xea, 0xe4, 0x64, 0x45, 0xe2, 0xdc, 0x9d, 0x6a, 0xf9, 0xcb, 0x9c, 0x66, + 0xb7, 0x67, 0x14, 0x52, 0x9d, 0x53, 0x38, 0xf0, 0x5b, 0x2d, 0x43, 0x95, 0xa7, 0x3e, 0xc8, 0x84, 0xb2, 0x52, 0xd4, + 0xcf, 0x01, 0xae, 0x9e, 0x12, 0x2c, 0x44, 0xb4, 0xd1, 0x70, 0xc4, 0xc8, 0xdd, 0x42, 0xb7, 0x9e, 0x9f, 0xa4, 0x3d, + 0x06, 0xfe, 0xb5, 0x0a, 0xd3, 0x2a, 0x58, 0x80, 0x53, 0xf3, 0x4c, 0xea, 0x30, 0x1f, 0xad, 0x7a, 0x65, 0xa0, 0x08, + 0xc2, 0x77, 0xd9, 0xf6, 0xa9, 0x6e, 0x4a, 0x9a, 0xdd, 0x3e, 0xd5, 0x5a, 0xd0, 0x4f, 0x24, 0xfc, 0x60, 0x35, 0x4e, + 0x79, 0x82, 0x99, 0x15, 0x05, 0x2a, 0x00, 0xbc, 0xbf, 0xf4, 0x1c, 0xe7, 0x2f, 0x2a, 0x65, 0xd0, 0x85, 0x58, 0xec, + 0x59, 0x9c, 0x6a, 0x26, 0x5e, 0x8d, 0xff, 0x97, 0xb5, 0xf1, 0xff, 0x62, 0x9c, 0x3a, 0x05, 0xd3, 0x68, 0x92, 0xd0, + 0x50, 0xb3, 0x4e, 0x24, 0x09, 0x50, 0xe8, 0x6d, 0x19, 0x27, 0x1f, 0x2f, 0x3c, 0xd0, 0xb8, 0x16, 0xe3, 0x34, 0xe1, + 0xcd, 0xb1, 0x3f, 0x65, 0xf1, 0xad, 0x37, 0x67, 0xcd, 0x69, 0x9a, 0xa4, 0xf9, 0xcc, 0x0f, 0x28, 0xce, 0x6f, 0x73, + 0x4e, 0xa7, 0xcd, 0x39, 0xc3, 0xcf, 0x69, 0x7c, 0x45, 0x39, 0x0b, 0x7c, 0x6c, 0x9f, 0x64, 0xcc, 0x8f, 0xad, 0xd7, + 0x7e, 0x96, 0xa5, 0xd7, 0x36, 0x7e, 0x97, 0x5e, 0xa6, 0x3c, 0xc5, 0x6f, 0x6e, 0x6e, 0x27, 0x34, 0xc1, 0x1f, 0x2e, + 0xe7, 0x09, 0x9f, 0xe3, 0xdc, 0x4f, 0xf2, 0x66, 0x4e, 0x33, 0x36, 0xee, 0x05, 0x69, 0x9c, 0x66, 0x4d, 0xc8, 0xd8, + 0x9e, 0x52, 0x2f, 0x66, 0x93, 0x88, 0x5b, 0xa1, 0x9f, 0x7d, 0xec, 0x35, 0x9b, 0xb3, 0x8c, 0x4d, 0xfd, 0xec, 0xb6, + 0x29, 0x6a, 0x78, 0x5f, 0xb6, 0xf7, 0xfd, 0xc7, 0xe3, 0x83, 0x1e, 0xcf, 0xfc, 0x24, 0x67, 0xb0, 0x4c, 0x9e, 0x1f, + 0xc7, 0xd6, 0xfe, 0x61, 0x7b, 0x9a, 0xef, 0xc8, 0x40, 0x9e, 0x9f, 0xf0, 0xe2, 0x02, 0xbf, 0x07, 0xb8, 0xdd, 0x4b, + 0x9e, 0xe0, 0xcb, 0x39, 0xe7, 0x69, 0xb2, 0x08, 0xe6, 0x59, 0x9e, 0x66, 0xde, 0x2c, 0x65, 0x09, 0xa7, 0x59, 0xef, + 0x32, 0xcd, 0x42, 0x9a, 0x35, 0x33, 0x3f, 0x64, 0xf3, 0xdc, 0x3b, 0x98, 0xdd, 0xf4, 0x40, 0xb3, 0x98, 0x64, 0xe9, + 0x3c, 0x09, 0xd5, 0x58, 0x2c, 0x89, 0x68, 0xc6, 0xb8, 0xf9, 0x42, 0x5c, 0x64, 0xe2, 0xc5, 0x2c, 0xa1, 0x7e, 0xd6, + 0x9c, 0x40, 0x63, 0x30, 0x8b, 0xda, 0x21, 0x9d, 0xe0, 0x6c, 0x72, 0xe9, 0x3b, 0x9d, 0xee, 0x23, 0xac, 0xff, 0x77, + 0x0f, 0x91, 0xd5, 0xde, 0x5c, 0xdc, 0x69, 0xb7, 0xff, 0x84, 0x7a, 0x2b, 0xa3, 0x08, 0x80, 0xbc, 0xce, 0xec, 0xc6, + 0xca, 0x53, 0xc8, 0x68, 0xdb, 0xd4, 0xb2, 0x37, 0xf3, 0x43, 0xc8, 0x07, 0xf6, 0xba, 0xb3, 0x9b, 0x02, 0x66, 0xe7, + 0xc9, 0x14, 0x53, 0x35, 0x49, 0xf5, 0xb4, 0xf8, 0xad, 0x10, 0x1f, 0x6d, 0x86, 0xb8, 0xab, 0x21, 0xae, 0xb0, 0xde, + 0x0c, 0xe7, 0x99, 0x88, 0xad, 0x7a, 0x9d, 0x5c, 0x02, 0x12, 0xa5, 0x57, 0x34, 0xd3, 0x70, 0x88, 0x87, 0xdf, 0x0c, + 0x46, 0x77, 0x33, 0x18, 0x47, 0x9f, 0x02, 0x23, 0x4b, 0xc2, 0x45, 0x7d, 0x5d, 0x3b, 0x19, 0x9d, 0xf6, 0x22, 0x0a, + 0xf4, 0xe4, 0x75, 0xe1, 0xf7, 0x35, 0x0b, 0x79, 0x24, 0x7f, 0x0a, 0x72, 0xbe, 0x96, 0xef, 0x0e, 0xdb, 0x6d, 0xf9, + 0x9c, 0xb3, 0x5f, 0xa9, 0xd7, 0x71, 0xa1, 0x42, 0x71, 0x81, 0x7f, 0x28, 0x4f, 0xf3, 0xd6, 0xb9, 0x27, 0xfe, 0x8b, + 0x79, 0xcc, 0xd7, 0x48, 0x51, 0xac, 0x0e, 0x45, 0xe3, 0x54, 0xcb, 0x4a, 0x29, 0x7c, 0xc0, 0x6d, 0x27, 0xb8, 0x23, + 0x61, 0xfd, 0xf2, 0x18, 0x27, 0x1b, 0xfc, 0x45, 0xe6, 0x5d, 0x78, 0x10, 0xe9, 0x30, 0x52, 0x0d, 0xd3, 0x5e, 0xd6, + 0x27, 0xed, 0x5e, 0xd6, 0x6c, 0x22, 0x27, 0x25, 0xc9, 0x30, 0x53, 0xc9, 0x79, 0x0e, 0x1b, 0xa4, 0xc2, 0xd8, 0xce, + 0x91, 0x97, 0xc2, 0x59, 0xd3, 0xe5, 0xb2, 0x0a, 0x03, 0x30, 0x71, 0x5a, 0xe3, 0x07, 0xae, 0x2a, 0xe0, 0xdc, 0xe0, + 0xe4, 0xbe, 0xbe, 0xde, 0x25, 0xd1, 0xbc, 0x22, 0x4e, 0x03, 0x81, 0x39, 0x77, 0xe6, 0xf3, 0x08, 0xbc, 0x14, 0xa5, + 0xf8, 0xa9, 0x52, 0x98, 0xec, 0x96, 0x8d, 0x06, 0x49, 0x99, 0xdf, 0x06, 0x79, 0x7c, 0x49, 0x01, 0xbd, 0x5c, 0x72, + 0x02, 0x3d, 0x56, 0xfd, 0x7f, 0xe0, 0x86, 0xa4, 0x4e, 0x5c, 0x96, 0x04, 0xf1, 0x3c, 0xa4, 0xb9, 0xe8, 0xa1, 0x12, + 0xe7, 0x70, 0x37, 0x44, 0x59, 0x4b, 0x34, 0x81, 0xde, 0x45, 0x36, 0x0f, 0x54, 0x84, 0x5b, 0x54, 0xca, 0xe7, 0xa6, + 0x78, 0xae, 0xda, 0xbe, 0xae, 0x92, 0x45, 0xa1, 0xa5, 0x3b, 0x4f, 0xd8, 0x2f, 0x73, 0x7a, 0xce, 0x42, 0xe3, 0xe4, + 0x2e, 0x4d, 0x82, 0x34, 0xa4, 0x1f, 0xde, 0xbd, 0x80, 0x6c, 0xf7, 0x34, 0x01, 0x12, 0x4b, 0xa4, 0xbf, 0x0b, 0xe7, + 0x24, 0x71, 0x43, 0x7a, 0xc5, 0x02, 0x3a, 0xb8, 0xd8, 0x5d, 0x6c, 0xac, 0x28, 0x5f, 0xa3, 0xa2, 0x75, 0x21, 0x92, + 0xfe, 0x04, 0x94, 0x17, 0xbb, 0x8b, 0x4b, 0x5e, 0xb4, 0x76, 0x17, 0x89, 0x1b, 0xa6, 0x53, 0x9f, 0x25, 0xf0, 0x3b, + 0x2f, 0x76, 0x17, 0x0c, 0x7e, 0xf0, 0xe2, 0xa2, 0xa8, 0x12, 0x45, 0x4b, 0x88, 0x8c, 0x29, 0x28, 0xdc, 0x75, 0x90, + 0xfb, 0x73, 0xca, 0x12, 0x51, 0x74, 0x57, 0xcf, 0x54, 0xf7, 0x0a, 0x48, 0xfe, 0x95, 0x48, 0x83, 0x59, 0x9b, 0xcb, + 0xe7, 0xf7, 0x35, 0x97, 0x69, 0xc2, 0x99, 0x48, 0x8b, 0xd7, 0xe1, 0x9c, 0xc8, 0xcf, 0xcf, 0x03, 0x79, 0x12, 0x35, + 0xaf, 0x4e, 0x5d, 0xf8, 0x02, 0xb1, 0xd2, 0x02, 0xa6, 0x99, 0x30, 0xf6, 0xe9, 0xf6, 0xa3, 0x92, 0xc9, 0x5d, 0xc6, + 0x5f, 0x49, 0x55, 0x79, 0x3a, 0xcf, 0x02, 0x88, 0xf5, 0x2a, 0x95, 0x62, 0xdd, 0x2b, 0x66, 0x0b, 0xfd, 0xcd, 0xc6, + 0xdc, 0x48, 0xb2, 0xe5, 0x70, 0xa6, 0xaf, 0xba, 0xb6, 0x83, 0x8a, 0x78, 0x22, 0xac, 0x19, 0x13, 0xab, 0x77, 0xce, + 0x42, 0x08, 0xbc, 0xb0, 0x50, 0x25, 0x2c, 0xd6, 0x26, 0x09, 0x2a, 0x52, 0x28, 0x32, 0x48, 0xe1, 0xb2, 0x9d, 0xb4, + 0x5a, 0x05, 0x42, 0x88, 0x8c, 0xeb, 0x81, 0xf0, 0x6d, 0x76, 0xf6, 0xf6, 0xf2, 0xea, 0x44, 0x1b, 0x53, 0x38, 0x5f, + 0x2e, 0x39, 0x75, 0x72, 0x79, 0xea, 0x26, 0x22, 0xa0, 0x8c, 0x31, 0x2c, 0xdf, 0x78, 0x29, 0x2e, 0x7b, 0xf2, 0xf2, + 0xa2, 0x17, 0x09, 0x24, 0x4a, 0x94, 0x11, 0x8d, 0xd4, 0x13, 0xad, 0x92, 0x61, 0xf3, 0x75, 0x79, 0x90, 0xbf, 0x86, + 0xf5, 0xf6, 0xca, 0xe2, 0x48, 0xab, 0x2a, 0x5a, 0x2d, 0xcd, 0xd3, 0x8c, 0x3b, 0x8e, 0x8f, 0x03, 0x44, 0xfa, 0xbe, + 0x98, 0xfd, 0xb1, 0xcc, 0xf7, 0x18, 0x34, 0x3b, 0x5e, 0xa7, 0xf4, 0x87, 0xd4, 0xce, 0x57, 0xcb, 0x6c, 0x33, 0x75, + 0x46, 0x17, 0xf0, 0x84, 0xcb, 0xdf, 0x0a, 0x7d, 0x55, 0x81, 0x9c, 0x5d, 0xf5, 0x5c, 0x4e, 0x12, 0x2b, 0x86, 0x26, + 0x95, 0x01, 0xa7, 0x06, 0xd5, 0x30, 0x1b, 0x61, 0xb6, 0x65, 0x6c, 0x54, 0x54, 0x88, 0x28, 0x37, 0xf7, 0x85, 0x54, + 0x82, 0xce, 0x0d, 0xea, 0xbe, 0x60, 0xda, 0x8d, 0x57, 0xa7, 0xbb, 0x42, 0xa1, 0xc8, 0xe0, 0x0c, 0x9b, 0xaa, 0x49, + 0x58, 0x6e, 0x49, 0xb2, 0x91, 0x78, 0x5d, 0xf9, 0x48, 0x25, 0x6d, 0x6c, 0xae, 0x22, 0x92, 0x21, 0x37, 0x01, 0x06, + 0x8e, 0x81, 0x9c, 0xeb, 0x29, 0x00, 0x8f, 0x19, 0x53, 0x38, 0xa9, 0xa4, 0x38, 0x0e, 0x5e, 0x48, 0xed, 0xde, 0xb3, + 0xdf, 0xbe, 0x39, 0x7b, 0x6f, 0x63, 0xb8, 0xea, 0x8c, 0x66, 0xb9, 0xb7, 0xb0, 0x55, 0x8e, 0x61, 0x13, 0xe2, 0xd5, + 0xb6, 0x67, 0xfb, 0x33, 0x38, 0xb4, 0x2d, 0x98, 0x6a, 0xeb, 0xa6, 0x79, 0x7d, 0x7d, 0xdd, 0x84, 0x13, 0x65, 0xcd, + 0x79, 0x16, 0x4b, 0x76, 0x13, 0xda, 0x45, 0x81, 0x5c, 0x1e, 0xd1, 0xa4, 0xbc, 0x0c, 0x29, 0x8d, 0xa9, 0x1b, 0xa7, + 0x13, 0x79, 0x1e, 0x76, 0xd5, 0x3d, 0x11, 0x5f, 0x1c, 0x8b, 0x4b, 0xbe, 0xfa, 0xc7, 0x5c, 0x5e, 0xaf, 0xc6, 0x33, + 0xf8, 0xd9, 0x87, 0xe0, 0xd5, 0x71, 0x8b, 0x47, 0xe2, 0xe1, 0x0c, 0x76, 0x93, 0x78, 0xda, 0x5d, 0xac, 0x51, 0xdd, + 0x00, 0xba, 0x88, 0xfa, 0x72, 0x6a, 0xb9, 0xa8, 0x75, 0xe1, 0xc5, 0x17, 0x17, 0xc5, 0x71, 0x0b, 0xfa, 0x6a, 0xe9, + 0x7e, 0x2f, 0xd3, 0xf0, 0x56, 0xb7, 0x2f, 0x29, 0x11, 0x2e, 0x7b, 0x4a, 0x48, 0x1f, 0xba, 0x80, 0x71, 0xc3, 0xbe, + 0xc0, 0x99, 0x62, 0xa1, 0xc3, 0xea, 0xa1, 0x18, 0x59, 0xc0, 0x30, 0x0b, 0x28, 0x01, 0x72, 0x83, 0xce, 0xc3, 0xb2, + 0x81, 0xd8, 0xed, 0xb2, 0x68, 0x1b, 0x80, 0xb2, 0x62, 0xb5, 0x7f, 0xa4, 0x9b, 0xbb, 0x22, 0x0b, 0x0d, 0x71, 0x68, + 0x02, 0x7f, 0x81, 0xe0, 0x5f, 0x01, 0xf8, 0x71, 0x4b, 0xa2, 0xe9, 0xc2, 0xbc, 0x76, 0x46, 0x5e, 0x08, 0x51, 0x22, + 0x73, 0x98, 0x71, 0xfc, 0x9e, 0xe3, 0x8f, 0x17, 0xa2, 0xaa, 0xd6, 0x12, 0x40, 0x7d, 0x05, 0x6d, 0xaa, 0xad, 0xd5, + 0xc1, 0x20, 0x8d, 0x63, 0x7f, 0x96, 0x53, 0x4f, 0xff, 0x50, 0x0a, 0x03, 0xe8, 0x1d, 0xeb, 0x1a, 0x9a, 0xca, 0x7b, + 0x3a, 0x05, 0x3d, 0x6e, 0x5d, 0x7d, 0xbc, 0xf2, 0x33, 0xa7, 0xd9, 0x0c, 0x9a, 0x97, 0x13, 0x54, 0xf0, 0x68, 0x61, + 0xaa, 0x1b, 0x0f, 0xdb, 0xed, 0x1e, 0x24, 0xa9, 0x36, 0xfd, 0x98, 0x4d, 0x12, 0x2f, 0xa6, 0x63, 0x5e, 0x70, 0x38, + 0x3d, 0xb8, 0xd0, 0xfa, 0x9d, 0xdb, 0x3d, 0xcc, 0xe8, 0xd4, 0x72, 0xe1, 0xef, 0xdd, 0x03, 0x17, 0x3c, 0xf4, 0x12, + 0x1e, 0x35, 0x45, 0x32, 0x34, 0x1c, 0xe5, 0xe0, 0x51, 0xed, 0x79, 0x61, 0x0c, 0x14, 0x50, 0xd0, 0x7d, 0x0b, 0x9e, + 0x59, 0x3c, 0xc2, 0x3c, 0x33, 0xeb, 0x25, 0x68, 0xb1, 0x36, 0x83, 0x75, 0x15, 0x6c, 0x1f, 0x15, 0xb9, 0xb0, 0x58, + 0x16, 0x6b, 0x78, 0x31, 0x54, 0xe9, 0x82, 0x25, 0xb3, 0x39, 0x1f, 0x0a, 0xcf, 0x7f, 0x06, 0x67, 0x48, 0x46, 0xd8, + 0x28, 0x01, 0x78, 0x46, 0xaa, 0x7d, 0xe0, 0xc7, 0x81, 0x03, 0x9d, 0x58, 0x4d, 0xeb, 0x28, 0xa3, 0x53, 0xd4, 0x9b, + 0xb2, 0xa4, 0x29, 0xdf, 0x1d, 0x1a, 0xba, 0x9b, 0xfb, 0x08, 0x9e, 0x0a, 0x57, 0xf4, 0x86, 0x45, 0x82, 0xef, 0x86, + 0x79, 0x5d, 0x8c, 0x8a, 0xa2, 0x97, 0x72, 0x67, 0xf8, 0xc2, 0x41, 0x23, 0xfc, 0xab, 0x71, 0x89, 0x8d, 0xad, 0xa9, + 0xda, 0xc6, 0x5d, 0xb4, 0xa5, 0x8a, 0x49, 0x97, 0xa2, 0xda, 0xaf, 0x04, 0x2a, 0xbe, 0x74, 0x6c, 0x9a, 0xcf, 0x9a, + 0x92, 0xfd, 0x34, 0x05, 0xf9, 0xd8, 0xd0, 0x14, 0x29, 0x77, 0x36, 0xa5, 0x0b, 0xc1, 0x59, 0xd4, 0x39, 0x16, 0xe9, + 0x71, 0x19, 0x95, 0xe7, 0x9e, 0xd4, 0xb3, 0x79, 0xd2, 0x09, 0xd5, 0xb6, 0xfe, 0xc5, 0x49, 0x9d, 0x4d, 0x81, 0xfc, + 0x2f, 0xef, 0xfa, 0xf3, 0xe3, 0x18, 0x06, 0xbc, 0xd0, 0x4a, 0x83, 0x79, 0x35, 0xca, 0x90, 0x8f, 0x1c, 0x54, 0xa8, + 0x3d, 0xf3, 0x44, 0xe8, 0xdd, 0xc6, 0x05, 0x83, 0x3b, 0x5c, 0x47, 0xd4, 0xe4, 0x09, 0x66, 0x06, 0x39, 0x01, 0xb5, + 0xdc, 0xf1, 0x5e, 0xc5, 0x66, 0xa4, 0xd6, 0x6e, 0x89, 0x09, 0x11, 0x3b, 0x4b, 0x42, 0xdb, 0xfa, 0x73, 0x10, 0xb3, + 0xe0, 0x23, 0xb1, 0x77, 0x17, 0x0e, 0x5a, 0x3f, 0x1a, 0x2a, 0x76, 0xa8, 0xe6, 0xb9, 0xa8, 0x1e, 0x6d, 0xc8, 0x5c, + 0x83, 0x9d, 0xca, 0xdb, 0x83, 0xec, 0x3e, 0xa8, 0x36, 0xc7, 0x2d, 0x39, 0x4e, 0xff, 0xa2, 0x38, 0xaf, 0x6e, 0x05, + 0xab, 0xa0, 0x00, 0x34, 0xcb, 0x72, 0x4b, 0xd0, 0x1f, 0xb1, 0xe5, 0x16, 0xaa, 0x59, 0x80, 0xd8, 0xa4, 0x7d, 0x64, + 0x5b, 0x92, 0xc1, 0x00, 0x9c, 0x5c, 0xf1, 0x1a, 0xdb, 0xfa, 0x73, 0x59, 0x46, 0x4b, 0xb7, 0x8f, 0xc8, 0x5b, 0x21, + 0x36, 0x8c, 0x05, 0xb6, 0xbe, 0x1b, 0x52, 0xee, 0xb3, 0x58, 0x36, 0xe9, 0x69, 0x2f, 0xc5, 0xca, 0x8c, 0x96, 0xcb, + 0xbc, 0x3e, 0x17, 0x56, 0xc7, 0xa0, 0x98, 0xd9, 0x71, 0xab, 0x82, 0x5b, 0xcc, 0x4c, 0xec, 0x0f, 0x33, 0x7e, 0x5a, + 0xcd, 0x50, 0xbe, 0xb3, 0xfe, 0x1c, 0x88, 0x93, 0x55, 0x00, 0x60, 0xaa, 0x00, 0x84, 0xc8, 0xbe, 0x54, 0x42, 0x1c, + 0x9f, 0xa4, 0x2e, 0xf7, 0xb3, 0x09, 0xe5, 0x2b, 0x88, 0xf5, 0x65, 0x22, 0x6f, 0x4f, 0x47, 0xf1, 0xd7, 0xa0, 0x0d, + 0xea, 0xd0, 0x82, 0x9e, 0x5b, 0x0c, 0x40, 0x55, 0x25, 0x1b, 0x35, 0xde, 0x08, 0x81, 0xec, 0x13, 0x8b, 0x23, 0xb9, + 0x7d, 0x2a, 0xb8, 0xbd, 0x8c, 0xc3, 0x59, 0x62, 0x2c, 0x01, 0x62, 0x61, 0x5b, 0x03, 0x09, 0x39, 0x0d, 0x25, 0xcc, + 0x24, 0x13, 0xad, 0xd2, 0xe2, 0xb8, 0x25, 0x6b, 0x4b, 0x76, 0x2c, 0x2b, 0x01, 0x12, 0xc4, 0x3e, 0xad, 0x70, 0x00, + 0xc9, 0xdf, 0x26, 0x1e, 0x42, 0x76, 0x55, 0x12, 0x9b, 0x38, 0x63, 0xd6, 0x3f, 0x8e, 0xfd, 0x4b, 0x1a, 0xf7, 0x77, + 0x17, 0xd9, 0x72, 0xd9, 0x2e, 0x8e, 0x5b, 0xf2, 0xd1, 0x3a, 0x16, 0x7c, 0x43, 0xde, 0x0d, 0x2a, 0x96, 0x18, 0x0e, + 0x6e, 0x42, 0x4a, 0xac, 0xce, 0x05, 0xf3, 0x54, 0x07, 0x85, 0x6d, 0x89, 0x2c, 0x14, 0x51, 0xa9, 0xd4, 0x69, 0x0a, + 0xdb, 0x62, 0xe1, 0x7a, 0x59, 0xce, 0xe9, 0x0c, 0x4a, 0xa3, 0xe5, 0xb2, 0x53, 0xd8, 0xd6, 0x94, 0x25, 0xf0, 0x94, + 0x2d, 0x97, 0xe2, 0x4c, 0xe4, 0x94, 0x25, 0x4e, 0x1b, 0xc8, 0xd6, 0xb6, 0xa6, 0xfe, 0x8d, 0x98, 0xb0, 0x7e, 0xe3, + 0xdf, 0x38, 0x1d, 0xf5, 0xca, 0x2d, 0xf1, 0x93, 0x03, 0xc5, 0x55, 0x2b, 0xea, 0xab, 0x15, 0x0d, 0xf1, 0x5c, 0x9e, + 0xf6, 0x22, 0x4e, 0x48, 0xfc, 0xcd, 0x2b, 0x1a, 0xea, 0x15, 0x9d, 0x6f, 0x59, 0xd1, 0xf9, 0x1d, 0x2b, 0x1a, 0xa8, + 0xd5, 0xb3, 0x4a, 0xdc, 0xa5, 0xcb, 0x65, 0xa7, 0x5d, 0x61, 0xef, 0xb8, 0x15, 0xb2, 0x2b, 0x58, 0x0d, 0xd0, 0xd4, + 0x38, 0x9b, 0xd2, 0xcd, 0x44, 0x59, 0x47, 0x31, 0xfd, 0x2c, 0x4c, 0x56, 0x58, 0xc8, 0xea, 0x58, 0x30, 0xe9, 0xba, + 0x0c, 0x4c, 0xfe, 0x91, 0x94, 0xcd, 0x00, 0x0f, 0x39, 0xe0, 0x21, 0xd2, 0x77, 0x85, 0x3a, 0xf6, 0x7b, 0x1b, 0xdb, + 0x96, 0xad, 0xc9, 0xfa, 0xa2, 0x38, 0x07, 0x19, 0x21, 0xe6, 0x77, 0x2f, 0x5a, 0x84, 0xda, 0x76, 0x7f, 0x3b, 0xcd, + 0x41, 0x0e, 0xc1, 0x75, 0x9a, 0x85, 0xb6, 0x27, 0xab, 0x7e, 0x16, 0xaa, 0xa6, 0x2c, 0x51, 0x19, 0x69, 0x5b, 0x69, + 0xad, 0x7a, 0x6f, 0x52, 0x5c, 0xf7, 0xf0, 0x50, 0xd6, 0x98, 0xf9, 0x9c, 0xd3, 0x2c, 0x51, 0x94, 0x6b, 0xdb, 0xff, + 0x21, 0xa8, 0x70, 0x03, 0x5f, 0x09, 0xf4, 0x02, 0x68, 0x02, 0x54, 0x3a, 0xb7, 0xe2, 0xf9, 0x52, 0x3c, 0xed, 0x54, + 0xca, 0xe6, 0x2d, 0x32, 0xf5, 0x7e, 0x59, 0x04, 0x66, 0xc8, 0x7c, 0x4a, 0xc3, 0x73, 0xc1, 0xa0, 0x07, 0xf1, 0x85, + 0x52, 0x1e, 0x57, 0xc4, 0x5d, 0xd5, 0x00, 0xdb, 0x3f, 0xcd, 0xbb, 0x8f, 0x0e, 0x4e, 0x6d, 0x2c, 0x79, 0x7c, 0x3a, + 0x1e, 0xdb, 0xa8, 0xb0, 0xee, 0xd7, 0xac, 0x73, 0xf0, 0xd3, 0xfc, 0xeb, 0x67, 0xed, 0xaf, 0xcb, 0xc6, 0x09, 0x10, + 0x91, 0x4a, 0x82, 0xd0, 0xa2, 0xca, 0x80, 0x57, 0xcf, 0x68, 0xec, 0x27, 0xdb, 0xa7, 0x33, 0x34, 0xa7, 0x93, 0xcf, + 0x28, 0x0d, 0x81, 0x38, 0xf1, 0x5a, 0xe9, 0x79, 0x4c, 0xaf, 0xa8, 0xbe, 0xa1, 0x71, 0xc3, 0x60, 0x1b, 0x5a, 0x04, + 0xe9, 0x3c, 0xe1, 0x2a, 0x1b, 0x44, 0xb1, 0x5a, 0x63, 0x4a, 0x17, 0x62, 0x0e, 0xa6, 0x3a, 0x7f, 0x2b, 0xe5, 0x5c, + 0x5d, 0x7a, 0x15, 0x17, 0xd8, 0x36, 0x00, 0xd8, 0x0a, 0xd9, 0x60, 0x4b, 0xb9, 0xd7, 0xc6, 0xed, 0x6d, 0xb0, 0xe1, + 0x0e, 0xf2, 0x6c, 0x7b, 0xa4, 0xf1, 0x24, 0x1c, 0xba, 0xb5, 0x4b, 0x35, 0xb6, 0xe2, 0xeb, 0x93, 0x18, 0xb8, 0xcc, + 0xa0, 0xb3, 0x84, 0xe6, 0xf9, 0x56, 0x04, 0x94, 0x8b, 0x88, 0xed, 0xaa, 0xb6, 0xbd, 0xa5, 0x17, 0xdc, 0xc6, 0xb0, + 0xc3, 0x04, 0xc0, 0x65, 0x58, 0x59, 0xd5, 0xa2, 0xe3, 0x31, 0x0d, 0x4a, 0x7f, 0x38, 0x04, 0x08, 0xc7, 0x2c, 0xe6, + 0x10, 0x27, 0x13, 0x01, 0x2c, 0xfb, 0x75, 0x9a, 0x50, 0x1b, 0xe9, 0x94, 0x57, 0x05, 0xbf, 0x92, 0xff, 0x9b, 0xe1, + 0x91, 0x3d, 0xd6, 0x61, 0x51, 0xa3, 0x2c, 0x97, 0xda, 0x5d, 0x53, 0x2b, 0xaf, 0x23, 0x32, 0x15, 0xfe, 0x98, 0x6d, + 0x1b, 0xe8, 0x7e, 0xdb, 0x64, 0xd1, 0xf9, 0xfa, 0xb0, 0xd3, 0x2e, 0x6c, 0x6c, 0x43, 0x77, 0xf7, 0xdd, 0x25, 0xa2, + 0xd5, 0x3e, 0xb4, 0x9a, 0x27, 0x9f, 0xd3, 0xae, 0xdb, 0x79, 0xdc, 0xb1, 0xb1, 0xbc, 0x6b, 0x01, 0x15, 0x25, 0x33, + 0x08, 0xc0, 0x43, 0xfc, 0xbb, 0xa7, 0x52, 0xef, 0xfc, 0x7e, 0xf0, 0x3c, 0xec, 0xb4, 0x6d, 0x6c, 0xe7, 0x3c, 0x9d, + 0x7d, 0xc6, 0x14, 0xf6, 0x6d, 0x6c, 0x07, 0x71, 0x9a, 0x53, 0x73, 0x0e, 0x52, 0x9d, 0xfd, 0xfd, 0x93, 0x90, 0x10, + 0xcd, 0x32, 0x9a, 0xe7, 0x96, 0xd9, 0xbf, 0x22, 0xa5, 0x4f, 0x30, 0xcc, 0x8d, 0x14, 0x97, 0x53, 0x2e, 0xf0, 0x22, + 0xaf, 0x41, 0x30, 0xa9, 0x4a, 0x96, 0xad, 0x11, 0x9b, 0x10, 0x01, 0x25, 0x63, 0x93, 0xda, 0xd5, 0x27, 0x47, 0xde, + 0xb0, 0xf5, 0xe4, 0xc0, 0x32, 0x70, 0xbe, 0x3e, 0x40, 0xad, 0x64, 0xca, 0x92, 0xf3, 0x0d, 0xa5, 0xfe, 0xcd, 0x86, + 0x52, 0x50, 0xd9, 0x4a, 0xe8, 0xd4, 0x15, 0x3d, 0x9f, 0xc6, 0x7a, 0xa5, 0xf8, 0x98, 0x20, 0x86, 0xc2, 0xff, 0xf8, + 0x09, 0x48, 0x8d, 0x65, 0x10, 0x3d, 0xfc, 0xf6, 0xe1, 0xa0, 0xe4, 0x73, 0x86, 0x2b, 0x7b, 0xf9, 0x7d, 0x33, 0x84, + 0xd2, 0x26, 0x38, 0xf9, 0xe3, 0xcf, 0x9a, 0x2b, 0xbd, 0xf9, 0x34, 0xc1, 0x19, 0x5a, 0xd5, 0xef, 0x58, 0x7a, 0x75, + 0xd4, 0x7f, 0x75, 0xed, 0x37, 0x14, 0x2b, 0xc5, 0xa7, 0x5c, 0xff, 0x20, 0x66, 0xd3, 0x8a, 0x04, 0xd6, 0xc1, 0x14, + 0x1a, 0x0f, 0x64, 0x7c, 0x99, 0x9d, 0x48, 0xd5, 0xe7, 0x1c, 0xce, 0xb1, 0xc2, 0x55, 0x21, 0xf3, 0x8c, 0x9e, 0xc7, + 0xe9, 0xf5, 0xea, 0xe5, 0x67, 0xdb, 0x2b, 0x47, 0x6c, 0x12, 0x19, 0x87, 0xd3, 0x28, 0x29, 0x17, 0xe1, 0xce, 0x01, + 0x8a, 0x7f, 0xf9, 0x67, 0xd7, 0xfd, 0x97, 0x7f, 0xfe, 0x64, 0x55, 0xe8, 0xbe, 0xb8, 0xc0, 0xbc, 0xea, 0x76, 0xfb, + 0xee, 0xda, 0x3c, 0x52, 0x1d, 0xe7, 0x9b, 0xeb, 0xac, 0x2d, 0x02, 0xbc, 0x5f, 0x5b, 0x82, 0xb5, 0x42, 0xb9, 0xfb, + 0xac, 0xdf, 0x02, 0x18, 0xcc, 0xeb, 0x93, 0x90, 0x41, 0xa5, 0xdf, 0x05, 0xda, 0x05, 0xf2, 0xee, 0xb5, 0x22, 0xbf, + 0x1d, 0xc3, 0x9f, 0x9a, 0xc3, 0xef, 0x04, 0x5f, 0xf9, 0x27, 0xe2, 0x8b, 0x8b, 0x32, 0x0b, 0xd1, 0x6c, 0x0a, 0x77, + 0x1c, 0x0c, 0xd6, 0x4a, 0x94, 0xe2, 0xe1, 0xb5, 0x51, 0x5f, 0x9c, 0xa1, 0x24, 0xf1, 0xc5, 0x2b, 0xb8, 0xd8, 0xe8, + 0xf8, 0x32, 0xd3, 0xce, 0xd6, 0x3b, 0x84, 0x03, 0x74, 0x51, 0x9f, 0x95, 0xe8, 0x74, 0x4d, 0x32, 0x40, 0x29, 0x98, + 0x1b, 0x00, 0x26, 0x8e, 0x2f, 0x94, 0xb5, 0x79, 0x2a, 0xdd, 0x30, 0xde, 0x2a, 0x69, 0x2b, 0xf7, 0x4c, 0x0d, 0xe9, + 0xd8, 0x7a, 0x2f, 0xf0, 0x25, 0x2a, 0xd3, 0xca, 0xba, 0x17, 0xae, 0x2e, 0xb0, 0x23, 0x4a, 0xf6, 0x73, 0xe5, 0xc7, + 0x57, 0xf7, 0x63, 0x7c, 0xdb, 0x05, 0xea, 0xd2, 0x5a, 0xfe, 0xa3, 0x55, 0x82, 0x65, 0x73, 0xb9, 0x49, 0x1f, 0xb8, + 0xf6, 0x39, 0xcd, 0xce, 0x23, 0x48, 0x84, 0xca, 0x3e, 0xc1, 0x9c, 0x60, 0xa5, 0x31, 0x15, 0x7f, 0x19, 0x51, 0x17, + 0x49, 0xff, 0x83, 0x38, 0x15, 0x83, 0x2c, 0x46, 0x18, 0xca, 0x58, 0x84, 0xff, 0xcf, 0xb7, 0xfe, 0xc3, 0xf0, 0xad, + 0xbb, 0x87, 0xa8, 0x9d, 0x91, 0xfe, 0xec, 0x85, 0xfc, 0x8f, 0xcd, 0xee, 0x72, 0xc1, 0xee, 0x7e, 0x03, 0xa3, 0xcb, + 0xff, 0x31, 0x8c, 0x4e, 0xd8, 0xc8, 0x9a, 0xd3, 0xad, 0x85, 0x9a, 0x6f, 0x5d, 0xff, 0xda, 0xbf, 0xad, 0xf6, 0x55, + 0x7c, 0x71, 0x72, 0xed, 0xdf, 0x56, 0x8b, 0xb0, 0x9d, 0x5d, 0xac, 0xf6, 0x31, 0xb0, 0xdf, 0xbc, 0xb6, 0x3d, 0xfb, + 0xcd, 0xd7, 0x5f, 0xdb, 0xf8, 0x22, 0xa7, 0x7c, 0x00, 0x85, 0x64, 0x77, 0xb1, 0xb3, 0x5a, 0x11, 0xdc, 0x28, 0x30, + 0x45, 0x11, 0xf6, 0x82, 0xa4, 0x43, 0xe3, 0x3d, 0xcb, 0xcf, 0xd3, 0xc4, 0x84, 0xe6, 0x2d, 0x58, 0xf6, 0x9f, 0x0b, + 0x8e, 0xe8, 0x65, 0x0d, 0x1e, 0x51, 0xba, 0x0a, 0x90, 0x28, 0xac, 0x41, 0x54, 0x5d, 0x19, 0x74, 0x37, 0xff, 0xaf, + 0xae, 0x45, 0x90, 0xb7, 0x7d, 0x44, 0x83, 0xf8, 0xe2, 0x73, 0xc4, 0x87, 0x1c, 0xac, 0xf2, 0xd8, 0x69, 0x77, 0xa7, + 0x5f, 0xec, 0x2e, 0xa2, 0xbd, 0x3d, 0x36, 0xb0, 0xb1, 0xb8, 0xa7, 0xa9, 0xd8, 0x24, 0x5c, 0x72, 0xf8, 0x93, 0xc1, + 0x9f, 0xb4, 0x62, 0xd4, 0x2c, 0x19, 0x67, 0x7e, 0x46, 0xc3, 0xed, 0x4c, 0xba, 0xbc, 0xdf, 0x48, 0x91, 0x86, 0x4c, + 0xc0, 0xce, 0xcf, 0x45, 0xea, 0xd1, 0x94, 0x81, 0x3e, 0xba, 0x63, 0x7e, 0xc5, 0x47, 0x5d, 0x88, 0x56, 0x7e, 0x04, + 0xc0, 0x44, 0x38, 0x25, 0x79, 0x99, 0xeb, 0x00, 0xb7, 0x6a, 0xaa, 0xec, 0x10, 0x6c, 0x23, 0xe1, 0x75, 0x0f, 0x49, + 0x5f, 0xa4, 0x3d, 0xbc, 0x48, 0xb8, 0x13, 0xba, 0x3c, 0x63, 0x53, 0x07, 0xe1, 0x4e, 0x1b, 0x21, 0xed, 0x6c, 0x08, + 0x49, 0x7f, 0x87, 0xe5, 0xaf, 0xfd, 0xd7, 0x4e, 0x28, 0x2e, 0xe2, 0x12, 0x9f, 0xee, 0x81, 0x43, 0x92, 0x4f, 0xe6, + 0xe3, 0x31, 0xcd, 0x1c, 0x7d, 0x00, 0xf0, 0xab, 0x03, 0x38, 0x63, 0x0c, 0x6f, 0x9f, 0xfa, 0xdc, 0xff, 0x96, 0xd1, + 0x6b, 0x27, 0x45, 0xbd, 0xac, 0xba, 0x9c, 0x31, 0xc4, 0x73, 0x44, 0xfa, 0x11, 0x24, 0xc6, 0xbf, 0x48, 0xf8, 0x7e, + 0xd7, 0x99, 0x7f, 0x75, 0x80, 0x43, 0xb8, 0xf2, 0x42, 0x67, 0x75, 0xcb, 0xbb, 0x4a, 0x3e, 0xb0, 0x84, 0x1f, 0xc9, + 0x63, 0x98, 0x29, 0x52, 0xee, 0xc3, 0x32, 0x23, 0xc6, 0xf2, 0xcb, 0x0e, 0x43, 0xd2, 0x0f, 0x1a, 0x44, 0x1e, 0xca, + 0x14, 0xb7, 0xec, 0x9e, 0x46, 0x7e, 0x76, 0x0a, 0x07, 0xbe, 0x01, 0xd0, 0x4b, 0x9e, 0xfa, 0x4e, 0x50, 0x7e, 0xc9, + 0xc9, 0x69, 0xfd, 0xd4, 0x68, 0x4d, 0xb0, 0x48, 0x8a, 0xa9, 0x8a, 0x5a, 0x50, 0x74, 0x6e, 0x16, 0x91, 0xc6, 0x6e, + 0x0b, 0xc3, 0x1e, 0xec, 0x6d, 0xf4, 0xd1, 0xea, 0xa5, 0x6b, 0x5e, 0x67, 0xfe, 0xac, 0x8c, 0x1b, 0x9c, 0xfa, 0x59, + 0xc6, 0x68, 0x66, 0x39, 0xcf, 0x7f, 0x45, 0xde, 0xbf, 0xfc, 0xf3, 0xe6, 0xf8, 0x81, 0x0a, 0x19, 0x58, 0x90, 0x5c, + 0xd2, 0x14, 0xe9, 0xd8, 0xc4, 0x0e, 0x64, 0x43, 0x5b, 0x87, 0x3b, 0xf6, 0x8f, 0xda, 0xed, 0xb6, 0x0a, 0x09, 0x74, + 0xe4, 0x4f, 0x88, 0x01, 0xc0, 0x4f, 0x78, 0x10, 0x51, 0x65, 0x62, 0xcb, 0x00, 0xe5, 0x51, 0x7b, 0x76, 0x63, 0xf7, + 0x61, 0x3b, 0x28, 0x28, 0xde, 0xd1, 0x19, 0xf5, 0xf9, 0x67, 0x8d, 0x9f, 0x89, 0x26, 0xe5, 0xf0, 0x1d, 0x3d, 0x74, + 0x35, 0xee, 0xca, 0xa0, 0x87, 0xab, 0x83, 0xbe, 0x67, 0x53, 0x71, 0x75, 0xd3, 0xb6, 0x51, 0x85, 0xa7, 0xba, 0x36, + 0x26, 0x97, 0x2d, 0x6c, 0x4b, 0x60, 0x3c, 0x4a, 0xe3, 0x90, 0x66, 0xc4, 0xa6, 0xee, 0xc4, 0xb5, 0x1e, 0xb7, 0xdb, + 0x6d, 0xdc, 0x3c, 0x38, 0x6c, 0xb7, 0xf1, 0xe1, 0xc3, 0x36, 0x6e, 0xc2, 0x1f, 0xd7, 0x75, 0x57, 0x60, 0xb8, 0x2b, + 0x6a, 0xdb, 0x69, 0x67, 0x74, 0xaa, 0x00, 0xbc, 0x33, 0xac, 0x58, 0xed, 0x09, 0xb8, 0x60, 0x5a, 0xed, 0x7b, 0x29, + 0xd9, 0xd4, 0x05, 0x07, 0x2a, 0x1d, 0x55, 0xf8, 0x0b, 0xd3, 0x2a, 0x68, 0x4a, 0xe5, 0xc5, 0x7f, 0x2f, 0x14, 0x21, + 0x78, 0xd6, 0x29, 0xdc, 0x5e, 0x2a, 0xe2, 0xa5, 0x90, 0x0a, 0x04, 0x1f, 0x48, 0xe3, 0x3e, 0x4b, 0xe0, 0xdb, 0x59, + 0x3a, 0x6a, 0xaa, 0x19, 0x55, 0xba, 0x92, 0x74, 0xfb, 0x40, 0x86, 0xa5, 0x37, 0x11, 0xc4, 0xe8, 0x01, 0xc2, 0xfe, + 0x7d, 0x1a, 0xa8, 0x15, 0x84, 0xfa, 0xc1, 0x7d, 0xea, 0x6b, 0xec, 0x8f, 0x1e, 0x88, 0xe4, 0xa4, 0x9d, 0x68, 0xb9, + 0xdc, 0xf1, 0x97, 0xcb, 0x9d, 0xe0, 0xfe, 0x33, 0x94, 0xcb, 0xab, 0x4f, 0x41, 0xc0, 0xcd, 0x9f, 0x12, 0xe8, 0x17, + 0x50, 0xee, 0x45, 0x58, 0x82, 0x24, 0x9f, 0x7c, 0xac, 0x06, 0x94, 0x8f, 0x41, 0xb1, 0x82, 0x94, 0x90, 0x44, 0xd2, + 0x3e, 0x5f, 0x2e, 0x15, 0xf1, 0xe3, 0x39, 0xf1, 0xcb, 0xa2, 0x8e, 0x8d, 0x67, 0x24, 0x28, 0x1f, 0x6d, 0x01, 0xf2, + 0x4c, 0x71, 0xa9, 0x0a, 0xe2, 0x6b, 0x3f, 0x4b, 0x4c, 0x80, 0x5f, 0xa7, 0x96, 0x1a, 0xd6, 0x9a, 0x65, 0xe9, 0x15, + 0x83, 0xe4, 0x97, 0x95, 0x81, 0xa7, 0x04, 0x2e, 0xfe, 0xea, 0x99, 0xa1, 0x70, 0xa3, 0x83, 0xf7, 0x9a, 0xcf, 0xc2, + 0x2d, 0x93, 0xe5, 0x04, 0xbd, 0x50, 0xcd, 0xcd, 0x9b, 0xeb, 0x69, 0xbd, 0xf3, 0xaf, 0xbd, 0x99, 0x7e, 0x78, 0x26, + 0xf3, 0x6c, 0xbc, 0x69, 0x79, 0xb2, 0xe6, 0x2d, 0x79, 0x0d, 0xb1, 0x1f, 0x5b, 0xf3, 0x6d, 0xb8, 0x67, 0x53, 0xf2, + 0xb8, 0x77, 0x2f, 0xcf, 0xa8, 0x9f, 0x05, 0xd1, 0x5b, 0x3f, 0xf3, 0xa7, 0x79, 0x6f, 0xac, 0x6f, 0xf1, 0xd2, 0x14, + 0x70, 0x3e, 0x16, 0x99, 0x4e, 0x49, 0x70, 0x6b, 0xe3, 0x10, 0xe1, 0xea, 0xbd, 0x84, 0x40, 0xfa, 0xb9, 0x6d, 0x3c, + 0x37, 0x5f, 0xc1, 0x3a, 0xdb, 0x78, 0x8a, 0xb0, 0x4c, 0x20, 0x7a, 0xfb, 0x47, 0xa6, 0x0e, 0x61, 0xc8, 0x75, 0xf1, + 0xc6, 0x6e, 0xf5, 0x95, 0x3b, 0x9d, 0x4c, 0xf4, 0x7e, 0x25, 0x99, 0x68, 0x03, 0x1a, 0xad, 0x8c, 0xe6, 0xb3, 0x34, + 0xc9, 0xa9, 0x8d, 0xdf, 0x43, 0x3b, 0x79, 0x15, 0xb3, 0xd9, 0x70, 0x8d, 0xe6, 0xca, 0xa6, 0xe2, 0x8d, 0x6c, 0x07, + 0x41, 0x9d, 0xf7, 0xdf, 0x97, 0x71, 0x7c, 0x1d, 0xdf, 0x11, 0x89, 0xe8, 0x8c, 0x6e, 0xc9, 0x95, 0xcd, 0xe9, 0x27, + 0x73, 0x65, 0xe3, 0x7b, 0xe5, 0xca, 0xe6, 0xf4, 0x8f, 0xce, 0x95, 0x65, 0xd4, 0xc8, 0x95, 0x05, 0x39, 0xf7, 0xf5, + 0xbd, 0x52, 0x2e, 0x75, 0x26, 0x5c, 0x7a, 0x9d, 0x93, 0x8e, 0x8a, 0x81, 0xc4, 0xe9, 0x04, 0xf2, 0x2d, 0xff, 0xf1, + 0xe9, 0x93, 0x71, 0x3a, 0x31, 0x93, 0x27, 0xe1, 0xc3, 0x24, 0x40, 0x76, 0x38, 0x23, 0x0b, 0xfb, 0xa7, 0x9b, 0xce, + 0x93, 0x61, 0xa7, 0xb7, 0xdf, 0x99, 0xda, 0x9e, 0x0d, 0x4e, 0x47, 0x51, 0xd0, 0xee, 0xed, 0xef, 0x43, 0xc1, 0xb5, + 0x51, 0xd0, 0x85, 0x02, 0x66, 0x14, 0x1c, 0x42, 0x41, 0x60, 0x14, 0x3c, 0x84, 0x82, 0xd0, 0x28, 0x78, 0x04, 0x05, + 0x57, 0x76, 0x31, 0x64, 0x65, 0x42, 0xf0, 0x23, 0x24, 0x6e, 0x30, 0xdc, 0xc9, 0xea, 0xa7, 0xb7, 0x23, 0xa2, 0xab, + 0x3c, 0x2a, 0x6f, 0x7e, 0x68, 0x1e, 0xe8, 0x8b, 0x0a, 0x2f, 0xbe, 0xb8, 0x00, 0xd6, 0x0a, 0x17, 0xb1, 0x60, 0x88, + 0x49, 0xca, 0x9a, 0xfb, 0xfa, 0xb5, 0xed, 0x95, 0x59, 0xb3, 0x6d, 0xdc, 0xd5, 0x79, 0xb3, 0x9e, 0x8d, 0x04, 0x5f, + 0x92, 0x2f, 0x0e, 0x1b, 0xa1, 0xea, 0x16, 0xee, 0x00, 0xac, 0x2e, 0xe0, 0xdc, 0x47, 0x78, 0xaa, 0x15, 0x20, 0xea, + 0xc0, 0x07, 0x18, 0xde, 0xb3, 0x29, 0xd5, 0xfb, 0x45, 0x0f, 0x60, 0x89, 0xcc, 0xe2, 0x5e, 0x54, 0x29, 0x46, 0x6f, + 0xf1, 0xb8, 0xba, 0xf3, 0xf5, 0x3d, 0x91, 0x77, 0xe8, 0x65, 0x58, 0x86, 0xb9, 0x66, 0x98, 0xfb, 0x13, 0x0f, 0x52, + 0x28, 0x21, 0x63, 0xc4, 0x1b, 0x13, 0x42, 0xda, 0x83, 0xb9, 0xf7, 0x16, 0x5f, 0x47, 0x34, 0xf1, 0xa6, 0x45, 0xaf, + 0x5c, 0x7f, 0x99, 0xd2, 0xf9, 0xbe, 0xbc, 0x28, 0x5c, 0xd0, 0x44, 0xf5, 0x56, 0x42, 0xd9, 0x2c, 0x69, 0x67, 0x4b, + 0xce, 0x9f, 0xa1, 0xec, 0x8c, 0xe3, 0xf4, 0xba, 0x09, 0xe2, 0x7e, 0x63, 0x1e, 0x20, 0xcc, 0xad, 0xcc, 0x03, 0x7c, + 0x09, 0xb0, 0x96, 0x4f, 0xef, 0xfd, 0x49, 0xf9, 0xfb, 0x15, 0xcd, 0x73, 0x7f, 0xa2, 0x6a, 0x6e, 0xcf, 0xfb, 0x13, + 0x20, 0x9a, 0x39, 0x7f, 0x1a, 0x08, 0x48, 0xce, 0x03, 0x84, 0x40, 0x40, 0x57, 0xe5, 0xea, 0xc1, 0xcc, 0xeb, 0x69, + 0x7e, 0x02, 0x55, 0xf5, 0x22, 0xee, 0x4f, 0xaa, 0x82, 0xe3, 0x59, 0x46, 0x55, 0x02, 0x21, 0x60, 0xb1, 0x38, 0x6e, + 0x41, 0x81, 0x7c, 0xbd, 0x25, 0x9d, 0x4f, 0x73, 0x97, 0xed, 0x49, 0x7d, 0x96, 0x4e, 0xe7, 0x33, 0x4f, 0xa6, 0x94, + 0xc7, 0x52, 0xd6, 0x33, 0xf2, 0xbe, 0xec, 0x04, 0xf0, 0x9f, 0x3a, 0x78, 0xf1, 0xe5, 0x78, 0x3c, 0xbe, 0x33, 0xbd, + 0xef, 0xcb, 0x70, 0x4c, 0xbb, 0xf4, 0xb0, 0x07, 0xa7, 0x16, 0x9a, 0x2a, 0x11, 0xad, 0x53, 0x08, 0xdc, 0x2d, 0xee, + 0x57, 0x19, 0x72, 0xd6, 0x78, 0xb4, 0xb8, 0x7f, 0xaa, 0x5f, 0x31, 0xcb, 0xe8, 0x62, 0xea, 0x67, 0x13, 0x96, 0x78, + 0xed, 0xc2, 0xbd, 0x5a, 0x28, 0x50, 0x8f, 0x8e, 0x8e, 0x0a, 0x37, 0xd4, 0x4f, 0xed, 0x30, 0x2c, 0xdc, 0x60, 0x51, + 0x4e, 0xa3, 0xdd, 0x1e, 0x8f, 0x0b, 0x97, 0xe9, 0x82, 0xfd, 0x6e, 0x10, 0xee, 0x77, 0x0b, 0xf7, 0xda, 0xa8, 0x51, + 0xb8, 0x54, 0x3d, 0x65, 0x34, 0xac, 0x1d, 0x7d, 0x78, 0xd4, 0x6e, 0x17, 0xae, 0x24, 0xb4, 0x05, 0xc4, 0xe4, 0xe4, + 0x4f, 0xcf, 0x9f, 0x73, 0x30, 0x98, 0x8a, 0x5e, 0xcc, 0x9d, 0xe1, 0xae, 0xba, 0x56, 0x52, 0x7e, 0x87, 0xb1, 0x40, + 0x23, 0xfc, 0xb5, 0x99, 0x39, 0x07, 0xc4, 0x2c, 0x32, 0xe6, 0x62, 0x9d, 0x58, 0x57, 0x7b, 0x0d, 0x94, 0x25, 0x5e, + 0x7f, 0x4d, 0xe2, 0x2a, 0xa1, 0x0e, 0xf8, 0x18, 0xd4, 0x94, 0xb7, 0x9f, 0x27, 0xdb, 0xa4, 0x47, 0xf6, 0x69, 0xe9, + 0x71, 0x79, 0x1f, 0xe1, 0x91, 0xfd, 0xe1, 0xc2, 0x23, 0x31, 0x85, 0x87, 0x64, 0x1d, 0xd7, 0x9c, 0xd8, 0x41, 0x44, + 0x83, 0x8f, 0x97, 0xe9, 0x4d, 0x13, 0xb6, 0x44, 0x66, 0x0b, 0xb1, 0x72, 0xf5, 0x5b, 0x33, 0xf9, 0x75, 0x67, 0xc6, + 0x47, 0x1c, 0x85, 0x8e, 0xff, 0x26, 0x21, 0xf6, 0x1b, 0x1d, 0xd8, 0x93, 0x25, 0xe3, 0x31, 0xb1, 0xdf, 0x8c, 0xc7, + 0xb6, 0xbe, 0x1c, 0xc7, 0xe7, 0x54, 0xd4, 0x7a, 0x5d, 0x2b, 0x11, 0xb5, 0xc0, 0xd0, 0xaf, 0xca, 0xcc, 0x02, 0x95, + 0x77, 0x67, 0xe6, 0xd8, 0xa9, 0x37, 0x21, 0xcb, 0x61, 0xab, 0xc1, 0xb7, 0x25, 0xeb, 0x97, 0xf3, 0x27, 0xb5, 0x2f, + 0x29, 0x95, 0x00, 0x6f, 0xf8, 0xfc, 0xd3, 0xea, 0xcd, 0x70, 0x13, 0xaa, 0x55, 0xfc, 0x27, 0xb7, 0x2f, 0x42, 0xe7, + 0x9a, 0xa3, 0x82, 0xe5, 0x6f, 0x92, 0x95, 0x5b, 0x1f, 0x24, 0x8c, 0x84, 0x98, 0xd3, 0x2a, 0x78, 0x3a, 0x99, 0xc4, + 0xe2, 0x30, 0x49, 0xcd, 0xe0, 0x96, 0xcd, 0x07, 0xb5, 0xf9, 0x7a, 0x66, 0x43, 0xf5, 0x79, 0x0d, 0xf1, 0xbd, 0x61, + 0x79, 0x5a, 0xf8, 0x4a, 0x7d, 0x78, 0x56, 0xc4, 0x04, 0x17, 0x8a, 0xc7, 0x2f, 0xe4, 0x19, 0x53, 0x8e, 0x59, 0x28, + 0x9b, 0xb3, 0xb0, 0x28, 0xd4, 0xe9, 0xfc, 0x90, 0xe5, 0x33, 0xd0, 0x9e, 0x64, 0x4b, 0xfa, 0x29, 0x16, 0x9e, 0x5f, + 0x1b, 0xc9, 0x6d, 0xb5, 0xe5, 0x2a, 0xb4, 0x9d, 0x26, 0xb3, 0x85, 0xae, 0x79, 0x61, 0x2b, 0x93, 0x4d, 0x23, 0xd1, + 0xb6, 0x24, 0x3e, 0x65, 0xda, 0x9d, 0x31, 0x43, 0xc8, 0xfc, 0x29, 0x17, 0x44, 0xbf, 0xd2, 0x05, 0x85, 0x69, 0x65, + 0x89, 0x37, 0x12, 0x5b, 0x22, 0x55, 0x2c, 0x9f, 0xf9, 0x89, 0x36, 0xe6, 0x24, 0x3f, 0xd8, 0x5d, 0x54, 0x2b, 0x5f, + 0xd8, 0x1a, 0x6c, 0x49, 0xbc, 0xfd, 0xe3, 0x16, 0x34, 0xe8, 0x5b, 0x35, 0xd0, 0x93, 0xb5, 0x0c, 0xb3, 0xbb, 0xf3, + 0xae, 0x3f, 0x5e, 0xb8, 0xf9, 0x35, 0x76, 0xf3, 0x6b, 0xeb, 0xab, 0x45, 0xf3, 0x9a, 0x5e, 0x7e, 0x64, 0xbc, 0xc9, + 0xfd, 0x59, 0x13, 0xbc, 0xa7, 0x22, 0x33, 0x44, 0xb1, 0x67, 0xa1, 0xa3, 0x4b, 0xd3, 0xaf, 0x37, 0xcf, 0x21, 0x3d, + 0x5b, 0x98, 0x51, 0x5e, 0x92, 0x26, 0xb4, 0x57, 0x3f, 0xbe, 0x67, 0x66, 0x18, 0x6b, 0x6c, 0x8d, 0x16, 0x29, 0xa4, + 0x73, 0xf3, 0x5b, 0xaf, 0xad, 0xd8, 0x7a, 0x5b, 0xa7, 0x0f, 0xb7, 0x37, 0xd6, 0xf7, 0x14, 0x72, 0x1b, 0x42, 0x7a, + 0x65, 0xeb, 0xf9, 0xcf, 0xdb, 0xf2, 0xbb, 0x3f, 0x75, 0x98, 0x0d, 0xf2, 0x49, 0xf4, 0xff, 0xc6, 0x29, 0xc0, 0xd5, + 0x62, 0x71, 0x98, 0xed, 0x3e, 0x90, 0x79, 0xfe, 0x98, 0xd3, 0x0c, 0xdf, 0xa7, 0xe6, 0xa5, 0xb8, 0x77, 0x62, 0x01, + 0x62, 0xc6, 0xeb, 0x1c, 0xd5, 0x53, 0xb1, 0xef, 0xee, 0xfe, 0xee, 0xe9, 0x17, 0x0a, 0x47, 0xfa, 0x1e, 0x56, 0xdb, + 0xee, 0xc1, 0x46, 0x88, 0xfd, 0x5b, 0x8f, 0x25, 0x42, 0xe6, 0x5d, 0x42, 0x52, 0x48, 0x6f, 0x96, 0xaa, 0x53, 0x99, + 0x19, 0x8d, 0xc5, 0xa7, 0xd7, 0xd5, 0x52, 0xec, 0x3f, 0x9c, 0xdd, 0xe8, 0xd5, 0xe8, 0xac, 0x9c, 0xb6, 0xfc, 0x43, + 0x0f, 0x55, 0x6e, 0x3f, 0xc5, 0x59, 0x3f, 0x18, 0x78, 0x38, 0xbb, 0xe9, 0x49, 0x41, 0xdb, 0xcc, 0x24, 0x54, 0xed, + 0xd9, 0x8d, 0x79, 0xac, 0xb4, 0xea, 0xc8, 0x72, 0xf7, 0x73, 0x8b, 0xfa, 0x39, 0xed, 0xc1, 0x97, 0xa6, 0x58, 0xe0, + 0xc7, 0x4a, 0x98, 0x4f, 0x59, 0x18, 0xc6, 0xb4, 0xa7, 0xe5, 0xb5, 0xd5, 0x79, 0x08, 0xa7, 0x32, 0xcd, 0x25, 0xab, + 0xaf, 0x8a, 0x81, 0xbc, 0x12, 0x4f, 0xfe, 0x65, 0x9e, 0xc6, 0xf0, 0x9d, 0xc7, 0x8d, 0xe8, 0x54, 0xc7, 0x15, 0xdb, + 0x15, 0xf2, 0xc4, 0xef, 0xfa, 0x5c, 0x0e, 0xdb, 0x7f, 0xea, 0x89, 0x05, 0x6f, 0xf7, 0x78, 0x3a, 0xf3, 0x9a, 0xfb, + 0xf5, 0x89, 0xc0, 0xab, 0x72, 0x0a, 0x78, 0xc3, 0xb4, 0x30, 0x48, 0x2b, 0xc9, 0xa7, 0x2d, 0xb7, 0xa3, 0xca, 0x44, + 0x07, 0x60, 0x84, 0x96, 0x45, 0x45, 0x7d, 0x32, 0xff, 0x98, 0xdd, 0xf2, 0x78, 0xf3, 0x6e, 0x79, 0xac, 0x77, 0xcb, + 0xdd, 0x14, 0xfb, 0xe5, 0xb8, 0x03, 0xff, 0xf5, 0xaa, 0x09, 0x79, 0x6d, 0x6b, 0x7f, 0x76, 0x63, 0x81, 0x9e, 0xd6, + 0xec, 0xce, 0x6e, 0xe4, 0xa1, 0x5a, 0x48, 0x5c, 0x6b, 0xc3, 0x31, 0x53, 0xdc, 0xb6, 0xa0, 0x10, 0xfe, 0x6f, 0xd7, + 0x5e, 0x75, 0x0e, 0xe0, 0x1d, 0xb4, 0x3a, 0x5c, 0x7f, 0xd7, 0xbd, 0x7b, 0xd3, 0x7a, 0x49, 0xca, 0x1d, 0x4f, 0x73, + 0x63, 0xe4, 0x72, 0xff, 0xf2, 0x92, 0x86, 0xde, 0x38, 0x0d, 0xe6, 0xf9, 0x3f, 0x29, 0xf8, 0x15, 0x12, 0xef, 0xdc, + 0xd2, 0x2b, 0xfd, 0xe8, 0xa6, 0xf2, 0x88, 0xaf, 0xee, 0x61, 0x51, 0xae, 0x93, 0x97, 0x07, 0x7e, 0x4c, 0x9d, 0xae, + 0x7b, 0xb0, 0x61, 0x13, 0xfc, 0x9b, 0xac, 0xcd, 0xc6, 0xc9, 0xfc, 0x5e, 0x64, 0xdc, 0x89, 0x84, 0xcf, 0xc2, 0x81, + 0xb9, 0x86, 0xed, 0xa3, 0xcd, 0xe0, 0x0e, 0xf5, 0x48, 0x23, 0x2d, 0x14, 0x94, 0xdc, 0x09, 0xe9, 0xd8, 0x9f, 0xc7, + 0xfc, 0xee, 0x5e, 0xb7, 0x51, 0xc6, 0x5a, 0xaf, 0x77, 0x30, 0xf4, 0xaa, 0xee, 0x3d, 0xb9, 0xf4, 0x97, 0x8f, 0x0f, + 0xe0, 0x3f, 0x79, 0xf8, 0xe5, 0xb2, 0xd2, 0xd5, 0xa5, 0xd5, 0x0b, 0xba, 0xfa, 0x55, 0x4d, 0x19, 0x97, 0x22, 0x5c, + 0xe8, 0xe3, 0xf7, 0xad, 0x0d, 0x5a, 0xe5, 0xbd, 0xaa, 0x2b, 0x2d, 0xeb, 0xb3, 0x6a, 0x7f, 0x5e, 0xe7, 0xf7, 0xac, + 0x1b, 0x48, 0xcd, 0xb5, 0x5e, 0x57, 0x7d, 0x7a, 0x7e, 0xad, 0xb2, 0xc6, 0xb8, 0xa8, 0x7f, 0x45, 0x2e, 0x4b, 0x13, + 0x45, 0xa6, 0xa2, 0x82, 0x95, 0x72, 0x25, 0xad, 0x94, 0x94, 0x92, 0x8b, 0xe3, 0xc1, 0xcd, 0x34, 0xb6, 0xae, 0xe4, + 0xfd, 0x38, 0xc4, 0xee, 0xb8, 0x6d, 0xdb, 0x12, 0x4e, 0x3a, 0xf8, 0x4c, 0x97, 0xfd, 0xe1, 0xfd, 0xd7, 0xcd, 0x23, + 0x7b, 0x00, 0x9a, 0xd6, 0xd5, 0x44, 0x68, 0x76, 0x2f, 0xfd, 0x5b, 0x9a, 0x9d, 0x77, 0x95, 0x0b, 0x5e, 0xe6, 0x8b, + 0x8b, 0x32, 0xab, 0x6b, 0x5b, 0x37, 0xd3, 0x38, 0xc9, 0x89, 0x1d, 0x71, 0x3e, 0xf3, 0x5a, 0xad, 0xeb, 0xeb, 0x6b, + 0xf7, 0x7a, 0xdf, 0x4d, 0xb3, 0x49, 0xab, 0xdb, 0x6e, 0xb7, 0xe1, 0x8b, 0x1f, 0xb6, 0x75, 0xc5, 0xe8, 0xf5, 0x93, + 0xf4, 0x86, 0xd8, 0x6d, 0xab, 0x6d, 0x75, 0xba, 0x47, 0x56, 0xa7, 0x7b, 0xe0, 0x3e, 0x3c, 0xb2, 0xfb, 0x5f, 0x58, + 0xd6, 0x71, 0x48, 0xc7, 0x39, 0xfc, 0xb0, 0xac, 0x63, 0xa1, 0x78, 0xc9, 0xdf, 0x96, 0xe5, 0x06, 0x71, 0xde, 0xec, + 0x58, 0x0b, 0xf5, 0x68, 0x59, 0x70, 0x8b, 0x90, 0x67, 0x7d, 0x39, 0xee, 0x8e, 0x0f, 0xc6, 0x8f, 0x7b, 0xaa, 0xb8, + 0xf8, 0xa2, 0x56, 0x1d, 0xcb, 0x7f, 0xbb, 0x46, 0xb3, 0x9c, 0x67, 0xe9, 0x47, 0xaa, 0x5c, 0xfb, 0x16, 0x88, 0x9e, + 0x8d, 0x4d, 0xbb, 0xeb, 0x23, 0x75, 0x8e, 0x2e, 0x83, 0x71, 0xb7, 0xaa, 0x2e, 0x60, 0x6c, 0x95, 0x40, 0x1e, 0xb7, + 0x34, 0xe8, 0xc7, 0x26, 0x9a, 0x3a, 0xcd, 0x4d, 0x88, 0xea, 0xd8, 0x6a, 0x8e, 0x13, 0x3d, 0xbf, 0x63, 0x38, 0xb4, + 0xae, 0x75, 0x55, 0x01, 0x81, 0x6d, 0x85, 0xc4, 0x7e, 0xd5, 0xe9, 0x1e, 0xe1, 0x4e, 0xe7, 0xa1, 0xfb, 0xf0, 0x28, + 0x68, 0xe3, 0x03, 0xf7, 0xa0, 0xb9, 0xef, 0x3e, 0xc4, 0x47, 0xcd, 0x23, 0x7c, 0xf4, 0xfc, 0x28, 0x68, 0x1e, 0xb8, + 0x07, 0xb8, 0xdd, 0x3c, 0x82, 0xc2, 0xe6, 0x51, 0xf3, 0xe8, 0xaa, 0x79, 0x70, 0x14, 0xb4, 0x45, 0x69, 0xd7, 0x3d, + 0x3c, 0x6c, 0x76, 0xda, 0xee, 0xe1, 0x21, 0x3e, 0x74, 0x1f, 0x3e, 0x6c, 0x76, 0xf6, 0xdd, 0x87, 0x0f, 0x5f, 0x1e, + 0x1e, 0xb9, 0xfb, 0xf0, 0x6e, 0x7f, 0x3f, 0xd8, 0x77, 0x3b, 0x9d, 0x26, 0xfc, 0xc1, 0x47, 0x6e, 0x57, 0xfe, 0xe8, + 0x74, 0xdc, 0xfd, 0x0e, 0x6e, 0xc7, 0x87, 0x5d, 0xf7, 0xe1, 0x63, 0x2c, 0xfe, 0x8a, 0x6a, 0x58, 0xfc, 0x81, 0x6e, + 0xf0, 0x63, 0xb7, 0xfb, 0x50, 0xfe, 0x12, 0x1d, 0x5e, 0x1d, 0x1c, 0xfd, 0x68, 0xb7, 0xb6, 0xce, 0xa1, 0x23, 0xe7, + 0x70, 0x74, 0xe8, 0xee, 0xef, 0xe3, 0x83, 0x8e, 0x7b, 0xb4, 0x1f, 0x35, 0x0f, 0xba, 0xee, 0xc3, 0x47, 0x41, 0xb3, + 0xe3, 0x3e, 0x7a, 0x84, 0xdb, 0xcd, 0x7d, 0xb7, 0x8b, 0x3b, 0xee, 0xc1, 0xbe, 0xf8, 0xb1, 0xef, 0x76, 0xaf, 0x1e, + 0x3d, 0x76, 0x1f, 0x1e, 0x46, 0x0f, 0xdd, 0x83, 0x6f, 0x0f, 0x8e, 0xdc, 0xee, 0x7e, 0xb4, 0xff, 0xd0, 0xed, 0x3e, + 0xba, 0x7a, 0xe8, 0x1e, 0x44, 0xcd, 0xee, 0xc3, 0x3b, 0x5b, 0x76, 0xba, 0x2e, 0xe0, 0x48, 0xbc, 0x86, 0x17, 0x58, + 0xbd, 0x80, 0xff, 0x23, 0xd1, 0xf6, 0xdf, 0xb0, 0x9b, 0x7c, 0xbd, 0xe9, 0x63, 0xf7, 0xe8, 0x51, 0x20, 0xab, 0x43, + 0x41, 0x53, 0xd7, 0x80, 0x26, 0x57, 0x4d, 0x39, 0xac, 0xe8, 0xae, 0xa9, 0x3b, 0xd2, 0xff, 0xab, 0xc1, 0xae, 0x9a, + 0x30, 0xb0, 0x1c, 0xf7, 0xdf, 0xb5, 0x9f, 0x72, 0xc9, 0x8f, 0x5b, 0x13, 0x49, 0xfa, 0x93, 0xfe, 0x17, 0xf2, 0x73, + 0x3e, 0x5f, 0x5c, 0x60, 0x7f, 0x9b, 0xe3, 0x23, 0xfe, 0xb4, 0xe3, 0x23, 0xa2, 0xf7, 0xf1, 0x7c, 0xc4, 0x7f, 0xb8, + 0xe7, 0xc3, 0x5f, 0x75, 0x9b, 0xdf, 0xf0, 0x35, 0x07, 0xc7, 0xaa, 0x55, 0xfc, 0x82, 0x3b, 0xc3, 0x14, 0x3e, 0x1d, + 0x5d, 0xf4, 0x6e, 0x38, 0x89, 0xa8, 0xe9, 0x07, 0x4a, 0x81, 0xc5, 0xde, 0x70, 0xc9, 0x63, 0x83, 0x6d, 0x08, 0x09, + 0x3f, 0x8d, 0x90, 0xef, 0xee, 0x83, 0x8f, 0xf0, 0x0f, 0xc7, 0x47, 0x60, 0xe2, 0xa3, 0xe6, 0xc9, 0x17, 0x9e, 0x06, + 0xe1, 0x29, 0x38, 0x13, 0xcf, 0x0e, 0xdc, 0x9a, 0xd1, 0xb0, 0x5b, 0xf4, 0x4a, 0x44, 0xee, 0x64, 0x70, 0xfd, 0xf9, + 0xe7, 0x04, 0x1d, 0xe4, 0x15, 0x39, 0xc4, 0x56, 0x6e, 0x99, 0x99, 0x90, 0x3a, 0xea, 0xa1, 0x14, 0x4a, 0x5d, 0xb7, + 0xed, 0xb6, 0x4b, 0x97, 0x0e, 0x5c, 0x8b, 0x44, 0x16, 0x29, 0xf7, 0xbd, 0x9d, 0x0e, 0x8e, 0xd3, 0x09, 0x5c, 0x96, + 0x24, 0x3e, 0x1f, 0x07, 0x27, 0x1e, 0x02, 0xf9, 0xe5, 0x3e, 0x48, 0x9f, 0x50, 0x8e, 0x1e, 0x3f, 0xfb, 0xf8, 0x37, + 0x08, 0x62, 0xea, 0x98, 0xc4, 0x14, 0xbc, 0x1d, 0xaf, 0x68, 0xc8, 0x7c, 0xc7, 0x76, 0x66, 0x19, 0x1d, 0xd3, 0x2c, + 0x6f, 0xd6, 0xee, 0xeb, 0x11, 0x57, 0xf5, 0x20, 0x5b, 0x41, 0x38, 0xce, 0xe0, 0x73, 0x48, 0x64, 0xa8, 0xfc, 0x8d, + 0xb6, 0x32, 0xc0, 0xec, 0x02, 0xeb, 0x92, 0x0c, 0x64, 0x6d, 0xa5, 0xb4, 0xd9, 0x52, 0x6b, 0xeb, 0xb8, 0xdd, 0x43, + 0x64, 0x89, 0x62, 0xf8, 0xd0, 0xcc, 0x0f, 0x4e, 0x73, 0xbf, 0xfd, 0x27, 0x64, 0x34, 0x2b, 0x3b, 0x1a, 0x29, 0x77, + 0x5b, 0x52, 0x7e, 0x8e, 0x70, 0x25, 0xec, 0x6a, 0x4b, 0x8a, 0xf8, 0x52, 0xce, 0xdd, 0x46, 0xbd, 0x44, 0x25, 0xcd, + 0xc9, 0x2b, 0x01, 0xc7, 0x6c, 0xe2, 0x18, 0xd7, 0x4d, 0x24, 0xf2, 0x43, 0x36, 0x70, 0x5b, 0x3d, 0x42, 0x45, 0x55, + 0x25, 0x41, 0x0b, 0x11, 0x6d, 0x61, 0x89, 0x95, 0x2c, 0x97, 0x4e, 0x02, 0x2e, 0x72, 0x62, 0xe0, 0x14, 0x9e, 0x51, + 0x0d, 0xc9, 0x09, 0x2e, 0x01, 0x12, 0x08, 0x26, 0x89, 0xfc, 0xb7, 0x2a, 0xd6, 0x3f, 0x94, 0xe3, 0xcb, 0x8d, 0xfd, + 0x64, 0x02, 0x54, 0xe8, 0x27, 0x93, 0x35, 0xb7, 0x9a, 0x0c, 0x18, 0xad, 0x94, 0x56, 0x5d, 0x55, 0xee, 0xb3, 0xfc, + 0xc9, 0xed, 0x7b, 0x75, 0xe3, 0xb5, 0x0d, 0xde, 0x69, 0x11, 0xdf, 0xa8, 0xbe, 0xce, 0xd3, 0x20, 0x0f, 0x8e, 0xa7, + 0x94, 0xfb, 0xf2, 0xb0, 0x1a, 0xe8, 0x13, 0x90, 0xcb, 0x62, 0x29, 0x6b, 0x54, 0x05, 0xf5, 0x89, 0x3c, 0xcc, 0x2f, + 0x45, 0x3d, 0xb6, 0xd4, 0x55, 0x71, 0x4d, 0xb1, 0x34, 0xa4, 0x83, 0xa5, 0x3f, 0x26, 0xf0, 0xc5, 0x71, 0x64, 0x92, + 0xa4, 0x76, 0xff, 0x41, 0x99, 0xeb, 0xb2, 0x6d, 0x11, 0x62, 0x96, 0x7c, 0x1c, 0x66, 0x34, 0xfe, 0x27, 0xf2, 0x80, + 0x05, 0x69, 0xf2, 0x60, 0x64, 0xa3, 0x1e, 0x77, 0xa3, 0x8c, 0x8e, 0xc9, 0x03, 0x90, 0xf1, 0x9e, 0xb0, 0x3e, 0x80, + 0x11, 0x36, 0x6e, 0xa6, 0x31, 0x16, 0x1a, 0xd3, 0x3d, 0x14, 0x22, 0x09, 0xae, 0xdd, 0x3d, 0xb4, 0x2d, 0x69, 0x13, + 0x8b, 0xdf, 0x7d, 0x29, 0x4e, 0x85, 0x12, 0x60, 0x75, 0xba, 0xee, 0x61, 0xd4, 0x75, 0x1f, 0x5f, 0x3d, 0x72, 0x8f, + 0xa2, 0xce, 0xa3, 0xab, 0x26, 0xfc, 0xdb, 0x75, 0x1f, 0xc7, 0xcd, 0xae, 0xfb, 0x18, 0xfe, 0xff, 0xf6, 0xc0, 0x3d, + 0x8c, 0x9a, 0x1d, 0xf7, 0xe8, 0x6a, 0xdf, 0xdd, 0x7f, 0xd9, 0xe9, 0xba, 0xfb, 0x56, 0xc7, 0x92, 0xed, 0x80, 0x5d, + 0x4b, 0xee, 0xfc, 0x60, 0x65, 0x43, 0x6c, 0x08, 0xc6, 0xc9, 0x03, 0x77, 0x36, 0x16, 0x67, 0xa4, 0xcd, 0xfd, 0xa9, + 0x9c, 0x75, 0x4f, 0xfd, 0x0c, 0xbe, 0x6c, 0x5a, 0xdf, 0xbb, 0xb5, 0x77, 0xb8, 0xc6, 0x2f, 0x36, 0x0c, 0x31, 0x13, + 0x11, 0x70, 0xf3, 0xae, 0x35, 0x2a, 0xee, 0xb0, 0x93, 0xdf, 0x82, 0x52, 0x51, 0xb0, 0x32, 0xbb, 0xc8, 0x20, 0x6b, + 0x59, 0x03, 0x12, 0x80, 0x04, 0x0d, 0xae, 0xe6, 0x8f, 0x56, 0x74, 0x9e, 0xc1, 0xfd, 0x04, 0x9a, 0x97, 0x30, 0xf1, + 0x45, 0x3e, 0x01, 0xc3, 0x8b, 0xb0, 0x58, 0x05, 0x0f, 0x8e, 0x05, 0x66, 0xa9, 0x71, 0x1b, 0x1d, 0xad, 0x72, 0x00, + 0x42, 0x06, 0xf7, 0x07, 0x16, 0x85, 0x9e, 0x59, 0xcd, 0x8b, 0x5b, 0x21, 0x51, 0xb0, 0x13, 0x9a, 0x0f, 0x6c, 0x28, + 0xb2, 0x3d, 0x5b, 0x78, 0x00, 0xed, 0xf2, 0xe3, 0xaf, 0x25, 0xdd, 0x57, 0x05, 0x58, 0x5c, 0x0e, 0x01, 0x9b, 0x1a, + 0xd0, 0x67, 0xa3, 0xbd, 0xbd, 0xad, 0xdb, 0x49, 0xe8, 0x97, 0x30, 0xb5, 0xea, 0x9b, 0x91, 0x26, 0xa7, 0xb2, 0xcd, + 0x75, 0x28, 0xfb, 0x15, 0x18, 0x46, 0x0a, 0x2d, 0x97, 0xd4, 0xe7, 0xae, 0x9f, 0xc8, 0x03, 0x06, 0x06, 0x3f, 0xc3, + 0x1d, 0xba, 0x8f, 0x8a, 0x94, 0xfb, 0x32, 0x67, 0xcc, 0x64, 0x03, 0x29, 0xf7, 0xf5, 0xdd, 0x4a, 0x3e, 0xaf, 0x9d, + 0xab, 0x8f, 0xba, 0xfd, 0x37, 0xef, 0x4f, 0x2c, 0xb9, 0x7b, 0x8f, 0x5b, 0x51, 0xb7, 0x7f, 0x2c, 0x5c, 0x2a, 0x32, + 0x2b, 0x80, 0xc8, 0xac, 0x00, 0x4b, 0x5d, 0x2a, 0x03, 0x81, 0xb6, 0xa2, 0x25, 0xa7, 0x2d, 0x4c, 0x0a, 0xe9, 0x0c, + 0x9e, 0xce, 0x63, 0xce, 0xe0, 0x9b, 0x47, 0x2d, 0x91, 0x12, 0x20, 0x52, 0x0c, 0xf4, 0x19, 0x55, 0xa5, 0x3c, 0x5e, + 0xf2, 0x44, 0xbb, 0x8e, 0xc7, 0x2c, 0xa6, 0xfa, 0x54, 0xaa, 0xea, 0xaa, 0xcc, 0x07, 0x5a, 0xaf, 0x9d, 0xcf, 0x2f, + 0x21, 0x27, 0x42, 0x67, 0x1f, 0x7d, 0x50, 0x0d, 0x8e, 0xc5, 0x50, 0x10, 0xd8, 0x97, 0x52, 0x5c, 0x7f, 0xdd, 0xb5, + 0xbe, 0xa4, 0x6a, 0xf6, 0x4a, 0x80, 0xc0, 0x4d, 0x1e, 0xd1, 0x7e, 0xbf, 0xf4, 0x26, 0x9b, 0xef, 0x8a, 0xe3, 0x56, + 0xb4, 0xdf, 0xbf, 0xf0, 0x26, 0xaa, 0xbf, 0x97, 0xe9, 0x64, 0x73, 0x5f, 0x71, 0x3a, 0x19, 0x88, 0x63, 0xf2, 0xf2, + 0xca, 0x27, 0xad, 0x1b, 0xa7, 0xb1, 0xdd, 0x3f, 0x56, 0xba, 0x82, 0x25, 0xa2, 0xee, 0xf6, 0x61, 0x5b, 0x9f, 0xbc, + 0x8f, 0xd3, 0x09, 0xec, 0x57, 0xd9, 0xc4, 0x18, 0xa4, 0xe6, 0x90, 0x8f, 0x3a, 0xfd, 0x63, 0xdf, 0x12, 0xac, 0x47, + 0xf0, 0x96, 0xdc, 0x6b, 0x41, 0xe3, 0x28, 0x9d, 0x52, 0x97, 0xa5, 0xad, 0x6b, 0x7a, 0xd9, 0xf4, 0x67, 0xac, 0xf2, + 0x7e, 0x83, 0x4e, 0x52, 0x0e, 0x99, 0xae, 0x64, 0x60, 0x75, 0x2b, 0x6f, 0xdc, 0x01, 0x98, 0x44, 0xda, 0x73, 0x27, + 0x5c, 0x76, 0x06, 0x58, 0x69, 0xff, 0xb8, 0xe5, 0xaf, 0x60, 0x44, 0x6c, 0xc5, 0x42, 0xf9, 0xe1, 0xc1, 0xee, 0xb9, + 0x14, 0xe9, 0x5f, 0x52, 0x5a, 0x68, 0x7f, 0xbd, 0x92, 0xe3, 0x85, 0xdd, 0xff, 0xd7, 0xff, 0xf1, 0xbf, 0x94, 0x0b, + 0xfe, 0xb8, 0x15, 0x75, 0x74, 0x5f, 0x2b, 0xab, 0x52, 0x1c, 0xc3, 0x3d, 0x36, 0x55, 0xcc, 0x98, 0xde, 0x34, 0x27, + 0x19, 0x0b, 0x9b, 0x91, 0x1f, 0x8f, 0xed, 0xfe, 0x76, 0x6c, 0xca, 0xf4, 0xc4, 0xa6, 0x8e, 0xb6, 0xae, 0x17, 0x01, + 0xbd, 0xfe, 0xa6, 0x4b, 0x19, 0x74, 0xc6, 0x97, 0xd8, 0xda, 0xe6, 0x15, 0x0d, 0xd5, 0xee, 0xab, 0x5d, 0xd3, 0x90, + 0xa8, 0x4f, 0x46, 0x2b, 0x06, 0x99, 0xd4, 0x6e, 0x67, 0x28, 0x6c, 0xab, 0x8c, 0x79, 0xfd, 0xdf, 0xff, 0xf9, 0x5f, + 0xfe, 0x9b, 0x7e, 0x84, 0x50, 0xd6, 0xbf, 0xfe, 0xf7, 0xff, 0xfc, 0x7f, 0xfe, 0xf7, 0x7f, 0x85, 0xf4, 0x34, 0x15, + 0xee, 0x12, 0x4c, 0xc5, 0xaa, 0x62, 0x5d, 0x92, 0xbb, 0x58, 0x70, 0xe8, 0x6d, 0xca, 0x72, 0xce, 0x82, 0xfa, 0x7d, + 0x0d, 0x67, 0x62, 0x40, 0xb1, 0x33, 0x15, 0x74, 0x62, 0x87, 0x17, 0x15, 0x41, 0xd5, 0x50, 0x2e, 0x08, 0xb7, 0x38, + 0x6e, 0x01, 0xbe, 0xef, 0x77, 0xdd, 0x8c, 0x5b, 0x2e, 0xc7, 0x42, 0x93, 0x09, 0x94, 0x14, 0x55, 0xb9, 0x05, 0xa1, + 0x97, 0x05, 0x3c, 0x7a, 0x5d, 0xa3, 0x58, 0xac, 0x5e, 0xad, 0x4d, 0xef, 0xe7, 0x79, 0xce, 0xd9, 0x18, 0x50, 0x2e, + 0xdd, 0xc8, 0x22, 0xca, 0xdd, 0x04, 0x55, 0x32, 0xbe, 0x2d, 0x44, 0x2f, 0x92, 0x40, 0x0f, 0x8e, 0xfe, 0x54, 0xfc, + 0x79, 0x0a, 0x0a, 0x9b, 0xe5, 0x4c, 0xfd, 0x1b, 0x65, 0xbd, 0x3f, 0x6c, 0xb7, 0x67, 0x37, 0x68, 0x51, 0x8d, 0x80, + 0xb7, 0x0d, 0x26, 0xe8, 0xd8, 0xec, 0x50, 0x84, 0xc7, 0x4b, 0x2f, 0x77, 0xdb, 0x02, 0x57, 0xb9, 0xd5, 0x2e, 0x8a, + 0xaf, 0x16, 0xc2, 0xd1, 0xca, 0x7e, 0x85, 0x30, 0xb6, 0xf2, 0x49, 0x5f, 0xa6, 0xe6, 0xe4, 0x16, 0x46, 0xab, 0xae, + 0x6c, 0x15, 0x75, 0xd6, 0x6f, 0x6e, 0x31, 0xc3, 0xf0, 0x66, 0x00, 0xfd, 0x00, 0x42, 0xe2, 0x51, 0x07, 0x47, 0xdd, + 0x45, 0xd9, 0x3d, 0xe7, 0xe9, 0xd4, 0x8c, 0xbb, 0x53, 0x9f, 0x06, 0x74, 0xac, 0x7d, 0xf9, 0xea, 0xbd, 0x8c, 0xa9, + 0x17, 0xd1, 0xfe, 0x86, 0xb1, 0x14, 0x48, 0x22, 0xde, 0x6e, 0xb5, 0x8b, 0x2f, 0x61, 0x07, 0x2e, 0xc6, 0x71, 0xea, + 0x73, 0x4f, 0x10, 0x6c, 0xcf, 0x8c, 0xde, 0xfb, 0xc0, 0x93, 0xd2, 0x85, 0x01, 0x4f, 0x4f, 0x56, 0x05, 0xaf, 0x7a, + 0xfd, 0x06, 0xc7, 0xc2, 0x15, 0xcd, 0xcd, 0xae, 0xa4, 0x53, 0xee, 0x3b, 0x15, 0x14, 0x7f, 0x5e, 0xf3, 0x66, 0x29, + 0x81, 0xd4, 0x45, 0x9b, 0xdf, 0x4b, 0xb1, 0x2f, 0xdf, 0x7e, 0xcf, 0x1d, 0x5b, 0x80, 0x69, 0xaf, 0xd6, 0x12, 0x85, + 0x50, 0xeb, 0x39, 0xf9, 0xae, 0xb4, 0xa8, 0xfc, 0xd9, 0x4c, 0x54, 0x44, 0xbd, 0xe3, 0x96, 0x54, 0x84, 0x81, 0x7b, + 0x88, 0x8c, 0x0f, 0x99, 0x60, 0xa1, 0x2a, 0xa9, 0xad, 0x20, 0x7f, 0xa9, 0xd4, 0x0b, 0xf8, 0x94, 0x78, 0xff, 0xff, + 0x01, 0x65, 0x21, 0x07, 0x4b, 0xe3, 0x97, 0x00, 0x00}; #else // Brotli (default, smaller) -constexpr uint8_t INDEX_BR[] PROGMEM = { - 0x1b, 0xc3, 0x97, 0x11, 0x55, 0xb5, 0x65, 0x2c, 0x8a, 0x8a, 0x55, 0x0b, 0xd0, 0xba, 0x80, 0x1b, 0x32, 0xb0, 0x81, - 0x4f, 0x27, 0x63, 0xf1, 0x7e, 0x88, 0xe3, 0xd8, 0x52, 0x84, 0x55, 0xe8, 0x35, 0x5b, 0x2b, 0x82, 0xe1, 0xed, 0x1f, - 0xfd, 0xde, 0x63, 0x38, 0x3a, 0x71, 0x78, 0xb0, 0x42, 0x17, 0x15, 0x54, 0x23, 0xe1, 0xaa, 0x28, 0x11, 0x94, 0x23, - 0xb4, 0xf4, 0x91, 0x3c, 0xfc, 0xff, 0xab, 0x5a, 0x56, 0x2d, 0x07, 0xcb, 0x09, 0x6f, 0x79, 0x15, 0x1c, 0xd2, 0x87, - 0x40, 0x38, 0x97, 0xce, 0x9d, 0x87, 0x67, 0xe0, 0xa0, 0x4d, 0x49, 0x1a, 0x27, 0xf0, 0xf5, 0x8d, 0x9d, 0x72, 0x02, - 0x12, 0x45, 0x49, 0x2b, 0x48, 0xe0, 0x6a, 0xff, 0x5f, 0x6d, 0x55, 0xf1, 0x54, 0x12, 0xc1, 0x2f, 0x1e, 0xc8, 0x83, - 0x0c, 0x92, 0x0d, 0x75, 0xd8, 0xf5, 0xdd, 0xc7, 0x75, 0x50, 0x6c, 0xa1, 0xb2, 0x85, 0xad, 0x6d, 0x63, 0xd1, 0x96, - 0x38, 0xaa, 0x65, 0x4d, 0x1e, 0x6c, 0x14, 0x4e, 0x10, 0xed, 0xbe, 0xaf, 0xf3, 0xff, 0xeb, 0xf7, 0xc6, 0x6f, 0xd9, - 0x77, 0x24, 0x81, 0xbb, 0x26, 0xd0, 0x31, 0x81, 0xae, 0x49, 0x8d, 0x23, 0xb0, 0x5a, 0x62, 0xe5, 0x49, 0x4a, 0x17, - 0xa7, 0x66, 0xda, 0xb2, 0x6a, 0x9d, 0xf0, 0x4d, 0xa4, 0x1d, 0x59, 0xe6, 0x00, 0x55, 0x58, 0x63, 0xf9, 0x0c, 0xfe, - 0xa0, 0xbd, 0xa5, 0xfa, 0x75, 0x9f, 0xcb, 0x49, 0x22, 0x0c, 0xe1, 0xd5, 0xb0, 0xd8, 0x13, 0x72, 0xd3, 0x25, 0x4e, - 0x48, 0x1b, 0xa2, 0x76, 0x4f, 0x5c, 0x42, 0xe2, 0x98, 0x6d, 0x9b, 0x80, 0x3e, 0x69, 0x90, 0xcf, 0x69, 0xa5, 0x2e, - 0xc0, 0x47, 0x4f, 0x5c, 0xac, 0x83, 0x99, 0xe7, 0xfc, 0xff, 0xdf, 0x96, 0x7e, 0xa5, 0xd5, 0x2d, 0x98, 0x45, 0x4a, - 0x81, 0x52, 0x20, 0x67, 0x6d, 0x8d, 0xbd, 0xc4, 0x49, 0xb0, 0x41, 0xa8, 0xba, 0xf7, 0xbe, 0x77, 0x47, 0x45, 0x3d, - 0xea, 0xaa, 0x52, 0xcf, 0x6f, 0xb2, 0x2d, 0xea, 0x11, 0x1f, 0x0b, 0x6d, 0x7e, 0xef, 0x55, 0x75, 0xab, 0xaa, 0x5b, - 0xf6, 0x74, 0x4b, 0xf6, 0x18, 0xe7, 0x1c, 0x59, 0xf6, 0xfe, 0x01, 0xd2, 0xff, 0x8b, 0xe4, 0xf9, 0xe7, 0x2c, 0x52, - 0x84, 0x51, 0x02, 0x9c, 0xed, 0xc9, 0x31, 0x0a, 0x01, 0xd3, 0xcd, 0x36, 0xdc, 0x74, 0x55, 0x87, 0x2a, 0x51, 0x4e, - 0xcf, 0x28, 0xc5, 0x71, 0xec, 0x2d, 0x23, 0x97, 0xcb, 0x55, 0x7d, 0xfd, 0xd6, 0x83, 0x1d, 0xc6, 0x0a, 0x21, 0x9e, - 0x5d, 0x46, 0xd3, 0xfc, 0xcd, 0xca, 0x21, 0x21, 0x24, 0xce, 0x75, 0x5d, 0x7f, 0xa6, 0x0d, 0xf7, 0x70, 0x16, 0xd1, - 0xc4, 0x38, 0xe2, 0x00, 0xf9, 0x14, 0x84, 0xa1, 0x23, 0x9d, 0x6e, 0xcc, 0x71, 0xee, 0xa1, 0xc8, 0x1a, 0xc1, 0xb4, - 0xda, 0x43, 0x30, 0xcf, 0xe1, 0xc0, 0x01, 0x34, 0xb2, 0x3c, 0xb7, 0x7f, 0xf5, 0x81, 0xad, 0xdb, 0xf5, 0x23, 0x32, - 0xe8, 0xf1, 0x66, 0xa5, 0x04, 0xdc, 0x46, 0x71, 0x3d, 0x0e, 0x94, 0x8d, 0x00, 0x35, 0xab, 0xb1, 0x1b, 0x92, 0xef, - 0xcd, 0xef, 0x3f, 0x1d, 0x1c, 0x84, 0x98, 0xe9, 0x3f, 0x54, 0xae, 0x9d, 0x84, 0x17, 0xa2, 0x2e, 0x69, 0x5b, 0xc0, - 0xd5, 0x10, 0x62, 0x1e, 0x06, 0x1e, 0xa2, 0xe0, 0xb5, 0xd7, 0xe2, 0xe9, 0xb4, 0xc6, 0x33, 0x43, 0xc6, 0x96, 0x8b, - 0x5c, 0x0f, 0xd4, 0x5c, 0x18, 0x1c, 0x0e, 0xba, 0x54, 0x85, 0xf3, 0x4c, 0x2e, 0xa2, 0x4d, 0xd7, 0x9a, 0x23, 0xba, - 0x9a, 0xf4, 0xba, 0xa4, 0xf4, 0xdc, 0xdf, 0x7c, 0x53, 0x67, 0xdc, 0x17, 0x7a, 0x7e, 0x49, 0x87, 0x3f, 0xe3, 0xbc, - 0x98, 0x12, 0x88, 0xe8, 0x78, 0x4f, 0x91, 0x72, 0x75, 0x32, 0xc8, 0xd7, 0x95, 0xca, 0xd2, 0xcf, 0x7f, 0x83, 0x7d, - 0x06, 0x6e, 0x11, 0x1b, 0xc7, 0xf9, 0x71, 0x99, 0x5f, 0x17, 0x63, 0x5e, 0x35, 0xf3, 0xd5, 0xe1, 0x70, 0xd9, 0xbb, - 0xc1, 0x75, 0x93, 0x66, 0x1f, 0xd6, 0x83, 0xa5, 0xdb, 0x37, 0x7f, 0x59, 0xd3, 0xe6, 0x66, 0x37, 0x45, 0x5b, 0x5b, - 0x7e, 0xf1, 0xd4, 0xd3, 0x0b, 0xb5, 0x90, 0xaf, 0xeb, 0x69, 0xc2, 0xcd, 0x5c, 0x30, 0xca, 0x16, 0xda, 0x1d, 0xf0, - 0xb9, 0xea, 0xb2, 0xfc, 0xba, 0x5d, 0x25, 0xf3, 0xe3, 0xe4, 0x1b, 0xf1, 0xdb, 0x25, 0x73, 0x7d, 0x31, 0x43, 0x95, - 0x9a, 0x88, 0x6a, 0xf8, 0x47, 0x20, 0x2d, 0xb7, 0xd7, 0x78, 0x6f, 0xe2, 0xbb, 0xa1, 0x85, 0x75, 0xa4, 0xae, 0x6a, - 0x11, 0x25, 0xb7, 0xdf, 0xcd, 0xab, 0xa1, 0x2c, 0x20, 0xff, 0xd6, 0x84, 0xc8, 0x33, 0xee, 0x86, 0x44, 0x55, 0x79, - 0x98, 0x27, 0x37, 0x80, 0x50, 0xa9, 0x8e, 0x88, 0xb5, 0xcc, 0x13, 0xf0, 0x74, 0x38, 0xc7, 0xd8, 0x86, 0xc0, 0x7b, - 0x1d, 0x9e, 0xa6, 0x3b, 0xf3, 0xc3, 0xb5, 0x00, 0x77, 0xc3, 0xca, 0x83, 0x98, 0xba, 0x41, 0x85, 0x3c, 0xd9, 0x29, - 0xc8, 0x79, 0x52, 0x60, 0x25, 0xbb, 0xa6, 0x39, 0xca, 0x76, 0xf2, 0xa6, 0x7d, 0x57, 0xa3, 0xcc, 0xd6, 0xb8, 0xe7, - 0xcd, 0xdf, 0xf9, 0x24, 0x84, 0x14, 0x7f, 0x63, 0x51, 0x9b, 0x98, 0x4a, 0x48, 0xb8, 0x74, 0x9a, 0x74, 0xbf, 0xf1, - 0x9d, 0x48, 0x62, 0x9e, 0xe7, 0x8a, 0x92, 0x75, 0xc8, 0x64, 0x1b, 0xbf, 0xde, 0x54, 0x9b, 0xb6, 0x5c, 0x42, 0xc3, - 0x1a, 0x1e, 0x3f, 0xa7, 0x59, 0xa4, 0x90, 0x50, 0xb2, 0xa7, 0x25, 0x61, 0x65, 0x41, 0xde, 0x83, 0x2f, 0x53, 0x38, - 0x7c, 0xb9, 0xd3, 0xe7, 0x0b, 0x42, 0x59, 0xb8, 0xa9, 0xc0, 0xc4, 0x7b, 0x1b, 0x2b, 0xcd, 0xd7, 0x51, 0x43, 0x30, - 0x93, 0x3f, 0x13, 0xd6, 0x31, 0xfe, 0x55, 0x33, 0xb5, 0x25, 0x44, 0x09, 0x3e, 0xfc, 0x5c, 0x85, 0xac, 0x1b, 0xc1, - 0xd4, 0xb4, 0x54, 0xf2, 0x05, 0x97, 0x72, 0x0e, 0x24, 0x80, 0x50, 0x03, 0x26, 0x7f, 0xce, 0x9a, 0xbe, 0x9f, 0xf1, - 0xf2, 0x7e, 0xc4, 0x9b, 0x26, 0x24, 0x96, 0x37, 0x92, 0x0d, 0x75, 0xff, 0x64, 0xa0, 0xec, 0x38, 0xa6, 0x7a, 0xc8, - 0x7c, 0x1f, 0x76, 0x7b, 0x1a, 0x19, 0x21, 0xc8, 0x7d, 0x33, 0x42, 0xc3, 0x6c, 0x5e, 0xf0, 0x0b, 0x41, 0xaf, 0x8c, - 0x34, 0xa9, 0x8a, 0x2a, 0xbc, 0xff, 0xf5, 0x0b, 0x21, 0x7a, 0x1c, 0xea, 0xd1, 0xff, 0x4e, 0xe9, 0x2e, 0xd5, 0x12, - 0xc3, 0x7a, 0x28, 0xbc, 0x54, 0xe7, 0x95, 0xaa, 0xcd, 0x05, 0x02, 0x30, 0xe4, 0x56, 0x22, 0xfb, 0x9b, 0x91, 0x04, - 0xec, 0x30, 0x53, 0xfe, 0x75, 0x2d, 0xc2, 0x32, 0xc1, 0xe5, 0xcf, 0x59, 0x65, 0xaf, 0xe2, 0x93, 0x94, 0x3e, 0x9a, - 0x23, 0xaa, 0x2c, 0x61, 0x7c, 0x59, 0x10, 0xa4, 0x3c, 0x9b, 0x17, 0x9b, 0xc6, 0x8d, 0xdc, 0x51, 0x7b, 0x10, 0xaf, - 0x72, 0x1d, 0xc7, 0x12, 0x95, 0xad, 0x72, 0x02, 0x90, 0x3c, 0xbb, 0xef, 0x06, 0x61, 0xb0, 0x9c, 0x10, 0xa9, 0x2e, - 0x23, 0xfc, 0x73, 0xae, 0x0a, 0x6e, 0x25, 0x9a, 0x55, 0xcd, 0xfd, 0x37, 0xe8, 0x62, 0xb7, 0xe0, 0x8e, 0xcf, 0xeb, - 0xb9, 0xe1, 0x2a, 0xbc, 0x29, 0xfc, 0xb6, 0x64, 0x90, 0x5e, 0x59, 0x8e, 0x26, 0xd1, 0xaa, 0x8e, 0x38, 0x89, 0x68, - 0x81, 0xb1, 0xd9, 0x7f, 0xd2, 0xe2, 0xbd, 0xa0, 0x13, 0x2a, 0x6d, 0x2f, 0xd5, 0xe5, 0x74, 0xc6, 0x0f, 0x2e, 0xa8, - 0xd7, 0xc5, 0xf9, 0x94, 0x45, 0x50, 0xe1, 0xdb, 0xd4, 0x9f, 0xe9, 0x9c, 0x7a, 0x9f, 0x2f, 0x37, 0xcd, 0x73, 0x8f, - 0x65, 0xb7, 0x5b, 0x6b, 0x14, 0xb7, 0xae, 0x42, 0x6a, 0xc3, 0x8d, 0x97, 0x71, 0x5b, 0x2b, 0x28, 0xae, 0x08, 0x4f, - 0xb5, 0xa6, 0x89, 0x34, 0x76, 0x89, 0x53, 0x36, 0xc6, 0xfb, 0x77, 0x4b, 0xdc, 0x57, 0x4b, 0x99, 0x32, 0xc4, 0x34, - 0x3c, 0xa1, 0xee, 0x5e, 0x9a, 0x1a, 0x0c, 0x0b, 0x1e, 0xbb, 0x45, 0x7c, 0x21, 0x55, 0x09, 0x0a, 0x46, 0xe5, 0x34, - 0x4f, 0xbc, 0x78, 0xe8, 0x5d, 0xb0, 0x04, 0x88, 0xb7, 0xe8, 0xf2, 0x7e, 0x01, 0xc1, 0x8a, 0xd6, 0x0a, 0xb8, 0x13, - 0x4d, 0x90, 0xf0, 0x02, 0x1d, 0x06, 0x19, 0xea, 0x0d, 0xc8, 0x66, 0x95, 0xe8, 0x9d, 0xb3, 0x63, 0x50, 0x5a, 0xcd, - 0xa2, 0xbd, 0x76, 0x9e, 0xde, 0x05, 0xb6, 0xe4, 0xfc, 0x9c, 0x66, 0x63, 0xc6, 0x48, 0x9c, 0x5e, 0x14, 0x31, 0x65, - 0x9e, 0xa8, 0xb9, 0xb6, 0x44, 0x75, 0x9a, 0xbb, 0x3b, 0x63, 0xc6, 0x89, 0xfd, 0x7a, 0x15, 0x7d, 0xd7, 0xc7, 0x55, - 0xcd, 0x80, 0x0b, 0xcc, 0x86, 0xb5, 0xf1, 0xff, 0x69, 0x28, 0x94, 0x82, 0xbf, 0x9a, 0x75, 0x83, 0xe2, 0x5e, 0x2c, - 0xa7, 0xae, 0x27, 0x42, 0xd7, 0xdf, 0x19, 0xd8, 0x8f, 0x77, 0x84, 0x4f, 0x50, 0x46, 0x36, 0x76, 0xfb, 0xa6, 0x34, - 0xc2, 0xf5, 0x2a, 0xf9, 0xbc, 0x7f, 0x6a, 0xfb, 0x86, 0xfa, 0xfc, 0x58, 0x1c, 0xfb, 0x57, 0x6f, 0x28, 0xa6, 0x0e, - 0xdc, 0xc5, 0xec, 0xb9, 0x68, 0xbe, 0xb5, 0xce, 0xd1, 0x83, 0x87, 0xfc, 0x30, 0xec, 0x9d, 0x6e, 0x2c, 0xa6, 0xa6, - 0x8d, 0x07, 0x1a, 0x43, 0x02, 0xbf, 0x66, 0x0e, 0x6b, 0xf5, 0x40, 0x1c, 0xb1, 0x6a, 0x93, 0x53, 0x91, 0xfa, 0x8d, - 0x2a, 0x63, 0x85, 0x79, 0x2d, 0xae, 0x64, 0x21, 0x95, 0x75, 0x18, 0x28, 0x64, 0xa4, 0x3d, 0xa3, 0xdc, 0xb3, 0x82, - 0x87, 0xb9, 0xfe, 0x5d, 0x70, 0x87, 0xaf, 0xef, 0xed, 0x87, 0x26, 0xbe, 0x85, 0xf1, 0xa2, 0xec, 0x54, 0x66, 0x89, - 0x72, 0xcc, 0x02, 0x51, 0x24, 0xcf, 0x88, 0xe6, 0xf8, 0x9a, 0x8d, 0x21, 0x01, 0x72, 0x23, 0x20, 0xc7, 0xdd, 0x7b, - 0xc5, 0xb1, 0x4d, 0x88, 0x40, 0xa1, 0xdd, 0x2d, 0x90, 0x85, 0x82, 0x4c, 0x12, 0x49, 0xee, 0x8e, 0x86, 0x12, 0xfb, - 0x63, 0xf5, 0xd2, 0x85, 0x5b, 0x44, 0xb2, 0xb1, 0x1b, 0x12, 0x08, 0xa4, 0xf1, 0x3e, 0xd5, 0xe7, 0x02, 0x61, 0x29, - 0x40, 0xc7, 0xc1, 0x3f, 0x49, 0x09, 0xab, 0x99, 0x0c, 0x69, 0x36, 0x70, 0x57, 0xe6, 0x65, 0x37, 0xec, 0x7f, 0x67, - 0xa3, 0x02, 0xc2, 0xf1, 0xa1, 0x7f, 0xec, 0x26, 0x28, 0x32, 0x50, 0xb4, 0x42, 0x3d, 0x14, 0x94, 0x6e, 0x28, 0x62, - 0x50, 0xed, 0x8f, 0x9b, 0xc2, 0xdc, 0xdd, 0xc0, 0x64, 0x89, 0x8a, 0xd6, 0x3c, 0x79, 0x2f, 0xea, 0xdb, 0x88, 0xc1, - 0x27, 0x33, 0x76, 0xe8, 0x66, 0xa2, 0x92, 0x5d, 0xaa, 0x0c, 0xac, 0x83, 0x75, 0x2a, 0x95, 0x02, 0x2f, 0x6a, 0x72, - 0xf7, 0x2d, 0x20, 0x2a, 0xde, 0xa9, 0x8b, 0xce, 0xa0, 0x85, 0x57, 0x5a, 0xe8, 0x0c, 0xfa, 0xe5, 0x8c, 0x42, 0xd2, - 0xb1, 0xa6, 0x76, 0xb9, 0x4e, 0x54, 0x0c, 0xf6, 0x84, 0x0d, 0x4a, 0xb4, 0xfc, 0x43, 0x9b, 0x92, 0x88, 0x30, 0xd7, - 0x3d, 0x1f, 0xfe, 0x71, 0x66, 0x48, 0x1f, 0xde, 0xea, 0x21, 0x95, 0x24, 0xc2, 0x27, 0x7c, 0x39, 0x88, 0xd7, 0x1d, - 0x90, 0x14, 0xb8, 0x77, 0x5d, 0xb1, 0x76, 0x2f, 0x3b, 0xe2, 0xe5, 0x64, 0x4b, 0xcd, 0xb8, 0x2e, 0x53, 0xd0, 0xa8, - 0xe3, 0x78, 0xcb, 0xa7, 0xb0, 0xe1, 0x5d, 0xe9, 0x33, 0x3a, 0x16, 0x98, 0x25, 0x90, 0x08, 0x91, 0xde, 0x3f, 0xba, - 0x73, 0xa1, 0xe6, 0x75, 0x92, 0x19, 0x0a, 0x91, 0x5a, 0x25, 0x37, 0x41, 0x85, 0xe3, 0xa9, 0x42, 0xec, 0x48, 0x4a, - 0xca, 0x84, 0x23, 0x4c, 0x8f, 0x2b, 0xa2, 0xa3, 0xe4, 0x3e, 0x69, 0x2a, 0x19, 0x73, 0xf5, 0xbf, 0x4c, 0x69, 0x82, - 0xd9, 0x95, 0xc3, 0x21, 0x11, 0x20, 0x65, 0x5a, 0x6a, 0x37, 0x78, 0x3f, 0x22, 0x3e, 0x15, 0x80, 0x4a, 0x44, 0xa2, - 0x70, 0xcf, 0x2e, 0xfa, 0xb3, 0x28, 0x21, 0x62, 0x30, 0x13, 0xd3, 0x59, 0xf2, 0xfe, 0x5a, 0xe9, 0xe4, 0x75, 0xa3, - 0xab, 0x51, 0xcd, 0xeb, 0x07, 0x95, 0x8f, 0x98, 0x2b, 0x97, 0x4f, 0x02, 0x19, 0x5b, 0x4d, 0xae, 0xa9, 0xcc, 0xb3, - 0xb2, 0xb8, 0x0a, 0x0b, 0x54, 0x1b, 0xb5, 0x80, 0xeb, 0x73, 0x9d, 0xdb, 0x10, 0x75, 0xce, 0x41, 0xe4, 0x80, 0x1d, - 0x56, 0xb3, 0xd0, 0xd9, 0x31, 0x7d, 0x70, 0x99, 0x1c, 0xe1, 0x34, 0x9d, 0xb9, 0x63, 0x68, 0xa7, 0xf7, 0x22, 0x39, - 0x0c, 0x2e, 0x56, 0xa0, 0x0b, 0x28, 0xa7, 0x86, 0x31, 0x8a, 0x1c, 0x50, 0x62, 0xa9, 0x94, 0x72, 0x01, 0x40, 0x8b, - 0xa2, 0xab, 0xa0, 0x0c, 0x85, 0x2a, 0x69, 0x64, 0x0b, 0x07, 0x2b, 0x8e, 0x11, 0xaf, 0xbd, 0xfa, 0x21, 0x10, 0xb2, - 0x68, 0xbb, 0x06, 0xea, 0x40, 0xa9, 0xe6, 0xad, 0x7f, 0x17, 0xb9, 0xe8, 0xc2, 0xb3, 0x32, 0x40, 0x99, 0x3f, 0xaa, - 0x36, 0xeb, 0x4e, 0x26, 0x2f, 0xae, 0x8c, 0x17, 0xaa, 0x6c, 0x78, 0x90, 0x3c, 0x4b, 0x64, 0x4a, 0x28, 0x70, 0xca, - 0x92, 0xc6, 0x3e, 0xf1, 0xc1, 0x8e, 0xfc, 0xf6, 0x84, 0xb9, 0x39, 0x56, 0x46, 0x35, 0xe2, 0xe9, 0x8b, 0xaa, 0xeb, - 0x9a, 0x21, 0x42, 0xcd, 0x3f, 0x7c, 0xed, 0xac, 0xff, 0xa6, 0x1b, 0x8d, 0xde, 0x91, 0x75, 0xd6, 0xe4, 0xdf, 0x86, - 0xc1, 0x9d, 0xce, 0x9e, 0xd5, 0x55, 0x83, 0x58, 0x2b, 0x28, 0x02, 0xe1, 0x80, 0x07, 0xbf, 0xa9, 0x8e, 0xf6, 0x9b, - 0x80, 0x25, 0xef, 0x18, 0xda, 0x93, 0xea, 0x8a, 0x09, 0x4d, 0xcb, 0xe7, 0x1f, 0x41, 0x73, 0xa5, 0x86, 0xb2, 0x2c, - 0xf8, 0xf0, 0x01, 0x65, 0x06, 0xa5, 0xca, 0x51, 0x3b, 0xdd, 0x86, 0x5c, 0x13, 0x68, 0xa2, 0x27, 0x41, 0x9e, 0xaf, - 0xbf, 0x70, 0x57, 0xa5, 0x32, 0x90, 0x6f, 0x98, 0xec, 0xc2, 0x5d, 0xb2, 0xba, 0xca, 0x39, 0xfb, 0x2f, 0x51, 0xcc, - 0xf3, 0xbc, 0xa7, 0x23, 0x23, 0xbd, 0x67, 0x47, 0x6e, 0x6a, 0xc5, 0xf9, 0x29, 0x4a, 0x48, 0x16, 0x6d, 0x59, 0x68, - 0xcb, 0x11, 0x8c, 0x81, 0x12, 0x3a, 0x12, 0xb9, 0x0c, 0x59, 0xd6, 0xb0, 0xcc, 0xbe, 0xe5, 0x7f, 0xe3, 0x90, 0x49, - 0x4a, 0x72, 0x9a, 0x5c, 0xf7, 0xb2, 0xb8, 0xec, 0xca, 0x12, 0x95, 0xd8, 0x51, 0x6b, 0xba, 0x12, 0x43, 0xf2, 0xd5, - 0x7b, 0xda, 0xb4, 0xd4, 0x7e, 0x84, 0x74, 0x67, 0x46, 0x0a, 0xfa, 0xa0, 0xea, 0x6d, 0x74, 0xc1, 0xd1, 0x06, 0x90, - 0x63, 0x49, 0xf2, 0x3c, 0x29, 0x06, 0xd9, 0x44, 0x0a, 0x25, 0xea, 0x49, 0x0e, 0x63, 0x59, 0xc2, 0xdc, 0x8f, 0x12, - 0xcc, 0xd2, 0xb2, 0x8c, 0x91, 0x3e, 0x2d, 0x62, 0xa7, 0x14, 0x8f, 0x50, 0xf9, 0x38, 0xbb, 0xef, 0xa6, 0xa1, 0x24, - 0xd5, 0x49, 0x99, 0x20, 0x68, 0xcf, 0x81, 0xd0, 0x31, 0x01, 0xf3, 0xbd, 0xa9, 0xe8, 0x2f, 0x7f, 0x8e, 0x2b, 0x16, - 0xf6, 0x1f, 0x52, 0x3c, 0x33, 0xb3, 0x9b, 0x5f, 0x59, 0xce, 0x91, 0xe5, 0xcc, 0xd0, 0x49, 0x9b, 0x42, 0x0a, 0x1b, - 0x67, 0xbb, 0xfe, 0x82, 0x34, 0xef, 0x8d, 0x0e, 0x47, 0x7a, 0x09, 0xbf, 0x2f, 0x04, 0xd7, 0x87, 0x24, 0x8c, 0x90, - 0xab, 0x2e, 0xaa, 0xdd, 0x3c, 0x79, 0x91, 0xc2, 0x6a, 0x47, 0x47, 0x5c, 0x8a, 0xdd, 0xdb, 0x59, 0xc4, 0x5a, 0x91, - 0x0a, 0xf6, 0xbd, 0x32, 0x91, 0x89, 0xcd, 0xa0, 0x04, 0x67, 0x2b, 0x03, 0x7a, 0x56, 0xbb, 0x78, 0x26, 0xaa, 0xa3, - 0x26, 0xd4, 0x59, 0x81, 0x67, 0xa8, 0x01, 0x64, 0xeb, 0xbc, 0x69, 0xc9, 0x9e, 0x0e, 0x96, 0x93, 0xd4, 0x50, 0x9a, - 0x45, 0x04, 0xfe, 0xd0, 0xdd, 0xde, 0x93, 0x88, 0x0c, 0x03, 0x3f, 0xce, 0x46, 0x94, 0x07, 0xa8, 0x19, 0xc3, 0x89, - 0xa5, 0x49, 0x92, 0x35, 0x5c, 0x98, 0xf7, 0x56, 0x04, 0x71, 0xdf, 0xc7, 0xb6, 0x88, 0xfa, 0xdb, 0x51, 0x26, 0xd8, - 0x57, 0xeb, 0x04, 0xa2, 0x62, 0x16, 0xca, 0xe4, 0x5b, 0x42, 0x78, 0xcb, 0x03, 0xc3, 0xd5, 0xf9, 0x86, 0xd9, 0x58, - 0xb5, 0x92, 0x23, 0x5f, 0x55, 0x0b, 0x3b, 0x20, 0x1c, 0xb5, 0x2f, 0x1d, 0xeb, 0x67, 0xb7, 0x2a, 0x7a, 0x3d, 0x2d, - 0xbf, 0xda, 0xd4, 0x95, 0xaa, 0x03, 0xb9, 0x18, 0xa3, 0x6c, 0xc6, 0xa4, 0xd1, 0x00, 0xb5, 0x80, 0x5c, 0x59, 0x47, - 0xaa, 0x78, 0x9a, 0x96, 0x70, 0x88, 0x07, 0x1e, 0xaf, 0xae, 0x1d, 0xe9, 0x25, 0xdb, 0xa1, 0x03, 0xda, 0x7a, 0x87, - 0x6f, 0xbd, 0x76, 0x2d, 0xc6, 0x8d, 0x05, 0xbc, 0x01, 0xa0, 0x12, 0x95, 0x0a, 0x2a, 0xd1, 0x20, 0xe5, 0x52, 0xc5, - 0x75, 0xd0, 0x29, 0xd7, 0xb4, 0x58, 0x59, 0x2b, 0xbc, 0xcb, 0x03, 0xfc, 0x69, 0x07, 0xa4, 0xb2, 0xae, 0x82, 0x62, - 0xd1, 0xfd, 0x16, 0x84, 0x14, 0xe2, 0xcd, 0xf4, 0xcd, 0x1c, 0xcc, 0xc9, 0x92, 0x35, 0x5f, 0xcb, 0x13, 0x61, 0xb1, - 0x72, 0x6b, 0x4e, 0xce, 0x91, 0x51, 0x49, 0xdf, 0xde, 0x03, 0xa2, 0x6e, 0x76, 0x79, 0xbf, 0xf8, 0xd1, 0x33, 0xee, - 0x29, 0xf0, 0xf1, 0x76, 0xbf, 0x1b, 0x1c, 0x7e, 0x78, 0xcb, 0xe1, 0x90, 0x33, 0x48, 0x43, 0x1a, 0x1b, 0xc8, 0x10, - 0xbc, 0x58, 0x15, 0x16, 0xfc, 0xb1, 0x6e, 0x75, 0x89, 0xe8, 0x3c, 0x05, 0xfc, 0x3d, 0x73, 0x17, 0xba, 0x3f, 0x20, - 0x72, 0x17, 0xf2, 0x78, 0xde, 0x65, 0x52, 0x3e, 0x42, 0x1a, 0x49, 0xee, 0xdf, 0x47, 0x9a, 0xca, 0xe4, 0x7c, 0xf3, - 0x97, 0x3c, 0x2a, 0x54, 0xd6, 0xc1, 0x14, 0x1a, 0x94, 0xd4, 0x39, 0x60, 0xd0, 0x46, 0xc6, 0x55, 0xbd, 0x1c, 0x3a, - 0x69, 0xf5, 0x79, 0xb6, 0x07, 0x99, 0x22, 0x10, 0x9d, 0x1e, 0x94, 0x51, 0x06, 0x42, 0x00, 0xbc, 0x80, 0x00, 0x68, - 0x09, 0x06, 0x03, 0xf8, 0x48, 0xf7, 0x74, 0xd0, 0x98, 0x8c, 0xd3, 0xa7, 0x92, 0x0c, 0x75, 0xa9, 0xfc, 0x9a, 0x58, - 0x8f, 0x92, 0x25, 0x62, 0x52, 0x41, 0x0b, 0xc5, 0x8c, 0xe2, 0x53, 0xf3, 0xce, 0xdc, 0xd2, 0xfb, 0x92, 0x19, 0xc6, - 0x99, 0x66, 0xa9, 0xd3, 0x46, 0xf2, 0x91, 0xba, 0x4f, 0x79, 0xb0, 0x4a, 0x10, 0x9e, 0x12, 0xaa, 0x32, 0x3c, 0xd4, - 0x35, 0x53, 0x8a, 0x67, 0x38, 0x86, 0xe3, 0xf2, 0xad, 0x45, 0xea, 0xe0, 0x83, 0xc4, 0xa7, 0xef, 0x63, 0xa8, 0xad, - 0xf9, 0xe3, 0xaf, 0x1d, 0x09, 0xbf, 0x8c, 0x32, 0xcc, 0x95, 0x88, 0x03, 0x2d, 0x53, 0x60, 0x87, 0x0b, 0x3d, 0x4f, - 0xbc, 0xdc, 0x97, 0xb8, 0xa3, 0x40, 0x0f, 0x46, 0xec, 0xa9, 0x0f, 0x99, 0xdb, 0x33, 0x91, 0xb5, 0x2d, 0xea, 0xf5, - 0xdf, 0x17, 0xdf, 0xe5, 0xc9, 0xed, 0x98, 0x27, 0x75, 0x8a, 0x0a, 0xb4, 0x52, 0x7b, 0xcb, 0x7c, 0x5f, 0xcc, 0xc2, - 0xa3, 0xef, 0x56, 0xc8, 0x18, 0x43, 0x33, 0xf2, 0x60, 0x63, 0x44, 0x50, 0x16, 0x3b, 0x98, 0xdf, 0x29, 0x24, 0x3f, - 0x3e, 0xc8, 0x41, 0x23, 0x0a, 0x82, 0xaa, 0xfd, 0x05, 0x85, 0xb2, 0x30, 0xf6, 0x37, 0xbe, 0xf6, 0x3d, 0xb2, 0xea, - 0x14, 0xcb, 0xe3, 0xec, 0x33, 0x3e, 0x67, 0x71, 0xc5, 0xaa, 0x05, 0x59, 0x91, 0x7b, 0x25, 0x9e, 0x8e, 0x59, 0xda, - 0x66, 0xd1, 0xb4, 0x4e, 0x00, 0xef, 0x63, 0xe7, 0xb6, 0xdc, 0x0f, 0xb5, 0xfe, 0x08, 0xe2, 0xea, 0x8b, 0xb0, 0xf6, - 0x3c, 0x08, 0x2b, 0xaf, 0xa2, 0x40, 0x31, 0x6d, 0x4b, 0x7e, 0xde, 0xdb, 0xcf, 0xd8, 0x46, 0xf1, 0x44, 0xb6, 0x9b, - 0xf7, 0xb6, 0x67, 0xdb, 0x38, 0x65, 0x4a, 0xfd, 0x6d, 0xae, 0x0f, 0xfe, 0x0f, 0x11, 0x3f, 0xe6, 0x40, 0xbf, 0x5f, - 0xac, 0xa6, 0xf9, 0xa9, 0xf9, 0x64, 0x85, 0xd2, 0x07, 0xf0, 0xe2, 0xb2, 0x79, 0x31, 0x7a, 0xb5, 0xf6, 0x52, 0xc4, - 0xf2, 0x20, 0xf4, 0x2d, 0x62, 0x68, 0x3b, 0xc8, 0x2e, 0xdf, 0xcf, 0x05, 0xba, 0x60, 0xac, 0x47, 0xbf, 0x01, 0x9e, - 0xb4, 0xda, 0x38, 0x64, 0xe0, 0x32, 0x99, 0x73, 0x13, 0x24, 0xdd, 0x07, 0x55, 0xbf, 0xb7, 0xaf, 0xf2, 0xb8, 0xa2, - 0x07, 0x2e, 0xa8, 0x30, 0xdd, 0x71, 0xd7, 0xb5, 0xba, 0xfa, 0xdb, 0x1d, 0x78, 0x9e, 0x1b, 0xed, 0x45, 0xef, 0x6f, - 0xe2, 0x50, 0xc4, 0x95, 0xec, 0x0b, 0x88, 0x44, 0x26, 0x7e, 0xb3, 0x99, 0x24, 0xf6, 0xe5, 0x02, 0x66, 0x2a, 0x99, - 0x66, 0xb1, 0xb6, 0x74, 0x3b, 0xa7, 0x07, 0xaf, 0x86, 0xe5, 0x6c, 0x30, 0x3c, 0xcb, 0xc0, 0xe5, 0x97, 0xd7, 0xb8, - 0xe3, 0xa6, 0xad, 0x7f, 0xe5, 0x96, 0xf0, 0x25, 0xb2, 0xd4, 0xb5, 0xe4, 0xcf, 0x28, 0xb5, 0x1f, 0x34, 0x95, 0x7f, - 0x12, 0xf4, 0xb4, 0xf4, 0x3e, 0x2f, 0x9d, 0xca, 0xaf, 0xdf, 0xb7, 0x3d, 0xad, 0x16, 0xaf, 0xfe, 0x3a, 0xb9, 0x5e, - 0xff, 0xaf, 0xed, 0x1a, 0x3d, 0xfa, 0x39, 0xbd, 0xb2, 0xbf, 0x13, 0x9b, 0x85, 0x61, 0x77, 0x3d, 0x5d, 0x5c, 0x8b, - 0xea, 0xcf, 0x0f, 0xfc, 0xb3, 0xd3, 0x4a, 0x98, 0x8f, 0xbf, 0xb7, 0x98, 0x4c, 0xad, 0x66, 0xb7, 0xa6, 0x7b, 0xf8, - 0xe9, 0xa7, 0x1e, 0x04, 0xda, 0x95, 0x9c, 0xbb, 0xa6, 0x61, 0x98, 0x45, 0x85, 0x1a, 0x4e, 0xd1, 0x83, 0x3e, 0xda, - 0x1e, 0x37, 0xdf, 0x64, 0xeb, 0xc0, 0xed, 0x65, 0x93, 0x27, 0xcd, 0xd6, 0xf5, 0xb5, 0xc5, 0xf5, 0x5c, 0xa6, 0xcb, - 0x29, 0xb8, 0x91, 0x09, 0x1f, 0xe2, 0xbf, 0x6c, 0xc1, 0x64, 0x15, 0x90, 0xfc, 0x81, 0x14, 0xd7, 0xb7, 0xab, 0xab, - 0x5f, 0x65, 0xb9, 0x12, 0xb3, 0x8e, 0xb4, 0x06, 0x5a, 0x07, 0x41, 0xa3, 0xe6, 0x71, 0x21, 0x66, 0xae, 0xe9, 0x68, - 0x57, 0x72, 0x82, 0x0b, 0x50, 0x18, 0x4d, 0xfd, 0xc7, 0x4f, 0xa1, 0x36, 0xf7, 0x1a, 0xe5, 0x08, 0xae, 0x89, 0x62, - 0xf7, 0x9a, 0xb0, 0xdd, 0x82, 0x71, 0xaa, 0x78, 0x87, 0xaa, 0xca, 0x8d, 0xf6, 0xc5, 0x4c, 0x03, 0xa2, 0xca, 0x72, - 0x12, 0xe0, 0x10, 0x6f, 0x16, 0x6e, 0x92, 0xc4, 0xff, 0x1e, 0xbb, 0x48, 0x99, 0x14, 0xfe, 0xb9, 0x22, 0x64, 0xb1, - 0xd0, 0x2f, 0xb7, 0x8e, 0xd8, 0xd4, 0x0d, 0xab, 0xeb, 0x0f, 0x92, 0x99, 0x90, 0x9a, 0x89, 0xb0, 0xa2, 0x66, 0xbe, - 0x05, 0xdc, 0x93, 0x82, 0x89, 0xe7, 0x22, 0x6f, 0x6d, 0xd7, 0xfc, 0x7d, 0xea, 0xb3, 0xc5, 0x47, 0x91, 0x05, 0xd0, - 0x0d, 0x01, 0x02, 0x1a, 0xd7, 0xa7, 0xc5, 0x7a, 0x6f, 0x3b, 0xef, 0x5f, 0xd2, 0x58, 0x74, 0x3b, 0xe3, 0xca, 0x79, - 0xab, 0x68, 0xec, 0xe9, 0x02, 0x28, 0x69, 0xf5, 0xe5, 0xc7, 0x55, 0x3f, 0xbc, 0xdf, 0xc4, 0xad, 0xde, 0xc7, 0xbb, - 0xfe, 0xb5, 0x50, 0x8e, 0x34, 0x7c, 0x3a, 0xa5, 0xe1, 0xe5, 0x4e, 0x35, 0x31, 0x67, 0x3c, 0x2d, 0xaf, 0x5c, 0xd8, - 0x0d, 0xd9, 0xc6, 0xef, 0x7c, 0x68, 0xeb, 0x81, 0x5f, 0x76, 0x98, 0x22, 0x34, 0x27, 0x9b, 0x29, 0x2f, 0x90, 0x98, - 0xeb, 0x9b, 0xad, 0x38, 0xd8, 0x7b, 0x84, 0x36, 0xe1, 0xaf, 0x97, 0x24, 0x2b, 0xc4, 0xa8, 0xc2, 0x41, 0x5e, 0x1e, - 0xf6, 0x29, 0x4c, 0x82, 0xf5, 0x25, 0x3c, 0x04, 0x8a, 0x3e, 0x62, 0x14, 0xcb, 0xf1, 0x16, 0xea, 0x58, 0x8e, 0x83, - 0xde, 0x2c, 0x1f, 0xb5, 0xb1, 0x8b, 0x9f, 0x31, 0xb0, 0x62, 0xab, 0x90, 0xd3, 0x88, 0x35, 0x5b, 0xf8, 0xde, 0xdc, - 0xfa, 0xa1, 0xde, 0x73, 0x37, 0x24, 0x1c, 0x6e, 0x2b, 0xcf, 0x2f, 0xaa, 0x2d, 0x2b, 0xb5, 0x01, 0xde, 0xd2, 0x9d, - 0x79, 0x50, 0x49, 0x9b, 0xf7, 0x44, 0x76, 0x6f, 0x54, 0xf5, 0xd7, 0x4e, 0x16, 0xe2, 0x97, 0xdf, 0x45, 0xdb, 0x59, - 0x0a, 0x22, 0x5c, 0x84, 0x21, 0xd4, 0x56, 0xa5, 0x47, 0x51, 0x9e, 0xba, 0xfd, 0x14, 0x37, 0xa5, 0xdd, 0x06, 0x19, - 0xe9, 0xfe, 0x0d, 0x90, 0x82, 0x5e, 0x80, 0x40, 0x88, 0x7b, 0xd9, 0x1c, 0x0a, 0x98, 0x64, 0x90, 0x40, 0x2f, 0x13, - 0xac, 0x03, 0xfe, 0xa0, 0xe4, 0xbd, 0xed, 0xc0, 0x03, 0xe0, 0x80, 0xc2, 0x61, 0xa4, 0x7c, 0x65, 0x4e, 0x75, 0x46, - 0x66, 0xcb, 0xaa, 0x75, 0x13, 0x8f, 0xa6, 0xeb, 0x0a, 0x9c, 0x37, 0x15, 0x9b, 0x7f, 0x26, 0x0a, 0xdb, 0x4e, 0xc4, - 0xa9, 0x2c, 0x14, 0x98, 0xa8, 0x31, 0x48, 0x4f, 0x0d, 0x96, 0xad, 0x4a, 0xd3, 0xf8, 0x10, 0xcb, 0x6e, 0x1c, 0xa5, - 0xeb, 0x7a, 0x63, 0xe3, 0xcc, 0x41, 0x98, 0xbd, 0xb1, 0xd7, 0xee, 0xf9, 0x96, 0xb5, 0x37, 0xad, 0x0e, 0xc5, 0x63, - 0x65, 0x71, 0x53, 0xf9, 0xf2, 0xcd, 0xb9, 0x31, 0xe5, 0x59, 0xe6, 0x1c, 0xc2, 0x5a, 0xf0, 0xdf, 0xb0, 0x1b, 0x36, - 0x6d, 0x2c, 0xf9, 0x72, 0x5f, 0x10, 0xc1, 0xee, 0x92, 0xbd, 0xc6, 0xec, 0xb2, 0xa4, 0x0a, 0x43, 0xf0, 0xce, 0x67, - 0x2a, 0x11, 0xcb, 0x1f, 0x51, 0x94, 0x6f, 0x3c, 0xb6, 0xfb, 0xc2, 0x2a, 0xc1, 0xb0, 0x5e, 0x13, 0x78, 0xe1, 0x79, - 0x4b, 0x49, 0x8b, 0x49, 0x79, 0x59, 0x66, 0xf7, 0xcb, 0xee, 0xca, 0x0d, 0xc2, 0xc3, 0xbd, 0xa4, 0x15, 0x62, 0x18, - 0x35, 0x95, 0x2a, 0x70, 0x31, 0x22, 0xea, 0x98, 0xa7, 0xdb, 0xb2, 0xc4, 0x2f, 0x4d, 0x25, 0x49, 0x2d, 0x74, 0x6b, - 0x07, 0x87, 0xdf, 0x59, 0xd2, 0x44, 0x88, 0x2d, 0x28, 0x5e, 0x77, 0x48, 0x60, 0xe7, 0x9c, 0x52, 0xbe, 0x9f, 0x3f, - 0xca, 0x2c, 0x19, 0x5e, 0x95, 0x03, 0x55, 0xc8, 0xce, 0x0c, 0x19, 0xad, 0xb8, 0xeb, 0x44, 0x12, 0xf4, 0x2e, 0x12, - 0x83, 0x15, 0xc2, 0x7e, 0x28, 0xca, 0x12, 0xfd, 0x80, 0x57, 0x93, 0x1b, 0x78, 0x11, 0x58, 0x75, 0xe5, 0xb1, 0x53, - 0x12, 0xe4, 0x00, 0xf9, 0x49, 0x70, 0xd6, 0xc3, 0x4c, 0xee, 0x8e, 0x40, 0x04, 0x8f, 0x77, 0x32, 0x11, 0xd0, 0x06, - 0xf2, 0x55, 0x5d, 0xdc, 0x89, 0x44, 0x15, 0x82, 0xbf, 0x24, 0x77, 0xf4, 0x6e, 0xab, 0x34, 0x1a, 0xed, 0x6c, 0x28, - 0x4c, 0xdc, 0xc6, 0x75, 0xe3, 0x1d, 0x2f, 0x98, 0xa3, 0xcd, 0xe8, 0xae, 0x58, 0x9e, 0xe7, 0xcd, 0x4d, 0x69, 0x04, - 0x4b, 0xea, 0x2b, 0xcc, 0x4b, 0xb3, 0x56, 0x74, 0x64, 0x6c, 0x11, 0x24, 0x06, 0xcc, 0x69, 0x67, 0xc9, 0x62, 0xf5, - 0xdb, 0x58, 0x8b, 0x1c, 0x47, 0x58, 0x96, 0x04, 0x10, 0xcd, 0x0b, 0x91, 0x17, 0x43, 0x8e, 0xa3, 0x25, 0xaa, 0x14, - 0x18, 0x73, 0x8b, 0x18, 0x76, 0xb1, 0xc9, 0xc9, 0x6d, 0x69, 0xa4, 0x68, 0xf8, 0x26, 0xa7, 0xa7, 0xf7, 0xd6, 0xf0, - 0xda, 0x88, 0x64, 0xe4, 0x60, 0x7b, 0x2e, 0xb7, 0x25, 0xac, 0xf2, 0x53, 0xbf, 0x5b, 0x6b, 0x15, 0x2d, 0x97, 0xe3, - 0x60, 0xb4, 0xd7, 0x92, 0x45, 0xbb, 0x01, 0x35, 0x7f, 0xc9, 0xba, 0x87, 0x8c, 0x35, 0xda, 0xb0, 0xd5, 0x41, 0x5d, - 0x88, 0x73, 0x3e, 0xf6, 0x15, 0xfe, 0x46, 0x9e, 0xc0, 0x4c, 0x53, 0x72, 0x91, 0xff, 0xbe, 0xbf, 0x54, 0x25, 0xf9, - 0xeb, 0x60, 0x27, 0x3c, 0x16, 0x6a, 0x64, 0xbf, 0x57, 0x13, 0x33, 0x99, 0x61, 0x87, 0xf7, 0x09, 0xb8, 0x42, 0x7c, - 0x31, 0xc0, 0xce, 0x2c, 0x9d, 0x4b, 0xda, 0xa9, 0x8c, 0x19, 0x97, 0xe2, 0x38, 0x93, 0x26, 0x1b, 0x5b, 0x53, 0x7f, - 0x7b, 0x89, 0x55, 0x4b, 0x9e, 0x5a, 0x9f, 0x5b, 0x5f, 0xe3, 0xe3, 0x1c, 0xf2, 0xd6, 0x87, 0x19, 0xff, 0x64, 0x2b, - 0x4c, 0xd8, 0x3b, 0xa2, 0x45, 0xb0, 0x63, 0xa3, 0x81, 0x9f, 0xde, 0x8b, 0xa3, 0x65, 0x09, 0xda, 0x3f, 0x80, 0x55, - 0x5d, 0x85, 0x9c, 0xc9, 0xf0, 0xf3, 0xa4, 0x91, 0x58, 0x24, 0xc5, 0x02, 0x38, 0xdf, 0xab, 0xd9, 0xef, 0xd5, 0xeb, - 0x93, 0xd5, 0x64, 0xc8, 0xa8, 0xb3, 0xa4, 0x2e, 0xd4, 0x9f, 0x5d, 0xdf, 0x36, 0x75, 0x1d, 0x9c, 0x88, 0xeb, 0x4f, - 0xd0, 0xb6, 0x75, 0xc7, 0xd0, 0x9c, 0xa0, 0xa6, 0x3c, 0x53, 0x1c, 0xeb, 0x4e, 0xbf, 0x19, 0x8f, 0x6c, 0x6e, 0x3e, - 0xda, 0x44, 0x36, 0x5e, 0x8c, 0xc4, 0x16, 0x5e, 0xe8, 0x05, 0xd0, 0xa2, 0xbe, 0xc7, 0x42, 0xfc, 0xe4, 0xe8, 0x19, - 0x4e, 0x60, 0x04, 0x72, 0x2d, 0x64, 0xa0, 0xa4, 0x27, 0x1a, 0xb6, 0x55, 0x73, 0x0e, 0x07, 0x2f, 0x3f, 0xb1, 0x2d, - 0x79, 0x44, 0x07, 0x75, 0x86, 0xfe, 0x4a, 0x3e, 0xdb, 0x4f, 0x15, 0xef, 0x30, 0xd5, 0xf6, 0xeb, 0x72, 0x9c, 0x35, - 0xd3, 0x79, 0xd7, 0x90, 0x85, 0xe8, 0xf9, 0x60, 0x7b, 0x86, 0xd4, 0xb1, 0x8a, 0x56, 0xcd, 0xe2, 0xf5, 0xf0, 0xee, - 0x91, 0x25, 0x66, 0xeb, 0x76, 0x67, 0x74, 0xd8, 0x41, 0x50, 0x43, 0x0c, 0xd6, 0x6d, 0x21, 0x91, 0x19, 0x25, 0xc7, - 0xba, 0x10, 0x1f, 0xc1, 0x0d, 0x41, 0x29, 0xf4, 0xb0, 0x97, 0xd2, 0x4a, 0x3f, 0xea, 0x22, 0x19, 0x26, 0x83, 0x47, - 0xb3, 0x5b, 0x36, 0xd7, 0x62, 0x17, 0x55, 0xfd, 0xb3, 0xec, 0xda, 0x15, 0xf4, 0xd1, 0x14, 0x75, 0x42, 0x0d, 0x22, - 0x90, 0xde, 0xca, 0xdf, 0xe2, 0xf8, 0x92, 0x6e, 0x5c, 0xbf, 0x1e, 0xde, 0x84, 0xcc, 0xf9, 0x00, 0x0e, 0x00, 0xd3, - 0xc9, 0xfb, 0x77, 0x0e, 0x25, 0x55, 0x85, 0x46, 0x5a, 0xdf, 0x91, 0x1b, 0x6c, 0xc7, 0xe4, 0x21, 0x3a, 0xbe, 0x76, - 0x33, 0x40, 0x80, 0x36, 0x16, 0xfa, 0x1c, 0x5a, 0x6f, 0x24, 0xad, 0x04, 0x4b, 0xa0, 0xb3, 0xfa, 0xa1, 0xa5, 0xf0, - 0xd2, 0x90, 0x98, 0xfa, 0x0d, 0x96, 0x45, 0xa2, 0x24, 0xe6, 0xcf, 0xc2, 0x2b, 0xdb, 0xaa, 0xf0, 0x30, 0xc6, 0x0a, - 0xd0, 0x86, 0x58, 0xfb, 0x15, 0xbb, 0x22, 0xb0, 0xf0, 0xde, 0x12, 0xc0, 0xbb, 0x66, 0x8e, 0x02, 0x01, 0xc7, 0x2b, - 0x1c, 0x44, 0x1a, 0x8c, 0x3f, 0x5b, 0x89, 0x23, 0x47, 0x9a, 0xd5, 0x47, 0xef, 0xdb, 0xfd, 0x69, 0x8a, 0x47, 0xea, - 0x1c, 0xb7, 0x9e, 0x64, 0x81, 0x3f, 0xec, 0x9e, 0x0b, 0x2f, 0xac, 0xc8, 0x5e, 0xf6, 0x77, 0xf7, 0x92, 0xc5, 0x4d, - 0x2f, 0xf1, 0x55, 0x2f, 0x93, 0xeb, 0x95, 0x43, 0x0d, 0x6a, 0x60, 0xf3, 0x7d, 0x8f, 0xaa, 0x82, 0xdc, 0x80, 0xbf, - 0x77, 0xf3, 0x11, 0xc6, 0xb5, 0x0b, 0x9c, 0xe3, 0x52, 0x3d, 0xcc, 0x45, 0xcc, 0xa8, 0xa4, 0x76, 0x94, 0x5c, 0x40, - 0xaa, 0x93, 0x94, 0x0b, 0x32, 0xac, 0x5c, 0xa0, 0xb7, 0xfa, 0x14, 0xaf, 0x9c, 0x2d, 0x4d, 0x82, 0x28, 0xea, 0xb9, - 0x7b, 0x85, 0x0a, 0xc1, 0x7f, 0x1d, 0xc8, 0x8c, 0x29, 0x2b, 0x30, 0x57, 0x13, 0xea, 0x30, 0x4b, 0x26, 0xbc, 0x3c, - 0x8a, 0xe1, 0xc5, 0x08, 0x32, 0x6d, 0xa9, 0x22, 0xaa, 0xdf, 0x43, 0xdf, 0xa4, 0x68, 0x56, 0xa3, 0x10, 0x73, 0x8e, - 0x25, 0x15, 0x5c, 0xaf, 0xea, 0x10, 0x7e, 0x44, 0xad, 0xee, 0x15, 0x8e, 0xeb, 0x9c, 0x31, 0xb9, 0xf3, 0xad, 0x07, - 0x9c, 0x9e, 0xc7, 0xe9, 0x01, 0xb9, 0x6d, 0x2a, 0xa2, 0xfa, 0x18, 0x0f, 0x43, 0x58, 0xab, 0x59, 0x66, 0x08, 0x99, - 0xb7, 0x0d, 0xc5, 0xaf, 0x84, 0x66, 0xf3, 0x67, 0x57, 0x43, 0xf2, 0x79, 0xf1, 0xc4, 0x8d, 0xeb, 0x54, 0xc5, 0x12, - 0x93, 0xbc, 0x08, 0x2b, 0xcc, 0xe1, 0xb2, 0x37, 0x63, 0xd1, 0xf9, 0xc9, 0x79, 0x08, 0x09, 0x58, 0x94, 0x8f, 0x68, - 0xed, 0x19, 0x50, 0x50, 0x2d, 0xc7, 0xa9, 0x5a, 0x03, 0xc6, 0xf6, 0x3c, 0x78, 0x4b, 0xf9, 0x2f, 0xdb, 0x03, 0xd4, - 0x93, 0xed, 0x9c, 0x62, 0x30, 0x86, 0xf0, 0x94, 0x6e, 0x03, 0x9c, 0x90, 0xca, 0x16, 0x0e, 0xaa, 0xf5, 0x37, 0x37, - 0x2f, 0xb4, 0x81, 0x12, 0x7f, 0x49, 0x91, 0xd6, 0x8a, 0x14, 0x8f, 0x30, 0x34, 0x05, 0x9f, 0xf1, 0xd8, 0x93, 0x78, - 0xcf, 0xdd, 0x97, 0x35, 0x22, 0x05, 0x71, 0x2e, 0xfc, 0x85, 0x08, 0x2b, 0x47, 0x88, 0x79, 0xca, 0x0d, 0x95, 0x64, - 0x5f, 0xda, 0xb3, 0xfa, 0x2a, 0x0b, 0x78, 0xea, 0xb2, 0x4f, 0xe1, 0x85, 0xc0, 0x1c, 0xae, 0x3d, 0x7a, 0x52, 0x3f, - 0x9a, 0x01, 0xab, 0x7a, 0x6e, 0x31, 0x8d, 0x50, 0x51, 0xb8, 0x84, 0x20, 0x6e, 0x29, 0x41, 0x84, 0x61, 0x66, 0x8d, - 0x87, 0x4f, 0x0c, 0xeb, 0x29, 0x0c, 0x00, 0xa6, 0xa4, 0xa1, 0x7b, 0x86, 0x32, 0x81, 0x7b, 0x69, 0x17, 0x28, 0xcf, - 0x5c, 0x65, 0x5e, 0x4e, 0xee, 0x31, 0x0c, 0x9c, 0xcc, 0xd6, 0x16, 0xd3, 0xc8, 0x88, 0xac, 0x03, 0x2d, 0xd4, 0x91, - 0xbf, 0x57, 0x2b, 0x2b, 0xbb, 0x6e, 0x02, 0xdd, 0x8b, 0x7a, 0x2f, 0xfd, 0x22, 0x42, 0xdc, 0x5e, 0xa5, 0xa7, 0x80, - 0x69, 0xf8, 0x14, 0xf7, 0x33, 0xa9, 0x10, 0x78, 0x15, 0xf4, 0x3c, 0x90, 0xc8, 0x1e, 0xe2, 0x0e, 0xea, 0x06, 0x00, - 0x92, 0x5b, 0xe2, 0x4a, 0x41, 0x5a, 0x1b, 0x52, 0x25, 0xeb, 0x3f, 0xb5, 0xd5, 0x28, 0x01, 0xc8, 0x47, 0xf6, 0xcd, - 0x91, 0xc6, 0x2b, 0x08, 0x2d, 0x5c, 0x48, 0x39, 0xcc, 0xe0, 0x51, 0x13, 0x74, 0x22, 0x33, 0xc1, 0xe6, 0x56, 0xb0, - 0x8d, 0x9f, 0xbc, 0x79, 0x1d, 0x89, 0x0a, 0xd3, 0x8f, 0xe2, 0xdf, 0xab, 0x3b, 0x6e, 0x8a, 0xb2, 0xe2, 0x21, 0xf7, - 0xfd, 0x1c, 0x3f, 0x37, 0x45, 0x49, 0x9a, 0x13, 0xbe, 0x54, 0x32, 0x9d, 0xb4, 0x17, 0x47, 0x6d, 0x8e, 0x96, 0x3d, - 0x00, 0x79, 0xc5, 0x4b, 0x60, 0xdd, 0x63, 0xe5, 0xbb, 0x96, 0x56, 0x99, 0xf9, 0x88, 0xaa, 0x2a, 0x2a, 0x24, 0x9a, - 0xe0, 0x22, 0xb3, 0x26, 0xb6, 0x71, 0x5a, 0x1c, 0x06, 0x90, 0x42, 0x63, 0x58, 0x31, 0xe4, 0xa1, 0xf9, 0xb1, 0x98, - 0x17, 0xe1, 0x89, 0x2c, 0xba, 0x2e, 0x28, 0xe5, 0x67, 0x48, 0x69, 0xa6, 0x8c, 0x6c, 0x8b, 0x65, 0x38, 0x9c, 0x01, - 0xc0, 0x5c, 0xeb, 0x88, 0xa8, 0x54, 0x98, 0xec, 0x98, 0x54, 0xc6, 0x5e, 0xc9, 0x9f, 0xa5, 0x84, 0x88, 0x08, 0xab, - 0xb3, 0x6d, 0x03, 0x62, 0x77, 0x0e, 0xea, 0x11, 0x9e, 0x2d, 0x33, 0xd8, 0xc4, 0xb2, 0x08, 0xce, 0x8a, 0xaa, 0xaa, - 0x46, 0xa3, 0xf9, 0xb0, 0x37, 0x34, 0xde, 0x64, 0xee, 0x0c, 0x85, 0xb0, 0x75, 0x3b, 0x42, 0x50, 0x6e, 0x3c, 0xdb, - 0x86, 0xd6, 0x86, 0x0f, 0x33, 0x37, 0xf5, 0x41, 0x5e, 0x2e, 0xa2, 0xaa, 0xd1, 0x66, 0xcf, 0x53, 0xca, 0xd3, 0x68, - 0xef, 0xa9, 0x3b, 0x29, 0x09, 0x95, 0xdd, 0x24, 0x2a, 0x0a, 0x6c, 0xe1, 0x59, 0x12, 0x12, 0x40, 0x71, 0xfb, 0xeb, - 0x50, 0x83, 0xfc, 0x8b, 0x91, 0x83, 0x03, 0x5f, 0xd5, 0x81, 0xb5, 0x2b, 0x20, 0xcd, 0xcf, 0x64, 0xd2, 0x5b, 0x7c, - 0xef, 0x6e, 0xe8, 0xc3, 0x47, 0x91, 0x22, 0xdc, 0x47, 0x68, 0x1d, 0x27, 0x21, 0x6e, 0x01, 0xf7, 0x9b, 0x6d, 0x2f, - 0x3e, 0x6a, 0x3a, 0x41, 0x66, 0x68, 0x5c, 0x1b, 0x2f, 0x9b, 0x56, 0xa4, 0x16, 0x28, 0x54, 0x20, 0x1f, 0xe9, 0x7e, - 0x2b, 0x01, 0x29, 0xc7, 0xa9, 0xdc, 0x76, 0xaf, 0x93, 0xf2, 0x1c, 0xee, 0x86, 0x8a, 0xe8, 0x75, 0x3d, 0xda, 0x12, - 0xd0, 0x62, 0x4e, 0x06, 0x7e, 0x3a, 0x5a, 0x45, 0xbe, 0x9c, 0x38, 0xc5, 0x39, 0x55, 0x2d, 0xd6, 0x3c, 0xf8, 0x78, - 0x97, 0x07, 0xc2, 0xfe, 0x92, 0x8c, 0x93, 0x31, 0x00, 0x59, 0xc5, 0x02, 0x73, 0xe0, 0x40, 0xed, 0x36, 0x27, 0x6a, - 0x03, 0x0e, 0xd4, 0xea, 0xce, 0x9e, 0xa6, 0x32, 0xd9, 0xa7, 0x94, 0xe3, 0x57, 0xf8, 0x82, 0xa5, 0xac, 0x46, 0xda, - 0x8a, 0x6a, 0x79, 0xae, 0xa5, 0x05, 0x50, 0x4b, 0x65, 0xa9, 0x6c, 0x29, 0xa4, 0x18, 0xdf, 0xec, 0xf1, 0x31, 0x12, - 0xba, 0x52, 0x3c, 0x4f, 0x2e, 0x03, 0xed, 0xf2, 0x37, 0x9e, 0xc2, 0x74, 0xc6, 0x04, 0xa6, 0xa9, 0x89, 0x2b, 0xb2, - 0x79, 0xff, 0xfe, 0xda, 0xa9, 0xb6, 0xec, 0x48, 0xaa, 0x40, 0xda, 0x2c, 0x76, 0x74, 0x69, 0x4c, 0x81, 0xe0, 0xaa, - 0xa1, 0xdb, 0xe2, 0x68, 0x77, 0xfe, 0xe1, 0xce, 0x2c, 0xa4, 0x6b, 0xa2, 0x1c, 0x49, 0xe4, 0xc7, 0xa5, 0x10, 0x50, - 0xa3, 0xbe, 0x10, 0x4e, 0xa4, 0xfe, 0xc8, 0x90, 0x4b, 0x17, 0xb3, 0x43, 0x6b, 0x54, 0xd7, 0x2d, 0x00, 0x2d, 0x7d, - 0x0f, 0x23, 0x43, 0x21, 0x6c, 0xc4, 0xb0, 0xcf, 0x53, 0xea, 0xe3, 0xca, 0x49, 0x17, 0x5d, 0x62, 0x29, 0x64, 0xde, - 0xc7, 0x24, 0x6f, 0x92, 0x46, 0x89, 0xc8, 0xeb, 0x2c, 0xe5, 0xa4, 0x2e, 0x4e, 0xe2, 0x28, 0x6f, 0x29, 0x64, 0x20, - 0xd5, 0x4d, 0x2a, 0xdd, 0x96, 0x4b, 0x25, 0xf4, 0x58, 0xf6, 0xb7, 0xe4, 0x06, 0xaf, 0xfb, 0x72, 0x1c, 0xfc, 0x31, - 0xf2, 0xcf, 0x13, 0x5b, 0x2c, 0x45, 0x07, 0xd0, 0x83, 0x20, 0xa5, 0x35, 0x40, 0xc2, 0xcf, 0xeb, 0x5b, 0xdf, 0x09, - 0xbe, 0x76, 0x04, 0xb4, 0x42, 0xb0, 0x72, 0xbd, 0x0a, 0x35, 0xdd, 0x5e, 0x36, 0x56, 0x65, 0x54, 0x75, 0xb0, 0x83, - 0x68, 0x89, 0x24, 0x04, 0xf8, 0x9c, 0xbc, 0x43, 0xea, 0x87, 0x9a, 0x74, 0xeb, 0x4b, 0xbe, 0x88, 0xd6, 0xb5, 0x92, - 0x67, 0x04, 0x57, 0xdf, 0xa8, 0xc9, 0xc2, 0xad, 0xe3, 0x27, 0x51, 0xd7, 0x4e, 0xd5, 0x15, 0x31, 0x07, 0x04, 0x98, - 0xaa, 0x86, 0x11, 0x75, 0x9f, 0x24, 0xc9, 0xbf, 0xc4, 0x54, 0x80, 0x0a, 0x96, 0x49, 0xbd, 0xfa, 0xbf, 0x6f, 0xb5, - 0xee, 0x7f, 0xbc, 0xc1, 0xba, 0x9a, 0xe7, 0xb7, 0x77, 0x7a, 0x00, 0x30, 0x80, 0x1f, 0x83, 0xaa, 0x0e, 0x5e, 0x6e, - 0xc7, 0x0b, 0xbb, 0x32, 0x05, 0xa9, 0x09, 0xf8, 0xac, 0x92, 0xfe, 0xcf, 0x91, 0x06, 0x82, 0xe6, 0x6b, 0x64, 0x6d, - 0x6c, 0x46, 0x24, 0x72, 0xdf, 0x65, 0x83, 0x8f, 0x57, 0xe1, 0xd9, 0x11, 0xf8, 0x65, 0x6c, 0x9d, 0xd3, 0x31, 0xcb, - 0x07, 0x09, 0x2c, 0x17, 0x6a, 0xbf, 0x7a, 0xcc, 0xf9, 0x44, 0x88, 0x53, 0x54, 0xa8, 0x27, 0xa8, 0x08, 0x32, 0x81, - 0x62, 0x91, 0x96, 0xa8, 0xe3, 0x2a, 0xce, 0x11, 0x16, 0x10, 0x5a, 0xa7, 0x44, 0x44, 0xbc, 0x1d, 0xd0, 0x11, 0xbc, - 0xad, 0x21, 0x27, 0xee, 0x38, 0x37, 0x6b, 0x1b, 0x98, 0xcb, 0x20, 0xd5, 0xa0, 0xe9, 0xee, 0x0b, 0x6c, 0xc0, 0x43, - 0x9c, 0x37, 0x8e, 0x4f, 0xe2, 0x72, 0x8b, 0x2c, 0x72, 0x0e, 0x45, 0x5d, 0x5e, 0xd4, 0xc8, 0xc4, 0x24, 0xa1, 0x0e, - 0x4f, 0x21, 0xa4, 0xdb, 0x17, 0x30, 0x98, 0x16, 0x4c, 0xe3, 0xac, 0x4e, 0x12, 0xc0, 0x2d, 0x9f, 0xde, 0x0f, 0xc3, - 0x97, 0x1e, 0x6a, 0x07, 0xd1, 0xb9, 0x88, 0xf8, 0x4d, 0xdb, 0xd4, 0x28, 0x4c, 0x1e, 0xae, 0xad, 0xef, 0xa9, 0xe1, - 0x23, 0x24, 0xe1, 0x5f, 0xc3, 0xa2, 0x08, 0x48, 0xdc, 0xa6, 0xb7, 0x5c, 0x30, 0xe9, 0x9d, 0x66, 0x21, 0xb4, 0xd9, - 0x0c, 0x52, 0xa5, 0x6a, 0x3e, 0xc0, 0xca, 0xb4, 0xd3, 0xff, 0xe4, 0xe4, 0xb6, 0x24, 0x05, 0x41, 0xb4, 0xd2, 0xef, - 0x4c, 0x99, 0xb0, 0xc6, 0x98, 0x40, 0xde, 0x15, 0x25, 0x9c, 0x67, 0xd0, 0x49, 0x2c, 0x00, 0x3b, 0x5a, 0x7f, 0x2f, - 0xff, 0x0e, 0x8b, 0xd1, 0xa9, 0xd1, 0x9b, 0x4e, 0x92, 0xa9, 0xd6, 0x7f, 0x7b, 0x00, 0x7f, 0x9c, 0x81, 0xb5, 0x3e, - 0x77, 0x81, 0xb5, 0xdb, 0x4d, 0x12, 0x52, 0xba, 0x25, 0xaf, 0xaa, 0xaf, 0x62, 0xdd, 0xa4, 0x54, 0xee, 0x67, 0xbf, - 0xbf, 0xbd, 0xd8, 0x32, 0x82, 0xc3, 0x3a, 0xa7, 0x18, 0x58, 0x80, 0x0d, 0x73, 0x19, 0x6e, 0x56, 0x3b, 0x81, 0xa0, - 0xa4, 0x97, 0xe4, 0xc3, 0x36, 0x43, 0xb2, 0xe3, 0x53, 0x2d, 0x91, 0xd0, 0x33, 0x9e, 0xf6, 0x35, 0x80, 0xc0, 0xbb, - 0x73, 0x93, 0xd2, 0x7a, 0xf3, 0x19, 0x79, 0xaf, 0x4a, 0x14, 0x91, 0x61, 0x4a, 0x8c, 0x67, 0x4e, 0x08, 0xd2, 0x7e, - 0xcd, 0x60, 0x6b, 0xbf, 0x19, 0x3c, 0x8d, 0x9b, 0x87, 0xe6, 0x88, 0x20, 0x72, 0x31, 0x7d, 0x90, 0xc2, 0x9f, 0x25, - 0xe3, 0xf2, 0x85, 0xcf, 0x6c, 0xc3, 0x75, 0x5a, 0x49, 0x80, 0x73, 0x8a, 0xbb, 0x79, 0xca, 0x33, 0xb1, 0x58, 0x9e, - 0xbc, 0x7c, 0xb5, 0xac, 0xd2, 0x14, 0x38, 0x83, 0x0f, 0x71, 0x19, 0xa9, 0x34, 0xc8, 0x94, 0x14, 0x3f, 0x9e, 0x27, - 0x93, 0x6e, 0x6f, 0x6a, 0x95, 0xb0, 0xab, 0x06, 0x87, 0xea, 0x13, 0x2a, 0x4b, 0x4f, 0xdb, 0x52, 0x0b, 0xcc, 0x63, - 0x1f, 0x04, 0x98, 0x7c, 0x93, 0x7d, 0xcc, 0xda, 0x11, 0x74, 0x0f, 0x4a, 0xe5, 0x32, 0x1e, 0x06, 0xb8, 0xa9, 0x82, - 0x00, 0xd2, 0xc7, 0x7a, 0x0a, 0x73, 0x79, 0x8f, 0x89, 0x0e, 0x8b, 0x1e, 0x46, 0xa0, 0x2e, 0xd4, 0xc2, 0x08, 0xcc, - 0x90, 0x07, 0x53, 0xb1, 0x74, 0xe2, 0x4f, 0x37, 0xd8, 0xfa, 0xdc, 0x8f, 0x73, 0x4d, 0xbb, 0x63, 0x96, 0x06, 0x48, - 0xaa, 0xa3, 0xee, 0x37, 0xfa, 0x46, 0x3d, 0x9e, 0x75, 0x8b, 0xf7, 0x69, 0x33, 0xa6, 0xbd, 0xa9, 0x3c, 0x1e, 0x55, - 0xdb, 0xef, 0xdf, 0x16, 0x97, 0xa8, 0x96, 0x92, 0xa5, 0x3d, 0xc8, 0xbe, 0x3b, 0xa3, 0x00, 0xb7, 0xef, 0x78, 0x13, - 0x1e, 0x20, 0xcf, 0x91, 0x4e, 0xcc, 0x6d, 0xd8, 0x53, 0x9d, 0x03, 0xed, 0xfc, 0xe7, 0x47, 0xa9, 0xb0, 0xf3, 0xf7, - 0xa7, 0x61, 0xe9, 0x3d, 0x49, 0x18, 0x05, 0x8e, 0xd2, 0xef, 0xde, 0x77, 0x59, 0xad, 0xf4, 0x71, 0x81, 0xcb, 0x9d, - 0xd7, 0x56, 0x9c, 0x78, 0xb1, 0x61, 0x3d, 0x25, 0x8f, 0x63, 0x14, 0x63, 0x6f, 0x7a, 0xb2, 0xee, 0x8c, 0xdd, 0x3d, - 0x85, 0xb5, 0x67, 0x3d, 0x25, 0x48, 0xb7, 0x92, 0x1f, 0xf7, 0xfe, 0x23, 0xb6, 0x53, 0x25, 0xdd, 0xf4, 0x07, 0xdb, - 0x2f, 0x3f, 0x3a, 0x3b, 0x88, 0x07, 0xad, 0xb3, 0x32, 0x9d, 0xa5, 0xeb, 0x2a, 0xe9, 0x72, 0xb8, 0x40, 0xde, 0xcd, - 0xc4, 0x73, 0x61, 0xae, 0xbf, 0xce, 0x36, 0x42, 0x05, 0xf6, 0xe5, 0x6a, 0xbc, 0xbd, 0x47, 0xcc, 0xe7, 0x72, 0x2e, - 0xfb, 0xde, 0xa2, 0x29, 0x04, 0x7d, 0x8b, 0x91, 0x72, 0xc1, 0x38, 0x4e, 0x90, 0x0f, 0xb8, 0x34, 0xde, 0x07, 0xb4, - 0x18, 0xa3, 0x5b, 0xf8, 0x79, 0x0c, 0xdb, 0x03, 0x6c, 0xcd, 0xfc, 0x73, 0xc2, 0x63, 0x5e, 0x88, 0x30, 0x4d, 0x1e, - 0x50, 0x53, 0xb2, 0x81, 0x0f, 0x36, 0x9c, 0x9f, 0x15, 0x12, 0xc3, 0x00, 0xcb, 0x23, 0x4f, 0xa7, 0x8d, 0xec, 0x69, - 0xa8, 0x2e, 0xcf, 0x73, 0xb5, 0xde, 0x82, 0x9e, 0x30, 0x9d, 0xe5, 0x65, 0x1a, 0xee, 0xd2, 0x3b, 0x13, 0xec, 0x70, - 0xb9, 0x6b, 0x38, 0x69, 0x99, 0x22, 0x55, 0x8e, 0xf3, 0xc6, 0x71, 0x9a, 0x33, 0x06, 0xe2, 0x29, 0xe6, 0xf5, 0xeb, - 0x54, 0x60, 0xd1, 0x62, 0x5c, 0xbe, 0x40, 0x69, 0x60, 0xea, 0x2f, 0x36, 0x32, 0x53, 0xa1, 0x75, 0x00, 0x31, 0x59, - 0x82, 0x3f, 0x1b, 0xa4, 0xb4, 0xa0, 0x10, 0x8d, 0x0a, 0xb7, 0xd5, 0x3f, 0xdc, 0x15, 0xb5, 0x4a, 0x13, 0xd1, 0xee, - 0x5d, 0xf1, 0xce, 0x10, 0xdb, 0xc5, 0x5b, 0x4e, 0x07, 0x50, 0x8c, 0x1a, 0x1d, 0xd0, 0xa4, 0x60, 0x7b, 0xb4, 0xfe, - 0x26, 0x29, 0xe5, 0x79, 0x66, 0x44, 0xf6, 0x58, 0xc0, 0xfa, 0x6e, 0x70, 0x18, 0x5b, 0x5b, 0x55, 0x58, 0xef, 0x9b, - 0x36, 0xc1, 0x1c, 0x80, 0xfd, 0x96, 0x44, 0xf1, 0xde, 0x27, 0x7f, 0x49, 0x8c, 0xd1, 0xf5, 0x3d, 0x17, 0x59, 0x7a, - 0x63, 0x28, 0x26, 0x48, 0xae, 0x69, 0x56, 0xc9, 0xe2, 0x18, 0x8d, 0x46, 0x41, 0xc9, 0x39, 0x31, 0x8e, 0x50, 0x36, - 0x40, 0x3d, 0x4d, 0x49, 0xe9, 0x02, 0x40, 0x66, 0x58, 0x4d, 0x0f, 0x0a, 0x60, 0x19, 0x8d, 0xb4, 0x40, 0xe5, 0x59, - 0x74, 0x14, 0xee, 0x79, 0x72, 0x9a, 0x6b, 0x66, 0xd5, 0xe0, 0xf0, 0x96, 0x07, 0x8a, 0xca, 0x79, 0x7a, 0x36, 0x99, - 0x8f, 0xe8, 0x20, 0xd2, 0x6b, 0x1a, 0xff, 0xb6, 0xaf, 0x44, 0x74, 0x70, 0x1b, 0x09, 0x16, 0x9c, 0xfd, 0x90, 0x93, - 0x21, 0x72, 0x7f, 0xb5, 0xce, 0xf4, 0x83, 0x0a, 0xfd, 0x6e, 0x35, 0x84, 0x48, 0xf9, 0x4a, 0xd8, 0xd8, 0x94, 0x3b, - 0x35, 0xe8, 0xbc, 0xd3, 0x30, 0x91, 0xa1, 0x70, 0x7c, 0xcf, 0x6d, 0xe9, 0x13, 0x6d, 0x56, 0x37, 0xce, 0xfc, 0xe3, - 0x01, 0x3f, 0x59, 0x9a, 0x22, 0x68, 0x4d, 0xa5, 0x4e, 0x07, 0xe8, 0x96, 0x48, 0x83, 0xa3, 0x7c, 0x60, 0x5e, 0x78, - 0xd0, 0x52, 0xbb, 0xa1, 0x44, 0x57, 0x74, 0x14, 0xee, 0x91, 0xe7, 0x02, 0x1a, 0x67, 0x0a, 0xba, 0x1e, 0x71, 0x54, - 0x3b, 0x63, 0x28, 0xc5, 0x1c, 0x0c, 0xe6, 0x50, 0xd6, 0x64, 0x8d, 0x7d, 0x6c, 0xbd, 0x44, 0x77, 0xe3, 0x19, 0x22, - 0xd7, 0x13, 0x1a, 0x87, 0xa4, 0xeb, 0x99, 0x91, 0xc2, 0x30, 0x84, 0x77, 0x40, 0xf2, 0xee, 0xc4, 0xfa, 0x5c, 0xa9, - 0xa4, 0x1d, 0x69, 0x63, 0x60, 0x17, 0xcf, 0x62, 0xb6, 0xb0, 0x22, 0x3b, 0x71, 0x6d, 0xbd, 0xb6, 0x76, 0xfd, 0x15, - 0xa2, 0xc2, 0x78, 0x6c, 0xeb, 0x70, 0x9e, 0x33, 0x72, 0xc5, 0x0d, 0x13, 0x3f, 0xcb, 0xae, 0xcf, 0x3c, 0x95, 0xf4, - 0x5f, 0x84, 0xd6, 0x4f, 0x5d, 0x71, 0x81, 0x75, 0xb7, 0x4d, 0xec, 0xfa, 0x75, 0x4a, 0x6a, 0x5d, 0xb9, 0x0b, 0xfd, - 0x8f, 0xad, 0xed, 0x58, 0x6d, 0xe6, 0x69, 0xde, 0x7f, 0xe8, 0x44, 0x1d, 0xe4, 0x9f, 0x7e, 0xb5, 0x1b, 0xb9, 0x91, - 0x16, 0x2f, 0xc9, 0xc7, 0xbb, 0x9e, 0xe6, 0x0b, 0x1e, 0xfb, 0xf3, 0x66, 0xd8, 0xf3, 0x32, 0xbf, 0x16, 0xec, 0xcf, - 0xd3, 0xd9, 0x67, 0x8e, 0x1e, 0xdf, 0x1f, 0xd2, 0xc4, 0xa3, 0xe9, 0xf3, 0xe4, 0xcf, 0xe4, 0x5c, 0x24, 0x8f, 0xc9, - 0x5e, 0xf5, 0xb6, 0xed, 0x22, 0xa5, 0x11, 0xa0, 0x8e, 0xde, 0xac, 0xdf, 0x85, 0x74, 0x4d, 0x32, 0x15, 0x0f, 0xca, - 0xa7, 0x7c, 0x20, 0xbe, 0xae, 0xbf, 0x4c, 0xf7, 0x50, 0x88, 0x78, 0x11, 0xf8, 0xef, 0xf9, 0xfe, 0x23, 0xb6, 0x59, - 0x57, 0x5a, 0x9d, 0xcd, 0x8d, 0x1e, 0xba, 0x7d, 0x75, 0x32, 0xf5, 0x89, 0x94, 0x51, 0x7a, 0x5d, 0x88, 0x97, 0x18, - 0x17, 0x37, 0xf9, 0x21, 0xdb, 0x7e, 0x18, 0x9f, 0xd7, 0xf8, 0x81, 0x08, 0x8a, 0xb0, 0x8f, 0x19, 0x32, 0x3e, 0x40, - 0x4d, 0xe5, 0x54, 0xb0, 0x62, 0x5a, 0xa9, 0x4a, 0x03, 0xa0, 0x69, 0xf4, 0x4b, 0x94, 0x7f, 0xa6, 0x07, 0xf2, 0xc3, - 0x1f, 0xbd, 0x75, 0xde, 0x3f, 0xa7, 0xff, 0xbe, 0xff, 0xfc, 0xa3, 0x06, 0x26, 0x05, 0x64, 0xdd, 0x87, 0x95, 0x6d, - 0x12, 0x8e, 0xca, 0xc6, 0x55, 0x56, 0x13, 0x75, 0x07, 0x99, 0x5e, 0xcd, 0x6c, 0xf7, 0xcd, 0x5b, 0xf6, 0xa1, 0x17, - 0xd1, 0x4c, 0xc9, 0xa3, 0x52, 0xe4, 0x1d, 0x72, 0x71, 0xf5, 0x39, 0x7c, 0x19, 0xeb, 0xaa, 0x90, 0x5f, 0xa9, 0x8a, - 0xe7, 0xa5, 0x0f, 0x82, 0xa8, 0x73, 0x72, 0x0c, 0x12, 0xc7, 0x91, 0x07, 0x14, 0xd8, 0x9f, 0xeb, 0x39, 0x74, 0xcf, - 0xeb, 0xcb, 0x09, 0x3c, 0x0d, 0x97, 0xb0, 0x5d, 0xef, 0xbc, 0x4b, 0x1f, 0x6a, 0x32, 0x4a, 0xb0, 0x8d, 0x74, 0x73, - 0xe8, 0xa0, 0x51, 0x3b, 0x7a, 0xe4, 0xe3, 0x9e, 0xf1, 0xd1, 0x05, 0x8a, 0xbe, 0xc7, 0xb9, 0xd1, 0x33, 0x57, 0x0e, - 0xfa, 0x5c, 0xae, 0xbb, 0xa6, 0xbd, 0xaa, 0x13, 0xa3, 0x63, 0x52, 0x79, 0x29, 0x0a, 0x20, 0x49, 0xaa, 0xa7, 0x2d, - 0x52, 0xfb, 0xa9, 0x9c, 0x0d, 0x6c, 0x9e, 0xe1, 0x5e, 0x3c, 0x13, 0x4a, 0x42, 0x37, 0xfc, 0xc5, 0xb9, 0xa6, 0x7d, - 0x61, 0x99, 0xaa, 0x30, 0xb8, 0x61, 0x35, 0x2d, 0x21, 0xe8, 0x35, 0x08, 0x36, 0x0d, 0xee, 0x3f, 0x8e, 0x20, 0xd8, - 0x04, 0x5a, 0x3b, 0x83, 0x94, 0x81, 0x33, 0x36, 0xe2, 0x1f, 0xae, 0x68, 0x10, 0xc9, 0xcd, 0x03, 0x4f, 0xe2, 0xe5, - 0xb0, 0x24, 0x52, 0xde, 0x40, 0x28, 0x08, 0x7a, 0x2a, 0xb8, 0x08, 0x52, 0xd0, 0x98, 0xf6, 0x98, 0x1d, 0xa8, 0x36, - 0x38, 0x6e, 0x80, 0xcb, 0x57, 0x49, 0xd9, 0xa4, 0xda, 0xd4, 0x65, 0xaa, 0x62, 0xc7, 0xe0, 0x91, 0x97, 0xd6, 0x41, - 0x7a, 0x81, 0x22, 0x68, 0x8a, 0x52, 0xa4, 0x57, 0x35, 0x1d, 0x85, 0xb6, 0xa8, 0x36, 0x18, 0x3d, 0xa8, 0x18, 0x28, - 0xa9, 0xb0, 0xd8, 0xc8, 0x46, 0xd1, 0x9f, 0x19, 0x22, 0x8c, 0xc2, 0x0f, 0xed, 0xca, 0xc8, 0x87, 0x8f, 0x6a, 0x98, - 0xbd, 0x9b, 0x44, 0xb1, 0xc8, 0x4b, 0x7d, 0x5e, 0xf3, 0x88, 0x9a, 0x9d, 0x26, 0xf9, 0xfc, 0x66, 0x35, 0x70, 0x8a, - 0x49, 0xc9, 0x4e, 0x78, 0xb7, 0x4a, 0x4c, 0x82, 0x88, 0xad, 0xdf, 0x3e, 0xf5, 0xbc, 0x1b, 0xb8, 0xb4, 0xf7, 0x23, - 0x61, 0x7b, 0x59, 0xf2, 0xe8, 0xf0, 0xb2, 0xa8, 0xb9, 0xf9, 0xc6, 0x9c, 0xea, 0x2a, 0xd5, 0x1b, 0x02, 0x7e, 0x95, - 0x8e, 0x5e, 0x94, 0x09, 0x0a, 0xa7, 0x36, 0xdd, 0x07, 0x93, 0x11, 0xd0, 0xd1, 0xb3, 0x1a, 0xcd, 0xf2, 0xf4, 0xd5, - 0x32, 0xb1, 0xc3, 0xc6, 0xe8, 0x23, 0x0a, 0xbc, 0x6c, 0x55, 0x06, 0x47, 0x1a, 0x55, 0xca, 0xcc, 0x0b, 0xa2, 0xea, - 0x44, 0xad, 0x60, 0x2f, 0x35, 0xf8, 0x0f, 0x08, 0xd3, 0x25, 0x0f, 0x9c, 0x1a, 0x80, 0x1c, 0xb3, 0x88, 0x74, 0x54, - 0x80, 0xdf, 0x3e, 0x4d, 0xcf, 0x98, 0x6b, 0xb8, 0xcb, 0x1a, 0x44, 0x11, 0x6d, 0x1f, 0xb1, 0x44, 0xd2, 0x1d, 0x2e, - 0x8c, 0x29, 0x42, 0xb8, 0x39, 0x2a, 0x04, 0x01, 0xac, 0x30, 0xf8, 0x12, 0xe3, 0x80, 0xb4, 0xa8, 0x7b, 0x14, 0x5e, - 0xb6, 0x0a, 0xbe, 0xcb, 0x05, 0xc7, 0x58, 0xd9, 0xbb, 0x90, 0x58, 0x17, 0xa2, 0x41, 0xb7, 0xfc, 0x7b, 0x84, 0xfc, - 0x6a, 0x68, 0x66, 0xb5, 0xf9, 0x0a, 0xee, 0x5b, 0xaf, 0x9d, 0x4d, 0x26, 0x30, 0xbb, 0x12, 0x55, 0x21, 0x8b, 0x90, - 0xb2, 0x17, 0x22, 0xd3, 0xb4, 0x95, 0x28, 0x39, 0x47, 0x40, 0x12, 0xd8, 0x02, 0x01, 0x36, 0xf8, 0xa1, 0x5a, 0x96, - 0x43, 0x09, 0x55, 0x0d, 0x8c, 0x90, 0xef, 0xc5, 0x02, 0x88, 0x5a, 0x56, 0xbd, 0xa2, 0x0c, 0xec, 0x68, 0xd9, 0xeb, - 0xac, 0x67, 0x40, 0xc9, 0x7e, 0x83, 0x40, 0x78, 0x1b, 0x9e, 0xbe, 0xff, 0x26, 0xe4, 0xd1, 0x99, 0x63, 0x4d, 0x58, - 0x78, 0x44, 0x6e, 0x1c, 0x60, 0xe5, 0x73, 0x5b, 0x82, 0x90, 0x05, 0xa5, 0xdf, 0x95, 0x2b, 0x7b, 0xd4, 0x67, 0xa6, - 0x46, 0x15, 0x82, 0xbc, 0xb9, 0xec, 0x03, 0x69, 0xa9, 0x03, 0xed, 0x1f, 0x90, 0x81, 0xc1, 0x09, 0xdc, 0x3b, 0x55, - 0x84, 0xb2, 0xc7, 0x18, 0xfe, 0xdc, 0xa6, 0xa6, 0x4d, 0xdc, 0xf3, 0x33, 0x98, 0x14, 0x03, 0x92, 0x95, 0x92, 0x7b, - 0x9e, 0xff, 0xae, 0x86, 0x2a, 0x48, 0x28, 0x4c, 0x4b, 0xf0, 0x24, 0x2b, 0x23, 0x84, 0xc8, 0x44, 0xc7, 0x41, 0xe7, - 0x40, 0xbc, 0xba, 0x37, 0x30, 0x9f, 0xd9, 0x31, 0x4b, 0x7e, 0xf7, 0x68, 0xb9, 0x4e, 0xc4, 0xb2, 0x86, 0x1f, 0x46, - 0xb3, 0x1b, 0xfb, 0x89, 0x70, 0xdd, 0xc2, 0x1a, 0x97, 0x06, 0xcf, 0xd0, 0xad, 0xf6, 0xf8, 0x4d, 0xce, 0x50, 0x4c, - 0xdb, 0x74, 0xac, 0x0e, 0xaf, 0xaf, 0xd5, 0xac, 0xb2, 0x85, 0x6a, 0xb7, 0x9c, 0x5f, 0xab, 0x6a, 0xcd, 0xd6, 0x6e, - 0xa1, 0x95, 0xd5, 0xe7, 0x3f, 0x8b, 0xf9, 0x9c, 0xc2, 0x62, 0x7e, 0x30, 0x80, 0xbb, 0x28, 0xe2, 0xc5, 0x89, 0xbb, - 0xe6, 0xda, 0xfe, 0xa0, 0xf6, 0xca, 0xe5, 0xe3, 0x6b, 0x8f, 0xfb, 0xef, 0x22, 0x46, 0xbd, 0xb0, 0x8f, 0x02, 0xb8, - 0x56, 0x23, 0x1e, 0x0f, 0x1f, 0x5d, 0xcc, 0xab, 0x35, 0xf5, 0x49, 0x1d, 0x79, 0xcf, 0x5d, 0xef, 0x5b, 0x5a, 0xb2, - 0x38, 0xad, 0x3c, 0xcd, 0x3e, 0x10, 0x23, 0xb3, 0x81, 0xd6, 0x9b, 0x34, 0x43, 0x86, 0x3b, 0x12, 0x7c, 0xb2, 0x52, - 0xf4, 0xe2, 0x64, 0xf7, 0xd4, 0x20, 0x52, 0xb2, 0xd1, 0xcc, 0x42, 0xa0, 0x96, 0x97, 0x21, 0xd3, 0x74, 0x2c, 0x0a, - 0x51, 0x0e, 0x2c, 0x28, 0x0f, 0x9a, 0x30, 0xc5, 0x93, 0x70, 0x1a, 0x47, 0x92, 0x62, 0x35, 0x0d, 0xb9, 0xcd, 0x49, - 0x89, 0x1a, 0xd2, 0xd5, 0xb9, 0xc1, 0x03, 0xad, 0x16, 0x98, 0xc0, 0xa1, 0x24, 0x05, 0x98, 0x6b, 0xa4, 0x67, 0x88, - 0x28, 0x04, 0x03, 0xf4, 0xfa, 0x96, 0x9d, 0x87, 0xce, 0xbb, 0x93, 0x72, 0x59, 0x53, 0x10, 0x6f, 0x3e, 0xf6, 0xa1, - 0x65, 0xa6, 0x75, 0x27, 0x37, 0x54, 0xf2, 0x7c, 0x09, 0xb5, 0x34, 0x81, 0xfb, 0x84, 0x8b, 0x6a, 0x26, 0x54, 0x21, - 0xff, 0x26, 0xf7, 0xfd, 0x62, 0xef, 0xc2, 0xbc, 0xba, 0x7d, 0x80, 0xcf, 0x8f, 0x97, 0x2a, 0x47, 0xe1, 0x93, 0x91, - 0xdc, 0x6a, 0x25, 0x9f, 0x67, 0x10, 0x32, 0xf3, 0x85, 0x9b, 0xbb, 0x1f, 0xb5, 0xe9, 0xc3, 0x26, 0x7f, 0xd6, 0x81, - 0xe5, 0xb6, 0x79, 0x25, 0x26, 0x7f, 0xac, 0x76, 0x2c, 0xda, 0x7b, 0x77, 0x85, 0x3e, 0x8a, 0xf5, 0xe9, 0x84, 0xbb, - 0x7f, 0xa8, 0x7c, 0x5e, 0x36, 0xfa, 0x48, 0x5d, 0xae, 0x8f, 0x7f, 0x85, 0xee, 0x8b, 0x84, 0x6a, 0x58, 0xe3, 0xbd, - 0x4c, 0xb9, 0x30, 0x7b, 0x81, 0x8d, 0xb9, 0x3a, 0xed, 0x66, 0x52, 0x82, 0x6e, 0xdb, 0xfb, 0x8f, 0xfc, 0x08, 0x67, - 0x11, 0x05, 0x37, 0xf9, 0x9d, 0x19, 0xcb, 0xa1, 0x3f, 0xdf, 0x99, 0x41, 0x2f, 0x1f, 0xc2, 0xde, 0xc5, 0xbb, 0xf4, - 0x01, 0xb9, 0x1e, 0xa7, 0x4a, 0x3e, 0xfb, 0xb1, 0xfe, 0x56, 0xc9, 0x3f, 0x8b, 0x84, 0x79, 0x6b, 0x62, 0x85, 0xb9, - 0x31, 0x49, 0x7e, 0xfb, 0xeb, 0xb6, 0xa5, 0x9d, 0xc9, 0xed, 0x62, 0xd3, 0xba, 0x85, 0x67, 0x42, 0x36, 0x81, 0x89, - 0xd9, 0x2f, 0x52, 0xd0, 0x95, 0x32, 0x35, 0x6e, 0x92, 0xd2, 0xca, 0xb3, 0xce, 0xdb, 0x77, 0xcc, 0x1c, 0xd7, 0xa7, - 0x2a, 0x0b, 0x72, 0xab, 0x28, 0xe4, 0x14, 0xda, 0x63, 0xe4, 0x12, 0x3a, 0x4c, 0x97, 0xdc, 0x45, 0xcc, 0x65, 0x11, - 0xd3, 0x7b, 0x79, 0xee, 0x93, 0x10, 0xd3, 0x66, 0x3b, 0xe5, 0x95, 0xfc, 0x0f, 0xb9, 0x79, 0x90, 0x55, 0x41, 0x1d, - 0x80, 0xe9, 0xfd, 0xd5, 0xfa, 0xf3, 0xd9, 0xd2, 0xe0, 0x54, 0x71, 0xf0, 0xf2, 0x53, 0x69, 0x72, 0xf3, 0xc6, 0x39, - 0xdd, 0x10, 0x95, 0x4a, 0x39, 0x16, 0x83, 0x8e, 0x91, 0xa3, 0x6a, 0x34, 0x8b, 0xf9, 0x04, 0x75, 0xed, 0x24, 0xfe, - 0x78, 0x26, 0x67, 0x35, 0x54, 0x73, 0x17, 0x7c, 0x72, 0x6c, 0x79, 0x37, 0x0d, 0xc5, 0x70, 0x7f, 0xb7, 0x55, 0x3f, - 0x67, 0x74, 0xd6, 0x4d, 0x0f, 0x05, 0x37, 0x70, 0xbe, 0xeb, 0xe1, 0x4b, 0x69, 0xdd, 0xaa, 0x59, 0x2a, 0x51, 0x10, - 0xaa, 0xa4, 0xb9, 0x7a, 0xc3, 0xd4, 0x40, 0x1f, 0x6a, 0xfe, 0x8e, 0x32, 0x98, 0xe2, 0x12, 0x00, 0x35, 0xc9, 0xe1, - 0xdb, 0xd4, 0x42, 0xc9, 0x48, 0x6f, 0x05, 0xe6, 0x18, 0xff, 0x1b, 0x48, 0x43, 0x26, 0x03, 0x6e, 0xf5, 0x35, 0xbf, - 0x99, 0xe4, 0xdf, 0x74, 0xdf, 0x07, 0xe7, 0xd3, 0x38, 0x7d, 0x0d, 0x05, 0xf6, 0x41, 0x7b, 0x9f, 0xf6, 0x9c, 0x29, - 0x69, 0x7b, 0x5c, 0x6d, 0xc5, 0x57, 0xdc, 0x4d, 0x61, 0xf0, 0x4f, 0x0f, 0x84, 0x22, 0xfa, 0x6e, 0xe0, 0x50, 0xb8, - 0x1d, 0x3f, 0x31, 0x8d, 0xa8, 0x43, 0xa6, 0xaa, 0x2f, 0x49, 0x3e, 0xda, 0xfc, 0x21, 0xac, 0x09, 0x8e, 0x1c, 0xe3, - 0xa6, 0x67, 0xa8, 0x88, 0xcc, 0x13, 0xaf, 0x76, 0x0f, 0x9c, 0x9a, 0x80, 0xeb, 0x79, 0x64, 0xde, 0xa7, 0xa9, 0x6d, - 0x70, 0xf1, 0x04, 0xb9, 0x73, 0x03, 0x78, 0xa7, 0x56, 0x57, 0xfb, 0x97, 0x5a, 0xef, 0x42, 0x84, 0x5b, 0x00, 0x51, - 0x8e, 0x5f, 0x64, 0x13, 0xb9, 0x7f, 0x70, 0xe6, 0x62, 0x4e, 0xc3, 0x3d, 0xd2, 0xa1, 0xe4, 0xee, 0x10, 0xb5, 0xce, - 0x2a, 0x67, 0x72, 0xa3, 0x98, 0x25, 0x93, 0x42, 0x00, 0x04, 0xa6, 0x55, 0xbe, 0x22, 0x02, 0xb8, 0x0a, 0x0b, 0x8d, - 0xa6, 0x28, 0xf2, 0x2b, 0xaa, 0xed, 0x67, 0xb4, 0x5b, 0xf6, 0xa3, 0xa3, 0x6b, 0xc7, 0x4c, 0x6a, 0x35, 0x71, 0xe9, - 0x48, 0x0a, 0x66, 0x98, 0xbc, 0xb9, 0x28, 0xe4, 0x15, 0x1f, 0xcc, 0x0f, 0x43, 0x02, 0x53, 0x69, 0x05, 0x85, 0x5c, - 0xaf, 0xb1, 0x33, 0x47, 0xa8, 0xe1, 0x34, 0x6b, 0x3c, 0x3d, 0x7d, 0x5e, 0x8a, 0xd7, 0x8e, 0x53, 0xb5, 0xcd, 0x38, - 0x1d, 0x2c, 0xc2, 0x79, 0x91, 0x76, 0x59, 0xb6, 0x12, 0x81, 0xec, 0xc7, 0xf4, 0x6f, 0xe3, 0xbc, 0xd0, 0xbf, 0x59, - 0xe7, 0x58, 0x9e, 0xc0, 0xc8, 0x12, 0x2d, 0x54, 0xc7, 0xfc, 0xa7, 0x56, 0x6c, 0xad, 0xf0, 0x9d, 0xa8, 0x24, 0xc6, - 0xab, 0x73, 0x69, 0xcf, 0x75, 0xe7, 0x87, 0x10, 0x2c, 0x0f, 0xf0, 0xb3, 0xb8, 0xca, 0xf7, 0x67, 0x85, 0x5b, 0xe9, - 0x3f, 0xeb, 0xe6, 0xef, 0x8a, 0xde, 0x93, 0x8f, 0x1f, 0x52, 0xc4, 0x34, 0x81, 0xf9, 0xae, 0x01, 0x34, 0x55, 0x48, - 0x58, 0x94, 0x2a, 0x6e, 0x43, 0x8e, 0xf7, 0xcf, 0xeb, 0x5e, 0xc4, 0xa2, 0x94, 0x23, 0xbb, 0x8e, 0x3b, 0x7e, 0x29, - 0x30, 0x03, 0x5c, 0xc0, 0xf7, 0x50, 0x4e, 0xa0, 0x1f, 0x3b, 0x8f, 0x8f, 0x44, 0x51, 0x38, 0x65, 0xbc, 0x53, 0x58, - 0xeb, 0xf0, 0x42, 0x79, 0x97, 0x6e, 0x14, 0xd3, 0xa8, 0x89, 0x9f, 0x32, 0xbe, 0xb1, 0x1a, 0x3d, 0xd6, 0x35, 0xba, - 0x1f, 0xcd, 0x8f, 0x82, 0x36, 0xb0, 0x88, 0xbd, 0xff, 0xd3, 0x21, 0xc5, 0xc4, 0xe4, 0xbc, 0x65, 0x2c, 0x70, 0x24, - 0xa4, 0xca, 0xad, 0xcc, 0xf7, 0xa9, 0x88, 0xca, 0xf4, 0x2b, 0x9c, 0xf1, 0x3b, 0x42, 0x44, 0x15, 0x16, 0xfb, 0xa7, - 0xd6, 0x3d, 0x66, 0xd2, 0xcd, 0xa6, 0x3e, 0x55, 0x20, 0x0d, 0x45, 0x9e, 0xaa, 0xe9, 0x25, 0x34, 0xb7, 0xbb, 0xcf, - 0xcf, 0x67, 0x8f, 0x0a, 0xf2, 0xf9, 0xef, 0x0f, 0xfa, 0xfe, 0xbe, 0x90, 0x07, 0xad, 0x6f, 0xe5, 0x33, 0xd4, 0xfe, - 0x75, 0x95, 0x3d, 0x35, 0xc0, 0x99, 0x22, 0x92, 0x97, 0xfc, 0x08, 0xa7, 0x6b, 0x6e, 0x96, 0xbe, 0x7a, 0xca, 0x75, - 0x3f, 0x5d, 0xce, 0x6a, 0x91, 0x1c, 0x33, 0x44, 0xd0, 0xde, 0xc8, 0xb8, 0xc7, 0xf7, 0x59, 0x22, 0x9d, 0x85, 0x09, - 0xba, 0x88, 0xea, 0xf6, 0x68, 0x43, 0xd9, 0xad, 0xe6, 0x2d, 0xf7, 0xc6, 0x1f, 0xe8, 0x7b, 0xd6, 0x8b, 0xa0, 0x34, - 0x94, 0x60, 0xc7, 0xdd, 0xa8, 0x71, 0x44, 0x3a, 0xd7, 0x1d, 0xa4, 0x91, 0x7b, 0xa1, 0x58, 0x52, 0xde, 0x77, 0xb3, - 0xa3, 0x30, 0x69, 0x81, 0x15, 0x76, 0xea, 0xf2, 0xe0, 0x6e, 0x5a, 0x98, 0x75, 0x0a, 0x85, 0xca, 0x74, 0x31, 0xf0, - 0xc5, 0xa6, 0xb4, 0xae, 0x57, 0x0e, 0xc8, 0x01, 0x8c, 0x8e, 0x83, 0x14, 0x26, 0xd5, 0x58, 0x92, 0xca, 0xd0, 0xd1, - 0x72, 0x68, 0x59, 0xaa, 0x14, 0x14, 0xbd, 0x03, 0x0c, 0xca, 0x78, 0xf6, 0xff, 0xc3, 0x9c, 0x0b, 0x83, 0x58, 0x0e, - 0xec, 0x57, 0x64, 0x5f, 0x5d, 0x77, 0xc2, 0x49, 0x54, 0x10, 0xa6, 0xba, 0x91, 0xa9, 0x47, 0x15, 0xd8, 0x84, 0x1c, - 0xd2, 0x24, 0xc9, 0xe9, 0xc0, 0x04, 0x72, 0xe4, 0x69, 0x13, 0x51, 0x17, 0x92, 0xca, 0xe5, 0x97, 0xce, 0xb7, 0x5f, - 0x71, 0xe6, 0x2f, 0x30, 0x92, 0x53, 0x4a, 0xc7, 0x3a, 0x8b, 0xf1, 0x3b, 0xed, 0x7e, 0x3a, 0x6f, 0xfb, 0x9c, 0x5f, - 0x72, 0x80, 0xde, 0x42, 0x55, 0xce, 0x10, 0x90, 0x1e, 0xfa, 0xfe, 0x7a, 0x47, 0xb5, 0xa0, 0x3b, 0x6e, 0xfa, 0xe4, - 0x33, 0xce, 0x5e, 0xad, 0x93, 0xcf, 0x4f, 0xde, 0x28, 0xc4, 0x48, 0x04, 0x5d, 0x3b, 0x52, 0x95, 0x76, 0x9f, 0x6f, - 0xe4, 0x2a, 0x5c, 0xae, 0x41, 0xb3, 0x83, 0xa2, 0x53, 0xda, 0xcf, 0x94, 0xb1, 0xae, 0x7e, 0x4a, 0x0e, 0x1b, 0xb0, - 0xd9, 0x10, 0xe3, 0x48, 0xdc, 0x78, 0xa5, 0x62, 0x80, 0x33, 0x37, 0xaa, 0x61, 0xa5, 0xb7, 0x9d, 0x93, 0x5f, 0xe2, - 0x95, 0x06, 0xcf, 0x13, 0x2c, 0xd1, 0x45, 0x9f, 0x57, 0x8f, 0xd3, 0x51, 0xe6, 0x1b, 0xea, 0xdf, 0xa0, 0xda, 0x26, - 0x5d, 0x88, 0x75, 0x9a, 0xce, 0x21, 0xcf, 0x95, 0x1f, 0xdd, 0x99, 0x20, 0x73, 0x47, 0x85, 0x6b, 0xd5, 0xa0, 0x40, - 0x53, 0xf6, 0xc9, 0x4a, 0x78, 0x74, 0xeb, 0x93, 0x4c, 0xda, 0x47, 0x6b, 0xe5, 0xc3, 0xad, 0x28, 0xfb, 0x77, 0xcf, - 0xbf, 0xf7, 0x99, 0xde, 0x22, 0x1d, 0xd7, 0xac, 0xf6, 0x2f, 0xf8, 0x29, 0xe7, 0x34, 0xde, 0x12, 0xa5, 0x44, 0xe5, - 0x87, 0xe3, 0x80, 0x58, 0xbc, 0x41, 0xfc, 0xd1, 0x0f, 0xdc, 0xec, 0x45, 0xac, 0x2f, 0x55, 0x5a, 0x54, 0x7f, 0xb2, - 0xc7, 0x55, 0x0d, 0x8e, 0x1f, 0xeb, 0xf9, 0x75, 0xac, 0xbc, 0x13, 0x0d, 0xb0, 0x56, 0x02, 0x1f, 0xdb, 0x95, 0x80, - 0x02, 0x22, 0xbd, 0x25, 0x6f, 0xcf, 0xff, 0x77, 0x8b, 0xfd, 0x7e, 0x47, 0xf7, 0xd3, 0xce, 0x8d, 0xaa, 0xd1, 0x69, - 0x53, 0x58, 0x0a, 0xdb, 0xee, 0xb3, 0xc0, 0x45, 0xc6, 0x80, 0x40, 0x35, 0xe6, 0x1f, 0x92, 0x30, 0xa7, 0xc0, 0xbb, - 0x3c, 0x38, 0x8e, 0xfc, 0x96, 0xfa, 0xc6, 0x0a, 0xf7, 0xef, 0xf6, 0xae, 0xb7, 0x20, 0x42, 0x65, 0x9f, 0xa0, 0xdc, - 0x91, 0x3f, 0xf6, 0xd3, 0x0b, 0xd4, 0x47, 0x61, 0xaf, 0x60, 0xd5, 0xd1, 0xa2, 0x6b, 0x07, 0x3a, 0x07, 0xbd, 0x1b, - 0x51, 0x51, 0xf9, 0x98, 0x8d, 0xa5, 0x66, 0x7c, 0x04, 0x30, 0x02, 0x58, 0x0c, 0x71, 0xe2, 0xda, 0x2b, 0xf7, 0x35, - 0xba, 0x02, 0x02, 0x8c, 0xe1, 0x1f, 0x36, 0x38, 0xb7, 0x5e, 0x59, 0x06, 0x9a, 0x1c, 0xe0, 0xd4, 0x26, 0x8b, 0x53, - 0x8b, 0x53, 0xbc, 0xdf, 0xf9, 0xc6, 0xe8, 0xed, 0x05, 0x39, 0x1d, 0xf0, 0x1e, 0x7b, 0x14, 0x15, 0x35, 0x64, 0xa0, - 0x85, 0x6f, 0xbb, 0x21, 0x62, 0x65, 0x30, 0x0b, 0xfa, 0x70, 0xee, 0xfb, 0xf3, 0x4b, 0x2a, 0x54, 0xff, 0x57, 0xaf, - 0xbd, 0xae, 0x5a, 0xf0, 0xc4, 0x35, 0xec, 0x2e, 0xb8, 0x72, 0x4a, 0xed, 0x58, 0xa5, 0xa7, 0x9d, 0x64, 0x50, 0x10, - 0xda, 0x21, 0x85, 0x67, 0xff, 0x67, 0x51, 0x7d, 0x9e, 0x5b, 0xcd, 0xa1, 0xfa, 0xcc, 0x3a, 0x0e, 0x88, 0x7f, 0x3f, - 0xca, 0xbb, 0x3a, 0x08, 0x50, 0x35, 0xe4, 0x93, 0x02, 0xf3, 0x5f, 0xf1, 0x2c, 0x6f, 0x44, 0xba, 0x9d, 0xd9, 0x7d, - 0x8d, 0xcb, 0x99, 0xdc, 0x4e, 0xe6, 0x9b, 0x79, 0xb8, 0xd9, 0x79, 0x7f, 0xbd, 0xa5, 0x4a, 0xda, 0x7a, 0xa5, 0x3e, - 0x3d, 0xe0, 0x38, 0x20, 0xd2, 0x66, 0x19, 0x46, 0x73, 0x7e, 0xee, 0x06, 0xbe, 0x1f, 0x16, 0x61, 0xbe, 0x5f, 0xfb, - 0xb5, 0xe0, 0xc6, 0x24, 0x6f, 0x24, 0x4e, 0xd4, 0xc0, 0x65, 0x8a, 0xa1, 0x2b, 0x05, 0xbc, 0x89, 0x43, 0x5f, 0xc3, - 0x94, 0x1d, 0xf4, 0x5e, 0xb8, 0x1e, 0xf5, 0x74, 0xc4, 0x05, 0xae, 0xba, 0x79, 0x24, 0x93, 0xcc, 0x37, 0x94, 0x31, - 0xde, 0xf0, 0xaa, 0xdf, 0xb8, 0x73, 0xaf, 0x93, 0x32, 0x80, 0x5d, 0x2a, 0x28, 0x7e, 0xbc, 0x6a, 0x55, 0x53, 0x9f, - 0x8a, 0x10, 0xb2, 0x90, 0x4b, 0x01, 0xee, 0xf2, 0xfc, 0x99, 0x7c, 0x1e, 0x5d, 0xdc, 0x0d, 0x55, 0x03, 0xd0, 0x6a, - 0xea, 0xeb, 0x02, 0xc6, 0x91, 0x27, 0x45, 0xca, 0x70, 0x66, 0x6d, 0x78, 0x51, 0xab, 0x4f, 0xb9, 0xa4, 0x21, 0xa3, - 0xb6, 0x53, 0xea, 0x41, 0xbe, 0xd6, 0xd9, 0xec, 0x91, 0x37, 0xb8, 0xa1, 0x65, 0xbb, 0x92, 0x8f, 0x20, 0x8a, 0x26, - 0xc0, 0x72, 0x96, 0xb6, 0x09, 0x32, 0xf8, 0x0e, 0x2d, 0x92, 0xc1, 0x10, 0xb1, 0xc0, 0x9e, 0x77, 0xab, 0xe2, 0xb5, - 0xbd, 0x9c, 0x6a, 0x27, 0xd3, 0xef, 0x72, 0x74, 0xf6, 0x81, 0x3a, 0x1c, 0xac, 0xea, 0x65, 0x17, 0x6a, 0xfd, 0xbb, - 0x1f, 0xda, 0x0a, 0x02, 0x59, 0x03, 0x27, 0x4a, 0x8a, 0xbd, 0x52, 0x65, 0x6b, 0xe4, 0x24, 0xc0, 0x5d, 0x1f, 0xcf, - 0x44, 0x14, 0xb3, 0x74, 0x4e, 0xbf, 0x0b, 0x42, 0x8f, 0x31, 0xac, 0x90, 0x6a, 0x02, 0x8d, 0x9f, 0x5c, 0xd1, 0xdd, - 0x60, 0x35, 0xd9, 0x33, 0xd2, 0x17, 0x63, 0x3d, 0xdd, 0xd9, 0x36, 0xa8, 0x43, 0xfb, 0x6c, 0xb6, 0xaf, 0x2b, 0x8c, - 0x58, 0xf9, 0xa2, 0x1a, 0x7b, 0x61, 0x0b, 0xdc, 0xa9, 0x0b, 0x91, 0xb1, 0x62, 0x66, 0x7a, 0xc2, 0x50, 0x70, 0x5c, - 0x23, 0x1f, 0xe3, 0xc4, 0x4c, 0xa4, 0xb4, 0x2b, 0x76, 0x9a, 0x12, 0x70, 0x0a, 0x84, 0x8e, 0x3d, 0xed, 0xd6, 0x6a, - 0x41, 0xf2, 0x60, 0xe9, 0xb2, 0x1f, 0xe2, 0x7a, 0x3c, 0x2d, 0x29, 0x84, 0x20, 0x5c, 0x9c, 0x9e, 0x75, 0xb3, 0x8c, - 0xe3, 0xc1, 0x94, 0xa6, 0xc3, 0xfe, 0x69, 0x3f, 0xad, 0x0c, 0x3e, 0xde, 0x57, 0x2b, 0x3c, 0x76, 0x20, 0xf8, 0x3c, - 0xfa, 0xde, 0x20, 0xcf, 0xff, 0x34, 0xc9, 0xff, 0x3a, 0x86, 0x40, 0x0d, 0x5b, 0xb1, 0xa0, 0x1b, 0xbe, 0xb1, 0x09, - 0x5e, 0xae, 0x99, 0x57, 0x3a, 0x5b, 0x8b, 0xb3, 0x88, 0x0c, 0x0b, 0x78, 0x7c, 0xf3, 0xe3, 0xfa, 0x23, 0x9c, 0x8d, - 0xcb, 0x18, 0xc3, 0x4b, 0x6e, 0x80, 0x24, 0x21, 0x5c, 0x42, 0xcc, 0x58, 0x77, 0xcf, 0x0d, 0x6e, 0xab, 0x8d, 0xaf, - 0x01, 0xb7, 0x9e, 0x2f, 0xc6, 0xcb, 0x84, 0xf3, 0xe9, 0xcf, 0xca, 0x75, 0x4f, 0xa5, 0xb9, 0x51, 0x6f, 0xb5, 0xfe, - 0xe3, 0xd9, 0xef, 0x1e, 0xb0, 0x24, 0xdc, 0x4f, 0x2d, 0xc3, 0x7c, 0xf2, 0xea, 0x92, 0x89, 0x10, 0xcf, 0x02, 0x5a, - 0x0e, 0x51, 0x88, 0x0f, 0x17, 0x90, 0xe7, 0xee, 0xe8, 0x70, 0xe4, 0x76, 0x9a, 0x4b, 0x23, 0x47, 0x76, 0x30, 0xb4, - 0xa8, 0xa4, 0x0b, 0xab, 0x95, 0x69, 0x9f, 0x4a, 0x37, 0x22, 0x72, 0x20, 0x31, 0x59, 0xb1, 0xcf, 0x30, 0xd3, 0x0e, - 0x9c, 0x7a, 0x40, 0xfc, 0xcf, 0x7f, 0x11, 0x19, 0xca, 0x09, 0x2f, 0xb5, 0xb0, 0x84, 0xa5, 0xca, 0x6c, 0x1f, 0x8f, - 0x9c, 0xca, 0xcc, 0xaa, 0x57, 0x8b, 0x78, 0xeb, 0x8d, 0x86, 0xa6, 0x13, 0x23, 0xc5, 0x07, 0xbe, 0x8c, 0x02, 0x2a, - 0x34, 0xe9, 0xa1, 0x88, 0xd7, 0xf3, 0xcc, 0x39, 0x04, 0xe0, 0x1b, 0x7d, 0xb7, 0x54, 0x75, 0x5d, 0x5f, 0xec, 0x77, - 0x29, 0xf7, 0x25, 0x05, 0xb1, 0xe4, 0xdc, 0x3d, 0xc6, 0x39, 0x1c, 0x39, 0x78, 0x6a, 0x24, 0x15, 0x75, 0x16, 0x89, - 0xc4, 0x82, 0x96, 0xd2, 0xed, 0xb0, 0xdc, 0x03, 0xa6, 0x51, 0xa8, 0x6a, 0xa3, 0xc7, 0x5d, 0x97, 0x00, 0xda, 0xec, - 0x24, 0x84, 0xf7, 0xf0, 0x7d, 0xb6, 0x98, 0x0b, 0xb9, 0xe0, 0x6c, 0x7f, 0x9b, 0x53, 0x29, 0x57, 0xb1, 0x67, 0xa3, - 0xb4, 0x43, 0xf3, 0xed, 0xdc, 0xb3, 0x5a, 0x32, 0x2b, 0x62, 0x0c, 0x91, 0xd3, 0x37, 0x92, 0xb6, 0x0d, 0xd2, 0xe1, - 0x70, 0xcd, 0x90, 0xe0, 0x73, 0x5e, 0x6b, 0x2f, 0xc5, 0x93, 0x71, 0x52, 0xf8, 0x6f, 0x26, 0xd9, 0x79, 0x6d, 0xfd, - 0xa3, 0x3f, 0x24, 0x5b, 0x01, 0xc1, 0x13, 0x7e, 0x35, 0xd9, 0xcc, 0xae, 0xb9, 0xf9, 0xbd, 0x86, 0xf6, 0xd4, 0x52, - 0xb0, 0x66, 0x02, 0xad, 0x4d, 0xca, 0xe7, 0xa2, 0x8f, 0xaf, 0x57, 0x77, 0x24, 0xee, 0xd3, 0x67, 0xd2, 0x35, 0x25, - 0x2f, 0x19, 0x0b, 0xca, 0x37, 0xbc, 0xd9, 0x1f, 0xa3, 0x08, 0xa8, 0x1a, 0x72, 0x05, 0xf5, 0x75, 0x4f, 0xa6, 0xcf, - 0xda, 0x41, 0x58, 0xaf, 0x02, 0x9b, 0x80, 0x22, 0x11, 0xfd, 0xb7, 0x79, 0x8f, 0x3e, 0xe4, 0xd0, 0x1d, 0xb2, 0x37, - 0xb0, 0x9e, 0xac, 0x74, 0x27, 0x11, 0x8a, 0x0a, 0x9f, 0x3b, 0x01, 0xa5, 0x64, 0x07, 0xa5, 0x06, 0x7c, 0xd6, 0x4b, - 0xe4, 0x98, 0xf9, 0xc6, 0xf1, 0x2e, 0xf1, 0x8e, 0xb2, 0xf8, 0xc0, 0x81, 0x9d, 0xea, 0xe7, 0xef, 0x63, 0xf9, 0x72, - 0x8c, 0x07, 0x91, 0xd1, 0xef, 0x45, 0x01, 0xbe, 0xec, 0x37, 0xcd, 0xc8, 0xf3, 0xaf, 0xbf, 0xa5, 0x8d, 0x3c, 0xf3, - 0x6b, 0xa9, 0x84, 0x65, 0xdd, 0x9d, 0x3f, 0x45, 0xbb, 0x94, 0xd8, 0x70, 0x5b, 0xf4, 0xc1, 0x20, 0x97, 0x1b, 0x00, - 0x1f, 0x70, 0x2e, 0xff, 0x99, 0xa3, 0x50, 0x3e, 0x52, 0xeb, 0xf2, 0x64, 0x29, 0xc4, 0x98, 0xfc, 0x8d, 0x51, 0x2d, - 0x67, 0xae, 0x8f, 0xfc, 0x7e, 0xc1, 0x2f, 0x9c, 0x9d, 0xee, 0x07, 0xc8, 0x02, 0x41, 0x8b, 0x15, 0x5d, 0xe9, 0xa1, - 0xa8, 0xa5, 0xaa, 0x18, 0x4a, 0xf3, 0x62, 0x2c, 0x2d, 0x8a, 0x69, 0x63, 0x0f, 0x5e, 0x20, 0x52, 0x70, 0x3d, 0x35, - 0x4b, 0xa6, 0xd0, 0x43, 0xcf, 0xe0, 0x9e, 0xa9, 0xa4, 0xac, 0x75, 0x4e, 0xc3, 0xc0, 0x0a, 0x66, 0xc4, 0x0a, 0x6b, - 0xab, 0x93, 0x96, 0xbd, 0x02, 0x31, 0x96, 0x05, 0x0a, 0x14, 0xaa, 0x83, 0x58, 0x49, 0x15, 0x12, 0xcc, 0xd9, 0x16, - 0x23, 0x3d, 0x5b, 0x49, 0x95, 0xee, 0x34, 0x34, 0xe7, 0x67, 0x85, 0xd1, 0x1b, 0x01, 0xdd, 0xa2, 0xb8, 0xbf, 0x5f, - 0xd7, 0xb0, 0x41, 0x66, 0xa5, 0x2a, 0xaa, 0x17, 0x51, 0x95, 0x69, 0x46, 0x5a, 0x82, 0xec, 0xf9, 0x0c, 0xe4, 0xaa, - 0x72, 0xd4, 0x1e, 0xd3, 0x5e, 0x00, 0xd5, 0xe8, 0xb3, 0xe8, 0xe8, 0x45, 0x9a, 0x43, 0x5d, 0x37, 0x6c, 0xa9, 0x15, - 0x65, 0x52, 0x50, 0xac, 0x91, 0xdf, 0x4e, 0xb2, 0x46, 0x44, 0x76, 0xb3, 0x8b, 0xef, 0xe9, 0xae, 0x92, 0x4d, 0xb2, - 0xf1, 0x69, 0x1c, 0x20, 0xb4, 0x4e, 0x48, 0x2c, 0x6a, 0xbc, 0xe0, 0x2d, 0xe1, 0xd2, 0x72, 0x18, 0x7f, 0x74, 0xbc, - 0xbf, 0xe4, 0x0a, 0x0f, 0xac, 0x48, 0xeb, 0x84, 0x45, 0x7e, 0x32, 0x2e, 0xe0, 0xd7, 0xfe, 0xac, 0x90, 0x59, 0x33, - 0x16, 0xb9, 0xf0, 0x59, 0x94, 0x27, 0xb1, 0xae, 0x2d, 0xdd, 0x93, 0x71, 0xff, 0xd8, 0x99, 0x3a, 0xde, 0x9f, 0x74, - 0x08, 0xfc, 0x02, 0x50, 0xaa, 0x1e, 0x10, 0xfb, 0xc0, 0xf7, 0x78, 0x65, 0x51, 0x04, 0x97, 0xe1, 0xf6, 0x90, 0x8a, - 0xf2, 0x34, 0x77, 0xd5, 0xb4, 0xf2, 0x17, 0x21, 0x09, 0xaa, 0xe1, 0x9b, 0x94, 0xa4, 0x40, 0xe9, 0xa3, 0x1c, 0x26, - 0x3d, 0x62, 0x9f, 0xdf, 0xfb, 0xd2, 0x67, 0xa7, 0xf0, 0xa5, 0x4f, 0xba, 0x66, 0x6d, 0x85, 0xb7, 0xff, 0x36, 0xb0, - 0x83, 0x99, 0x1e, 0xc5, 0xef, 0x87, 0x38, 0x2d, 0xd6, 0x32, 0xea, 0xce, 0x2f, 0x96, 0xbd, 0x0d, 0xf9, 0x3f, 0xfc, - 0xee, 0x82, 0xd3, 0xf0, 0xfd, 0x69, 0xd5, 0xdd, 0x78, 0x5b, 0x39, 0x74, 0xbf, 0x75, 0xbf, 0x6d, 0xa5, 0x5b, 0x3a, - 0x79, 0xe1, 0x7f, 0x7b, 0xef, 0x9c, 0xfb, 0x96, 0x8b, 0xcf, 0x24, 0x33, 0x5e, 0x7d, 0x12, 0xeb, 0x74, 0xac, 0x20, - 0xbd, 0x72, 0x91, 0x49, 0xaf, 0xb7, 0xc6, 0xf1, 0x8b, 0xc4, 0xda, 0xee, 0xc4, 0x9a, 0x7d, 0x50, 0x35, 0x79, 0x8c, - 0xc1, 0xa3, 0xad, 0x2c, 0xa6, 0x3f, 0x58, 0xe7, 0xd1, 0xff, 0x36, 0xb2, 0x39, 0x6e, 0x5c, 0x8e, 0x36, 0x7f, 0xb1, - 0x44, 0x3c, 0x5b, 0xed, 0x57, 0xde, 0xa7, 0xb5, 0x25, 0x8b, 0xbf, 0x49, 0x4b, 0xd2, 0x5a, 0x8c, 0xef, 0xe5, 0x83, - 0x25, 0x7d, 0x8a, 0x3d, 0x07, 0x11, 0x1a, 0xb1, 0xe6, 0x82, 0x1a, 0xb9, 0x4e, 0x57, 0x02, 0xb1, 0x0b, 0x3f, 0x1f, - 0x7a, 0x8a, 0x0b, 0xa4, 0x3a, 0xc8, 0xcb, 0x40, 0x35, 0x51, 0x40, 0x3f, 0x50, 0x53, 0x82, 0x9c, 0x67, 0x7b, 0xc7, - 0xb7, 0x87, 0xaf, 0xf1, 0xf6, 0xcc, 0xd2, 0x46, 0x64, 0x7e, 0x8b, 0x07, 0x47, 0x58, 0x9a, 0xdb, 0x09, 0x93, 0x45, - 0xd8, 0x42, 0xd1, 0xf2, 0x0e, 0xd9, 0x63, 0xf3, 0x4b, 0x03, 0xd5, 0x85, 0x1c, 0xb1, 0x9e, 0xff, 0x5a, 0xef, 0xd2, - 0x15, 0xbc, 0x4b, 0xfb, 0xa2, 0x97, 0x66, 0x3d, 0x04, 0x40, 0x77, 0xea, 0xcf, 0x40, 0x95, 0xb9, 0xa1, 0x28, 0x7d, - 0x7e, 0x9d, 0xb5, 0xa7, 0xda, 0x55, 0xe0, 0xde, 0x69, 0x78, 0x76, 0x42, 0x56, 0x2e, 0x11, 0xfd, 0x18, 0xec, 0x73, - 0x02, 0xfd, 0x41, 0x33, 0xb4, 0xaf, 0x3b, 0x3a, 0x71, 0x09, 0x28, 0x13, 0x75, 0xb8, 0x24, 0x46, 0x30, 0x7e, 0x90, - 0x83, 0x0d, 0xbc, 0x5a, 0xdf, 0xa5, 0x24, 0xae, 0xba, 0x73, 0x08, 0xd1, 0x6b, 0xde, 0xff, 0xea, 0xd7, 0x60, 0xbf, - 0xde, 0xe8, 0x26, 0x23, 0xf2, 0xf7, 0xfc, 0x9c, 0x4b, 0x34, 0x63, 0x88, 0x10, 0x55, 0x2c, 0x5a, 0xf3, 0x1c, 0x0b, - 0x58, 0x9e, 0x97, 0xe0, 0xbb, 0x7c, 0xde, 0x75, 0x62, 0xab, 0x89, 0xa5, 0x6a, 0x30, 0x62, 0x17, 0x0e, 0xde, 0x07, - 0x29, 0x30, 0x70, 0x59, 0x76, 0xd2, 0x7d, 0x47, 0x32, 0x1b, 0x1f, 0x34, 0x07, 0x04, 0x37, 0x3d, 0xc8, 0x02, 0x7f, - 0x23, 0x8c, 0xa1, 0x85, 0x07, 0xeb, 0xa1, 0xd9, 0xa6, 0xbc, 0x06, 0x17, 0xdc, 0x74, 0xa5, 0x22, 0xd5, 0x97, 0xb6, - 0x5a, 0x06, 0x0a, 0x4b, 0xb3, 0x60, 0xe0, 0x5b, 0x5a, 0x2c, 0x8b, 0x10, 0x83, 0x3c, 0x5c, 0xe7, 0x24, 0x84, 0xf2, - 0x04, 0xa1, 0x0e, 0x05, 0x66, 0x3c, 0x38, 0x9d, 0x22, 0x70, 0x30, 0xaf, 0x39, 0xaf, 0x3b, 0xbf, 0x67, 0xc2, 0x32, - 0x4b, 0x8f, 0x7b, 0x0d, 0x97, 0x4b, 0x13, 0x39, 0xd0, 0xbd, 0xd9, 0xfb, 0xdb, 0x2d, 0xf2, 0xbe, 0x11, 0x5a, 0xb1, - 0x0d, 0x17, 0x15, 0x17, 0x59, 0xb4, 0xba, 0xc4, 0xb8, 0x93, 0xae, 0x57, 0xee, 0x85, 0xdd, 0x7a, 0xea, 0x9e, 0xac, - 0x37, 0x4c, 0xe1, 0xd3, 0xb0, 0x8c, 0x25, 0xc4, 0x1a, 0x4b, 0x47, 0xff, 0xbb, 0x2c, 0x7c, 0x96, 0xb5, 0x07, 0x0c, - 0x85, 0xb8, 0x3f, 0xd3, 0xe3, 0x27, 0x00, 0x0e, 0xc7, 0x13, 0x1f, 0x27, 0xe0, 0x40, 0x6b, 0x49, 0xa1, 0x5b, 0x33, - 0x01, 0x11, 0x6b, 0x4b, 0xfe, 0x4b, 0x5f, 0x89, 0x52, 0xf4, 0xd9, 0xb1, 0xeb, 0xdc, 0xe4, 0xfd, 0xdb, 0xea, 0x58, - 0x35, 0x2d, 0x21, 0xef, 0xba, 0x05, 0xea, 0x61, 0xf9, 0xfb, 0xae, 0x58, 0xd1, 0x11, 0x08, 0x3c, 0x7f, 0x63, 0x5a, - 0xac, 0x4f, 0x06, 0x48, 0xd7, 0x83, 0x4f, 0x58, 0x24, 0x9e, 0x82, 0x63, 0x20, 0x8e, 0xab, 0xe1, 0x23, 0xf3, 0xc3, - 0xff, 0x2c, 0x27, 0x56, 0xa6, 0x9d, 0xc9, 0xa9, 0x63, 0xaa, 0x23, 0x83, 0x00, 0xfa, 0x22, 0xf7, 0xb8, 0xee, 0xbf, - 0x3a, 0xb5, 0x54, 0x31, 0x6c, 0xb6, 0x37, 0x71, 0x6b, 0xee, 0xc4, 0x7a, 0xa5, 0xcb, 0xe0, 0xd3, 0xca, 0xfd, 0xe2, - 0xf4, 0x43, 0x26, 0xcc, 0xe0, 0x6c, 0xf1, 0xe5, 0x83, 0x4d, 0x0f, 0x19, 0x0d, 0x80, 0x09, 0x4b, 0xe9, 0x27, 0x83, - 0x94, 0x3e, 0x3f, 0x31, 0x12, 0xda, 0x36, 0xf8, 0x67, 0x6e, 0x2e, 0x47, 0x39, 0xd0, 0x7f, 0x18, 0xec, 0x62, 0xa3, - 0xb7, 0x49, 0x18, 0x3f, 0x40, 0x9a, 0xbd, 0xd1, 0x8f, 0x7a, 0xcd, 0xa1, 0x25, 0x16, 0xe5, 0xd5, 0x93, 0xfc, 0x98, - 0x65, 0x46, 0x01, 0xf8, 0x90, 0xf7, 0x1e, 0xfa, 0xe7, 0x98, 0x62, 0xce, 0xe1, 0xeb, 0xf8, 0xd7, 0x0e, 0x62, 0x6d, - 0xed, 0x74, 0xcd, 0xff, 0xce, 0x5c, 0x78, 0x6a, 0x73, 0xc2, 0x8c, 0xb6, 0x5f, 0xbd, 0xba, 0xda, 0x74, 0x18, 0x01, - 0x34, 0xbe, 0xc2, 0xe8, 0xb1, 0x09, 0xdd, 0x06, 0x66, 0x24, 0xe0, 0x9e, 0x67, 0xd2, 0x95, 0x8e, 0x3f, 0x16, 0xf0, - 0x66, 0xe6, 0x77, 0xa0, 0x09, 0x77, 0x57, 0xd2, 0x68, 0x4b, 0x92, 0x1c, 0xf9, 0x6d, 0xc1, 0x44, 0xb1, 0x75, 0xeb, - 0x26, 0xbc, 0x16, 0xf8, 0xff, 0xf8, 0x41, 0x00, 0xf2, 0x6e, 0x51, 0xb3, 0xa4, 0x76, 0x9a, 0xe6, 0x2b, 0x4d, 0x29, - 0xbb, 0xb0, 0x72, 0x93, 0x5d, 0xce, 0xfc, 0xff, 0x30, 0x82, 0x9b, 0x1c, 0x3e, 0x89, 0x1e, 0xda, 0x57, 0x80, 0xa4, - 0x07, 0x44, 0x17, 0x0f, 0x5a, 0x38, 0x7e, 0x23, 0xca, 0x1c, 0x2c, 0x6c, 0x0b, 0x96, 0x05, 0x83, 0x28, 0x7b, 0x04, - 0xf3, 0x0b, 0x1d, 0x98, 0xfc, 0x4d, 0xef, 0x28, 0xe7, 0x10, 0xe9, 0xd5, 0x77, 0x25, 0xa7, 0xae, 0x9d, 0x5e, 0xfa, - 0xbf, 0x69, 0x02, 0x4c, 0xe6, 0xb6, 0xbe, 0x4e, 0x2d, 0x5f, 0xf8, 0x3c, 0x6b, 0x2f, 0x8a, 0x71, 0xfc, 0xed, 0x8a, - 0x8e, 0x77, 0xc6, 0xac, 0x77, 0x14, 0x35, 0xad, 0xae, 0x7d, 0x75, 0xd3, 0x82, 0x6e, 0x9c, 0x10, 0x3c, 0xc6, 0x87, - 0x58, 0x7e, 0xde, 0x7c, 0x93, 0x50, 0xc7, 0xdf, 0xc6, 0x1e, 0x6f, 0x9b, 0x29, 0x3c, 0xb1, 0x03, 0x7e, 0x47, 0xf7, - 0x96, 0x8e, 0x57, 0x57, 0x4c, 0x7c, 0x08, 0x4e, 0x45, 0x80, 0xd7, 0x93, 0x24, 0xbe, 0x29, 0x89, 0x83, 0x0d, 0xa7, - 0xd6, 0xb6, 0xc2, 0x7d, 0x59, 0xbb, 0x43, 0x16, 0x4e, 0xe3, 0x9b, 0xa8, 0x1b, 0x1e, 0x71, 0xc6, 0x49, 0xdc, 0xea, - 0xa9, 0xef, 0xb6, 0x41, 0x8c, 0xc0, 0xe1, 0x0a, 0x30, 0x3e, 0x11, 0x3e, 0xc4, 0x6c, 0x6a, 0x00, 0xa7, 0x22, 0xb0, - 0xea, 0x87, 0x03, 0x4c, 0xd9, 0xfd, 0x94, 0x0f, 0xe9, 0x88, 0xc0, 0xcc, 0xf4, 0x10, 0xe5, 0xfd, 0xe7, 0xf0, 0x48, - 0xde, 0x9f, 0x4f, 0x86, 0xe1, 0x50, 0xc8, 0xb5, 0xd9, 0xb0, 0x07, 0x56, 0xbe, 0xe0, 0x06, 0xe7, 0x6b, 0xb6, 0xed, - 0xda, 0x84, 0xdc, 0xfc, 0x23, 0x9e, 0x61, 0x97, 0x98, 0xde, 0xdc, 0x3b, 0x5d, 0x47, 0x3d, 0xdf, 0x2b, 0x17, 0x52, - 0xc3, 0x3e, 0xd1, 0xe2, 0xd1, 0xf3, 0x6c, 0xa4, 0x75, 0xc7, 0xc9, 0x5b, 0xfd, 0x4b, 0x72, 0x24, 0x04, 0xc5, 0x4f, - 0xb2, 0xf6, 0x3e, 0x91, 0x6d, 0xdf, 0x05, 0xcf, 0xad, 0xf6, 0x3a, 0x42, 0x77, 0xfa, 0xd3, 0x23, 0x6c, 0x4a, 0xe7, - 0xe7, 0x0e, 0xa0, 0x3a, 0x92, 0xf3, 0xd9, 0xe5, 0x1e, 0x06, 0xe5, 0x70, 0xc5, 0x33, 0x70, 0xb7, 0x62, 0xe6, 0x21, - 0xd2, 0x35, 0x5a, 0x7f, 0xf7, 0x82, 0x37, 0x5c, 0x33, 0x59, 0x1b, 0x91, 0xdc, 0x16, 0xf2, 0x30, 0xc7, 0x08, 0x65, - 0xbe, 0xa0, 0xfc, 0x70, 0xd1, 0x66, 0x72, 0x96, 0x84, 0x2f, 0x24, 0x0a, 0xfc, 0x49, 0x95, 0x67, 0xae, 0x1c, 0x2f, - 0x57, 0x6c, 0x60, 0x2e, 0xe8, 0x8b, 0x51, 0x40, 0x22, 0x73, 0xb7, 0xfc, 0x32, 0xcf, 0x90, 0xfc, 0x35, 0x72, 0x6d, - 0x07, 0x58, 0xbe, 0xce, 0x53, 0x06, 0x37, 0x2e, 0x5a, 0x0b, 0x1d, 0x5f, 0x4a, 0x26, 0x41, 0x91, 0x42, 0xa8, 0xb4, - 0x48, 0x68, 0xd4, 0x2a, 0x15, 0x30, 0x6e, 0xa1, 0xa1, 0xdf, 0x6b, 0xd5, 0xe7, 0x4f, 0xd8, 0xf9, 0x63, 0x94, 0xff, - 0x51, 0x5c, 0x04, 0xc8, 0x91, 0xb7, 0xa8, 0x1b, 0xf0, 0x4c, 0x91, 0xd4, 0x66, 0x8e, 0x4b, 0x24, 0x1c, 0x83, 0x64, - 0x61, 0xb7, 0x61, 0xef, 0x7f, 0xc7, 0xd7, 0x54, 0x90, 0x30, 0x88, 0xf0, 0xeb, 0x2c, 0x83, 0x6e, 0xe0, 0x22, 0x98, - 0x6a, 0x84, 0x07, 0x1c, 0x45, 0xdd, 0x35, 0xab, 0x80, 0x13, 0x28, 0x41, 0xc9, 0x22, 0x89, 0x1f, 0x77, 0xa0, 0xff, - 0x5f, 0x8a, 0x51, 0x7d, 0xd6, 0xd7, 0xb7, 0x15, 0xd4, 0x43, 0x87, 0x02, 0x15, 0x19, 0x37, 0xc0, 0x66, 0x8f, 0x8f, - 0x45, 0x0e, 0xd8, 0x30, 0xf9, 0xaf, 0xb0, 0xb0, 0x52, 0xd9, 0x72, 0x3a, 0xfc, 0xcb, 0x35, 0x8b, 0x83, 0x3d, 0x3c, - 0x4c, 0xe3, 0x30, 0x3e, 0xa5, 0x25, 0x7d, 0x5e, 0xe8, 0xa4, 0x51, 0xd1, 0xf9, 0x71, 0x9e, 0xf5, 0x7d, 0x57, 0xf2, - 0xf8, 0x35, 0x5e, 0x9f, 0xd9, 0x53, 0x74, 0x9d, 0x1f, 0x7e, 0xf4, 0xa3, 0xb1, 0x65, 0xfc, 0x37, 0x7a, 0x61, 0x4f, - 0x17, 0x94, 0x96, 0x81, 0xf7, 0xe9, 0xd1, 0x62, 0x25, 0xfb, 0x82, 0x1c, 0x7d, 0x8c, 0x8e, 0xf6, 0x78, 0x4e, 0xf9, - 0x79, 0x16, 0xe7, 0xfd, 0xed, 0x6b, 0xbc, 0x38, 0xf3, 0xac, 0x5c, 0xeb, 0xcd, 0xa7, 0xde, 0x06, 0xec, 0x2d, 0x70, - 0x3f, 0x89, 0xdd, 0x40, 0x84, 0x93, 0x60, 0x0c, 0xd3, 0xbd, 0x69, 0x44, 0x03, 0xec, 0x77, 0xed, 0xf9, 0xc0, 0x03, - 0xfd, 0xcf, 0xe6, 0xf5, 0xe0, 0xdc, 0x6e, 0x54, 0x53, 0x0a, 0x70, 0xc1, 0x64, 0x45, 0x31, 0x46, 0x82, 0x48, 0x23, - 0xbd, 0x1d, 0x1d, 0xb9, 0xa8, 0x2b, 0x9c, 0x26, 0xba, 0xe4, 0x69, 0xe2, 0x26, 0x65, 0x6b, 0x99, 0x00, 0x50, 0x96, - 0x64, 0xec, 0xd0, 0xf3, 0x7a, 0x80, 0xf4, 0x0e, 0x72, 0x42, 0x2c, 0xc7, 0x25, 0x90, 0x2d, 0x19, 0x7c, 0xfb, 0x0f, - 0xab, 0x40, 0x5e, 0x6f, 0xe8, 0xb0, 0x09, 0x69, 0xf6, 0x38, 0x3d, 0x7d, 0x71, 0x00, 0xae, 0x44, 0xa6, 0x67, 0x9a, - 0x06, 0x17, 0x7d, 0x8e, 0x3e, 0x34, 0xc2, 0x5a, 0x60, 0x2a, 0xea, 0xb4, 0xe5, 0xad, 0x52, 0x71, 0xf3, 0xe0, 0x78, - 0x0a, 0x07, 0x43, 0x33, 0x30, 0x22, 0x7f, 0xfa, 0x0f, 0x1b, 0xc6, 0x72, 0x24, 0xad, 0x6c, 0x98, 0xbf, 0xec, 0x72, - 0x2b, 0x37, 0x4b, 0x12, 0x9a, 0x86, 0x5e, 0x3d, 0x88, 0x15, 0xde, 0xa9, 0xff, 0xe7, 0x41, 0x69, 0x83, 0x38, 0x87, - 0x64, 0x01, 0x51, 0x3c, 0x47, 0x38, 0xc5, 0xa0, 0xc5, 0x6c, 0x90, 0xc3, 0x94, 0x81, 0xc0, 0x2b, 0xab, 0x37, 0x81, - 0x1b, 0x71, 0xb9, 0xec, 0xe9, 0xd4, 0x6b, 0xae, 0x9d, 0xd4, 0x26, 0xb2, 0x08, 0x57, 0xf8, 0xcd, 0x07, 0x80, 0xae, - 0x36, 0xd4, 0x51, 0x08, 0xe4, 0x08, 0x9b, 0xe7, 0x8a, 0x14, 0xdd, 0x78, 0x7c, 0x1b, 0xf6, 0x2d, 0x47, 0x88, 0xcd, - 0x8f, 0xb9, 0x6b, 0x8d, 0x06, 0x8d, 0x4c, 0x32, 0x6c, 0x5c, 0x0a, 0x76, 0x92, 0xa0, 0x87, 0x1a, 0xc7, 0x38, 0x94, - 0x15, 0x7a, 0x1e, 0x19, 0xe3, 0x88, 0xaf, 0x7c, 0xc9, 0x9a, 0x93, 0x68, 0x91, 0x8a, 0x81, 0xfd, 0x1c, 0xbe, 0xce, - 0x0b, 0x41, 0x2e, 0x8e, 0xb8, 0xe9, 0xa9, 0x21, 0xa7, 0x3e, 0x19, 0x14, 0xa8, 0x88, 0x5b, 0xaf, 0x2d, 0x68, 0x98, - 0x47, 0x01, 0x71, 0x6e, 0x16, 0x38, 0xc2, 0x29, 0x2c, 0x6a, 0xff, 0xe0, 0xe8, 0xbc, 0x75, 0xb4, 0x40, 0x90, 0xda, - 0x09, 0x67, 0x38, 0x99, 0xd1, 0x11, 0x32, 0xc3, 0xe5, 0xf1, 0x71, 0x53, 0xd3, 0x5a, 0x53, 0xa7, 0x95, 0x22, 0xc9, - 0x0c, 0x69, 0x26, 0xb0, 0xc4, 0x0f, 0xdb, 0xde, 0x5c, 0xa4, 0x62, 0x45, 0xe0, 0x2d, 0x66, 0xfc, 0x5c, 0xd8, 0x81, - 0xe2, 0xd5, 0x84, 0x0e, 0x6c, 0xaa, 0xfc, 0xdc, 0xe6, 0xa6, 0x27, 0xfc, 0xc2, 0x61, 0xfa, 0x75, 0x26, 0xfa, 0x59, - 0x98, 0xa3, 0xd5, 0x41, 0x2f, 0x5c, 0x21, 0xe3, 0xc4, 0x33, 0x64, 0xd9, 0x94, 0x43, 0xf7, 0x1a, 0x25, 0x8a, 0xa4, - 0x01, 0x39, 0xda, 0x43, 0x4e, 0x2e, 0xf3, 0xa4, 0xd5, 0x34, 0x2a, 0xbb, 0x24, 0xe1, 0x2d, 0x7e, 0xe4, 0x31, 0xa1, - 0x44, 0xf9, 0x3c, 0xcd, 0x33, 0x92, 0xac, 0x71, 0xb7, 0xa3, 0xe0, 0x1a, 0xbd, 0xb5, 0xba, 0xac, 0xd5, 0xb0, 0x9f, - 0xc8, 0xbf, 0x52, 0x47, 0x6f, 0x52, 0x3c, 0x18, 0x04, 0x19, 0x86, 0xab, 0x96, 0xdd, 0x43, 0x8b, 0x1e, 0xfb, 0xa2, - 0xfa, 0x77, 0x83, 0x60, 0xe2, 0x49, 0x21, 0x84, 0x96, 0x91, 0x03, 0xfd, 0x37, 0x55, 0xaa, 0x25, 0x12, 0xde, 0x3b, - 0x9f, 0xb3, 0x77, 0x13, 0xa4, 0x04, 0xb3, 0x4d, 0x95, 0x7b, 0x20, 0x5c, 0x87, 0x80, 0xd7, 0x0d, 0x77, 0xa8, 0x77, - 0x91, 0x5b, 0x11, 0x74, 0x29, 0x05, 0x88, 0x88, 0x00, 0x8c, 0x5e, 0x0c, 0x34, 0x1c, 0xa6, 0x19, 0xac, 0x44, 0x82, - 0x7f, 0x95, 0x85, 0x21, 0xb5, 0xec, 0x28, 0x07, 0xc0, 0x66, 0xb3, 0x11, 0x8c, 0xbe, 0x60, 0x05, 0x7c, 0x36, 0x89, - 0xff, 0xc6, 0xa9, 0x6a, 0xa6, 0x75, 0x23, 0xe5, 0xdd, 0xb8, 0xb1, 0x76, 0x81, 0x18, 0xa6, 0xa5, 0x75, 0x3b, 0x21, - 0x09, 0x28, 0x0d, 0x0b, 0x9f, 0x3c, 0xbf, 0x3d, 0x46, 0xd7, 0xdf, 0x1f, 0x3e, 0x30, 0x49, 0x14, 0x19, 0x55, 0x32, - 0x30, 0x4d, 0x84, 0x8c, 0x6f, 0x11, 0x3a, 0x1e, 0x8e, 0xa6, 0x45, 0x7e, 0xea, 0x75, 0x6c, 0x37, 0x50, 0x8f, 0x2f, - 0xbf, 0xb6, 0xdc, 0x39, 0xd1, 0xda, 0xe9, 0xb8, 0x3f, 0x04, 0x9a, 0x9c, 0x88, 0xaa, 0xb2, 0x18, 0x25, 0xfb, 0x87, - 0x81, 0x6d, 0x24, 0x39, 0xf5, 0x54, 0x18, 0x77, 0x6f, 0x73, 0x4f, 0x87, 0x89, 0x8b, 0x23, 0xff, 0xcb, 0x1f, 0xc1, - 0xb5, 0x52, 0xbc, 0xf2, 0x1d, 0xd8, 0x72, 0x09, 0x57, 0x0e, 0x56, 0x0d, 0x02, 0xa2, 0x94, 0x00, 0x72, 0x1d, 0x1f, - 0x1d, 0xe3, 0x24, 0x79, 0xd7, 0x33, 0xd6, 0xd4, 0x6c, 0x49, 0x69, 0xe6, 0x63, 0x5f, 0x53, 0x69, 0x1e, 0xe7, 0x9a, - 0x60, 0x96, 0x64, 0xd9, 0x49, 0x56, 0x80, 0xd7, 0x74, 0x0d, 0xf3, 0x55, 0x85, 0x40, 0x30, 0xdf, 0x55, 0x99, 0x8b, - 0x53, 0x85, 0xb8, 0x1d, 0x09, 0x6d, 0xaa, 0x45, 0x78, 0xe1, 0xc0, 0x38, 0x9c, 0x5f, 0x33, 0x2d, 0x06, 0x06, 0x20, - 0x57, 0x52, 0x6f, 0x84, 0xf3, 0xf4, 0x20, 0x6f, 0x43, 0xf1, 0xa4, 0xc0, 0x56, 0xac, 0x78, 0xf0, 0xad, 0x97, 0x46, - 0xb3, 0xca, 0x68, 0x97, 0x5b, 0x71, 0xa6, 0xf3, 0xa4, 0xcf, 0x4e, 0x9b, 0xd2, 0xad, 0x87, 0x80, 0x0a, 0xe5, 0xf9, - 0x19, 0xcf, 0xc7, 0x2b, 0xc4, 0x39, 0xe6, 0xac, 0x09, 0xd5, 0x73, 0x61, 0xfd, 0x3a, 0x3d, 0x64, 0xff, 0x7a, 0xbc, - 0xb5, 0xce, 0x54, 0xdc, 0x3c, 0x53, 0xc8, 0x54, 0xfc, 0x15, 0xf7, 0x5e, 0x3c, 0x30, 0x5c, 0x93, 0xc7, 0x90, 0x67, - 0x2a, 0x27, 0x7c, 0x69, 0xa0, 0xe9, 0x20, 0xaa, 0xd3, 0xad, 0x10, 0x57, 0x5d, 0x18, 0xa2, 0x70, 0xf7, 0x29, 0x2f, - 0x48, 0xeb, 0xb0, 0xf7, 0x54, 0xa3, 0xc3, 0xa0, 0xa6, 0x40, 0x9d, 0x16, 0x83, 0x95, 0xb4, 0x5c, 0x50, 0x0e, 0x05, - 0x33, 0xd5, 0x06, 0x1e, 0xd8, 0x9a, 0xff, 0x7f, 0x40, 0x21, 0x7a, 0xdc, 0xf6, 0xaf, 0xfc, 0x05, 0x73, 0xc2, 0x8c, - 0x25, 0x33, 0x3c, 0xbc, 0xda, 0x19, 0x7f, 0x0a, 0xba, 0xb1, 0x6a, 0xa3, 0x8c, 0x55, 0x39, 0x51, 0x06, 0xf7, 0x93, - 0xa5, 0x98, 0x3a, 0x85, 0x0b, 0x31, 0x95, 0x97, 0x7d, 0xa4, 0x92, 0x52, 0x1c, 0x79, 0x51, 0x01, 0xc0, 0xbc, 0x0b, - 0x34, 0x65, 0xca, 0x93, 0x20, 0x09, 0x5c, 0x54, 0x01, 0xd1, 0x8c, 0x97, 0x73, 0x7a, 0xe3, 0x2b, 0x8e, 0x7b, 0xc4, - 0x49, 0x74, 0xe6, 0x10, 0xd4, 0xf5, 0xf9, 0x29, 0x23, 0x62, 0x41, 0xd8, 0x57, 0x8c, 0x43, 0xd9, 0x4e, 0x58, 0x5f, - 0xac, 0xd7, 0x77, 0xde, 0x24, 0x8a, 0xa4, 0x2b, 0xb3, 0x7c, 0xe4, 0xfb, 0x0c, 0x99, 0xad, 0x92, 0x8d, 0x38, 0x86, - 0x32, 0xce, 0x19, 0x8f, 0x62, 0x03, 0x81, 0xb3, 0xa5, 0x2f, 0xb5, 0x90, 0xe3, 0xb2, 0x34, 0x94, 0xc7, 0xc0, 0xb9, - 0x2b, 0xdb, 0x9b, 0xd7, 0xf1, 0x31, 0xe7, 0xfd, 0xb7, 0xeb, 0x4d, 0xad, 0xa8, 0x3f, 0x63, 0xd5, 0x86, 0x7d, 0x81, - 0xa2, 0x79, 0x30, 0xeb, 0x74, 0x8e, 0xf2, 0x88, 0x27, 0x9c, 0x3c, 0x8b, 0x3a, 0xd6, 0xae, 0x8f, 0x92, 0x17, 0x67, - 0xaf, 0xa3, 0xe2, 0x34, 0x88, 0x7e, 0x59, 0xcd, 0x52, 0x07, 0xd7, 0x77, 0xc8, 0x5e, 0xe6, 0x72, 0xed, 0x44, 0xa9, - 0x86, 0x06, 0xce, 0x9f, 0xe4, 0xab, 0xd8, 0x3e, 0xe1, 0x2c, 0x67, 0xa2, 0xca, 0x7e, 0x9d, 0x07, 0xa9, 0x30, 0xe7, - 0xf8, 0xe3, 0xd2, 0x86, 0xe8, 0x2b, 0x22, 0x24, 0xe6, 0xac, 0xfb, 0xce, 0xa6, 0x2c, 0x71, 0xe9, 0xc3, 0x08, 0xa1, - 0x9f, 0x18, 0xa1, 0x19, 0x47, 0x3d, 0x6f, 0xfe, 0xb7, 0x91, 0xef, 0xb3, 0x9e, 0x53, 0x74, 0xc7, 0x7b, 0xb2, 0x1a, - 0x2a, 0xdb, 0xe9, 0x67, 0xee, 0xd4, 0x9e, 0x82, 0x53, 0x7b, 0x82, 0x4a, 0x90, 0xc0, 0xcf, 0x0b, 0xcf, 0xf1, 0xa4, - 0xd9, 0x54, 0x17, 0x4f, 0xdb, 0x5f, 0x8d, 0xcf, 0x2f, 0x95, 0x8d, 0xaf, 0x38, 0xbb, 0x52, 0x68, 0x67, 0x78, 0x4b, - 0x67, 0xae, 0x0c}; +const uint8_t INDEX_BR[] PROGMEM = { + 0x1b, 0xe2, 0x97, 0xa3, 0x90, 0xa2, 0x95, 0x55, 0x51, 0x04, 0x1b, 0x07, 0x80, 0x20, 0x79, 0x0e, 0x50, 0xab, 0x02, + 0xdb, 0x98, 0x16, 0xf4, 0x7b, 0x22, 0xa3, 0x4d, 0xd3, 0x86, 0xc1, 0x26, 0x48, 0x49, 0x60, 0xbe, 0xb3, 0xc9, 0xa1, + 0x8c, 0x96, 0x10, 0x1b, 0x21, 0xcf, 0x48, 0x68, 0xce, 0x10, 0x34, 0x32, 0x7c, 0xbf, 0x71, 0x7b, 0x03, 0x8f, 0xdd, + 0x37, 0x06, 0x9a, 0x30, 0x50, 0xe4, 0x08, 0x47, 0x68, 0xec, 0x93, 0xdc, 0x7d, 0x53, 0xf5, 0x4f, 0xd7, 0x8a, 0xcf, + 0x2f, 0x85, 0x3a, 0x6c, 0xa9, 0x63, 0xcb, 0xf5, 0xc8, 0x18, 0xe3, 0xf5, 0xdb, 0x0c, 0x05, 0x9b, 0x48, 0x2c, 0x42, + 0x21, 0xa0, 0x2c, 0x25, 0xf7, 0xcb, 0xb7, 0xaa, 0x65, 0xd5, 0x7f, 0x3e, 0x2f, 0x94, 0x2b, 0x53, 0xa7, 0x0f, 0x4e, + 0x56, 0xa9, 0xf7, 0xce, 0x54, 0x88, 0x39, 0xef, 0xea, 0x3d, 0x69, 0x56, 0xd0, 0x52, 0x96, 0x0a, 0x5b, 0x35, 0xd4, + 0x42, 0xd6, 0x35, 0x10, 0xf3, 0x7f, 0xec, 0x95, 0xd3, 0x2a, 0xfe, 0x4d, 0x22, 0x6a, 0xd6, 0x69, 0x6e, 0x7b, 0x5a, + 0xdd, 0x33, 0x58, 0x21, 0xa4, 0x33, 0xd6, 0x61, 0x05, 0xf5, 0x08, 0xa9, 0x10, 0x32, 0xf5, 0xdc, 0x04, 0x59, 0x72, + 0x41, 0xf4, 0x09, 0xfa, 0x08, 0x08, 0x9b, 0xcc, 0xf3, 0x2d, 0x71, 0xb4, 0x1c, 0xe3, 0x04, 0x64, 0x9a, 0x96, 0x5b, + 0x16, 0x58, 0xed, 0xbf, 0x37, 0xd5, 0x2a, 0x6d, 0x80, 0x66, 0xcf, 0xba, 0xd4, 0xb8, 0xd4, 0x38, 0x65, 0x10, 0x57, + 0x3a, 0xe7, 0x93, 0xe0, 0x82, 0x90, 0x78, 0xef, 0xfd, 0xff, 0x96, 0xed, 0x30, 0x44, 0x77, 0x13, 0x3b, 0x70, 0x92, + 0xe8, 0xb0, 0xf4, 0x25, 0x5a, 0xc9, 0xff, 0xff, 0xbb, 0x01, 0x76, 0x03, 0x94, 0x16, 0x20, 0xa5, 0x2d, 0x8a, 0xe2, + 0x56, 0x51, 0xd4, 0xdc, 0x18, 0xcb, 0x99, 0xb3, 0x4e, 0x3b, 0x55, 0x67, 0x5d, 0x64, 0xa3, 0xc4, 0xf8, 0xec, 0x2a, + 0xb7, 0x51, 0x68, 0x6c, 0x7a, 0xd9, 0x85, 0x97, 0x9e, 0xcb, 0x61, 0x26, 0xbe, 0xeb, 0x3a, 0x6b, 0xe5, 0x38, 0xd8, + 0x63, 0xa8, 0xb5, 0xba, 0xbe, 0xbd, 0x1d, 0xe3, 0xc4, 0x11, 0xa3, 0x08, 0xc4, 0xfe, 0x26, 0x3a, 0xfb, 0x01, 0x5d, + 0xb4, 0xfc, 0x1d, 0x78, 0xca, 0xb2, 0xac, 0x61, 0x39, 0x21, 0xe5, 0x6b, 0x5a, 0x70, 0x76, 0x57, 0x91, 0x4c, 0x8c, + 0x3d, 0x0e, 0x50, 0x4e, 0x41, 0x1c, 0xda, 0x62, 0xd2, 0xf1, 0x25, 0xce, 0x03, 0xf4, 0xfa, 0x3b, 0xc1, 0xc4, 0xed, + 0xc1, 0xdf, 0x8f, 0xe1, 0xc0, 0x0e, 0x34, 0x72, 0x3c, 0xb3, 0x3f, 0xfa, 0xc0, 0xe6, 0xcd, 0xf4, 0x01, 0x19, 0xf4, + 0x28, 0x5b, 0xde, 0x02, 0x6e, 0xa2, 0x24, 0x59, 0x76, 0x94, 0x8d, 0x00, 0x35, 0xab, 0xbe, 0xd9, 0xc0, 0xfb, 0xfa, + 0x97, 0x4f, 0x8f, 0x6e, 0xa4, 0x28, 0x0a, 0xfd, 0x43, 0xd9, 0xf2, 0xb2, 0xc2, 0x75, 0x49, 0x97, 0x8c, 0xcd, 0x61, + 0xb3, 0x64, 0x52, 0x1e, 0x46, 0x1e, 0xa2, 0xe0, 0xb5, 0x26, 0xd3, 0xf5, 0x3c, 0x9e, 0x19, 0x32, 0x45, 0xb9, 0x28, + 0xf5, 0x40, 0xcf, 0xa5, 0xf1, 0xcd, 0x8d, 0x29, 0x55, 0xe3, 0x2c, 0x43, 0x12, 0xa2, 0x4d, 0x37, 0xda, 0x94, 0x3e, + 0x49, 0x88, 0x4f, 0x2c, 0xa5, 0x67, 0xbe, 0xb7, 0x75, 0x28, 0xb8, 0x2f, 0x0d, 0x75, 0xce, 0x87, 0x3f, 0xe3, 0x30, + 0x5a, 0x25, 0x92, 0x30, 0xd3, 0x2d, 0x45, 0xca, 0xe5, 0x49, 0xc7, 0x4d, 0x13, 0x95, 0xa5, 0x9f, 0x7f, 0x2d, 0x49, + 0x46, 0x5a, 0x49, 0x11, 0x12, 0xd2, 0xdd, 0x38, 0x3c, 0x31, 0x63, 0x5e, 0xb6, 0xe6, 0xde, 0xcd, 0xcd, 0x6d, 0x67, + 0x46, 0x53, 0x16, 0x86, 0xd4, 0xad, 0x07, 0xac, 0xdb, 0xd7, 0x9f, 0x48, 0xcc, 0xa6, 0x4d, 0x9f, 0x6c, 0x8b, 0xf2, + 0xcb, 0xcd, 0x1b, 0x1e, 0xa9, 0x39, 0x37, 0x4d, 0xcd, 0x80, 0x9b, 0x19, 0x67, 0x64, 0x70, 0xb0, 0x38, 0x00, 0x9f, + 0xab, 0x26, 0xca, 0xb7, 0xbb, 0x55, 0x50, 0xcf, 0x31, 0x65, 0x12, 0xb6, 0x2b, 0x36, 0x9d, 0x17, 0x2b, 0x50, 0xd1, + 0x13, 0x72, 0x80, 0x7f, 0x88, 0x62, 0xe4, 0xee, 0x40, 0xb7, 0x56, 0xd2, 0x66, 0xa3, 0xb0, 0x0e, 0x31, 0xeb, 0x9a, + 0x27, 0xc1, 0xd5, 0x7b, 0x63, 0xb3, 0x84, 0x1b, 0xc8, 0xbf, 0x35, 0x29, 0x8a, 0x3c, 0xd0, 0x66, 0x03, 0x2a, 0x3e, + 0xb9, 0x79, 0x4c, 0x16, 0x01, 0xaa, 0xe8, 0x0e, 0x01, 0x1c, 0x73, 0x05, 0x78, 0x3a, 0x9c, 0x23, 0xb8, 0x18, 0x78, + 0xc5, 0xcd, 0x53, 0x7f, 0xb4, 0x61, 0xb8, 0x11, 0xa4, 0xcd, 0xc6, 0x27, 0x27, 0xb6, 0xae, 0x51, 0x01, 0x1d, 0xec, + 0xe4, 0x6b, 0x99, 0xe4, 0xdb, 0x2d, 0xbb, 0x66, 0x38, 0x54, 0xf5, 0xf2, 0xba, 0xc3, 0x24, 0x41, 0xfa, 0xae, 0x46, + 0x43, 0xdd, 0xfd, 0x9d, 0x0d, 0x52, 0x98, 0x1c, 0x7f, 0xab, 0x29, 0x83, 0x0f, 0xb9, 0x11, 0xe0, 0xd3, 0x49, 0x86, + 0xef, 0xbb, 0xdf, 0x0a, 0x04, 0x76, 0x11, 0x07, 0x48, 0x7f, 0x86, 0x4c, 0x3a, 0x84, 0xf5, 0xb6, 0x32, 0x54, 0x59, + 0xed, 0xd5, 0xf1, 0xf0, 0xf8, 0x39, 0x2d, 0x10, 0x85, 0x11, 0xd2, 0xef, 0x72, 0xcb, 0xd2, 0x8f, 0xf2, 0x2e, 0x7c, + 0x9b, 0x28, 0xa6, 0x07, 0x7f, 0x7a, 0x7c, 0x43, 0x28, 0x0b, 0x3f, 0xe5, 0x98, 0x64, 0x6f, 0x63, 0xad, 0xd9, 0x90, + 0x34, 0x84, 0x30, 0xf9, 0x53, 0x6e, 0xea, 0xe3, 0x5f, 0x36, 0x39, 0xe7, 0x26, 0x49, 0xf0, 0xe9, 0xe7, 0x32, 0x50, + 0x58, 0x41, 0x1e, 0xaa, 0x98, 0x6f, 0x6b, 0xfa, 0x94, 0x4b, 0x20, 0x01, 0x84, 0x2a, 0x32, 0x84, 0x73, 0x5a, 0x39, + 0x4a, 0x57, 0xbc, 0xbd, 0x86, 0xa4, 0xb2, 0x77, 0x99, 0xe5, 0xdd, 0x44, 0x5d, 0xd5, 0xde, 0x5b, 0x94, 0x7e, 0xec, + 0x53, 0xdd, 0x67, 0xb8, 0x8d, 0xbb, 0x1d, 0x65, 0xf4, 0xe8, 0xe4, 0x73, 0x3d, 0xbc, 0xba, 0xd9, 0x30, 0xbe, 0x1f, + 0xeb, 0x0b, 0x21, 0xaf, 0x24, 0x9a, 0x44, 0xa2, 0x0a, 0xbf, 0xfe, 0xfa, 0x06, 0x14, 0x59, 0x76, 0x6d, 0x97, 0x7e, + 0xe0, 0x70, 0x8c, 0x41, 0x28, 0xac, 0x0b, 0xad, 0x4b, 0xb5, 0xbb, 0x54, 0xeb, 0x0d, 0x05, 0x24, 0xc7, 0xad, 0x04, + 0xfb, 0x9b, 0x12, 0x44, 0xec, 0x20, 0x03, 0xff, 0xba, 0x91, 0xa0, 0x50, 0xba, 0x24, 0xed, 0x9c, 0x96, 0x7e, 0xef, + 0x6f, 0x24, 0x6c, 0xd1, 0x8c, 0x55, 0xe9, 0x0f, 0x8c, 0x2f, 0x8b, 0x62, 0x44, 0x3c, 0x1b, 0x46, 0x3b, 0x49, 0x99, + 0xdc, 0xd6, 0x7a, 0x70, 0x5d, 0xe5, 0x2a, 0x62, 0x2e, 0x54, 0xab, 0x44, 0xf2, 0xf4, 0x61, 0xb2, 0xd8, 0x07, 0x8b, + 0x01, 0x3e, 0x04, 0x19, 0xe1, 0x5d, 0x8e, 0x2a, 0xff, 0x86, 0xa3, 0x59, 0xe5, 0xcc, 0x8d, 0xd3, 0x51, 0x6f, 0xc1, + 0x15, 0x9f, 0x37, 0x73, 0x3d, 0x49, 0x99, 0xca, 0x53, 0x3a, 0x96, 0x0c, 0x92, 0x2b, 0x8b, 0xde, 0x08, 0x68, 0x52, + 0x87, 0x31, 0xb2, 0x68, 0x81, 0xb1, 0xe9, 0x9f, 0x78, 0xf1, 0x22, 0xe8, 0x84, 0x48, 0xdb, 0x49, 0x4d, 0xd2, 0xea, + 0x80, 0x1f, 0xec, 0x50, 0x77, 0x66, 0xe7, 0x13, 0x36, 0x02, 0x85, 0x6f, 0xdd, 0x68, 0xe0, 0x4b, 0x6c, 0x5b, 0xbe, + 0x18, 0xca, 0xaf, 0x92, 0x97, 0xdd, 0x4e, 0x90, 0x28, 0x4e, 0x48, 0x42, 0x62, 0xc3, 0xf1, 0xf7, 0x71, 0x59, 0x2b, + 0x24, 0x2e, 0x4b, 0xf1, 0x52, 0x2d, 0x7b, 0xbf, 0x8f, 0x5d, 0x1a, 0x29, 0x6b, 0xdd, 0xed, 0x8b, 0x0d, 0xa3, 0xaf, + 0x1a, 0x94, 0x32, 0xc4, 0x54, 0x3d, 0xa1, 0xee, 0x41, 0x42, 0x00, 0xc3, 0xc2, 0x23, 0x57, 0x52, 0x9c, 0x48, 0x54, + 0x42, 0x82, 0x61, 0xb1, 0xcb, 0x1b, 0xae, 0x8f, 0xfa, 0x30, 0x6c, 0x00, 0xc4, 0x1b, 0x74, 0x7c, 0x99, 0x51, 0x60, + 0x45, 0x6d, 0x05, 0xe0, 0x44, 0x15, 0x24, 0x98, 0xb1, 0x40, 0x5f, 0xa1, 0x5e, 0x43, 0x55, 0xae, 0x10, 0xbd, 0x9d, + 0x80, 0x41, 0x6e, 0x35, 0x5d, 0xe8, 0xb2, 0x34, 0x7a, 0x1b, 0xb8, 0x29, 0xad, 0x6d, 0xd3, 0xb4, 0x4f, 0x32, 0x0e, + 0x4e, 0xd7, 0xb3, 0x98, 0x12, 0x37, 0xd4, 0x5c, 0x19, 0xbd, 0x26, 0xaa, 0xbb, 0x5b, 0x7d, 0x92, 0xd3, 0xb7, 0xd3, + 0x2e, 0xfa, 0x6e, 0xf6, 0x2b, 0xaa, 0xc4, 0x24, 0x46, 0x6d, 0x58, 0xe9, 0x7e, 0x8d, 0x27, 0x23, 0x14, 0xbc, 0x15, + 0xaf, 0x1b, 0x88, 0x7b, 0xd1, 0x9d, 0xba, 0x9c, 0x08, 0xd2, 0xf9, 0x9b, 0x81, 0xfd, 0xf8, 0x94, 0xc1, 0x0a, 0xf1, + 0xc8, 0xfa, 0x4e, 0x5b, 0x87, 0x86, 0x34, 0xed, 0x92, 0xcf, 0xfd, 0x49, 0xda, 0xd7, 0x25, 0xc9, 0xe3, 0x22, 0xdf, + 0x9e, 0xdd, 0x53, 0x30, 0x15, 0xe0, 0x2c, 0x5a, 0xcf, 0x41, 0xb3, 0x0d, 0xa4, 0x3a, 0x7b, 0x70, 0xc8, 0x9e, 0x4e, + 0x6b, 0xa7, 0xeb, 0xa3, 0x55, 0xd5, 0xc6, 0x45, 0x89, 0x21, 0x81, 0x5f, 0xb1, 0x29, 0x01, 0xe4, 0x40, 0xe4, 0xd1, + 0x6b, 0xe3, 0x4b, 0xe1, 0xfa, 0xf5, 0x12, 0x7d, 0x82, 0x59, 0xb9, 0xfa, 0x07, 0x0d, 0xa9, 0xa4, 0xd5, 0x80, 0x90, + 0x91, 0xfa, 0x8c, 0xf2, 0xcc, 0x0a, 0xee, 0x97, 0xce, 0x17, 0x31, 0x3a, 0x3c, 0xfd, 0x6e, 0x3f, 0x34, 0xf6, 0x2d, + 0x94, 0x17, 0x65, 0xa5, 0x32, 0x73, 0x94, 0x13, 0x80, 0x24, 0x96, 0x3c, 0x25, 0xd2, 0xc6, 0xb7, 0xad, 0x2d, 0x11, + 0xc1, 0x37, 0x7c, 0x88, 0x77, 0xee, 0x05, 0xc7, 0x26, 0x21, 0x81, 0x0a, 0xed, 0x76, 0x01, 0x14, 0x54, 0x90, 0x89, + 0x23, 0xc9, 0xd5, 0xd1, 0x20, 0xb1, 0x3f, 0x56, 0x36, 0x1d, 0x3c, 0x22, 0x92, 0xb5, 0xcd, 0x06, 0xd0, 0x91, 0xc6, + 0xab, 0x4a, 0x92, 0x83, 0x08, 0x4b, 0x00, 0x3a, 0x56, 0xfe, 0x49, 0x4a, 0x5c, 0x4e, 0xd0, 0x85, 0x41, 0xc1, 0x5d, + 0x1a, 0xc6, 0xcd, 0x26, 0xb9, 0xb0, 0x52, 0x01, 0xfd, 0x78, 0xe8, 0xc7, 0x63, 0x0f, 0x45, 0x0a, 0x82, 0x56, 0x88, + 0x87, 0x9c, 0xd2, 0x81, 0x22, 0xfa, 0xa5, 0xfe, 0x71, 0x9b, 0x37, 0xbf, 0x26, 0xe6, 0x46, 0x89, 0x8a, 0xe6, 0x3c, + 0xa6, 0x52, 0xd4, 0xc7, 0x88, 0xc1, 0x3f, 0x66, 0xec, 0xd0, 0x61, 0xa2, 0x92, 0x5e, 0xaa, 0x54, 0xac, 0x83, 0x75, + 0x26, 0x95, 0x02, 0xed, 0xd4, 0xf8, 0xe2, 0x9b, 0x48, 0x12, 0xbc, 0x13, 0xb3, 0xce, 0x20, 0x85, 0x97, 0x2a, 0xac, + 0x95, 0xe8, 0x97, 0x2d, 0x0a, 0xa2, 0xc4, 0x35, 0xb4, 0x0e, 0x69, 0x42, 0x11, 0xec, 0x09, 0x1d, 0x94, 0x68, 0xf9, + 0x87, 0xb6, 0xca, 0x48, 0x82, 0x72, 0xdf, 0xf3, 0xc1, 0xbb, 0xcb, 0x80, 0xf4, 0xf0, 0x51, 0x0f, 0x29, 0x24, 0x16, + 0x3e, 0x61, 0xcb, 0x01, 0x5d, 0xb7, 0x41, 0x52, 0x00, 0xef, 0xaa, 0x62, 0x79, 0xd9, 0x2c, 0x88, 0xbb, 0x93, 0x35, + 0x35, 0x63, 0xbf, 0x4c, 0x60, 0xa7, 0x82, 0xa3, 0xd5, 0xb6, 0x09, 0x6b, 0xa9, 0x96, 0x24, 0xa3, 0x63, 0x81, 0x59, + 0x02, 0x89, 0x10, 0xe9, 0xfe, 0x58, 0x9c, 0x03, 0x31, 0xaf, 0x93, 0xcc, 0x80, 0xf3, 0xd4, 0x2a, 0x47, 0x13, 0x28, + 0x1c, 0xc7, 0x72, 0xbe, 0x26, 0x29, 0xc9, 0x13, 0x0e, 0xb0, 0x1a, 0xaf, 0xb0, 0x8e, 0x82, 0xfb, 0xb8, 0xa6, 0xa4, + 0xcc, 0xee, 0x7f, 0x99, 0xd2, 0xc4, 0x60, 0x57, 0xa2, 0x03, 0x12, 0x40, 0x4a, 0xb3, 0xd4, 0x62, 0xf0, 0x79, 0x44, + 0x3c, 0x16, 0x82, 0x89, 0x88, 0x44, 0xe1, 0x2b, 0x5d, 0xcb, 0xcf, 0xbc, 0x04, 0x84, 0xca, 0x4c, 0x83, 0xce, 0x92, + 0xd7, 0xaa, 0xa4, 0x86, 0xf6, 0x1b, 0xed, 0x46, 0x35, 0x2b, 0x9f, 0x14, 0x3e, 0x64, 0x1d, 0xb9, 0x7f, 0x1a, 0x98, + 0x64, 0xbd, 0xc9, 0x29, 0x95, 0x76, 0x96, 0xaf, 0xfe, 0xf5, 0x05, 0x8a, 0x8d, 0xaa, 0xa3, 0xe9, 0xb6, 0x3e, 0xda, + 0x10, 0x75, 0xf6, 0x11, 0x71, 0xc0, 0x13, 0x56, 0x33, 0x97, 0x5f, 0x65, 0xf8, 0xe1, 0x32, 0x39, 0x20, 0x45, 0x73, + 0x66, 0xa2, 0x6b, 0xfa, 0xef, 0x22, 0x39, 0x70, 0x89, 0xad, 0xc0, 0x14, 0x50, 0x46, 0x15, 0x63, 0x64, 0x39, 0x90, + 0xc4, 0x52, 0xc9, 0xe5, 0x7c, 0x84, 0x16, 0x59, 0x57, 0x4e, 0x19, 0x0a, 0x95, 0xd3, 0xc8, 0x1c, 0x36, 0x29, 0x8e, + 0x61, 0x5e, 0x96, 0xea, 0x79, 0x86, 0x90, 0x26, 0xdd, 0xd5, 0xa7, 0x88, 0x42, 0xcd, 0xaa, 0x7d, 0x17, 0xa6, 0xbe, + 0x08, 0x57, 0x85, 0x01, 0xf2, 0xfc, 0x61, 0x2d, 0xb2, 0xce, 0xa4, 0xf1, 0x62, 0x67, 0xbc, 0xa0, 0xb2, 0x61, 0x24, + 0x59, 0x96, 0x38, 0x28, 0x41, 0xe0, 0x94, 0x90, 0xc6, 0x3e, 0x71, 0xb8, 0x2d, 0x3f, 0x1e, 0x33, 0xb7, 0xe9, 0x50, + 0x46, 0x31, 0xe2, 0xea, 0x49, 0x95, 0x75, 0x0d, 0xe7, 0x21, 0xe6, 0x0f, 0x2f, 0x8b, 0xda, 0x6f, 0xba, 0xd2, 0xe8, + 0xc6, 0xa1, 0xb3, 0x02, 0x6d, 0x4f, 0x27, 0x73, 0x3a, 0x7d, 0x11, 0x57, 0x75, 0x52, 0x10, 0x50, 0x04, 0xc2, 0x1e, + 0x8f, 0xfe, 0x51, 0x1a, 0xed, 0x1f, 0x01, 0x4b, 0xd6, 0x31, 0xd8, 0x93, 0x6a, 0x8f, 0x09, 0x49, 0xcb, 0xdb, 0x1f, + 0x81, 0xb9, 0x52, 0x25, 0xd1, 0x43, 0xf0, 0xe1, 0x08, 0xa5, 0x05, 0x85, 0x64, 0xd3, 0x93, 0x6e, 0x43, 0xa6, 0x09, + 0x98, 0xe8, 0x71, 0x90, 0x67, 0xc3, 0x1b, 0x17, 0x55, 0x88, 0x3e, 0x3e, 0x30, 0xd9, 0xa5, 0x63, 0xb4, 0x69, 0x95, + 0x6d, 0xf6, 0x9f, 0xa1, 0xd8, 0xef, 0xf7, 0xd7, 0xcc, 0xa1, 0x48, 0xef, 0x3a, 0x23, 0x37, 0xb1, 0xe0, 0xfc, 0x14, + 0x25, 0xc6, 0xb3, 0xb6, 0x34, 0xa4, 0xe5, 0x10, 0x45, 0x48, 0x0e, 0x1d, 0x82, 0xbe, 0x0c, 0x19, 0x56, 0x57, 0xe8, + 0xf0, 0x2d, 0xfd, 0x82, 0x43, 0x26, 0x29, 0x39, 0xd2, 0x64, 0xbf, 0x97, 0xc4, 0x64, 0x57, 0xba, 0xa8, 0x40, 0x87, + 0xd5, 0xb4, 0x13, 0x43, 0xb2, 0xd5, 0xbb, 0xda, 0x66, 0xa9, 0xe5, 0x08, 0xee, 0xce, 0x03, 0xc9, 0x1f, 0x81, 0xaa, + 0xe7, 0xd1, 0x19, 0x47, 0x0b, 0x44, 0x9d, 0x4b, 0x92, 0xdb, 0x49, 0x31, 0xc8, 0x26, 0x52, 0x28, 0x90, 0xae, 0x10, + 0x8d, 0x61, 0x31, 0x6d, 0x3f, 0x08, 0x1c, 0x2c, 0x75, 0x9b, 0x64, 0xa4, 0xcf, 0x9d, 0xdd, 0x26, 0xc5, 0x23, 0x54, + 0x1e, 0xb5, 0xee, 0xbb, 0x69, 0x49, 0x90, 0xea, 0x24, 0x4f, 0x10, 0xb4, 0x67, 0x63, 0xef, 0x98, 0x80, 0xf9, 0xde, + 0x54, 0xcc, 0xaf, 0xa7, 0x6e, 0xc2, 0xc2, 0xee, 0x43, 0x8a, 0x5b, 0x66, 0x76, 0xf2, 0x9d, 0xf9, 0x1c, 0x69, 0xce, + 0x0c, 0x9d, 0xd4, 0x29, 0x24, 0xb3, 0xb1, 0xb7, 0xf4, 0x17, 0xa4, 0x79, 0x77, 0x2f, 0x3a, 0x94, 0x4d, 0xf8, 0x3d, + 0x21, 0xb8, 0x1e, 0x91, 0xc3, 0x08, 0xbe, 0xea, 0x90, 0xd8, 0xcd, 0x46, 0x2b, 0x52, 0x68, 0xed, 0x68, 0x88, 0x4b, + 0xb6, 0x7b, 0x37, 0x0b, 0x00, 0x88, 0x90, 0xd3, 0xef, 0x95, 0x86, 0x8c, 0x2d, 0xfd, 0xe2, 0x8c, 0xad, 0x14, 0xe8, + 0x59, 0x2d, 0xe2, 0x09, 0xaf, 0x09, 0x29, 0x41, 0x67, 0x85, 0x63, 0x86, 0xea, 0x43, 0xd0, 0xce, 0x1b, 0x4a, 0xb6, + 0x74, 0x30, 0x9c, 0xb8, 0x86, 0x92, 0x2d, 0x8c, 0x18, 0x1f, 0xba, 0xd9, 0x7b, 0x5a, 0x24, 0x43, 0xc1, 0x8f, 0x54, + 0x11, 0xe5, 0x22, 0x6a, 0x46, 0x68, 0x6c, 0x69, 0x30, 0x8a, 0x36, 0x9c, 0x9b, 0x77, 0x57, 0x04, 0x71, 0xd9, 0x27, + 0x56, 0x52, 0xc4, 0x8f, 0x83, 0xc4, 0xe9, 0x57, 0xab, 0x11, 0xf4, 0x32, 0x67, 0xa1, 0x34, 0xbe, 0x29, 0x85, 0x79, + 0xe4, 0x81, 0xc1, 0xb2, 0xb5, 0x0d, 0xd3, 0x3e, 0x69, 0x59, 0x3d, 0x5f, 0x55, 0x03, 0xdb, 0x20, 0x1c, 0xb5, 0x2c, + 0x1d, 0xeb, 0xe7, 0xb3, 0x8a, 0x5e, 0x37, 0xf2, 0xaf, 0x56, 0xac, 0xc5, 0x17, 0x20, 0x3b, 0x63, 0x98, 0xcd, 0x98, + 0x34, 0x2a, 0xa0, 0x16, 0x92, 0x29, 0x6b, 0x8b, 0x8a, 0xa7, 0x49, 0x09, 0x1b, 0x1a, 0x70, 0x34, 0x2d, 0x0b, 0xe9, + 0xc5, 0xeb, 0xa1, 0x7d, 0x70, 0xd6, 0xe1, 0x73, 0xcb, 0xd2, 0x23, 0x58, 0x0d, 0x78, 0x8d, 0x88, 0x12, 0x44, 0x2a, + 0xa4, 0x44, 0x85, 0x94, 0x43, 0x15, 0xd3, 0x41, 0xa7, 0x5c, 0x53, 0x67, 0xa5, 0x95, 0x79, 0x97, 0xc6, 0xf8, 0xd3, + 0x22, 0xa4, 0xb0, 0xae, 0x80, 0xc1, 0xa2, 0xf8, 0x0d, 0x04, 0xc0, 0x8b, 0x35, 0xd3, 0x33, 0x31, 0x30, 0xc7, 0x4b, + 0x5a, 0xde, 0x4b, 0x13, 0x66, 0xb1, 0x74, 0x63, 0x53, 0xa8, 0x8f, 0x8c, 0x42, 0x7a, 0xce, 0x25, 0x20, 0xea, 0xa6, + 0xc7, 0x97, 0xd9, 0x7a, 0xcf, 0xb8, 0x24, 0xff, 0x75, 0xbe, 0xdd, 0x9b, 0x15, 0x0e, 0xcf, 0x3d, 0x72, 0x38, 0x70, + 0x06, 0xa9, 0x48, 0x63, 0x06, 0x39, 0x05, 0x2f, 0x7a, 0x85, 0x19, 0x7f, 0xa4, 0x2b, 0x59, 0x22, 0x0a, 0x4f, 0x00, + 0x7f, 0x57, 0x2d, 0x42, 0xb7, 0x07, 0x84, 0xef, 0x42, 0xc6, 0x67, 0x35, 0x4c, 0xf2, 0x47, 0x18, 0x23, 0xf1, 0xe5, + 0x7b, 0x70, 0x53, 0x99, 0x8c, 0x6f, 0x7e, 0xcb, 0x92, 0x40, 0x65, 0x19, 0x4c, 0x53, 0x83, 0x92, 0x3a, 0xfb, 0x04, + 0x79, 0xe4, 0xbc, 0xaa, 0x1b, 0xa6, 0x4e, 0x9a, 0x49, 0x1e, 0xf4, 0x41, 0xa6, 0x08, 0x44, 0xa7, 0x8b, 0x61, 0xe4, + 0x81, 0x10, 0x00, 0xcf, 0x11, 0x88, 0xb4, 0x04, 0xce, 0x00, 0x8e, 0xe9, 0x9c, 0x0c, 0x1a, 0x91, 0xd1, 0xf8, 0xa9, + 0x51, 0x84, 0x8a, 0x54, 0xae, 0x63, 0xc7, 0xe1, 0x68, 0x89, 0x68, 0x94, 0xdf, 0x40, 0x31, 0x05, 0xff, 0xd2, 0xb8, + 0xb5, 0x69, 0xd7, 0x7b, 0xe2, 0x19, 0xc6, 0x96, 0xa6, 0x99, 0xa6, 0x45, 0xd1, 0x48, 0xdd, 0x67, 0x0c, 0x57, 0x2c, + 0x41, 0x9b, 0x84, 0xa2, 0x0c, 0xa3, 0x3a, 0xa6, 0x4a, 0x71, 0x0b, 0x47, 0x68, 0x54, 0xbe, 0xb5, 0x08, 0xed, 0xfd, + 0xc4, 0xf1, 0xe9, 0x32, 0x42, 0x5a, 0x9f, 0x1f, 0xbd, 0x2c, 0x30, 0xfd, 0x32, 0x9c, 0xa1, 0xaf, 0x44, 0x44, 0x34, + 0xad, 0x02, 0x3b, 0x1c, 0xe8, 0x6a, 0xc3, 0x4b, 0x73, 0x17, 0xb7, 0x35, 0xd1, 0x83, 0x33, 0xf6, 0x54, 0x86, 0xf4, + 0xed, 0x99, 0xc8, 0xba, 0x28, 0xea, 0xf6, 0xb7, 0x93, 0xaf, 0xe1, 0xb1, 0xf9, 0x78, 0x4c, 0xea, 0x14, 0xe5, 0x6b, + 0xa2, 0xd6, 0xea, 0x5a, 0x1f, 0x82, 0x99, 0x79, 0xf4, 0x5c, 0x31, 0x19, 0xe3, 0xd4, 0x8c, 0x8c, 0xac, 0xef, 0x59, + 0xe2, 0xc5, 0x36, 0xf1, 0x3b, 0x85, 0xe4, 0x47, 0xc7, 0x19, 0xd2, 0x88, 0x82, 0xa0, 0xca, 0xfc, 0x8a, 0x42, 0x19, + 0x18, 0xe9, 0xe7, 0xb6, 0xf6, 0x03, 0x72, 0xc5, 0x28, 0x96, 0xf1, 0x6c, 0x33, 0x3e, 0xe5, 0xea, 0x1f, 0x57, 0x34, + 0xc8, 0xb2, 0xb4, 0xdf, 0x89, 0xa7, 0x6d, 0x1e, 0xda, 0x66, 0x5e, 0xd9, 0x24, 0x02, 0x78, 0x95, 0x26, 0xd9, 0xf6, + 0x70, 0xaa, 0xf5, 0x47, 0xe0, 0x57, 0x5e, 0x41, 0x80, 0xcb, 0x49, 0x58, 0xb9, 0x8b, 0x02, 0x45, 0xb5, 0x2d, 0xb8, + 0x7c, 0xb0, 0x4b, 0x9f, 0x47, 0xb1, 0x44, 0x36, 0xf7, 0xc0, 0x6c, 0x51, 0x44, 0x78, 0x4a, 0xbd, 0xad, 0x51, 0xef, + 0xdf, 0x4d, 0x11, 0x1f, 0x71, 0x24, 0x77, 0x27, 0xab, 0x6e, 0x1c, 0x95, 0x47, 0x5a, 0x28, 0xfd, 0x00, 0x2f, 0x2e, + 0x9a, 0x4b, 0x97, 0x8a, 0xc7, 0x5e, 0x0a, 0xd9, 0x46, 0xc2, 0xdc, 0x22, 0x4e, 0x6d, 0xfb, 0x6a, 0xf2, 0xfd, 0x5c, + 0xd0, 0x24, 0x31, 0xeb, 0x4b, 0x97, 0xd6, 0x86, 0x4f, 0x32, 0xbd, 0xb3, 0xcf, 0x7a, 0xf6, 0x64, 0xce, 0xe4, 0xc6, + 0xe0, 0x39, 0xa8, 0xfa, 0xbd, 0xfd, 0x94, 0xba, 0x6e, 0x78, 0x94, 0xc4, 0x94, 0x26, 0x7f, 0xe1, 0x4e, 0x92, 0xe9, + 0xae, 0x33, 0x1f, 0x25, 0xdd, 0x37, 0x1c, 0xce, 0xde, 0xdf, 0xc6, 0x5d, 0x81, 0x54, 0x16, 0x1f, 0x43, 0x24, 0x3c, + 0xf1, 0xeb, 0xad, 0x31, 0xe0, 0xa1, 0x40, 0xc0, 0x83, 0x4a, 0xba, 0x59, 0xac, 0x15, 0x1d, 0xe7, 0x74, 0xff, 0x66, + 0x13, 0xce, 0x0a, 0xc3, 0x93, 0x1c, 0x27, 0xb1, 0xcb, 0xab, 0xdc, 0x4e, 0xa5, 0xad, 0x7e, 0x9a, 0x6c, 0xc0, 0x5b, + 0x68, 0x43, 0xd1, 0x72, 0x7c, 0x86, 0x5d, 0xf5, 0x43, 0x53, 0xf9, 0x27, 0xa1, 0x14, 0xc7, 0x36, 0x0d, 0x63, 0x0d, + 0xb9, 0xfe, 0xbe, 0x1d, 0xc8, 0xb4, 0x7a, 0xf3, 0xcf, 0xd9, 0xf7, 0xea, 0xed, 0x58, 0x37, 0x3c, 0x91, 0xde, 0x0e, + 0xfe, 0x7a, 0x48, 0x8a, 0x62, 0x79, 0x7b, 0x55, 0xfd, 0xd7, 0x16, 0xbf, 0x7e, 0xac, 0x2e, 0x6b, 0x2c, 0x96, 0xf9, + 0xf8, 0xb2, 0x1a, 0x4f, 0x2d, 0xef, 0xdf, 0x4e, 0xf5, 0x87, 0x2f, 0x3f, 0xf5, 0x1a, 0xb8, 0x3a, 0x73, 0x9e, 0xa4, + 0x57, 0x14, 0xfb, 0x28, 0x57, 0xc1, 0x4b, 0xf8, 0x20, 0x3f, 0x6d, 0x8f, 0xeb, 0xa7, 0xfb, 0x65, 0x3d, 0x1f, 0x68, + 0xc9, 0xe3, 0x66, 0xeb, 0xed, 0x8d, 0xe6, 0xd5, 0x5e, 0xa6, 0x75, 0x0e, 0x1b, 0x13, 0x7c, 0x28, 0xb7, 0x8a, 0x82, + 0xf1, 0x26, 0x20, 0xf9, 0x03, 0x31, 0xaf, 0xde, 0x36, 0xbb, 0xbe, 0xfc, 0x58, 0xac, 0x59, 0x5e, 0x61, 0x01, 0x96, + 0x35, 0x1a, 0x9a, 0xb3, 0x01, 0x67, 0x49, 0x7a, 0xaf, 0xae, 0xce, 0x9c, 0xe0, 0x9c, 0xc9, 0xed, 0x4d, 0xfc, 0xc7, + 0x4f, 0x53, 0x6d, 0xce, 0x32, 0xcb, 0xe1, 0x2f, 0x8e, 0x62, 0x67, 0x71, 0xd8, 0x6e, 0xc0, 0xfa, 0xaa, 0xe3, 0x2d, + 0xaa, 0xca, 0x56, 0xe7, 0x62, 0x26, 0x4b, 0x44, 0xe5, 0x76, 0xd2, 0xe1, 0x40, 0x37, 0x73, 0x6b, 0x1f, 0xf8, 0xdf, + 0x63, 0x17, 0x2a, 0x9d, 0xc2, 0x3f, 0x97, 0x47, 0x05, 0x17, 0x72, 0x9b, 0x6c, 0x2e, 0xb9, 0x91, 0xee, 0x58, 0x9f, + 0xbc, 0xb1, 0x33, 0x13, 0x46, 0x33, 0x11, 0x56, 0xd8, 0x0f, 0x47, 0xc0, 0x3d, 0x2e, 0x18, 0x7b, 0x2e, 0xfc, 0xd6, + 0xc5, 0x96, 0xbd, 0x77, 0x7d, 0x36, 0xf9, 0x28, 0x64, 0x01, 0xfb, 0x0d, 0x81, 0x1d, 0x68, 0xdc, 0x1c, 0x47, 0x3b, + 0x24, 0xeb, 0x08, 0xe6, 0xa2, 0x5b, 0xc9, 0x56, 0x06, 0xbf, 0x55, 0xb8, 0x9f, 0xdc, 0x05, 0x20, 0x69, 0xf5, 0xee, + 0xc7, 0x5e, 0xdf, 0x7f, 0xd5, 0xcf, 0x5b, 0xbd, 0xca, 0xd8, 0x3e, 0x19, 0xf8, 0x48, 0x83, 0xae, 0x77, 0xc3, 0xcb, + 0x95, 0x6a, 0xa2, 0xcd, 0xb8, 0x59, 0x5e, 0xc9, 0xe8, 0x0d, 0xe9, 0xda, 0xee, 0x3c, 0x54, 0x27, 0x37, 0x5e, 0xb6, + 0x4c, 0x06, 0x78, 0x55, 0x67, 0x33, 0xf9, 0x05, 0x62, 0x7d, 0x7d, 0xd3, 0xc5, 0x16, 0x9a, 0xd9, 0x23, 0xd4, 0x09, + 0x7f, 0xbd, 0x8c, 0xa2, 0x52, 0x24, 0x7a, 0x69, 0x76, 0xf2, 0x78, 0xde, 0x1b, 0x6f, 0x0c, 0x59, 0x4e, 0xa6, 0x87, + 0x20, 0xd1, 0x47, 0xf4, 0x92, 0xc5, 0xf5, 0x0e, 0x83, 0xcf, 0x73, 0x94, 0x66, 0xd9, 0xb0, 0xf2, 0x45, 0xfc, 0x8c, + 0x8e, 0x25, 0x1b, 0xf9, 0xb8, 0x85, 0xad, 0x64, 0xd9, 0xe6, 0x7e, 0xd7, 0x3b, 0x5b, 0xd5, 0xcb, 0x37, 0x1b, 0xcb, + 0xee, 0x58, 0x79, 0x7e, 0x51, 0xa9, 0x59, 0x0a, 0x3b, 0x7c, 0x8e, 0x47, 0x6b, 0xc1, 0x92, 0xdb, 0xbc, 0x1c, 0x21, + 0xbd, 0x97, 0xa4, 0xbf, 0x76, 0xb2, 0x14, 0xc9, 0x87, 0xb2, 0xac, 0xf2, 0x1f, 0x92, 0x24, 0x62, 0x55, 0x14, 0xa6, + 0x1c, 0x64, 0x9e, 0x7c, 0x28, 0x37, 0xbd, 0x78, 0xe7, 0xab, 0xc2, 0x4f, 0x75, 0xd8, 0x4b, 0xeb, 0x37, 0x20, 0x0a, + 0x66, 0x01, 0x7c, 0x21, 0xee, 0x45, 0xb3, 0xeb, 0x99, 0x3c, 0x06, 0x09, 0xf4, 0x39, 0xf0, 0x0f, 0x3e, 0xfa, 0x01, + 0x05, 0x9f, 0x8b, 0x0e, 0x8c, 0x00, 0x00, 0x72, 0xc7, 0xa1, 0xec, 0xf9, 0xbd, 0x29, 0x57, 0x28, 0xab, 0xa1, 0x5a, + 0x9b, 0xfc, 0x49, 0x73, 0x1d, 0x09, 0xce, 0x7b, 0xa4, 0xc8, 0x3f, 0xf1, 0x7a, 0x36, 0x69, 0x1a, 0x62, 0x17, 0x15, + 0x0a, 0x4c, 0x54, 0xe9, 0x24, 0x4f, 0x95, 0x2c, 0xb5, 0x2a, 0x59, 0xa3, 0x07, 0x1f, 0x76, 0xeb, 0xb5, 0x79, 0x55, + 0x1e, 0x92, 0x9f, 0x25, 0x10, 0xa6, 0x7f, 0xa5, 0x65, 0xb9, 0xbd, 0xa1, 0x4a, 0xe5, 0x20, 0x0f, 0xc1, 0x23, 0xab, + 0xfc, 0xba, 0x7c, 0xe2, 0xc1, 0x8d, 0x28, 0x7f, 0xa5, 0xcf, 0x21, 0xa8, 0x04, 0xfe, 0x5b, 0x36, 0x9b, 0xbe, 0xf2, + 0xf9, 0xf6, 0xc7, 0x73, 0x41, 0x04, 0x4f, 0x97, 0xec, 0x0d, 0xe6, 0x6a, 0x6f, 0x50, 0x9a, 0xac, 0x7b, 0x67, 0x43, + 0xcc, 0x05, 0xcb, 0x1f, 0xd1, 0xe6, 0xdf, 0x27, 0xdf, 0xec, 0x85, 0x96, 0x40, 0xd3, 0x7a, 0x0d, 0xd0, 0x32, 0xcf, + 0x3b, 0x32, 0xd8, 0x23, 0xef, 0x52, 0x1e, 0x56, 0x1c, 0x57, 0xbd, 0xe4, 0xef, 0xe1, 0x2d, 0xdc, 0x69, 0x1b, 0x29, + 0x12, 0xf5, 0x3a, 0xab, 0x14, 0x81, 0xf3, 0x1e, 0xbe, 0x9a, 0xf3, 0x74, 0xaa, 0x21, 0xf1, 0x4b, 0xc7, 0xdc, 0x86, + 0x0a, 0xeb, 0x65, 0x31, 0xbb, 0xbf, 0x7b, 0x83, 0xdc, 0x12, 0x9b, 0xdf, 0x3b, 0x6f, 0x01, 0x48, 0xb5, 0xfa, 0x94, + 0xb2, 0x23, 0xff, 0x51, 0xaa, 0x1d, 0xf0, 0x2a, 0x1f, 0xa8, 0xa0, 0x1a, 0x33, 0xa4, 0xb4, 0xe2, 0xaa, 0x13, 0x49, + 0xd0, 0xdb, 0xa2, 0x64, 0xb0, 0x91, 0x29, 0xec, 0x43, 0x5e, 0x94, 0xe8, 0xfb, 0x52, 0xc9, 0x72, 0x0b, 0xaf, 0x1c, + 0xcb, 0x5a, 0xee, 0x1b, 0x25, 0x7e, 0x98, 0x20, 0x3f, 0x0d, 0x2f, 0x32, 0xf2, 0xe4, 0x6e, 0x71, 0x24, 0xf0, 0xf1, + 0x49, 0x26, 0x82, 0x7d, 0x03, 0x79, 0x92, 0x5c, 0x3c, 0x89, 0x44, 0x65, 0x62, 0xbb, 0xe4, 0xe8, 0xe8, 0xde, 0x24, + 0xa9, 0xd7, 0xd2, 0xe8, 0x50, 0xe8, 0xb8, 0x8d, 0x42, 0x6d, 0x1d, 0xcf, 0xd9, 0x94, 0x8d, 0x47, 0x77, 0xc9, 0x76, + 0x11, 0xb7, 0x87, 0xd2, 0x08, 0x95, 0xd4, 0x26, 0xe8, 0x58, 0x9a, 0x06, 0x91, 0xc7, 0x03, 0x5b, 0x84, 0x88, 0x3e, + 0x9b, 0x4a, 0x67, 0x39, 0xc4, 0x6a, 0x3b, 0x1f, 0x58, 0x8e, 0x2d, 0x87, 0x2c, 0x09, 0x28, 0x9a, 0x95, 0x22, 0xe1, + 0x60, 0xe0, 0x38, 0x9a, 0xa3, 0x4a, 0x81, 0x31, 0x73, 0x35, 0x87, 0x9d, 0xaf, 0x33, 0x72, 0x2c, 0x8d, 0x34, 0x1b, + 0xbe, 0x2e, 0x45, 0x77, 0x6b, 0xeb, 0x63, 0x6d, 0x44, 0x32, 0xb2, 0xc9, 0x9e, 0xcb, 0xdc, 0x13, 0x56, 0xe9, 0xa9, + 0xdc, 0x8d, 0x95, 0x94, 0x15, 0xe7, 0xf9, 0x64, 0xb4, 0xdb, 0x90, 0x45, 0xab, 0x06, 0xab, 0xf1, 0x25, 0xd3, 0xee, + 0xb3, 0x2f, 0xb5, 0x0d, 0xda, 0x6a, 0x52, 0x17, 0x68, 0xce, 0xc3, 0xba, 0xc2, 0xdf, 0xc8, 0x13, 0x38, 0x93, 0x9e, + 0xbd, 0xea, 0x2e, 0x3f, 0xaa, 0x51, 0xda, 0xee, 0x2d, 0x5c, 0x31, 0x8f, 0xb9, 0x0a, 0xc1, 0xae, 0xd5, 0x44, 0x4f, + 0x66, 0x90, 0xc3, 0xf7, 0x04, 0x5c, 0x8e, 0xfc, 0x66, 0x80, 0xed, 0xd9, 0x38, 0x97, 0xb4, 0x53, 0xde, 0x27, 0x2d, + 0x45, 0x7e, 0xc6, 0x4d, 0xd6, 0x9c, 0x28, 0xff, 0x97, 0x42, 0xac, 0xb2, 0xe0, 0x0b, 0xeb, 0x23, 0xeb, 0xef, 0xd1, + 0xa9, 0x4e, 0x79, 0xeb, 0xd5, 0x8c, 0x7e, 0x2b, 0x2a, 0x4c, 0xd8, 0x3b, 0xa0, 0xc1, 0x64, 0xc7, 0x5a, 0x0d, 0xed, + 0x6e, 0xd9, 0xd1, 0xa2, 0x38, 0x6d, 0xcf, 0x68, 0x55, 0x7b, 0x21, 0x23, 0x1e, 0x7e, 0x6e, 0x34, 0x12, 0x8b, 0xa4, + 0x58, 0x40, 0xe7, 0x2b, 0xea, 0xfd, 0xb5, 0x9c, 0xd3, 0x93, 0xd6, 0x64, 0xf0, 0xa8, 0x33, 0xa7, 0xce, 0x55, 0x9f, + 0xbd, 0xde, 0xd5, 0xa1, 0xf2, 0x27, 0xe2, 0xfa, 0x13, 0x34, 0x55, 0x55, 0x73, 0xd7, 0x4a, 0x90, 0x9a, 0xb2, 0x6c, + 0xe2, 0xc8, 0xa7, 0xc9, 0xf7, 0xf9, 0x4c, 0x87, 0xec, 0xc3, 0x75, 0xa8, 0x8a, 0x17, 0x23, 0xb1, 0x05, 0x21, 0xb9, + 0x70, 0x82, 0x65, 0x51, 0xdf, 0x63, 0x29, 0x8a, 0xa3, 0x84, 0x92, 0xe1, 0x05, 0x8a, 0xc0, 0x51, 0x0b, 0x0c, 0x94, + 0xe4, 0x44, 0x1d, 0x1a, 0xc9, 0xce, 0xd3, 0xc1, 0x8b, 0x4f, 0x6c, 0xf2, 0x2d, 0xa1, 0x43, 0x3a, 0x43, 0x79, 0x05, + 0xdf, 0x8b, 0x77, 0x45, 0x9e, 0x30, 0xd5, 0xd2, 0x31, 0xcf, 0xbd, 0x66, 0xfa, 0xd8, 0x35, 0x78, 0x21, 0xba, 0x0e, + 0x97, 0x67, 0x48, 0x19, 0xab, 0x48, 0xd5, 0x34, 0xed, 0x87, 0x77, 0x87, 0x28, 0x49, 0x55, 0xb6, 0xdb, 0x7b, 0xa7, + 0x2d, 0x44, 0x09, 0xa4, 0xc9, 0xba, 0x0d, 0x8c, 0x64, 0x7a, 0xce, 0xb1, 0x2a, 0x45, 0x31, 0x86, 0x19, 0x82, 0x5c, + 0xe8, 0xb0, 0x15, 0x92, 0x4a, 0x3f, 0xca, 0x22, 0xe8, 0x26, 0x9d, 0xf1, 0x60, 0x96, 0x81, 0x51, 0xe1, 0xf8, 0xa4, + 0xde, 0x85, 0x77, 0x6d, 0x73, 0x72, 0x68, 0x15, 0x69, 0x02, 0x75, 0x2c, 0x90, 0x1e, 0xe5, 0x6f, 0xbe, 0x7b, 0x8c, + 0x6b, 0xd3, 0xaf, 0x8b, 0x55, 0x21, 0x33, 0x76, 0x02, 0x07, 0x90, 0x69, 0xbb, 0xf9, 0x9d, 0x7d, 0xa3, 0x24, 0x05, + 0x23, 0xad, 0xe7, 0x9e, 0x19, 0x5c, 0x8c, 0xc9, 0x42, 0xb4, 0xad, 0x76, 0xd3, 0x47, 0x07, 0x6d, 0x64, 0xe5, 0x35, + 0x00, 0xab, 0x24, 0x2d, 0x39, 0x1b, 0xc0, 0xc2, 0x6a, 0xc7, 0x36, 0x4c, 0x2f, 0x0d, 0x80, 0x4d, 0xbf, 0x05, 0x58, + 0xc0, 0x0b, 0xa2, 0xfd, 0xcc, 0xbc, 0xd2, 0x2c, 0xc2, 0x03, 0xef, 0x13, 0x80, 0x0d, 0xb1, 0x52, 0x51, 0x2d, 0x16, + 0x0b, 0xaf, 0x14, 0x0b, 0x5a, 0xd3, 0xcc, 0x96, 0x4e, 0xc0, 0xfa, 0x72, 0x47, 0xa1, 0x3a, 0xe5, 0xaf, 0xa8, 0xc4, + 0x9a, 0x43, 0x55, 0xf1, 0xd1, 0xfd, 0x66, 0x7d, 0x9a, 0x62, 0x91, 0xda, 0xa7, 0xd6, 0x93, 0x0c, 0xf0, 0x76, 0x8b, + 0xe7, 0xc0, 0x4b, 0xcb, 0xa2, 0xf7, 0xbd, 0x9d, 0xb5, 0x64, 0x51, 0xdd, 0x72, 0x7c, 0xd5, 0xca, 0xe4, 0x74, 0x25, + 0x97, 0x82, 0x32, 0x14, 0xf9, 0x9e, 0x27, 0x49, 0x21, 0xae, 0x4f, 0x1f, 0xcc, 0x7c, 0x84, 0x71, 0x65, 0xc6, 0x8b, + 0x6f, 0xc5, 0xc3, 0x8c, 0x27, 0xa5, 0x48, 0x6a, 0x79, 0x51, 0x01, 0xa9, 0xc6, 0x28, 0xbe, 0x20, 0x43, 0xcf, 0xf9, + 0x7a, 0xd4, 0xa7, 0xb8, 0x73, 0xb6, 0x24, 0x0e, 0xa2, 0x70, 0x98, 0x3e, 0x2b, 0x54, 0x08, 0xfe, 0x3b, 0x20, 0x26, + 0x19, 0x82, 0x00, 0x73, 0x22, 0xa1, 0x0e, 0xb2, 0xf0, 0x84, 0x67, 0x7b, 0xc9, 0x68, 0x25, 0xa3, 0x13, 0xa9, 0x32, + 0x11, 0x51, 0xf9, 0xed, 0xca, 0x26, 0x41, 0xb3, 0xec, 0xa5, 0x28, 0xdc, 0x88, 0x25, 0x11, 0x5c, 0x2b, 0x83, 0x9b, + 0x7e, 0x44, 0xa9, 0xee, 0x96, 0x86, 0xeb, 0x8c, 0x65, 0x72, 0xe1, 0x1b, 0x6f, 0xe8, 0xfa, 0xd2, 0xf5, 0x67, 0xe4, + 0xb6, 0xa8, 0xf0, 0xc9, 0x47, 0xf2, 0xc0, 0xb9, 0xbe, 0x9a, 0x66, 0x3a, 0xc7, 0xbc, 0x8b, 0x50, 0x5c, 0x63, 0xb2, + 0xcd, 0x7e, 0xdb, 0x2c, 0xc1, 0xeb, 0xfc, 0x59, 0xb2, 0x9c, 0xa6, 0x02, 0x76, 0x31, 0xf1, 0x8b, 0xa0, 0x44, 0x1b, + 0xae, 0x7a, 0xff, 0xde, 0xd6, 0x7a, 0xf7, 0xd9, 0x07, 0x1c, 0x16, 0xe5, 0x6f, 0xd4, 0xf6, 0x0c, 0x28, 0xa8, 0xe4, + 0xb9, 0xab, 0xd6, 0x80, 0xb1, 0x1d, 0xc3, 0x0f, 0x51, 0x1f, 0xed, 0x1a, 0xa0, 0xae, 0xd9, 0xca, 0x29, 0x06, 0x63, + 0x00, 0x1d, 0xdd, 0xfa, 0xd4, 0x20, 0x75, 0x58, 0xd8, 0x94, 0xd6, 0xdb, 0x58, 0xbc, 0xd0, 0x22, 0xe6, 0x72, 0x95, + 0x2c, 0xad, 0xf9, 0x1a, 0xfe, 0x37, 0xb8, 0xa6, 0xe0, 0x77, 0x94, 0xbf, 0x92, 0x78, 0xd7, 0x9d, 0xd3, 0x0a, 0x89, + 0x82, 0x79, 0x2e, 0xbc, 0x22, 0xc2, 0x4a, 0x9f, 0x10, 0x73, 0xcc, 0x75, 0x99, 0x93, 0x7d, 0xe1, 0xd0, 0x1a, 0xa9, + 0x43, 0xc0, 0xa5, 0xfb, 0x1e, 0x4f, 0x2f, 0xf8, 0x12, 0x5d, 0x1d, 0xdf, 0xcb, 0x3c, 0x9a, 0x01, 0xab, 0xba, 0x6f, + 0x31, 0x09, 0x53, 0x51, 0x46, 0x09, 0x41, 0xdc, 0x54, 0x22, 0x0b, 0x43, 0xcf, 0x1a, 0x57, 0x1f, 0x3b, 0xad, 0xa7, + 0x0c, 0x00, 0x28, 0x25, 0x09, 0xdd, 0x33, 0x94, 0x31, 0xa3, 0x97, 0x56, 0x81, 0x72, 0xcb, 0xd5, 0xc1, 0xcb, 0xce, + 0x3d, 0x86, 0x81, 0x9d, 0xd9, 0x5a, 0x67, 0x1a, 0x07, 0x22, 0xcb, 0x40, 0x80, 0x38, 0xc8, 0xb7, 0xa9, 0xd2, 0x58, + 0x74, 0x03, 0xd4, 0xb5, 0xa8, 0x0f, 0xd2, 0x8e, 0x2c, 0xc4, 0x19, 0x26, 0x3a, 0x06, 0xf6, 0xa7, 0x97, 0x68, 0xa4, + 0xa4, 0x42, 0xe0, 0x95, 0x31, 0xf3, 0x40, 0x22, 0x7b, 0xa0, 0x1d, 0x94, 0x0d, 0x80, 0x24, 0x67, 0x8e, 0x2b, 0x05, + 0x69, 0x2d, 0x23, 0x96, 0xd0, 0xff, 0x13, 0x2b, 0x8d, 0x32, 0x01, 0xf9, 0xc8, 0xa1, 0x4d, 0x49, 0xe3, 0x79, 0x78, + 0x2d, 0x1c, 0x48, 0x3e, 0x4c, 0x7f, 0x9c, 0x05, 0x8d, 0xc8, 0x94, 0xd3, 0xb9, 0x15, 0x6c, 0xe3, 0x8b, 0x3b, 0x4f, + 0x23, 0x51, 0x61, 0xfa, 0xcc, 0x77, 0x96, 0xb7, 0x93, 0x55, 0x84, 0xe5, 0x8f, 0xb9, 0xec, 0xa7, 0xe8, 0x7f, 0xad, + 0xa2, 0x24, 0xc9, 0x06, 0x5f, 0x2a, 0x99, 0x8e, 0xdb, 0xf3, 0x6b, 0xad, 0x8d, 0x96, 0xee, 0x01, 0xce, 0x78, 0x0f, + 0xaa, 0x3b, 0x12, 0x7a, 0xc8, 0x69, 0xcd, 0x53, 0x87, 0xa8, 0xaa, 0xa0, 0x84, 0x44, 0x63, 0x5c, 0xa4, 0xd6, 0x44, + 0xea, 0x9b, 0xc5, 0xd3, 0x00, 0x92, 0x69, 0x0c, 0x2a, 0xaa, 0xdc, 0x57, 0xcf, 0x6c, 0x5e, 0x4c, 0x4f, 0xa4, 0xc9, + 0x74, 0x41, 0x2e, 0x3f, 0xc5, 0xe8, 0x66, 0x4a, 0xc9, 0xb2, 0x58, 0x86, 0xc3, 0x1e, 0x23, 0xcc, 0x01, 0x43, 0x44, + 0xa5, 0xc2, 0x78, 0xc3, 0xa4, 0xbc, 0x9f, 0x94, 0xfc, 0x69, 0x8a, 0xf3, 0x0b, 0x61, 0xf5, 0x61, 0x5b, 0x07, 0xb8, + 0x3a, 0x07, 0xe5, 0x08, 0xb7, 0x96, 0x07, 0xd8, 0xd8, 0x98, 0x47, 0x7b, 0x39, 0x55, 0x25, 0x22, 0xd3, 0xac, 0x3b, + 0x58, 0x52, 0xde, 0xa4, 0xef, 0x0c, 0x99, 0xb0, 0x75, 0x33, 0x14, 0x41, 0x6e, 0x3c, 0xc9, 0xae, 0xb1, 0xd5, 0x07, + 0x81, 0xb3, 0x7a, 0x44, 0x5e, 0xc6, 0xa3, 0xaa, 0xce, 0xaa, 0xed, 0x94, 0xfc, 0x34, 0xbc, 0x4f, 0xae, 0x3b, 0xc9, + 0x09, 0x95, 0xd5, 0x24, 0x2a, 0x0a, 0x8a, 0xc2, 0xf3, 0x24, 0x20, 0x82, 0xe2, 0x8c, 0xfa, 0xa1, 0x8a, 0xfa, 0xa6, + 0xc8, 0xfe, 0x71, 0xea, 0xd5, 0xbe, 0xe5, 0x25, 0x80, 0x24, 0x3f, 0x93, 0x49, 0x77, 0x8c, 0x7b, 0x67, 0x5d, 0x1e, + 0x3e, 0x4a, 0x14, 0xe6, 0x3e, 0x14, 0x57, 0x31, 0x49, 0x51, 0x2c, 0x01, 0xf7, 0xca, 0x65, 0x2f, 0x1e, 0x49, 0x3a, + 0x41, 0x66, 0xa8, 0x5c, 0x1b, 0xef, 0x9b, 0x7a, 0xa4, 0xea, 0x49, 0x96, 0x02, 0x79, 0xe4, 0xee, 0x77, 0x46, 0x50, + 0xf2, 0xfc, 0xb7, 0xec, 0x8a, 0xd7, 0x49, 0x79, 0x86, 0x36, 0x1b, 0x22, 0x7a, 0x5d, 0x8e, 0x36, 0x05, 0xcc, 0x31, + 0x23, 0x02, 0xfd, 0x73, 0xb0, 0x0a, 0xf9, 0xb2, 0xe3, 0x14, 0xdb, 0x54, 0x01, 0x4a, 0xb9, 0xf7, 0xfd, 0x55, 0x1e, + 0x08, 0xfb, 0x33, 0x92, 0x9c, 0x49, 0x46, 0x44, 0x50, 0xb6, 0xc0, 0x23, 0xb0, 0x2f, 0xa4, 0x8b, 0x13, 0xb5, 0x0a, + 0xfb, 0x82, 0x9a, 0xb3, 0x67, 0xa9, 0x88, 0xea, 0x14, 0x7d, 0xfc, 0xca, 0xb8, 0x60, 0x2e, 0xab, 0x91, 0xb6, 0x22, + 0x5a, 0x9e, 0xaa, 0x04, 0x04, 0xb5, 0x10, 0x0b, 0xb1, 0xa9, 0x80, 0x60, 0x7c, 0xaf, 0xa7, 0x27, 0x18, 0x31, 0x0b, + 0xc5, 0x8b, 0xcc, 0xe5, 0x44, 0xbb, 0xfc, 0x87, 0x9b, 0x30, 0x9d, 0x33, 0x86, 0x34, 0x22, 0xa9, 0x47, 0xd6, 0xef, + 0x5f, 0x2f, 0x2f, 0x54, 0x65, 0x13, 0x49, 0x65, 0x23, 0xee, 0xe6, 0x73, 0x9d, 0x1b, 0xd3, 0x44, 0x70, 0xc5, 0x92, + 0xd9, 0x62, 0xe3, 0xe9, 0xfc, 0xc3, 0x95, 0x59, 0x48, 0xd7, 0x44, 0x39, 0x92, 0xc8, 0x4f, 0x0a, 0xc1, 0x43, 0x8d, + 0xf2, 0x42, 0x18, 0x91, 0xfa, 0x6f, 0x86, 0xdc, 0x75, 0x29, 0xda, 0xd5, 0x46, 0x75, 0xd9, 0x02, 0xd8, 0xd2, 0xd7, + 0x30, 0x32, 0x14, 0x42, 0x47, 0x0c, 0x92, 0xdc, 0xa5, 0x3e, 0x2a, 0x19, 0xc8, 0xa2, 0x2b, 0xcc, 0x40, 0x99, 0x7b, + 0xe8, 0xe4, 0x8d, 0x93, 0x28, 0x01, 0xb9, 0x9f, 0x99, 0x4f, 0xea, 0xec, 0x24, 0xf2, 0x62, 0x2d, 0x05, 0x74, 0xa4, + 0xba, 0x4e, 0x25, 0x56, 0x59, 0xad, 0x84, 0x9e, 0x08, 0x76, 0x17, 0xcd, 0xe0, 0x55, 0x9b, 0xe7, 0xe9, 0xb1, 0xe6, + 0x9f, 0xc7, 0x57, 0x94, 0xb7, 0x35, 0x91, 0x16, 0x74, 0x22, 0xb4, 0xfa, 0xc0, 0x7d, 0xdd, 0x5c, 0xd9, 0x1a, 0xe4, + 0x65, 0x01, 0xd0, 0x72, 0xce, 0x72, 0x7a, 0x12, 0xca, 0xba, 0x79, 0x5e, 0x26, 0x99, 0x7b, 0x15, 0x07, 0x5b, 0x40, + 0x73, 0x01, 0x37, 0xc1, 0x67, 0x1f, 0x27, 0xa4, 0x7e, 0xa8, 0x3c, 0x56, 0x36, 0xdf, 0xd6, 0x60, 0xee, 0xdb, 0xc4, + 0xe9, 0xb0, 0xd9, 0x24, 0x22, 0x26, 0x73, 0x37, 0xb6, 0xde, 0x08, 0x67, 0x2d, 0x54, 0xed, 0x11, 0xf3, 0x84, 0x00, + 0x53, 0xd5, 0x20, 0x7c, 0xda, 0xc7, 0x49, 0x4c, 0x6f, 0x11, 0x15, 0xa0, 0x5c, 0x62, 0x52, 0xaf, 0xdc, 0xa5, 0xa5, + 0xd6, 0xbd, 0x4f, 0x17, 0x58, 0x57, 0xba, 0x78, 0xbc, 0xd3, 0x7d, 0xe0, 0x00, 0x70, 0x3f, 0x83, 0xaa, 0x55, 0x5e, + 0xaa, 0xea, 0x0b, 0x6a, 0x69, 0x82, 0x94, 0x04, 0xbc, 0x55, 0x49, 0xef, 0xe7, 0x99, 0x06, 0x82, 0xe6, 0x6b, 0x64, + 0x75, 0xe4, 0x0b, 0x91, 0xc8, 0x43, 0xcf, 0x4b, 0x7c, 0xbc, 0x08, 0xcf, 0x09, 0x1e, 0xbf, 0x8c, 0xad, 0x0b, 0x3a, + 0x65, 0xfe, 0x20, 0x81, 0xe5, 0x40, 0xed, 0xda, 0xe5, 0xeb, 0x38, 0x11, 0xec, 0x14, 0x05, 0xea, 0x29, 0x2a, 0x40, + 0x83, 0x40, 0xd1, 0x48, 0x0b, 0xe8, 0x24, 0xf9, 0x39, 0xa6, 0x05, 0x84, 0xd4, 0x29, 0x10, 0x31, 0xdf, 0x0e, 0xcb, + 0x11, 0xdc, 0x95, 0x22, 0x27, 0x9e, 0x38, 0x37, 0x6b, 0xe5, 0xcb, 0x7d, 0x88, 0xaa, 0x73, 0x7f, 0x7c, 0x83, 0x3b, + 0x70, 0x15, 0xdb, 0x8d, 0xe3, 0x1f, 0x71, 0xbf, 0x49, 0x16, 0x72, 0x0e, 0x44, 0x8a, 0xbc, 0x1c, 0x11, 0x22, 0x13, + 0x87, 0x3a, 0xdc, 0x84, 0x90, 0x8e, 0x2f, 0xa0, 0x3f, 0x8e, 0x98, 0xc6, 0x56, 0x9d, 0x26, 0x20, 0xe7, 0x3c, 0xbe, + 0x3d, 0x9d, 0xde, 0xba, 0xa8, 0x1e, 0x44, 0xdb, 0x22, 0xe2, 0x87, 0xb6, 0xa8, 0x51, 0xa8, 0x3c, 0x9c, 0x5a, 0x5f, + 0x53, 0xc3, 0x31, 0xc4, 0xe1, 0xdf, 0x06, 0x48, 0x00, 0x85, 0xdd, 0x26, 0xd7, 0x5c, 0xd0, 0xe9, 0x9d, 0xa4, 0x23, + 0xb4, 0xd6, 0xf4, 0x53, 0xb9, 0x6a, 0xd6, 0xc1, 0xca, 0xb4, 0xd3, 0xfb, 0x6c, 0xe3, 0xb6, 0x38, 0x01, 0x41, 0xb4, + 0xd2, 0xeb, 0x9b, 0x30, 0x61, 0x89, 0x31, 0x06, 0xde, 0x17, 0x62, 0xce, 0x53, 0x98, 0x49, 0xcc, 0xc7, 0x70, 0xb4, + 0x3a, 0x8b, 0x77, 0x6e, 0xd1, 0xa5, 0xbd, 0xd1, 0x9b, 0x36, 0x92, 0xa9, 0x84, 0x8e, 0x05, 0xf0, 0xc7, 0xe9, 0xa8, + 0x1d, 0x71, 0x07, 0x04, 0xd8, 0xca, 0x12, 0xe3, 0xd2, 0x2d, 0xd8, 0xaa, 0x6c, 0xf9, 0xb4, 0x71, 0xae, 0xdc, 0xcf, + 0xd6, 0x2e, 0x74, 0x44, 0x70, 0x58, 0x97, 0x34, 0x07, 0xe6, 0x63, 0xc1, 0x5c, 0x8a, 0x8b, 0xd5, 0x4e, 0x01, 0x12, + 0xb4, 0x92, 0x3c, 0x5c, 0x66, 0x48, 0x7a, 0x7c, 0xa2, 0x2e, 0x12, 0x72, 0xc6, 0x8d, 0xb6, 0x06, 0xec, 0xe0, 0xdd, + 0x5e, 0x8f, 0xb4, 0xee, 0xbc, 0x45, 0xde, 0x8b, 0xe2, 0x05, 0xa4, 0x9a, 0x02, 0x71, 0x65, 0x83, 0x20, 0xed, 0x3a, + 0x25, 0xac, 0xbf, 0x19, 0x2c, 0x8d, 0xdb, 0x77, 0x6d, 0x4a, 0x0f, 0x7a, 0x76, 0xa6, 0x87, 0x5c, 0xf8, 0xb3, 0xa2, + 0x6f, 0x5f, 0x79, 0xcb, 0x36, 0xec, 0xa7, 0xa5, 0x00, 0xb2, 0xba, 0xb8, 0x1b, 0xe7, 0xbc, 0x60, 0x8b, 0xa5, 0xc9, + 0xe9, 0xab, 0x65, 0x85, 0x9a, 0xc0, 0x1e, 0x7c, 0xa0, 0x65, 0xa4, 0x52, 0x5f, 0x29, 0x29, 0x5a, 0x1e, 0x1a, 0x93, + 0x6c, 0x6d, 0x6a, 0x85, 0xb8, 0xaa, 0x06, 0xab, 0xea, 0xe1, 0x12, 0x4b, 0x4b, 0xdb, 0x52, 0x0b, 0xcd, 0x75, 0xef, + 0x05, 0x98, 0x7c, 0x8f, 0x1e, 0xb1, 0xbc, 0x00, 0xba, 0xfb, 0x85, 0x7c, 0x19, 0x87, 0x41, 0x5a, 0x54, 0x41, 0x00, + 0xe9, 0x75, 0x1d, 0xc3, 0xa6, 0x61, 0x8d, 0x89, 0x0e, 0x8b, 0x3e, 0x8d, 0x40, 0x45, 0xa8, 0x81, 0x21, 0xd8, 0x42, + 0xae, 0x4c, 0xc5, 0xd2, 0xa9, 0x97, 0xc9, 0xe2, 0xd2, 0xe7, 0x5e, 0x6c, 0x6b, 0xd2, 0x15, 0xb3, 0x54, 0x41, 0x5c, + 0x1a, 0x75, 0xbd, 0xd1, 0x37, 0x6a, 0x79, 0xd0, 0x35, 0xde, 0xe3, 0x26, 0x19, 0xd6, 0xa6, 0x72, 0x7d, 0x94, 0x6c, + 0xb7, 0xff, 0x59, 0xb9, 0x44, 0xb5, 0xe4, 0x2c, 0xad, 0xb1, 0xea, 0x61, 0x8b, 0x02, 0x5c, 0xbe, 0xe3, 0x4e, 0xc6, + 0x00, 0x59, 0x8e, 0xb4, 0x61, 0x6e, 0x1d, 0xce, 0x65, 0x1b, 0x68, 0xfb, 0xcd, 0xa7, 0x92, 0x60, 0xeb, 0x57, 0x4f, + 0xa7, 0xb1, 0x4d, 0x91, 0xc3, 0x28, 0x70, 0x14, 0x9e, 0xbb, 0xe7, 0xbc, 0x5a, 0x29, 0xe3, 0x12, 0xdb, 0xed, 0x73, + 0xd3, 0x4f, 0x5e, 0xd9, 0x86, 0xf5, 0x14, 0x5f, 0x8f, 0x91, 0x8d, 0xbd, 0xe7, 0xc9, 0x7a, 0x32, 0x16, 0x77, 0x0c, + 0x80, 0x8b, 0x92, 0x62, 0xb8, 0x5b, 0xc1, 0xc5, 0x83, 0x5f, 0xf8, 0x7c, 0x2a, 0xa7, 0x9b, 0x34, 0xee, 0xcd, 0xbf, + 0xed, 0xed, 0x85, 0x07, 0xcd, 0x24, 0x7d, 0x99, 0xa5, 0xcb, 0x2a, 0xb9, 0x16, 0xc8, 0xb5, 0x1b, 0x9e, 0x8b, 0x72, + 0xbd, 0x69, 0x6b, 0x23, 0x4c, 0x50, 0xbc, 0x1c, 0xf8, 0xdb, 0xbb, 0xf8, 0xed, 0xb9, 0xec, 0xf9, 0xb9, 0xb7, 0x68, + 0x08, 0x31, 0xdf, 0xbc, 0x8f, 0x2a, 0x2b, 0x8e, 0x63, 0xe2, 0x7d, 0x3e, 0x34, 0xde, 0xeb, 0x1a, 0x2d, 0x63, 0xae, + 0xf0, 0xf3, 0x18, 0xaa, 0xda, 0xc7, 0xcd, 0x2d, 0xff, 0x6c, 0x77, 0x9d, 0x95, 0xa2, 0x30, 0x9b, 0xc9, 0xc3, 0xd2, + 0x94, 0xb4, 0x3b, 0x0e, 0x36, 0xdc, 0x3e, 0x2b, 0x00, 0x73, 0x00, 0x2c, 0x8f, 0x74, 0x7d, 0x16, 0x7b, 0x16, 0xca, + 0xb6, 0x8b, 0x38, 0x54, 0x6f, 0x61, 0x57, 0xdc, 0x9c, 0xe5, 0x59, 0xea, 0x6e, 0xe3, 0x0b, 0x03, 0x54, 0x3d, 0xe4, + 0x8e, 0x39, 0x92, 0x96, 0x09, 0xa6, 0x4a, 0x7e, 0xbb, 0x71, 0xdc, 0xcc, 0x19, 0x3b, 0xf1, 0x0c, 0xb3, 0x79, 0xea, + 0x0d, 0x2b, 0x9a, 0xf7, 0xed, 0x2b, 0xf7, 0x34, 0x30, 0xf1, 0xad, 0x8d, 0xca, 0x54, 0xf6, 0x75, 0x00, 0x94, 0x2c, + 0xd1, 0x9f, 0x76, 0x51, 0x5a, 0x57, 0x08, 0xa3, 0xc2, 0xa9, 0xf2, 0x0f, 0xd6, 0x92, 0x56, 0x31, 0x11, 0x8b, 0xa3, + 0x23, 0xcd, 0x19, 0xe0, 0x96, 0x78, 0xcb, 0xa8, 0x03, 0xc5, 0x98, 0xd1, 0xc6, 0x4c, 0xca, 0x6a, 0x8f, 0x66, 0x07, + 0xc2, 0xc8, 0x73, 0x6d, 0x11, 0xe9, 0x28, 0x60, 0xbd, 0x54, 0x70, 0xe0, 0x37, 0xef, 0x55, 0xa0, 0x79, 0xdf, 0xb3, + 0x01, 0xe5, 0x00, 0xee, 0x37, 0x74, 0x94, 0xd4, 0xa6, 0x8d, 0xbf, 0xe4, 0x8a, 0xd1, 0xd5, 0x83, 0x24, 0xd0, 0x36, + 0x63, 0xc0, 0x07, 0x4c, 0xae, 0xa8, 0x42, 0xfa, 0x34, 0x46, 0xde, 0x28, 0x90, 0x9c, 0x63, 0xd3, 0x50, 0x4c, 0x3b, + 0xac, 0x27, 0x91, 0x94, 0x0e, 0x22, 0x64, 0x8a, 0xc5, 0xf4, 0xa0, 0x0e, 0x96, 0x64, 0xa4, 0x75, 0x2a, 0x6f, 0x45, + 0x47, 0xfd, 0x9e, 0x8d, 0xa0, 0x39, 0xb6, 0xac, 0x2a, 0xd4, 0x37, 0xcb, 0x2d, 0x13, 0x95, 0x74, 0xf3, 0x6c, 0x2a, + 0x1f, 0x97, 0x83, 0xc8, 0xa6, 0x69, 0xc7, 0x6f, 0xfb, 0xbc, 0xc7, 0x0c, 0xee, 0x62, 0x84, 0x82, 0xac, 0x6d, 0xc8, + 0x60, 0x8f, 0x3c, 0x5c, 0xd0, 0x2d, 0xfd, 0x40, 0xa1, 0xdf, 0xae, 0x96, 0x00, 0x7e, 0x4a, 0xe0, 0x2b, 0x41, 0x6f, + 0x37, 0xb9, 0x53, 0xbb, 0xce, 0x3d, 0xef, 0x13, 0xd9, 0x0b, 0x27, 0x0f, 0x92, 0x6d, 0x5b, 0xa2, 0x6d, 0xd5, 0x8d, + 0x5b, 0xfe, 0xb1, 0xc3, 0x4f, 0x4a, 0x53, 0x44, 0xad, 0x49, 0xea, 0xb4, 0xb1, 0xdc, 0x12, 0xb5, 0xa3, 0xc1, 0x51, + 0xba, 0x11, 0x5e, 0xb8, 0xdf, 0x86, 0xfd, 0x86, 0x82, 0xb1, 0x1c, 0xbd, 0x72, 0x17, 0x1d, 0x0b, 0x68, 0x1c, 0x29, + 0xe8, 0xd8, 0x1e, 0x47, 0xb5, 0x31, 0x86, 0x72, 0xcc, 0xde, 0x70, 0x0c, 0x65, 0x35, 0x06, 0x6a, 0x63, 0xeb, 0x26, + 0x74, 0x37, 0x9e, 0x88, 0xe4, 0x30, 0xa0, 0x71, 0x40, 0xea, 0x96, 0x19, 0xa9, 0xfc, 0x3a, 0x27, 0x2c, 0x10, 0x83, + 0x3b, 0xf6, 0x78, 0xa1, 0x7d, 0xc1, 0x30, 0xc4, 0x11, 0xe8, 0x16, 0x8f, 0x62, 0xb6, 0xa8, 0x0c, 0xf5, 0xe2, 0xca, + 0x5a, 0x98, 0xc0, 0xda, 0x11, 0xa2, 0x42, 0x7f, 0x6c, 0xf3, 0x5d, 0x3b, 0x14, 0xe4, 0x8a, 0x1f, 0xc6, 0xfe, 0x32, + 0xfd, 0x68, 0xe4, 0xa9, 0xa4, 0xff, 0x22, 0x8c, 0x7e, 0xea, 0x84, 0x95, 0x13, 0x40, 0xf0, 0x27, 0x48, 0x72, 0xdb, + 0x78, 0x3f, 0x4c, 0x69, 0xa6, 0xff, 0xb1, 0xb1, 0xe9, 0x8a, 0xf7, 0x43, 0x3f, 0xcc, 0x1f, 0x3a, 0x51, 0x07, 0xf9, + 0xa7, 0x5f, 0x3c, 0x74, 0x5c, 0x8f, 0xed, 0x63, 0x4c, 0xdd, 0xb1, 0xa5, 0xf9, 0x78, 0xec, 0xda, 0x4b, 0xb6, 0xdb, + 0xf6, 0xe3, 0xf0, 0x64, 0x78, 0x38, 0x64, 0x43, 0x1a, 0xb8, 0xf7, 0xfc, 0x72, 0x8e, 0x3d, 0x4f, 0xde, 0x3d, 0xf4, + 0xe9, 0x81, 0x9c, 0x8b, 0x94, 0x31, 0xd9, 0x2d, 0x9e, 0xb6, 0x5d, 0xa4, 0x34, 0x02, 0xd4, 0xd1, 0x1b, 0xe1, 0x63, + 0x41, 0xd7, 0x24, 0x55, 0xc8, 0xa0, 0x7c, 0x86, 0x49, 0xa3, 0xea, 0x0f, 0xf1, 0x0c, 0x85, 0x88, 0x83, 0xc0, 0x7f, + 0xf9, 0x67, 0x8f, 0xd6, 0x13, 0xa7, 0xd5, 0x69, 0x6d, 0x78, 0xec, 0xf7, 0x65, 0x97, 0xa5, 0x9e, 0x94, 0x51, 0xba, + 0xcd, 0xc4, 0x4b, 0x8c, 0xcc, 0x4d, 0x7e, 0xc8, 0xb1, 0x3d, 0x75, 0x0f, 0x93, 0xff, 0x42, 0x04, 0x45, 0xb8, 0xc7, + 0x02, 0x19, 0xef, 0x21, 0x50, 0x39, 0x15, 0xa2, 0x98, 0x96, 0x8b, 0xb3, 0x05, 0xb0, 0x6b, 0xf4, 0x4b, 0x64, 0x45, + 0x3c, 0x33, 0x9e, 0xdf, 0xb5, 0x36, 0xd7, 0x01, 0xfc, 0x7e, 0x6d, 0xf4, 0x64, 0x46, 0xab, 0x80, 0xac, 0xfb, 0xa0, + 0x0c, 0x2e, 0x09, 0x4f, 0xa5, 0x3d, 0x97, 0xd5, 0x58, 0xd3, 0x7e, 0xa0, 0x57, 0x33, 0xfd, 0x69, 0xfb, 0xac, 0x21, + 0x74, 0x3d, 0x9a, 0x29, 0x05, 0x54, 0xaa, 0x7c, 0x50, 0x66, 0x5f, 0x5f, 0x40, 0x38, 0xa2, 0x55, 0xc8, 0x2f, 0x15, + 0xa7, 0x87, 0xb1, 0x8d, 0x82, 0x20, 0xdf, 0x79, 0x86, 0xc8, 0x0f, 0xc9, 0x13, 0x2a, 0xec, 0xce, 0xfd, 0x02, 0xf4, + 0x45, 0x85, 0xa7, 0xf4, 0xfd, 0x69, 0x8e, 0xdb, 0xd5, 0xbc, 0x8f, 0xef, 0x03, 0x19, 0x25, 0x58, 0x46, 0xba, 0x39, + 0x74, 0xd2, 0xa8, 0x1d, 0x3d, 0xf2, 0x95, 0x48, 0x8e, 0x2e, 0xd0, 0xf4, 0x3d, 0xd6, 0x86, 0x17, 0x49, 0x4a, 0xd0, + 0xa7, 0x72, 0x2d, 0xc9, 0xb0, 0x57, 0x75, 0x60, 0x74, 0x44, 0xde, 0x5e, 0x8a, 0x0d, 0x90, 0x24, 0xd5, 0xd3, 0x12, + 0xa1, 0xfd, 0x50, 0xce, 0x7a, 0x53, 0x7e, 0x89, 0x7b, 0xf1, 0x84, 0x57, 0x46, 0x74, 0xc3, 0x5f, 0x7c, 0x13, 0xe2, + 0x5e, 0x28, 0xee, 0x8b, 0x02, 0x96, 0x25, 0x54, 0x11, 0x41, 0x6f, 0x1a, 0xa8, 0x1c, 0x0c, 0xfd, 0xb1, 0x28, 0xf0, + 0x6c, 0x05, 0x58, 0x96, 0x09, 0x29, 0x03, 0x47, 0x6c, 0x44, 0xff, 0x4a, 0x9a, 0xfa, 0x29, 0xa5, 0xb9, 0x6f, 0x49, + 0xbc, 0xec, 0x17, 0x84, 0x94, 0x37, 0x10, 0x0a, 0x82, 0x96, 0x0a, 0xde, 0x04, 0x29, 0x68, 0x4c, 0x3b, 0xcc, 0x95, + 0x41, 0xd9, 0xe3, 0xb8, 0x01, 0x2e, 0x5f, 0x39, 0xa8, 0x4d, 0xd5, 0xeb, 0x24, 0xb6, 0x2a, 0x6e, 0xf4, 0x9f, 0xe8, + 0xd6, 0xda, 0x0f, 0x07, 0x28, 0x82, 0xb6, 0x28, 0x9b, 0xf4, 0x8a, 0xc6, 0xb3, 0x30, 0x16, 0x96, 0x3d, 0x46, 0x0f, + 0x6a, 0x06, 0x4a, 0x2a, 0xac, 0x36, 0x54, 0x28, 0xe6, 0x53, 0xbb, 0x08, 0xa3, 0xf0, 0x41, 0x53, 0x19, 0x79, 0xf8, + 0xd0, 0x9d, 0x46, 0xef, 0xc6, 0x51, 0x2c, 0x72, 0x43, 0x9b, 0xd7, 0x2c, 0x45, 0xc2, 0xa4, 0x49, 0x3e, 0xbd, 0x6c, + 0xd6, 0xb3, 0x66, 0xd2, 0xb2, 0x15, 0x7a, 0xd5, 0x78, 0x63, 0x20, 0x52, 0xd4, 0x6f, 0xbe, 0x4e, 0x7a, 0xb5, 0x9e, + 0xc3, 0xec, 0x47, 0xc2, 0xf2, 0xa2, 0xe8, 0x7a, 0xa6, 0xdb, 0xbc, 0x6a, 0xa3, 0x3b, 0x73, 0xaa, 0xaf, 0xd4, 0x60, + 0x08, 0xf8, 0x95, 0x73, 0x79, 0x50, 0x26, 0xa8, 0x9c, 0xd8, 0x76, 0x0f, 0x6d, 0x46, 0x40, 0x07, 0xcf, 0xb2, 0xd3, + 0xcc, 0x97, 0xaf, 0x96, 0x49, 0x31, 0xac, 0x77, 0xa9, 0x43, 0x81, 0x97, 0x7b, 0x95, 0xfe, 0x81, 0x46, 0x95, 0x32, + 0xf2, 0x82, 0xa8, 0x3a, 0xd1, 0x5e, 0x70, 0x10, 0xc7, 0x1d, 0xfe, 0x3d, 0xe2, 0x70, 0xc9, 0x3d, 0x87, 0x1d, 0x40, + 0x4e, 0x59, 0x44, 0x3a, 0xca, 0xc7, 0x77, 0x8f, 0xbe, 0x65, 0xcc, 0x31, 0xd2, 0x65, 0xf5, 0x53, 0x11, 0x6d, 0x1f, + 0x51, 0x12, 0xe9, 0x0e, 0x07, 0xfb, 0x14, 0x21, 0xde, 0x6c, 0x8a, 0x41, 0x00, 0x2b, 0x74, 0xbe, 0x44, 0x74, 0x42, + 0x5a, 0xd4, 0x03, 0x0a, 0x87, 0xad, 0x82, 0xcf, 0x72, 0xc1, 0x09, 0x96, 0xfe, 0x10, 0x13, 0xab, 0x52, 0x24, 0x3b, + 0x34, 0xcb, 0xbf, 0x4c, 0x6d, 0xaf, 0x96, 0xa6, 0x51, 0x6d, 0x1e, 0xc1, 0x7d, 0xe3, 0xb2, 0xa4, 0x68, 0x05, 0x76, + 0x97, 0xbd, 0x54, 0xc8, 0xc2, 0x86, 0x6b, 0x2f, 0x79, 0xa6, 0x6d, 0x4b, 0x5e, 0x34, 0x78, 0x40, 0x12, 0xd8, 0x7c, + 0x01, 0xac, 0xff, 0x71, 0xb5, 0x2c, 0x43, 0x2d, 0x54, 0x35, 0x30, 0x42, 0xbe, 0xdb, 0x75, 0x04, 0xd1, 0x9e, 0x55, + 0x37, 0xbf, 0x06, 0x26, 0x5a, 0xf6, 0x26, 0xb0, 0x74, 0x90, 0x45, 0x0b, 0x81, 0x60, 0xe7, 0xfe, 0x7c, 0xed, 0xb2, + 0xd8, 0xce, 0x78, 0x8c, 0x35, 0x61, 0xe1, 0x11, 0xb9, 0x71, 0x80, 0x95, 0xc7, 0x65, 0x09, 0x42, 0x56, 0x94, 0x61, + 0x57, 0xee, 0x1c, 0x50, 0x8f, 0x85, 0x1a, 0x55, 0x08, 0xb2, 0xd6, 0x67, 0xaf, 0xa7, 0x8a, 0x35, 0xc9, 0xfd, 0x3e, + 0x28, 0x30, 0x38, 0x83, 0xbb, 0x4d, 0x45, 0x28, 0x7d, 0x48, 0xe1, 0x4f, 0x6d, 0xba, 0x3e, 0x4b, 0x7b, 0x9e, 0x82, + 0x49, 0xb1, 0x20, 0x5e, 0x2b, 0xf9, 0xe7, 0xe9, 0x2f, 0x12, 0xa8, 0x83, 0x94, 0xdc, 0x98, 0x3e, 0xe2, 0xb5, 0x11, + 0x42, 0x64, 0xac, 0xe7, 0xa0, 0x71, 0x20, 0x9c, 0x52, 0x30, 0xa8, 0x9c, 0xd9, 0x32, 0x8b, 0xe9, 0x78, 0x67, 0x4b, + 0x9d, 0x90, 0x6d, 0x0d, 0x3f, 0xf0, 0x66, 0x1a, 0xfb, 0x89, 0x70, 0xdd, 0xdc, 0xe4, 0x5b, 0x83, 0x67, 0xe8, 0x14, + 0x33, 0x7e, 0x93, 0x31, 0x14, 0xd3, 0xd6, 0x3d, 0x17, 0x4f, 0x4f, 0x4f, 0xc5, 0xa8, 0xb2, 0xb9, 0xe2, 0x61, 0xbc, + 0x1c, 0xab, 0x6a, 0x55, 0x15, 0xd3, 0x42, 0x2b, 0xab, 0xcf, 0x7f, 0x16, 0xc3, 0x25, 0xba, 0xc5, 0x70, 0xb6, 0x08, + 0x6d, 0xa2, 0x88, 0x16, 0x8d, 0x74, 0xcd, 0xd5, 0xfd, 0x4e, 0xdd, 0x95, 0xec, 0xe3, 0xab, 0x77, 0xfb, 0x1f, 0x12, + 0x46, 0xad, 0x97, 0xee, 0x14, 0x90, 0x57, 0x23, 0x9e, 0xf7, 0x5f, 0xcf, 0x29, 0xaf, 0x5a, 0x5e, 0x1a, 0x7d, 0x14, + 0x3c, 0x67, 0xfa, 0xdc, 0xd0, 0xf8, 0x45, 0xd3, 0x28, 0xcd, 0x3e, 0x50, 0x23, 0xbb, 0x81, 0xd6, 0x9b, 0xb4, 0x43, + 0xc6, 0x3b, 0x12, 0x7c, 0xb2, 0x42, 0x78, 0x69, 0xdc, 0x9e, 0x38, 0x89, 0x94, 0x62, 0x34, 0x55, 0x29, 0x54, 0xb5, + 0xce, 0x0a, 0x4d, 0x5b, 0x55, 0x21, 0xc9, 0x81, 0x03, 0xa5, 0x93, 0x21, 0xcc, 0xf1, 0xa4, 0x9c, 0xc4, 0x93, 0xa4, + 0x59, 0xcd, 0x43, 0x4e, 0x79, 0x51, 0x92, 0x86, 0xf4, 0x75, 0xe6, 0x14, 0x80, 0x66, 0x03, 0x25, 0x70, 0x28, 0x49, + 0x01, 0x66, 0x1a, 0xd2, 0x33, 0x44, 0x14, 0x82, 0x01, 0x7a, 0x73, 0x15, 0x13, 0x8f, 0x13, 0x6f, 0x1b, 0xed, 0xb2, + 0xa6, 0x20, 0x9e, 0x7c, 0xec, 0x7d, 0xb3, 0x98, 0xd6, 0x9d, 0x5c, 0x50, 0xc9, 0xf3, 0xc5, 0xd4, 0xd2, 0x04, 0xee, + 0x13, 0x32, 0xd5, 0x8c, 0xa9, 0x42, 0xfe, 0x4d, 0xee, 0xdb, 0xd1, 0x7e, 0x2c, 0x8e, 0xc5, 0xbb, 0x33, 0x34, 0xdd, + 0xcd, 0x55, 0x8e, 0xdc, 0x37, 0x23, 0xb9, 0xd5, 0xb2, 0xa6, 0x11, 0x84, 0x2c, 0x7c, 0xe1, 0x7a, 0xed, 0xf5, 0xf1, + 0x7d, 0xd6, 0xfd, 0xab, 0x0d, 0xc7, 0x8b, 0xe6, 0x25, 0x1f, 0xd2, 0x5d, 0x31, 0xb1, 0x68, 0xaf, 0xfc, 0x24, 0xa9, + 0x77, 0x6a, 0x3d, 0x66, 0xc2, 0xdd, 0x3f, 0x94, 0xa6, 0x31, 0xd3, 0x3b, 0xea, 0x78, 0x3f, 0xba, 0xc3, 0x1c, 0x8a, + 0x98, 0x6a, 0x58, 0xdd, 0x48, 0xa5, 0x5c, 0x98, 0x9e, 0x61, 0x63, 0xae, 0x4e, 0x3b, 0x4a, 0x4a, 0xd0, 0xa9, 0x5a, + 0xff, 0x51, 0x1e, 0xe1, 0x34, 0x55, 0xc1, 0x4f, 0x5e, 0x6d, 0xc7, 0xa2, 0x6b, 0x2f, 0x47, 0x6b, 0xd1, 0xb3, 0x1d, + 0xe5, 0x84, 0x7d, 0x7c, 0x8f, 0x50, 0x75, 0x7d, 0xb1, 0x3e, 0xfd, 0xb5, 0xfe, 0x56, 0xee, 0x06, 0x2a, 0x81, 0x3a, + 0x1b, 0xcb, 0xec, 0x5a, 0x13, 0x17, 0xb6, 0xbf, 0x6e, 0x53, 0xab, 0x06, 0x4e, 0xf6, 0x6a, 0xc3, 0xca, 0x9a, 0xcf, + 0x84, 0x6c, 0x7c, 0x93, 0xb2, 0x5f, 0x88, 0xe1, 0x27, 0xa9, 0x4d, 0x4d, 0x9b, 0xa4, 0xb5, 0xfc, 0x2c, 0xd7, 0xcd, + 0xdb, 0x56, 0xc4, 0xe9, 0xbe, 0x28, 0x82, 0x9c, 0x22, 0x09, 0xd9, 0xc6, 0x78, 0x84, 0xb0, 0x85, 0x0e, 0xe2, 0x5c, + 0xba, 0x88, 0xb1, 0x2c, 0x62, 0x78, 0x2f, 0x8f, 0x7d, 0x12, 0x6a, 0xda, 0x68, 0xa7, 0x2c, 0xb2, 0xff, 0x3e, 0xd3, + 0x8f, 0x8b, 0x2a, 0xa8, 0x03, 0x30, 0xbd, 0xbf, 0x6a, 0x7b, 0xb9, 0x38, 0xea, 0x37, 0x15, 0x07, 0x57, 0xff, 0x94, + 0x36, 0x37, 0x6c, 0xaa, 0xf9, 0x86, 0xa8, 0x54, 0xca, 0xbe, 0x18, 0xf4, 0x8c, 0xec, 0x55, 0xa3, 0x51, 0xcc, 0xa7, + 0xd0, 0xb2, 0x44, 0xfc, 0xf1, 0x54, 0x28, 0x6a, 0xa8, 0xe6, 0x2e, 0xe4, 0xe4, 0xd8, 0x30, 0xf6, 0x27, 0x93, 0xdd, + 0x9e, 0xb6, 0xea, 0xa7, 0xac, 0x67, 0x48, 0x87, 0x87, 0x82, 0x1f, 0xb8, 0xdc, 0x75, 0xf1, 0xa6, 0xec, 0xdd, 0xaa, + 0x45, 0x2a, 0x51, 0x10, 0x2a, 0x9b, 0x7d, 0xf5, 0x86, 0xa9, 0x81, 0x1e, 0x6a, 0xf4, 0x40, 0x19, 0x4c, 0xf1, 0x09, + 0x80, 0x9a, 0xd6, 0xe1, 0xd3, 0xd4, 0x42, 0xd9, 0x48, 0xdf, 0x0b, 0xcc, 0x30, 0xfd, 0xd7, 0x61, 0xb2, 0x42, 0x06, + 0xfc, 0xea, 0x69, 0x79, 0x33, 0xce, 0xbf, 0xe7, 0xb6, 0x87, 0xde, 0xa7, 0x7e, 0xfa, 0x2a, 0x89, 0x71, 0x0f, 0xf6, + 0xf7, 0x69, 0xe6, 0x4c, 0xc9, 0xd8, 0x51, 0x01, 0x24, 0x54, 0xdc, 0x4c, 0x61, 0x08, 0x4f, 0x17, 0x82, 0x22, 0x86, + 0xae, 0x6f, 0xd7, 0xf3, 0x3b, 0xbe, 0x62, 0x1e, 0x51, 0xbb, 0x4c, 0xd5, 0x50, 0xd2, 0xfa, 0x30, 0x1b, 0x10, 0xd6, + 0x04, 0x4f, 0x8e, 0x70, 0xc3, 0xd2, 0x55, 0x44, 0x66, 0xc1, 0x0a, 0xcf, 0xc0, 0xa9, 0x09, 0xb8, 0x6e, 0x8a, 0xcc, + 0x7b, 0x9c, 0x00, 0xce, 0xc7, 0x63, 0x1c, 0xed, 0x29, 0xe0, 0xed, 0xb2, 0xba, 0xda, 0x5b, 0x6a, 0xbd, 0x73, 0x1e, + 0xda, 0x44, 0x10, 0x95, 0xf8, 0x79, 0x36, 0x91, 0xfb, 0x07, 0x6f, 0xce, 0xab, 0xc9, 0x96, 0xa4, 0x43, 0xc9, 0xdf, + 0x41, 0xd1, 0x9b, 0xac, 0xb0, 0x92, 0x1b, 0xc5, 0x22, 0x99, 0x34, 0x02, 0x20, 0x30, 0xaf, 0xf2, 0x1d, 0x11, 0xc0, + 0x55, 0x58, 0x68, 0x34, 0x45, 0x51, 0x5e, 0x51, 0x6d, 0x9e, 0xd1, 0xee, 0xd8, 0xaf, 0xe7, 0xb8, 0x2c, 0xd7, 0x96, + 0xd4, 0x6a, 0x2c, 0xeb, 0x48, 0x8a, 0x66, 0x18, 0xbc, 0x39, 0x2f, 0x05, 0x2f, 0xf1, 0xc1, 0x3c, 0x6f, 0x89, 0xaf, + 0x54, 0x5a, 0x41, 0x23, 0xd7, 0x6b, 0x8a, 0x99, 0x03, 0x9a, 0xd3, 0x65, 0x7a, 0x97, 0xe2, 0xfd, 0xeb, 0x15, 0xbf, + 0x2c, 0x5b, 0xaa, 0xba, 0xee, 0xfa, 0x93, 0x15, 0x71, 0x5c, 0x64, 0xb1, 0x6f, 0x59, 0xb4, 0x19, 0xec, 0x10, 0xfb, + 0x31, 0xed, 0xf3, 0x28, 0xcf, 0xb5, 0xcf, 0x36, 0x3f, 0x97, 0x10, 0x47, 0x96, 0x68, 0xbd, 0x3a, 0x62, 0x3f, 0xb5, + 0x64, 0x63, 0xb9, 0xef, 0x44, 0x29, 0x76, 0xb4, 0xb8, 0x90, 0xe6, 0x42, 0x1f, 0x3c, 0xd7, 0x83, 0xa5, 0x0c, 0x7f, + 0x16, 0x57, 0xb6, 0xf4, 0xaa, 0x1c, 0xad, 0xf4, 0x9f, 0x75, 0xa3, 0x87, 0x53, 0x9b, 0x62, 0xea, 0xde, 0x47, 0xc2, + 0x34, 0xa1, 0xf9, 0xbe, 0x21, 0x36, 0x55, 0x4c, 0x14, 0x44, 0x23, 0x6d, 0x03, 0xc7, 0xfb, 0xe7, 0xf5, 0x95, 0xa7, + 0xbc, 0x94, 0xfc, 0xe1, 0x3a, 0x6e, 0x79, 0x63, 0x68, 0x32, 0xf1, 0x06, 0xad, 0x07, 0x39, 0x81, 0x6d, 0x6c, 0x9f, + 0x1e, 0x69, 0x8f, 0xc2, 0x09, 0xe9, 0x4e, 0x39, 0xb4, 0x0e, 0xd7, 0x27, 0xef, 0xd0, 0x85, 0x28, 0x8d, 0x4c, 0xfc, + 0x84, 0xf4, 0xc6, 0x69, 0x74, 0xaa, 0xab, 0x7f, 0xf2, 0xbc, 0xb3, 0xd8, 0x37, 0xb0, 0xa0, 0xde, 0xff, 0xe9, 0xc6, + 0x50, 0x62, 0x3c, 0x6f, 0x19, 0x71, 0x4c, 0x84, 0xa4, 0xdc, 0x4a, 0xbe, 0x4f, 0x22, 0x2a, 0xb5, 0x52, 0x38, 0xa3, + 0x17, 0xf4, 0x88, 0x1a, 0x2c, 0x9e, 0x9f, 0x5a, 0xe7, 0xc0, 0xa4, 0x1b, 0xe5, 0xa5, 0x51, 0x20, 0x0d, 0x22, 0x4f, + 0xcd, 0xf4, 0x0c, 0x9a, 0xb7, 0x0f, 0xaf, 0x03, 0xf7, 0x9e, 0x20, 0x9f, 0xff, 0xfe, 0x30, 0xdc, 0xde, 0x1a, 0x68, + 0x96, 0xf5, 0x39, 0x76, 0x51, 0xeb, 0x8b, 0x15, 0x7a, 0x58, 0x80, 0xdd, 0x13, 0x92, 0xeb, 0x3f, 0x05, 0xe8, 0x1a, + 0xcc, 0xb2, 0x55, 0xc7, 0xbc, 0x6d, 0xfb, 0xb7, 0xf3, 0x2a, 0xdc, 0x1d, 0x33, 0x10, 0x68, 0x77, 0xc6, 0x38, 0x87, + 0xff, 0x67, 0x89, 0x64, 0x15, 0xc6, 0xe4, 0xa2, 0xbd, 0x6e, 0x0f, 0x97, 0xc4, 0x6e, 0xb5, 0x66, 0x39, 0xd3, 0x76, + 0x60, 0xeb, 0x39, 0x2f, 0xa2, 0xd2, 0x20, 0xc1, 0x4e, 0x6a, 0x43, 0x03, 0x44, 0x32, 0xe8, 0xf6, 0x52, 0xc6, 0xbd, + 0x20, 0x9f, 0x01, 0x7d, 0x6d, 0x67, 0x2e, 0xbd, 0x31, 0x35, 0xae, 0x70, 0x52, 0x97, 0x9d, 0xbb, 0xc9, 0x70, 0xd6, + 0x3e, 0x16, 0xca, 0xd7, 0x63, 0x81, 0x2f, 0xac, 0x8f, 0xd3, 0xf4, 0xc1, 0x1d, 0xd9, 0x47, 0x93, 0x63, 0x2f, 0xa6, + 0xa4, 0x2a, 0x33, 0x18, 0x65, 0x08, 0xb4, 0x74, 0x2d, 0xcb, 0x94, 0x62, 0x8f, 0xde, 0x3e, 0x9c, 0x32, 0x6e, 0xfa, + 0x79, 0x98, 0x73, 0xd0, 0x89, 0x65, 0x8b, 0xe7, 0x15, 0xd9, 0xc3, 0xd4, 0x9d, 0x00, 0x89, 0x04, 0x61, 0xa2, 0x0b, + 0x95, 0x7a, 0x90, 0x61, 0x4d, 0x78, 0x84, 0x34, 0x71, 0x71, 0x3a, 0x32, 0x61, 0x77, 0xe4, 0x49, 0x07, 0x51, 0x07, + 0x86, 0xca, 0xd5, 0x73, 0xfe, 0xd0, 0x63, 0xb2, 0x17, 0x14, 0xd9, 0xf6, 0x48, 0xe1, 0x9c, 0x79, 0xf3, 0x21, 0x7b, + 0xe8, 0x5f, 0x37, 0xbd, 0xe6, 0x88, 0x05, 0xf7, 0xb7, 0x50, 0x81, 0x32, 0x04, 0xdc, 0x1f, 0xfa, 0xee, 0x36, 0x47, + 0xad, 0xa0, 0x33, 0x30, 0x7d, 0xb2, 0xcf, 0xf4, 0x62, 0x4d, 0x69, 0xb8, 0x6f, 0x46, 0xce, 0xe0, 0x4e, 0xd0, 0xb5, + 0x33, 0xa9, 0xb4, 0xbb, 0x7c, 0x21, 0xa8, 0xf0, 0xe1, 0x1a, 0xb4, 0x3a, 0x88, 0x9c, 0x92, 0xfe, 0x4e, 0x48, 0x75, + 0xb5, 0x29, 0x26, 0xdc, 0x40, 0xcd, 0x06, 0x8a, 0xa3, 0x70, 0xe3, 0x07, 0x89, 0x01, 0x66, 0x6e, 0xa4, 0x61, 0x25, + 0xaf, 0x9d, 0x87, 0x5f, 0xec, 0x07, 0x39, 0xcf, 0x63, 0x2a, 0xd1, 0x43, 0x9f, 0x56, 0x75, 0xfd, 0x21, 0xe6, 0x1b, + 0x6a, 0x9f, 0x41, 0x6d, 0x93, 0x10, 0xa2, 0x4e, 0xd3, 0x3e, 0xe6, 0x59, 0xf9, 0xd1, 0xc1, 0x84, 0x98, 0x7b, 0x32, + 0xd0, 0xaa, 0x5d, 0x81, 0xa5, 0xec, 0x52, 0x95, 0x70, 0xed, 0xd4, 0x6f, 0x2a, 0x69, 0x17, 0xab, 0x95, 0x57, 0xa7, + 0xd8, 0xb3, 0x7f, 0xe7, 0xda, 0xfb, 0x90, 0xf1, 0x99, 0xe8, 0x58, 0xb3, 0xda, 0xbd, 0xee, 0x27, 0xce, 0x69, 0xbc, + 0xc4, 0x46, 0x09, 0xe5, 0x87, 0x69, 0x40, 0x3c, 0x78, 0x83, 0x78, 0xd7, 0x4f, 0x6c, 0xf6, 0xe2, 0xaa, 0x2f, 0x35, + 0x5a, 0xa8, 0x3f, 0xe9, 0xc3, 0xa3, 0x1a, 0x9c, 0x3c, 0x5c, 0x86, 0x27, 0x5f, 0x79, 0x3b, 0x19, 0xe0, 0xb1, 0x12, + 0xf8, 0xdc, 0x5a, 0x02, 0x4a, 0x47, 0x24, 0xaf, 0xe4, 0x03, 0xfa, 0x7f, 0x0e, 0xcf, 0x87, 0x5d, 0x8f, 0x9f, 0x2d, + 0x6d, 0xa8, 0x45, 0x27, 0x1d, 0x61, 0x09, 0x6a, 0x7b, 0x48, 0x43, 0x88, 0x8c, 0x1d, 0x81, 0x69, 0xcc, 0x9f, 0x14, + 0x61, 0x1e, 0x81, 0xf7, 0x39, 0x03, 0x8e, 0xda, 0x96, 0xf8, 0xc2, 0x09, 0x77, 0xef, 0xf2, 0xe1, 0x37, 0xf0, 0xbd, + 0xb2, 0x4b, 0x58, 0x6e, 0xab, 0x1d, 0xbb, 0xd9, 0x04, 0x9a, 0xa3, 0x28, 0x6e, 0xbf, 0x99, 0x68, 0xd1, 0xb3, 0xc3, + 0x7e, 0x0e, 0xba, 0x97, 0xa1, 0x42, 0xf9, 0x98, 0xf6, 0x99, 0xdc, 0xaf, 0x47, 0x80, 0x22, 0xe0, 0x10, 0x43, 0x6c, + 0xff, 0xd8, 0x2b, 0x0f, 0xb5, 0x9e, 0x05, 0x04, 0x14, 0xc3, 0x9f, 0x5c, 0x70, 0x66, 0xfa, 0xe0, 0x18, 0x30, 0x39, + 0x00, 0xd4, 0x06, 0x17, 0x8d, 0xc5, 0x29, 0xfe, 0xbf, 0xf3, 0x8d, 0xe4, 0xed, 0xba, 0x38, 0x1d, 0xf1, 0x2e, 0x9f, + 0x51, 0x54, 0xcc, 0x90, 0x42, 0x0b, 0xbf, 0xe8, 0x06, 0xc2, 0x4a, 0x11, 0x0b, 0x7a, 0x2b, 0x1f, 0xdb, 0xcb, 0x63, + 0x14, 0xaa, 0xff, 0xab, 0x97, 0xec, 0x8f, 0x5a, 0xf0, 0xd8, 0xa5, 0x58, 0xde, 0xf0, 0x91, 0x53, 0xaa, 0x87, 0xbb, + 0x78, 0xb3, 0x1d, 0x06, 0x05, 0xbd, 0x1d, 0x10, 0x6f, 0xfd, 0x9f, 0x25, 0x49, 0xb6, 0xdc, 0x6a, 0x86, 0x24, 0xb9, + 0xae, 0x8e, 0x3b, 0xe2, 0xdf, 0x8f, 0x78, 0x57, 0x1b, 0x1d, 0xaa, 0xf6, 0x7c, 0x5c, 0x67, 0xfe, 0x2b, 0xce, 0xf2, + 0x86, 0xa4, 0xd3, 0xcc, 0xee, 0x6b, 0x5c, 0xce, 0x65, 0x3b, 0x99, 0x2f, 0x66, 0x77, 0xb3, 0xfd, 0xf2, 0xfd, 0x96, + 0x2a, 0x63, 0xeb, 0xf9, 0x45, 0xf3, 0x31, 0xc7, 0x1d, 0x91, 0x94, 0x65, 0x18, 0xcb, 0xf9, 0xb9, 0x4b, 0xf3, 0xe3, + 0x0f, 0xc2, 0x9b, 0x1f, 0xbf, 0x78, 0x28, 0x38, 0x9d, 0x62, 0x2a, 0x23, 0x4e, 0x95, 0xce, 0x9c, 0x24, 0x86, 0xa9, + 0x14, 0x68, 0x26, 0xba, 0xbe, 0x06, 0xc9, 0x00, 0xbd, 0x82, 0xa6, 0xc3, 0xd0, 0x9f, 0xf1, 0x01, 0xae, 0x3a, 0x79, + 0xa6, 0x92, 0xcc, 0x17, 0x8c, 0x31, 0x5e, 0xf0, 0x43, 0xbf, 0xf0, 0xe4, 0x5e, 0x3b, 0x32, 0x80, 0x21, 0x15, 0x7b, + 0xfc, 0x78, 0xd1, 0x7c, 0x79, 0x69, 0x44, 0x08, 0x55, 0xc8, 0x52, 0x80, 0xa7, 0x3c, 0x7f, 0x26, 0xab, 0xeb, 0xd9, + 0x6f, 0x36, 0x5d, 0x69, 0xb8, 0xaf, 0xa6, 0x9e, 0x2a, 0x60, 0x6c, 0xb9, 0x91, 0x8f, 0x29, 0x66, 0xd6, 0x06, 0xeb, + 0x74, 0x50, 0xab, 0xc7, 0x1c, 0xe3, 0xa9, 0xa0, 0x2e, 0xa6, 0xd4, 0x93, 0x3c, 0xd6, 0xd9, 0xf4, 0x41, 0x36, 0xb8, + 0x81, 0x71, 0xc5, 0xc9, 0x47, 0x10, 0x45, 0x13, 0x60, 0x39, 0x4f, 0x5b, 0x44, 0x11, 0x7c, 0x87, 0x66, 0x14, 0xc1, + 0x10, 0xb1, 0x88, 0x2d, 0xef, 0x56, 0xc9, 0xbc, 0xbd, 0xec, 0x72, 0x92, 0xe9, 0xb7, 0xa5, 0xcc, 0x49, 0xa2, 0xc1, + 0xc1, 0x2a, 0x9f, 0xb5, 0xea, 0xa6, 0x1f, 0xec, 0x4b, 0x28, 0x00, 0x8e, 0xcc, 0xc0, 0x81, 0x92, 0x62, 0x56, 0xaa, + 0x8a, 0x1a, 0x39, 0x08, 0x70, 0xf2, 0xc3, 0x3f, 0x54, 0x5f, 0x84, 0xa5, 0xb3, 0xdb, 0x29, 0x08, 0x3d, 0xc1, 0x08, + 0x91, 0x40, 0xe3, 0x27, 0x97, 0x6c, 0xfa, 0xef, 0xdc, 0xcc, 0x48, 0x5f, 0xfe, 0xbd, 0x9e, 0xec, 0x6d, 0x6b, 0x50, + 0x30, 0xb9, 0x1e, 0xed, 0xeb, 0x58, 0x2b, 0x96, 0x4e, 0xa8, 0x4b, 0x7f, 0x71, 0x05, 0x3e, 0xa9, 0x09, 0x91, 0xb1, + 0x62, 0xa6, 0x32, 0x6b, 0x29, 0x78, 0xae, 0x7e, 0xcc, 0x65, 0x60, 0x26, 0x52, 0xda, 0x15, 0x93, 0xa6, 0x34, 0xf3, + 0x29, 0x17, 0xd1, 0xb3, 0x67, 0x5d, 0xa7, 0xa1, 0xb5, 0x0e, 0xac, 0xcb, 0x7e, 0x88, 0xb7, 0xf9, 0xd5, 0x99, 0xa6, + 0x30, 0xca, 0xf9, 0xab, 0xf3, 0x0e, 0x8b, 0x72, 0xb3, 0xbe, 0x62, 0x3e, 0xec, 0x1d, 0xda, 0x69, 0x65, 0xf4, 0xf1, + 0x5c, 0xad, 0x70, 0xdf, 0x81, 0x90, 0xf3, 0xe8, 0x7b, 0x03, 0x1e, 0xff, 0x0a, 0xff, 0xbf, 0x3e, 0x04, 0xda, 0xb1, + 0x15, 0x0c, 0xdd, 0xf0, 0x89, 0x4d, 0x70, 0x8f, 0x86, 0x99, 0xd3, 0xd9, 0xca, 0xef, 0x43, 0x22, 0xea, 0x16, 0x70, + 0xb7, 0x8b, 0x1f, 0xd7, 0x3e, 0xc3, 0xd5, 0xc8, 0xc6, 0x18, 0x0e, 0xb9, 0x01, 0xb2, 0x84, 0xf0, 0x09, 0x09, 0x63, + 0xdd, 0x39, 0x3f, 0x38, 0xa3, 0x31, 0xbe, 0xfb, 0x5b, 0xe7, 0xf9, 0x66, 0xbc, 0x8d, 0xf9, 0x75, 0xf2, 0x4d, 0xe7, + 0x7a, 0xa0, 0xf3, 0xf4, 0xa0, 0xd6, 0x6a, 0xfd, 0xc3, 0x4d, 0xef, 0x5d, 0x0c, 0x4b, 0xb8, 0x9f, 0x3a, 0xba, 0xb9, + 0x7b, 0x13, 0x11, 0x11, 0xa8, 0x3f, 0x78, 0x68, 0xd1, 0xf3, 0x09, 0xd4, 0xe9, 0x12, 0x22, 0xfa, 0xa3, 0xcd, 0x9e, + 0xdb, 0xc9, 0x9c, 0x3a, 0x79, 0xb2, 0x8d, 0xae, 0x45, 0x25, 0x5f, 0x58, 0x2c, 0xf3, 0x3e, 0x6d, 0xdd, 0x88, 0xc8, + 0x81, 0xc4, 0x64, 0xc5, 0x36, 0xc3, 0xd4, 0xd0, 0x71, 0xea, 0x22, 0xf1, 0x3f, 0xef, 0xeb, 0xc4, 0x50, 0xf2, 0xb2, + 0xd4, 0x02, 0x0b, 0x4b, 0x55, 0xd8, 0x3e, 0xee, 0x39, 0x95, 0x85, 0x55, 0x37, 0x46, 0xbc, 0x75, 0xdf, 0x76, 0x4d, + 0xc7, 0x26, 0x8a, 0xd7, 0x5f, 0xbf, 0x02, 0xad, 0x21, 0x3d, 0x16, 0xf1, 0x7e, 0x91, 0x8e, 0x63, 0x00, 0xde, 0x31, + 0x74, 0x0b, 0x77, 0xcb, 0xb2, 0x6a, 0xcf, 0xfb, 0x74, 0x0c, 0x25, 0x45, 0xb1, 0x94, 0xdc, 0x3d, 0x62, 0xeb, 0x71, + 0x94, 0xe0, 0xa9, 0xee, 0x3d, 0xbd, 0x45, 0x2a, 0x91, 0xa5, 0xa3, 0xf4, 0xd8, 0xcf, 0x29, 0x60, 0xea, 0xa5, 0xf8, + 0x7d, 0xf4, 0x68, 0x59, 0x32, 0x40, 0x8b, 0x8d, 0x58, 0xe5, 0x1d, 0x5b, 0xc1, 0x5a, 0x9c, 0x92, 0x63, 0xbc, 0xed, + 0xdd, 0x97, 0x54, 0xca, 0x5d, 0xcc, 0x6c, 0x94, 0x76, 0x6a, 0xbc, 0x1c, 0x1c, 0xa7, 0xa5, 0xb0, 0x22, 0xc6, 0x18, + 0x39, 0xbb, 0x12, 0xb4, 0x35, 0x42, 0x77, 0xb8, 0x66, 0x89, 0xff, 0xbe, 0xac, 0x2d, 0x6e, 0x25, 0x90, 0x91, 0x2f, + 0xc3, 0x37, 0xe5, 0x9b, 0xa0, 0xad, 0xfe, 0x62, 0x8f, 0xbe, 0x56, 0x10, 0x32, 0xe1, 0x57, 0x7c, 0x35, 0xba, 0xe6, + 0xf6, 0x7d, 0xd9, 0x4d, 0x56, 0x69, 0x92, 0x9d, 0x40, 0x6b, 0x93, 0xca, 0xb9, 0xf0, 0xf0, 0x39, 0x77, 0x47, 0x92, + 0x3e, 0x7d, 0x2a, 0xcc, 0x28, 0x79, 0xc9, 0x54, 0x50, 0x3a, 0xc8, 0x66, 0x7f, 0x82, 0x25, 0xa8, 0x87, 0x7c, 0x41, + 0x6d, 0xdd, 0xe3, 0xe9, 0xf3, 0x1a, 0x88, 0xeb, 0x65, 0xc3, 0x0a, 0x44, 0x22, 0xfa, 0x6f, 0xb3, 0x8f, 0x3e, 0x64, + 0x73, 0x42, 0xf6, 0xfa, 0x66, 0x8e, 0xd3, 0x9d, 0x44, 0x28, 0xca, 0x1d, 0xb7, 0x03, 0x4a, 0x29, 0x0e, 0x4a, 0xd5, + 0xf0, 0xd8, 0x2c, 0x91, 0x63, 0xe6, 0x07, 0xa7, 0xbb, 0xd8, 0x4f, 0x5c, 0x8b, 0x5f, 0xd8, 0xb1, 0x53, 0x79, 0xf3, + 0xcf, 0xbe, 0x7c, 0xd9, 0xc7, 0x83, 0xc8, 0xe8, 0x0f, 0x42, 0x11, 0x5f, 0xf6, 0x9b, 0x26, 0xf1, 0xe2, 0x17, 0xdf, + 0xd2, 0x53, 0x3c, 0xf7, 0x6b, 0x75, 0x11, 0xb7, 0x75, 0xf7, 0xbe, 0x8a, 0x76, 0x29, 0xb1, 0xe1, 0x36, 0x0c, 0x4f, + 0x93, 0xe4, 0xf4, 0x00, 0xe0, 0x03, 0xce, 0xe5, 0x3f, 0x73, 0x14, 0xca, 0x47, 0x2e, 0xc3, 0xf9, 0x62, 0x11, 0x62, + 0x4c, 0xfe, 0xc6, 0x18, 0xa5, 0x35, 0x6f, 0x9f, 0xb7, 0x77, 0xbf, 0x71, 0x6c, 0x78, 0x6d, 0xbc, 0x89, 0x86, 0x8a, + 0x16, 0xe5, 0x4d, 0xe1, 0x53, 0x5e, 0x17, 0x76, 0x79, 0xaf, 0xf0, 0x98, 0xf7, 0x0b, 0x4f, 0xf9, 0x60, 0xed, 0xd1, + 0x68, 0x45, 0x48, 0xc1, 0xb5, 0x40, 0xd6, 0x85, 0x42, 0x97, 0x71, 0x04, 0xf7, 0x94, 0x17, 0x6d, 0xcd, 0xef, 0xd0, + 0x44, 0x96, 0xff, 0x07, 0x62, 0x85, 0xd5, 0xe9, 0x07, 0x4d, 0xf1, 0x0a, 0xc4, 0x58, 0xe6, 0x58, 0x8a, 0xd5, 0xed, + 0x7f, 0xd6, 0x52, 0x31, 0x1e, 0x73, 0xb6, 0x99, 0x81, 0xbe, 0x5a, 0xbe, 0xc2, 0xc6, 0x40, 0xe3, 0xeb, 0x4d, 0x69, + 0xf5, 0x1a, 0x58, 0x8b, 0xfd, 0x7c, 0x4d, 0x23, 0x59, 0x89, 0xb0, 0x52, 0xe5, 0x61, 0x60, 0xa2, 0x2a, 0xf3, 0x8c, + 0x74, 0x04, 0xc5, 0xf3, 0xe9, 0x0b, 0xbe, 0x72, 0xd4, 0xda, 0x67, 0x05, 0xa8, 0x86, 0xc7, 0x42, 0x47, 0x2f, 0x8c, + 0xec, 0xea, 0xba, 0xa5, 0xa6, 0xb6, 0x67, 0x5f, 0x12, 0x6b, 0xe4, 0xb7, 0xe3, 0x67, 0x52, 0x24, 0xb4, 0x6c, 0xfc, + 0x3e, 0x8f, 0x77, 0xb1, 0xf7, 0x95, 0x86, 0x34, 0x40, 0x68, 0x9d, 0x90, 0x59, 0xd4, 0x74, 0xc1, 0x4b, 0xc2, 0xa7, + 0xa5, 0x8f, 0xe9, 0x47, 0xc7, 0xfb, 0x8b, 0xaf, 0xf0, 0x00, 0x47, 0x5a, 0xbb, 0xd8, 0xe4, 0xc7, 0xe3, 0x02, 0x7e, + 0xed, 0x37, 0x1d, 0x0a, 0x6b, 0xc6, 0x2a, 0x97, 0xde, 0xb4, 0xab, 0x8b, 0xe0, 0x6b, 0x4b, 0x9f, 0xf1, 0xb8, 0x7f, + 0xec, 0x4d, 0x1d, 0xef, 0x4f, 0x7a, 0x04, 0xbe, 0x01, 0x28, 0x15, 0x35, 0x88, 0x7d, 0x10, 0x7a, 0xbc, 0xb3, 0x2a, + 0x82, 0xcb, 0xf0, 0x38, 0xa4, 0xed, 0xf9, 0x32, 0xb3, 0xab, 0xc7, 0xf8, 0x8d, 0x90, 0x04, 0xdd, 0xf0, 0x4e, 0x5a, + 0x12, 0xa0, 0xf4, 0x51, 0x09, 0x93, 0x1c, 0xb1, 0xcf, 0x2f, 0x5a, 0xf6, 0xa6, 0x8d, 0x4e, 0xe1, 0x5b, 0x8f, 0x98, + 0x67, 0x6d, 0x99, 0xf3, 0x9f, 0x06, 0x71, 0x30, 0x93, 0xa3, 0xf8, 0xfd, 0x10, 0xe7, 0x45, 0x15, 0x75, 0xe9, 0xc5, + 0x6c, 0x6f, 0x03, 0xb6, 0xf0, 0xbb, 0x0f, 0xb3, 0x81, 0xef, 0x4f, 0x7d, 0xb9, 0xd6, 0xa1, 0x9e, 0xd1, 0xfd, 0x56, + 0x75, 0xdb, 0xc7, 0x91, 0x75, 0xf2, 0x9c, 0xc5, 0xc3, 0xe8, 0xdd, 0xf7, 0x85, 0xaf, 0x71, 0x66, 0xb4, 0xf8, 0x24, + 0x2a, 0x0a, 0x2b, 0x97, 0x41, 0xb9, 0x7c, 0x4d, 0x55, 0xb5, 0x47, 0x9b, 0x2f, 0x62, 0x74, 0x5e, 0xfc, 0x5e, 0xa7, + 0x8f, 0xba, 0xc6, 0xeb, 0x48, 0xf9, 0x68, 0x5f, 0x16, 0xc3, 0x1f, 0xac, 0x20, 0xb4, 0x98, 0xd8, 0xec, 0xb1, 0x5f, + 0x8e, 0x16, 0xa7, 0x67, 0x69, 0x33, 0xec, 0x34, 0x6d, 0xb5, 0x71, 0x3b, 0xd8, 0x6f, 0x1d, 0xd2, 0x92, 0xc4, 0x8b, + 0xf1, 0x15, 0x2a, 0x7f, 0xc0, 0x43, 0xec, 0x39, 0x48, 0xd0, 0x88, 0x35, 0xe7, 0xb7, 0xc8, 0x75, 0xba, 0x16, 0x48, + 0x5d, 0xf8, 0x7a, 0xe8, 0x61, 0xd2, 0x22, 0xd5, 0x41, 0x59, 0x06, 0xba, 0x89, 0x02, 0xfa, 0x9e, 0xba, 0x2d, 0xc8, + 0x45, 0xf6, 0xf7, 0x9c, 0x9d, 0xbe, 0xc6, 0xfb, 0x73, 0x0b, 0x3b, 0x51, 0xf8, 0xcd, 0x1f, 0x93, 0x18, 0xd6, 0xdc, + 0x76, 0x91, 0x2d, 0x82, 0xde, 0x6c, 0x5a, 0x3e, 0x28, 0x07, 0x6c, 0x7e, 0x69, 0xa1, 0xca, 0xc8, 0x11, 0xeb, 0xf9, + 0x6f, 0xf7, 0x63, 0x97, 0x98, 0x57, 0x41, 0xa8, 0x5e, 0xa9, 0x2a, 0x31, 0x80, 0x3e, 0xa9, 0x3d, 0x03, 0x75, 0x66, + 0x76, 0x55, 0xe9, 0xf5, 0xeb, 0xac, 0x3e, 0xd4, 0xee, 0x02, 0xf7, 0x4e, 0xc3, 0xb3, 0x13, 0x6b, 0x25, 0x8b, 0xe8, + 0x23, 0x24, 0x61, 0x02, 0xfd, 0x7e, 0xd7, 0xb5, 0xaf, 0x7b, 0x3a, 0x96, 0x05, 0x94, 0x89, 0x3a, 0x5c, 0x9c, 0x20, + 0x18, 0x3f, 0xc8, 0x71, 0x80, 0x6d, 0xe4, 0xc7, 0x2e, 0x8b, 0xab, 0xfe, 0x1c, 0x28, 0x92, 0xa0, 0xb9, 0x96, 0xfb, + 0x35, 0xb8, 0xaf, 0xef, 0x74, 0x93, 0x15, 0xd9, 0x65, 0x98, 0x33, 0xde, 0x30, 0xc6, 0x08, 0x51, 0xc5, 0x22, 0x9e, + 0xe7, 0xb8, 0x81, 0xe5, 0x71, 0x09, 0xde, 0x58, 0xce, 0x3b, 0xa3, 0xda, 0xf2, 0x6c, 0x80, 0xa6, 0xb4, 0x62, 0x1b, + 0x95, 0x6a, 0x65, 0x0c, 0x0c, 0x64, 0xcb, 0x4e, 0xa6, 0xef, 0xa9, 0x2c, 0xc6, 0xfb, 0x77, 0x47, 0x04, 0x37, 0x3d, + 0xca, 0x7c, 0x7d, 0x10, 0xc6, 0xd0, 0xdc, 0xc3, 0xa0, 0x62, 0xb7, 0x4d, 0x39, 0x06, 0x17, 0x5c, 0x74, 0xa2, 0x26, + 0x35, 0x94, 0x45, 0xb5, 0x8c, 0x14, 0x5e, 0xcd, 0x8a, 0xbe, 0xee, 0x69, 0xf1, 0x5a, 0x84, 0x18, 0x94, 0xe1, 0xba, + 0x24, 0x21, 0x54, 0x26, 0x08, 0x7d, 0xa8, 0x30, 0xa5, 0xc2, 0xeb, 0x94, 0x80, 0xfd, 0x3d, 0xcf, 0x79, 0xdd, 0xfb, + 0x5d, 0x3b, 0x2c, 0xb3, 0xe4, 0xb8, 0xd7, 0x70, 0xbb, 0x82, 0xbb, 0x23, 0xcf, 0x46, 0x76, 0x6b, 0x64, 0xf2, 0xbe, + 0x56, 0x0c, 0xe9, 0xb6, 0x60, 0x2a, 0x2e, 0x8a, 0x68, 0x95, 0xc5, 0xb8, 0x1d, 0xf8, 0x95, 0xbb, 0x45, 0xb3, 0x9e, + 0x3a, 0x93, 0xf5, 0x86, 0x21, 0x7c, 0x1a, 0x96, 0xb1, 0x84, 0x58, 0xbd, 0x1e, 0xf9, 0x7f, 0x97, 0x85, 0x47, 0x45, + 0xbb, 0x4f, 0x28, 0xc4, 0xbd, 0xc9, 0x8c, 0x37, 0x03, 0x70, 0x90, 0x63, 0x88, 0x63, 0x70, 0xa0, 0xb5, 0xac, 0xd0, + 0xa9, 0x91, 0x80, 0x88, 0xb5, 0x25, 0x7f, 0xd3, 0x5b, 0xec, 0x2a, 0x7a, 0x6d, 0xdb, 0x77, 0x8e, 0x7f, 0xfe, 0xb6, + 0xda, 0xd6, 0x4d, 0x2c, 0xe4, 0x9d, 0x91, 0x41, 0x3d, 0xb0, 0xbf, 0xef, 0x88, 0x13, 0x6d, 0x81, 0xc0, 0xd5, 0x07, + 0xd3, 0x62, 0x7d, 0xbc, 0x10, 0x31, 0x3f, 0xf8, 0x18, 0x26, 0xf1, 0x14, 0x1d, 0x7d, 0xc6, 0xe7, 0x86, 0x8f, 0xc2, + 0x0f, 0xff, 0xb3, 0x1c, 0x58, 0x99, 0x74, 0x24, 0xa7, 0x8e, 0xa9, 0x8e, 0x02, 0x02, 0xe8, 0x4c, 0xee, 0x91, 0xef, + 0xbf, 0x3a, 0xb4, 0x54, 0xb1, 0x6c, 0x3a, 0x43, 0xb3, 0x93, 0x4e, 0xac, 0x5b, 0xcc, 0x06, 0x9f, 0x38, 0xf7, 0x8b, + 0xcb, 0x0f, 0xe9, 0xc9, 0x61, 0x7f, 0x7b, 0xd2, 0x68, 0xd3, 0x63, 0x46, 0x03, 0x60, 0x0c, 0x2b, 0xfd, 0x78, 0x90, + 0xd2, 0xeb, 0x27, 0x6a, 0xa2, 0x65, 0x43, 0x78, 0x66, 0x3c, 0xba, 0x0c, 0x91, 0xfe, 0xc3, 0xa0, 0x78, 0xd8, 0x6c, + 0xbd, 0x32, 0x5f, 0xb0, 0x9a, 0x83, 0xd1, 0x0b, 0x82, 0x66, 0xc3, 0x16, 0x8b, 0xca, 0xea, 0x71, 0x7e, 0x84, 0x59, + 0x50, 0x00, 0x3e, 0x65, 0x6d, 0x80, 0xfe, 0x39, 0xe6, 0x98, 0x0b, 0x88, 0x46, 0xa3, 0x36, 0x52, 0x6d, 0xf5, 0xbc, + 0xe2, 0x9f, 0xa9, 0x38, 0x50, 0xeb, 0x3d, 0x39, 0x66, 0x7b, 0xca, 0xea, 0x6a, 0x93, 0x4a, 0x03, 0xb4, 0xbe, 0x4c, + 0xf0, 0xb5, 0x0e, 0xb5, 0x04, 0x72, 0x56, 0xc0, 0x67, 0x96, 0x56, 0x97, 0xd9, 0x3d, 0xe7, 0xf8, 0xbd, 0x78, 0xf7, + 0xa0, 0x33, 0xee, 0x36, 0xdf, 0x6d, 0x06, 0x3b, 0x2b, 0x91, 0xdf, 0x0f, 0x1c, 0xb0, 0xf5, 0xce, 0xf1, 0xb2, 0x16, + 0x78, 0xbf, 0x85, 0x41, 0x00, 0xf2, 0x7e, 0x81, 0x5d, 0xd2, 0x38, 0x0d, 0xf3, 0x95, 0xb6, 0x94, 0xc6, 0xb8, 0x72, + 0xfc, 0x94, 0x33, 0xff, 0x3f, 0xd4, 0x58, 0x19, 0xc7, 0x4f, 0x6c, 0x80, 0x76, 0x15, 0x20, 0xc9, 0x01, 0xd1, 0xc1, + 0x93, 0x16, 0x8f, 0xdf, 0x08, 0x0a, 0xfd, 0x6f, 0xae, 0xf9, 0xf5, 0x86, 0x41, 0x6c, 0x7b, 0x84, 0xf0, 0x0b, 0x6d, + 0xd8, 0xfc, 0x4d, 0x67, 0xcd, 0x25, 0x44, 0x72, 0xfd, 0x1d, 0x29, 0xa9, 0xab, 0xe7, 0x91, 0xfb, 0x93, 0x06, 0xc0, + 0xa4, 0xb2, 0xfa, 0x3a, 0xed, 0xf9, 0xc2, 0xeb, 0x79, 0x07, 0xb1, 0x19, 0xc7, 0xef, 0x8e, 0x98, 0xf8, 0x50, 0x54, + 0xd5, 0x59, 0xd4, 0xb4, 0x3a, 0xf6, 0xd6, 0x49, 0x07, 0x3a, 0x71, 0x41, 0xf0, 0x18, 0xbf, 0x04, 0xfb, 0x79, 0xf3, + 0x43, 0x42, 0x1d, 0xbf, 0xeb, 0x87, 0xe4, 0x7a, 0x37, 0x85, 0x07, 0x76, 0xc0, 0xf7, 0xf0, 0xc1, 0xda, 0x44, 0xd3, + 0xb9, 0x10, 0x1f, 0x42, 0x52, 0x11, 0x90, 0xf5, 0x24, 0x4e, 0x6e, 0x4a, 0x92, 0x60, 0xc3, 0x5e, 0xd6, 0xb6, 0x82, + 0xc3, 0xb9, 0x76, 0x87, 0x22, 0x9c, 0x46, 0x07, 0xdd, 0x0c, 0x8f, 0x38, 0xe3, 0xa4, 0x6e, 0x65, 0xea, 0xb3, 0x6d, + 0x10, 0x89, 0x91, 0x70, 0x05, 0x04, 0x9f, 0x08, 0x1e, 0x8c, 0x98, 0x1a, 0x20, 0xa9, 0x08, 0x70, 0xfd, 0xb0, 0x8d, + 0x50, 0x76, 0x3f, 0xe5, 0x27, 0x7c, 0x12, 0x43, 0x0e, 0x39, 0xac, 0xc3, 0xf3, 0xe7, 0x70, 0xd1, 0x50, 0x2c, 0xce, + 0x1c, 0x67, 0x5e, 0x94, 0xd5, 0xb4, 0x50, 0x9c, 0x58, 0xf9, 0x82, 0x07, 0x5c, 0x6f, 0xc0, 0xbc, 0x9d, 0x0a, 0x76, + 0xc6, 0x33, 0x5e, 0x61, 0x4a, 0x4c, 0x6f, 0x77, 0xce, 0x2b, 0x5d, 0xb9, 0x55, 0x14, 0xaf, 0x1a, 0xb4, 0x67, 0x46, + 0x5c, 0xf8, 0x3b, 0xad, 0x8d, 0x6e, 0xd9, 0xa5, 0x71, 0xf8, 0x37, 0x4a, 0x24, 0x04, 0x9b, 0x9f, 0x78, 0xe3, 0x3d, + 0xb4, 0x6b, 0xdf, 0x05, 0x87, 0x59, 0x7e, 0xfb, 0x1a, 0xfd, 0xe9, 0x4d, 0xcf, 0xb0, 0x28, 0xbd, 0x9f, 0x99, 0x83, + 0xea, 0x40, 0x56, 0x57, 0x87, 0x03, 0x0c, 0xda, 0xe1, 0x8e, 0x57, 0x90, 0x6e, 0xc5, 0x2c, 0x43, 0xa4, 0x33, 0x19, + 0xfd, 0xdd, 0x8b, 0x79, 0xc1, 0x3a, 0x04, 0x66, 0x1f, 0x0d, 0x73, 0x02, 0x17, 0xab, 0x0c, 0x0a, 0xa1, 0x0a, 0x21, + 0x7c, 0x1c, 0xe6, 0x8a, 0x9c, 0x06, 0x52, 0xe1, 0x8a, 0x9c, 0xfa, 0xa4, 0x83, 0x72, 0x1d, 0x3a, 0x5f, 0xad, 0x71, + 0x3c, 0xc5, 0x84, 0xbe, 0x18, 0x78, 0xa8, 0xaf, 0xd8, 0x2c, 0x3e, 0xf7, 0x42, 0x64, 0xfd, 0x0d, 0x98, 0xdc, 0xe0, + 0x65, 0x75, 0x9f, 0x85, 0x10, 0xb3, 0x70, 0x99, 0x19, 0xa9, 0x5f, 0x8a, 0x5a, 0x4f, 0xa3, 0x11, 0xa0, 0xd6, 0x3c, + 0xa0, 0x55, 0xcb, 0x10, 0x61, 0xfc, 0x25, 0xb4, 0xf4, 0x7b, 0xed, 0xe0, 0x86, 0x5f, 0xc5, 0x34, 0x1c, 0xc3, 0xfc, + 0x47, 0x11, 0x7a, 0x88, 0x01, 0x97, 0x71, 0x4d, 0xad, 0x5c, 0x8d, 0x06, 0xb9, 0x62, 0x7c, 0x01, 0x90, 0x32, 0x18, + 0x60, 0xac, 0x59, 0x28, 0x9e, 0x7f, 0xc7, 0x1f, 0x82, 0x08, 0xf5, 0x6a, 0x1f, 0xfb, 0xd1, 0x0d, 0x31, 0xa6, 0x36, + 0x3e, 0x26, 0x38, 0xf8, 0xd8, 0x5a, 0x69, 0xdf, 0x74, 0x95, 0x35, 0xc2, 0x09, 0xb4, 0xe0, 0xca, 0x3c, 0x88, 0x0f, + 0xa7, 0x36, 0xff, 0x2f, 0xc5, 0xaa, 0x1e, 0xbb, 0xfb, 0xfb, 0x23, 0x5c, 0x0f, 0x9d, 0x72, 0x90, 0x57, 0xb8, 0x00, + 0x2e, 0xbb, 0xea, 0x9c, 0x57, 0xbe, 0xb2, 0x4c, 0xfe, 0x16, 0x0e, 0x96, 0x0f, 0xca, 0x71, 0x3a, 0xfd, 0xcb, 0xb5, + 0x8b, 0xa3, 0x3d, 0x98, 0x4f, 0xd3, 0x30, 0xfe, 0x49, 0x2c, 0x7d, 0x5e, 0xd0, 0xd9, 0x6f, 0x48, 0x1b, 0x3f, 0x2e, + 0xb2, 0x7d, 0xe8, 0xba, 0x3c, 0x7f, 0x8d, 0xb7, 0xe7, 0x76, 0x4d, 0x9b, 0xce, 0xf7, 0x3f, 0xa5, 0xb3, 0x71, 0xcf, + 0xf8, 0x6f, 0xf4, 0x44, 0x27, 0xdf, 0x18, 0x7f, 0x48, 0x6b, 0xe3, 0xd3, 0x20, 0xbe, 0x6c, 0x0b, 0xb2, 0x87, 0x73, + 0x78, 0x1a, 0xce, 0x17, 0x94, 0x5f, 0x64, 0x71, 0xd1, 0x9f, 0xbe, 0xc6, 0x8b, 0x73, 0xcf, 0xcb, 0xb5, 0xd6, 0x7c, + 0x6a, 0x6d, 0xc0, 0xd6, 0x02, 0xe7, 0x46, 0xed, 0x96, 0x49, 0xaa, 0x56, 0xde, 0x88, 0xe9, 0x6c, 0x1a, 0x51, 0x07, + 0xfb, 0x7d, 0x7b, 0xdc, 0xf1, 0x40, 0xff, 0xb3, 0x79, 0x5d, 0x71, 0x6d, 0xd5, 0x4d, 0x77, 0x56, 0xe0, 0x0d, 0x93, + 0xa5, 0x23, 0x3c, 0x2b, 0x88, 0x34, 0xd2, 0x07, 0xa4, 0x65, 0x6d, 0xdb, 0x12, 0x43, 0xbb, 0x59, 0xc9, 0x34, 0x71, + 0x5b, 0x33, 0x5c, 0xe2, 0x4c, 0x08, 0x10, 0x49, 0xa6, 0x18, 0xba, 0xd6, 0x0c, 0x90, 0xde, 0x41, 0x49, 0x88, 0x65, + 0xbf, 0x04, 0x8a, 0x25, 0x83, 0x4f, 0xff, 0x61, 0x45, 0x4c, 0x8e, 0x37, 0x74, 0x70, 0x2a, 0x68, 0xf6, 0xd8, 0x8e, + 0xb9, 0x08, 0xc2, 0x97, 0x28, 0xf4, 0x4c, 0x63, 0x27, 0x57, 0x6d, 0x8e, 0x9e, 0xd8, 0x09, 0x6b, 0x1a, 0x05, 0x55, + 0xbb, 0xdf, 0xde, 0x2a, 0x15, 0x37, 0x57, 0x9c, 0xcf, 0x60, 0x8c, 0x27, 0x1d, 0x41, 0xe4, 0xcf, 0xfe, 0x02, 0xca, + 0xd0, 0x25, 0x8c, 0xb2, 0x65, 0xde, 0x8f, 0x26, 0xb7, 0x52, 0xc7, 0x92, 0xd0, 0xd4, 0xf5, 0xea, 0x8a, 0x54, 0xe1, + 0xfe, 0x2e, 0xfc, 0xb3, 0x06, 0x71, 0x87, 0x38, 0x87, 0x64, 0x01, 0x51, 0x3d, 0x63, 0x25, 0xc5, 0x20, 0x66, 0x36, + 0x28, 0x61, 0x4a, 0x9f, 0xb4, 0xda, 0x6a, 0x9d, 0x1c, 0x7b, 0x5c, 0xae, 0xea, 0x42, 0xd6, 0x2d, 0x7f, 0xa4, 0x45, + 0x22, 0x2d, 0x70, 0x85, 0xef, 0x2c, 0x00, 0x5d, 0x09, 0xe0, 0x29, 0x04, 0x72, 0x98, 0x84, 0xbf, 0x95, 0x55, 0xf4, + 0xe0, 0xfe, 0x6d, 0x98, 0x5b, 0x8e, 0x40, 0xc2, 0x87, 0xb9, 0x69, 0x8d, 0x3a, 0x8d, 0x4c, 0x6b, 0xd8, 0xba, 0x04, + 0xe2, 0x24, 0x41, 0x0b, 0x35, 0xf6, 0x71, 0x28, 0x1c, 0x7a, 0x1e, 0xb9, 0x49, 0xae, 0xe5, 0xca, 0x97, 0xa2, 0x39, + 0x89, 0x3d, 0x52, 0xd1, 0xb1, 0x9f, 0x91, 0xe3, 0xbc, 0x10, 0xe4, 0xe2, 0x48, 0x9a, 0x9e, 0x6a, 0x92, 0x43, 0x9b, + 0x0c, 0x2a, 0x94, 0xdb, 0x2c, 0x68, 0x73, 0x1b, 0xb1, 0xbf, 0x8e, 0x88, 0x0b, 0x1b, 0x40, 0x22, 0x9c, 0x5c, 0x55, + 0xfd, 0x2d, 0xb9, 0xbe, 0x6e, 0x7c, 0x55, 0x0b, 0x19, 0x0f, 0x28, 0x19, 0x4e, 0xea, 0xed, 0x19, 0x0a, 0xc3, 0xc5, + 0xfc, 0xb4, 0xbe, 0xb0, 0xd6, 0xd4, 0x6e, 0xa5, 0x48, 0x0a, 0x43, 0x9a, 0xf2, 0x44, 0xe2, 0x87, 0x65, 0x77, 0xb1, + 0x49, 0xc5, 0x8a, 0xc0, 0xfb, 0x9c, 0xf9, 0x73, 0xe1, 0xd4, 0x1a, 0xff, 0x21, 0xc0, 0xad, 0x39, 0x38, 0xa8, 0xbf, + 0x8b, 0xdc, 0x64, 0xab, 0x1e, 0x38, 0x4d, 0x7e, 0x74, 0x45, 0x3f, 0x8b, 0x62, 0xdc, 0x83, 0x41, 0x9e, 0xb3, 0x46, + 0x1c, 0x27, 0x5e, 0xa1, 0xc8, 0xa6, 0x12, 0xba, 0xdb, 0x75, 0xa6, 0x88, 0xeb, 0x90, 0xa3, 0x19, 0x72, 0x72, 0x38, + 0x4e, 0x5a, 0xcd, 0xa3, 0xb2, 0x49, 0x12, 0x9e, 0xe2, 0x47, 0xee, 0x13, 0x8a, 0x5d, 0x9f, 0x85, 0x32, 0x23, 0xce, + 0x19, 0x67, 0xdb, 0x0b, 0xae, 0xd1, 0x5b, 0x73, 0x90, 0x8e, 0x1d, 0xf6, 0xfc, 0x89, 0x22, 0x4c, 0x21, 0x65, 0xa7, + 0x26, 0x6d, 0xd2, 0x55, 0x97, 0x71, 0x9f, 0x0e, 0x75, 0x1c, 0x52, 0x3d, 0x3b, 0x1c, 0xea, 0xa5, 0x2d, 0x4f, 0x1c, + 0xe2, 0xca, 0x87, 0xfe, 0x38, 0xf2, 0xeb, 0xc2, 0x7a, 0x51, 0xc8, 0xf8, 0xa4, 0xd0, 0x49, 0x4b, 0x95, 0x78, 0x00, + 0xb7, 0x95, 0x4d, 0x6f, 0xcb, 0xd4, 0xda, 0xd0, 0x71, 0xe9, 0x6f, 0x02, 0xa4, 0x90, 0xc5, 0xa9, 0x5c, 0x0a, 0xe5, + 0x9a, 0xf1, 0xe2, 0xb0, 0xe2, 0xf6, 0xd5, 0x7d, 0xda, 0x57, 0x14, 0x1d, 0x20, 0x10, 0x11, 0x5a, 0x01, 0xc2, 0x17, + 0x26, 0x70, 0x75, 0x95, 0xa5, 0xb0, 0x8e, 0x09, 0xc1, 0x53, 0xf8, 0x46, 0x6a, 0xa5, 0x55, 0x46, 0xc4, 0x05, 0xdb, + 0x8d, 0x50, 0xf6, 0x00, 0x1a, 0x10, 0xc3, 0x49, 0xfc, 0x2f, 0x4f, 0x55, 0xcb, 0xb4, 0x5b, 0xc9, 0xa5, 0x91, 0x76, + 0xa3, 0x2d, 0xde, 0x98, 0x56, 0x14, 0x14, 0x13, 0x92, 0xbe, 0xd2, 0xa0, 0xd5, 0xb1, 0xf5, 0x9b, 0xbd, 0x5e, 0xbc, + 0x3a, 0xbe, 0xe3, 0xe4, 0x60, 0x94, 0x63, 0xc9, 0x20, 0x53, 0x11, 0xca, 0xc5, 0x45, 0xd8, 0x7a, 0xd8, 0xd9, 0x16, + 0xda, 0x69, 0xd0, 0x71, 0xb7, 0x82, 0x1a, 0x84, 0xf9, 0xd0, 0x73, 0xa7, 0xdb, 0x3e, 0x5d, 0x19, 0xb7, 0x8b, 0x78, + 0x95, 0xe3, 0x54, 0x55, 0x09, 0xa4, 0x64, 0xf3, 0x31, 0x48, 0x95, 0x24, 0x47, 0xa6, 0x0a, 0xeb, 0x1e, 0x6c, 0xef, + 0x98, 0x30, 0x09, 0x79, 0xe4, 0x7d, 0xf8, 0x27, 0x84, 0x5a, 0x8a, 0x7e, 0xdb, 0xf6, 0x6d, 0xc9, 0xe1, 0x95, 0xa3, + 0x55, 0x83, 0x80, 0xd8, 0x88, 0x00, 0x35, 0x8f, 0x8f, 0xf6, 0x26, 0x6e, 0xbd, 0xa3, 0x72, 0x37, 0x35, 0x7e, 0xcf, + 0x56, 0x76, 0x1e, 0xf9, 0x1d, 0xaf, 0xec, 0xe3, 0x42, 0x15, 0xec, 0x92, 0x12, 0x3d, 0xc9, 0xfa, 0xf1, 0xca, 0xa6, + 0x35, 0xfb, 0x79, 0x7d, 0x41, 0xc8, 0xe6, 0x55, 0xf6, 0xc8, 0xab, 0x42, 0xbd, 0x18, 0x09, 0x63, 0xaa, 0x43, 0x78, + 0xe3, 0xc8, 0xd8, 0x9f, 0x17, 0x32, 0x8d, 0x81, 0x05, 0x28, 0xb4, 0xd4, 0xbb, 0x11, 0x4f, 0x8f, 0x65, 0x56, 0xa4, + 0x75, 0x27, 0x5c, 0xc5, 0x7a, 0x09, 0x3f, 0xba, 0x0d, 0x58, 0x58, 0x29, 0xdd, 0x22, 0x97, 0x77, 0x75, 0x91, 0xf5, + 0xd9, 0x6b, 0x13, 0x43, 0xef, 0x92, 0x42, 0x85, 0xb2, 0x63, 0xca, 0x8a, 0xf9, 0x0a, 0x69, 0x8e, 0x05, 0x6f, 0x42, + 0xfd, 0xbc, 0x2d, 0x7f, 0x87, 0x2a, 0x16, 0x7f, 0x5d, 0xd1, 0x5b, 0xa7, 0x6a, 0xb6, 0xcf, 0x14, 0x33, 0x65, 0x3b, + 0x17, 0xee, 0x8b, 0xfb, 0x8d, 0x6f, 0x88, 0xa7, 0x62, 0xd5, 0x77, 0x45, 0x71, 0xe4, 0xa0, 0xc9, 0x20, 0xaa, 0x93, + 0xb5, 0x10, 0x77, 0x5d, 0x19, 0x92, 0x70, 0xe7, 0x09, 0x33, 0x48, 0xe7, 0xb0, 0x71, 0x55, 0x23, 0xd3, 0xa0, 0xe6, + 0x40, 0x9d, 0x54, 0x83, 0x15, 0xb4, 0x42, 0x52, 0xf6, 0x14, 0x33, 0x51, 0x07, 0xee, 0xf7, 0x9a, 0xfd, 0x3f, 0xa0, + 0x12, 0x7d, 0xdd, 0xf5, 0x57, 0x7d, 0x0b, 0xe7, 0x82, 0x05, 0x4b, 0x2a, 0xfb, 0x72, 0x5b, 0x6f, 0xfc, 0x29, 0x6c, + 0xea, 0xd4, 0xad, 0xbb, 0x5d, 0xea, 0x72, 0x9a, 0x0d, 0xce, 0x3b, 0x47, 0x31, 0x77, 0x0a, 0x1f, 0x62, 0x2e, 0x2f, + 0xd9, 0x44, 0x25, 0x57, 0x71, 0xe2, 0x45, 0x0d, 0x00, 0xf3, 0x0e, 0x90, 0x9c, 0x29, 0x61, 0x94, 0xf8, 0x73, 0x52, + 0x01, 0xd5, 0x94, 0xae, 0xb3, 0xb3, 0xee, 0x17, 0x7b, 0xfe, 0x8a, 0xbc, 0xbe, 0x72, 0x0c, 0xea, 0xe6, 0xbc, 0x20, + 0xa7, 0x98, 0x5f, 0x34, 0x25, 0x63, 0x4f, 0xb7, 0xad, 0xaa, 0x93, 0xb5, 0xcb, 0x8b, 0xda, 0x44, 0x89, 0x74, 0xc9, + 0x0d, 0x2f, 0xf5, 0xb6, 0xbc, 0x66, 0xcb, 0x93, 0x75, 0x7a, 0x2a, 0xd6, 0xd8, 0xbe, 0x08, 0x63, 0x7d, 0x18, 0x5d, + 0xe9, 0x49, 0x07, 0x39, 0x2d, 0x4b, 0x4b, 0xb9, 0x8b, 0x9c, 0x5b, 0xba, 0x5d, 0x3a, 0xcc, 0x8f, 0x19, 0x6b, 0x6f, + 0x8d, 0x8d, 0xad, 0xe5, 0xe6, 0xbf, 0xae, 0x6c, 0xc3, 0x54, 0xa1, 0x68, 0x01, 0x4c, 0xcf, 0x26, 0x87, 0xf5, 0x01, + 0x35, 0x53, 0x6f, 0x51, 0xbb, 0xe2, 0xf5, 0x4e, 0xf4, 0xbc, 0xfb, 0x0e, 0x6a, 0x86, 0x5a, 0x8f, 0x92, 0x68, 0xa9, + 0x7d, 0xef, 0x5b, 0x4a, 0x5b, 0xe6, 0xb1, 0xf2, 0xa2, 0xd4, 0x43, 0xfd, 0xea, 0x8f, 0xd3, 0xda, 0xb8, 0x27, 0xbc, + 0x65, 0xa3, 0xae, 0xe2, 0x63, 0x9f, 0xe7, 0xc2, 0xcc, 0x2c, 0x3e, 0x97, 0xd6, 0x83, 0x5f, 0x4e, 0xbb, 0x99, 0x39, + 0x3d, 0xbe, 0xa7, 0x83, 0xc4, 0x5c, 0x7a, 0x2f, 0x43, 0xa0, 0x68, 0x85, 0x66, 0x1d, 0x35, 0xcc, 0x79, 0x9f, 0x3a, + 0xc6, 0xcf, 0x7b, 0x4c, 0xc9, 0x1d, 0x3f, 0xe3, 0xf5, 0xd0, 0xa6, 0x9f, 0x3e, 0x66, 0xce, 0x87, 0x89, 0xf0, 0x6a, + 0x57, 0xa3, 0x13, 0x56, 0xe0, 0xeb, 0xa5, 0xc7, 0xc9, 0xa7, 0xbd, 0xaa, 0x5a, 0x5a, 0xdf, 0x7f, 0x6b, 0x62, 0x80, + 0xa9, 0x52, 0x7e, 0x45, 0xfb, 0xb9, 0xc6, 0x62, 0x86, 0x97, 0x74, 0xd9, 0xcb, 0x00}; // Backwards compatibility alias #define INDEX_GZ INDEX_BR diff --git a/esphome/components/web_server/server_index_v3.h b/esphome/components/web_server/server_index_v3.h index 1f61b19fb5..b7c15df32b 100644 --- a/esphome/components/web_server/server_index_v3.h +++ b/esphome/components/web_server/server_index_v3.h @@ -9,7 +9,7 @@ namespace esphome::web_server { #ifdef USE_WEBSERVER_GZIP -constexpr uint8_t INDEX_GZ[] PROGMEM = { +const uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xcc, 0xbd, 0x7b, 0x7f, 0x1a, 0xb9, 0xb2, 0x28, 0xfa, 0xf7, 0x3d, 0x9f, 0xc2, 0xee, 0x9d, 0xf1, 0xb4, 0x8c, 0x68, 0x03, 0x36, 0x8e, 0xd3, 0x58, 0xe6, 0xe4, 0x39, 0xc9, 0x3c, 0x92, 0x4c, 0x9c, 0x64, 0x26, 0xc3, 0xb0, 0x33, 0xa2, 0x11, 0xa0, 0xa4, 0x91, 0x98, 0x96, 0x88, 0xed, 0x01, @@ -3634,58 +3634,58 @@ constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x36, 0x16, 0x43, 0x14, 0xa2, 0x85, 0xb1, 0x9f, 0x44, 0x7a, 0x6a, 0xf7, 0x8b, 0xa8, 0x63, 0x84, 0xe7, 0x2e, 0x8e, 0x40, 0xbb, 0x60, 0xb2, 0xd8, 0xed, 0x3a, 0x06, 0xb0, 0x93, 0x12, 0x46, 0xf3, 0x4c, 0x91, 0xc8, 0x45, 0x3d, 0x95, 0x78, 0xf0, 0xa9, 0x53, 0x8d, 0x99, 0x83, 0xf0, 0x54, 0x31, 0xd4, 0xc0, 0xc7, 0x7a, 0xaf, 0x4d, 0xc8, 0x99, 0xff, - 0xaf, 0xbd, 0x67, 0x5b, 0x6e, 0xdb, 0x48, 0xf6, 0x3d, 0x5f, 0x01, 0xc1, 0x5e, 0x1b, 0xb0, 0x01, 0x08, 0x20, 0x75, - 0xa1, 0x49, 0x81, 0x4a, 0x6c, 0xcb, 0x49, 0x76, 0x95, 0x38, 0x65, 0x2b, 0xde, 0x8b, 0x56, 0x25, 0x82, 0xe4, 0x90, - 0xc4, 0x1a, 0x04, 0x58, 0x00, 0x28, 0x4a, 0xa1, 0xb1, 0xdf, 0xb2, 0x9f, 0x70, 0xbe, 0x61, 0xbf, 0xec, 0x54, 0x77, - 0xcf, 0x00, 0x83, 0x0b, 0x29, 0x2a, 0x76, 0xb2, 0x7b, 0xaa, 0x4e, 0x25, 0xb6, 0x89, 0xc1, 0xcc, 0xa0, 0xe7, 0xd6, - 0xdd, 0xd3, 0xd7, 0x32, 0xa5, 0xb4, 0x2b, 0xf8, 0x57, 0x58, 0x21, 0x57, 0x4a, 0x69, 0xcb, 0x81, 0x98, 0xd3, 0xed, - 0xe3, 0xaa, 0x81, 0x55, 0xa1, 0xb8, 0x27, 0x83, 0x99, 0x60, 0x65, 0xd7, 0x89, 0x79, 0x98, 0x75, 0x69, 0xc3, 0x1b, - 0x81, 0x0b, 0xa6, 0x87, 0x1b, 0x6a, 0x8d, 0xbb, 0x5e, 0x29, 0x14, 0x66, 0x5c, 0x9e, 0xd4, 0x13, 0xaf, 0xfc, 0x0c, - 0x5b, 0xba, 0x52, 0x05, 0x7c, 0x63, 0x2a, 0x95, 0x40, 0x0a, 0x16, 0x9c, 0xd2, 0xe7, 0xad, 0x34, 0x3a, 0x8f, 0x56, - 0x42, 0x70, 0x7c, 0xe2, 0x35, 0x14, 0xe2, 0x39, 0xe9, 0x8e, 0x4e, 0x02, 0xfa, 0xe1, 0x64, 0x1b, 0x28, 0x40, 0x56, - 0x4c, 0x70, 0xce, 0xb6, 0x0e, 0xe8, 0x32, 0x75, 0xfd, 0x78, 0x2d, 0xb6, 0x5c, 0x36, 0xf0, 0xf3, 0xb4, 0xb0, 0x25, - 0x96, 0x23, 0xe3, 0x53, 0x2f, 0x47, 0x21, 0x6d, 0xa8, 0xc6, 0xf7, 0x87, 0xeb, 0x37, 0xf2, 0x2d, 0x16, 0x3d, 0x32, - 0xa2, 0xe9, 0xcd, 0x55, 0xd8, 0x2d, 0x1b, 0x69, 0x4d, 0x80, 0x91, 0xe0, 0x49, 0x0c, 0x01, 0x03, 0x33, 0x23, 0xea, - 0xff, 0x36, 0xae, 0xa2, 0x7e, 0xb4, 0xbf, 0xcb, 0x91, 0xff, 0x4f, 0x6f, 0xdf, 0x5f, 0x80, 0x5e, 0xcb, 0x43, 0x45, - 0xf4, 0x5a, 0xe5, 0x36, 0x2c, 0x26, 0x68, 0x8a, 0xd4, 0xae, 0xea, 0x2d, 0x80, 0x3a, 0xe3, 0x8d, 0x61, 0xff, 0xd6, - 0x5c, 0xad, 0x56, 0x26, 0x58, 0xb4, 0x9a, 0xcb, 0x38, 0x20, 0xee, 0x70, 0xac, 0x66, 0x02, 0xa9, 0xb3, 0x0a, 0x52, - 0x87, 0x70, 0xb8, 0x3c, 0x9f, 0xca, 0xfb, 0x59, 0xb4, 0xfa, 0x26, 0x08, 0x64, 0xb1, 0x8d, 0x60, 0xe2, 0xb8, 0x24, - 0xa3, 0x84, 0x0c, 0x34, 0xd0, 0x3e, 0x59, 0x7e, 0x72, 0xcd, 0xed, 0x05, 0xc6, 0xd7, 0xc3, 0xbb, 0x6b, 0xae, 0x93, - 0xc8, 0xe3, 0x11, 0xbf, 0x1f, 0x9c, 0x8c, 0xfd, 0x1b, 0x05, 0x39, 0x4d, 0x57, 0x05, 0x67, 0xae, 0x80, 0x0d, 0x97, - 0x69, 0x1a, 0x85, 0x66, 0x1c, 0xad, 0xd4, 0xfe, 0x09, 0x3d, 0x88, 0x0a, 0x1e, 0x3d, 0xaa, 0xca, 0xd7, 0xa3, 0xc0, - 0x1f, 0x7d, 0x74, 0xd5, 0xc7, 0x6b, 0xdf, 0xed, 0x57, 0xf8, 0x49, 0x3b, 0x53, 0xfb, 0x00, 0xab, 0xf2, 0x4d, 0x10, - 0x9c, 0xec, 0x53, 0x8b, 0xfe, 0xc9, 0xfe, 0xd8, 0xbf, 0xe9, 0x4b, 0xa9, 0x61, 0xb8, 0xde, 0xd4, 0xe5, 0x21, 0x38, - 0x73, 0x4b, 0xb3, 0x04, 0x63, 0x3a, 0x8c, 0x98, 0x56, 0x5c, 0x7e, 0x21, 0xd6, 0x0c, 0xc1, 0xab, 0x8d, 0x50, 0x9c, - 0x1e, 0xc0, 0x55, 0xef, 0xd3, 0x27, 0x2d, 0xb7, 0x43, 0x9d, 0x49, 0x41, 0xda, 0x50, 0xcd, 0x87, 0x55, 0x0c, 0x8c, - 0x34, 0xa3, 0x6b, 0x22, 0x94, 0x5c, 0xa0, 0x1b, 0xa3, 0xcc, 0xc0, 0x0c, 0x3b, 0xde, 0x02, 0x34, 0x8e, 0xfc, 0xa7, - 0x74, 0x23, 0x1e, 0x41, 0x56, 0x6d, 0x09, 0x89, 0xeb, 0x92, 0xce, 0x85, 0x4e, 0x21, 0x8f, 0x13, 0x08, 0xca, 0x12, - 0xfc, 0x0e, 0xe9, 0x41, 0xb4, 0x40, 0x87, 0xac, 0x6e, 0x79, 0x70, 0x1e, 0x2f, 0x13, 0x79, 0xd4, 0xc4, 0xbc, 0x9c, - 0x96, 0x56, 0xa8, 0x5b, 0x5d, 0x2f, 0x11, 0x35, 0x72, 0x2f, 0xd9, 0xb4, 0x64, 0xa0, 0xc3, 0xd3, 0x52, 0xa3, 0x42, - 0x73, 0xc1, 0xab, 0x4f, 0x52, 0x1c, 0x31, 0x43, 0xbb, 0x4c, 0x8c, 0xe8, 0xaa, 0xa0, 0x53, 0x09, 0x21, 0xca, 0x6e, - 0x94, 0x15, 0x01, 0x9c, 0x69, 0xd5, 0xfb, 0x8f, 0xd7, 0x21, 0x12, 0xb6, 0xc4, 0xed, 0x97, 0xf7, 0x41, 0xea, 0x0d, - 0x4d, 0xda, 0xcc, 0xaa, 0xf2, 0xf5, 0x78, 0x18, 0xe4, 0x8b, 0x4d, 0x87, 0x60, 0xe6, 0x85, 0xe3, 0x80, 0x5d, 0x78, - 0xc3, 0xef, 0xb0, 0xce, 0xeb, 0x61, 0xf0, 0x0a, 0x2a, 0x64, 0x6a, 0xff, 0xf1, 0x9a, 0x48, 0x77, 0x13, 0xc2, 0xce, - 0x68, 0x0b, 0x54, 0xbf, 0xc3, 0x53, 0x2e, 0xb1, 0x98, 0x5a, 0x23, 0xb0, 0x44, 0x6e, 0x29, 0x8e, 0x6d, 0x19, 0x32, - 0x9e, 0xf2, 0x07, 0xf6, 0xa6, 0xc2, 0x4f, 0x2d, 0xc0, 0x15, 0x89, 0x13, 0x2c, 0xef, 0x4c, 0x19, 0x58, 0x22, 0xab, - 0xef, 0xa2, 0x95, 0x80, 0x94, 0x4f, 0x00, 0x85, 0xa8, 0x3c, 0x7d, 0x3f, 0x38, 0x91, 0xd5, 0x42, 0x28, 0x3b, 0xa7, - 0x7e, 0xe1, 0x57, 0xa6, 0x2a, 0x45, 0x02, 0xa8, 0xc5, 0xad, 0xda, 0x3f, 0xd9, 0x97, 0x6b, 0xf7, 0x07, 0xdd, 0x33, - 0x69, 0x70, 0xd8, 0xab, 0xb8, 0x37, 0x5f, 0x16, 0x0f, 0xd9, 0x95, 0x02, 0xb7, 0xe4, 0x0c, 0x4a, 0x60, 0x8e, 0xca, - 0x4d, 0x6a, 0xe4, 0x07, 0x52, 0x26, 0x16, 0x04, 0x8a, 0x76, 0x8f, 0xc0, 0x8f, 0x91, 0xde, 0xcd, 0x97, 0x90, 0x2c, - 0x33, 0x45, 0x6f, 0x03, 0xfe, 0x6f, 0x31, 0x25, 0x28, 0xe9, 0x66, 0x61, 0x12, 0xc5, 0x2a, 0x0c, 0xb3, 0x9a, 0x37, - 0x49, 0x91, 0xf2, 0xb5, 0xe1, 0x80, 0x1b, 0xc9, 0x2a, 0x4c, 0xd8, 0x7e, 0xb5, 0xa9, 0x34, 0xee, 0x81, 0x5e, 0xfc, - 0x50, 0xf8, 0x60, 0x2a, 0x48, 0x2b, 0x07, 0x70, 0x73, 0x3e, 0xaa, 0xcb, 0xc7, 0xbe, 0xf1, 0xe7, 0xc8, 0x18, 0x7a, - 0xc6, 0xb5, 0x67, 0xfc, 0x10, 0x5e, 0x65, 0x8d, 0x8b, 0x97, 0xe7, 0x92, 0x33, 0x58, 0x4f, 0x83, 0x08, 0x4c, 0xe5, - 0x4b, 0x85, 0x6f, 0x71, 0x9b, 0x91, 0x0b, 0x2f, 0x9e, 0x32, 0x91, 0xc2, 0x4d, 0xbc, 0x15, 0xb2, 0x03, 0x5d, 0x9a, - 0x16, 0x08, 0x4f, 0xb6, 0xc7, 0x4d, 0xeb, 0x7c, 0x6b, 0x94, 0xc6, 0xc1, 0x9f, 0xd8, 0x1d, 0xb0, 0x59, 0x49, 0x1a, - 0x2d, 0x40, 0x66, 0xe5, 0x4d, 0xb9, 0x0e, 0xc2, 0xd0, 0xd8, 0x6e, 0x9f, 0xfb, 0xf4, 0x89, 0x49, 0x59, 0xc5, 0xd2, - 0x68, 0x3a, 0x0d, 0x98, 0x26, 0x65, 0x1f, 0xcb, 0x3f, 0x73, 0xba, 0x67, 0x8b, 0xc8, 0xd5, 0x7a, 0xb6, 0xe9, 0x60, + 0xaf, 0xbd, 0xa7, 0xdd, 0x6e, 0x13, 0x49, 0xf6, 0xff, 0x3c, 0x05, 0x26, 0xd9, 0x04, 0x12, 0xc0, 0x20, 0xf9, 0x43, + 0x91, 0x8c, 0x3c, 0x93, 0xc4, 0x99, 0x8f, 0xf5, 0x4c, 0xe6, 0x24, 0x9e, 0xec, 0xbd, 0xeb, 0xf5, 0xb1, 0x90, 0xd4, + 0x92, 0xd8, 0x20, 0xd0, 0x01, 0x64, 0xd9, 0xa3, 0xb0, 0xcf, 0xb2, 0x8f, 0x70, 0x9f, 0x61, 0x9f, 0xec, 0x9e, 0xaa, + 0xea, 0x86, 0x06, 0x81, 0x2c, 0x4f, 0x32, 0xb3, 0x7b, 0xcf, 0xb9, 0x67, 0x26, 0x89, 0x68, 0xba, 0x9b, 0xea, 0xaf, + 0xaa, 0xea, 0xfa, 0x2c, 0x53, 0x4a, 0xbb, 0x82, 0x7f, 0x85, 0x15, 0x72, 0xa5, 0x94, 0xb6, 0x1c, 0x88, 0x39, 0xdd, + 0x3e, 0xae, 0x1a, 0x58, 0x15, 0x8a, 0x7b, 0x32, 0x98, 0x09, 0x56, 0x76, 0x9d, 0x98, 0x87, 0x59, 0x97, 0x36, 0xbc, + 0x11, 0xb8, 0x60, 0x7a, 0xd8, 0x50, 0x6b, 0xdc, 0xf5, 0x4a, 0xa1, 0x30, 0xe3, 0xf2, 0xa4, 0x9e, 0x78, 0xe5, 0x67, + 0xd8, 0xd2, 0x95, 0x2a, 0xe0, 0x1b, 0x53, 0xa9, 0x04, 0x52, 0xb0, 0xe0, 0x94, 0x3e, 0x6f, 0xa5, 0xd1, 0x79, 0xb4, + 0x12, 0x82, 0xe3, 0x13, 0xaf, 0xa6, 0x10, 0xcf, 0x49, 0x77, 0x74, 0x12, 0xd0, 0x0f, 0x27, 0x6b, 0xa0, 0x00, 0x59, + 0x31, 0xc1, 0x39, 0xdb, 0x3a, 0xa0, 0xcb, 0xd4, 0xf5, 0xe3, 0xb5, 0xd8, 0x72, 0xd9, 0xc0, 0xcf, 0xd3, 0xc2, 0x96, + 0x58, 0x8e, 0x8c, 0x4f, 0xbd, 0x1c, 0x85, 0xb4, 0xa6, 0x1a, 0xdf, 0x1f, 0xae, 0x5f, 0xcb, 0xb7, 0x58, 0xf4, 0xc8, + 0x88, 0xa6, 0xd7, 0x57, 0x61, 0xb7, 0x6c, 0xa4, 0xd5, 0x01, 0x46, 0x82, 0x27, 0x31, 0x04, 0x0c, 0xcc, 0x8c, 0xa8, + 0xff, 0xdb, 0xb8, 0x8a, 0xfa, 0xd1, 0xfe, 0x2e, 0x47, 0xfe, 0x3f, 0xbf, 0x7d, 0x7f, 0x01, 0x7a, 0x2d, 0x0f, 0x15, + 0xd1, 0x6b, 0x95, 0xdb, 0xb0, 0x98, 0xa0, 0x29, 0x52, 0xbb, 0xaa, 0xb7, 0x00, 0xea, 0x8c, 0x37, 0x86, 0xfd, 0x5b, + 0x73, 0xb5, 0x5a, 0x99, 0x60, 0xd1, 0x6a, 0x2e, 0xe3, 0x80, 0xb8, 0xc3, 0xb1, 0x9a, 0x09, 0xa4, 0xce, 0x2a, 0x48, + 0x1d, 0xc2, 0xe1, 0xf2, 0x7c, 0x2a, 0xef, 0x67, 0xd1, 0xea, 0x9b, 0x20, 0x90, 0xc5, 0x36, 0x82, 0x89, 0xe3, 0x92, + 0x8c, 0x12, 0x32, 0xd0, 0x40, 0xfb, 0x64, 0xf9, 0xc9, 0x35, 0xb7, 0x17, 0x18, 0x5f, 0x0f, 0xef, 0xae, 0xb9, 0x4e, + 0x22, 0x8f, 0x47, 0xfc, 0x7e, 0x70, 0x32, 0xf6, 0x6f, 0x14, 0xe4, 0x34, 0x5d, 0x15, 0x9c, 0xb9, 0x02, 0x36, 0x5c, + 0xa6, 0x69, 0x14, 0x9a, 0x71, 0xb4, 0x52, 0xfb, 0x27, 0xf4, 0x20, 0x2a, 0x78, 0xf4, 0xa8, 0x2a, 0x5f, 0x8f, 0x02, + 0x7f, 0xf4, 0xd1, 0x55, 0x1f, 0xaf, 0x7d, 0xb7, 0x5f, 0xe1, 0x27, 0xed, 0x4c, 0xed, 0x03, 0xac, 0xca, 0x37, 0x41, + 0x70, 0xb2, 0x4f, 0x2d, 0xfa, 0x27, 0xfb, 0x63, 0xff, 0xa6, 0x2f, 0xa5, 0x86, 0xe1, 0x7a, 0x53, 0x97, 0x87, 0xe0, + 0xcc, 0x2d, 0xcd, 0x12, 0x8c, 0xe9, 0x30, 0x62, 0x5a, 0x71, 0xf9, 0x85, 0x58, 0x33, 0x04, 0xaf, 0x36, 0x42, 0x71, + 0x7a, 0x00, 0x57, 0xbd, 0x4f, 0x9f, 0xb4, 0xdc, 0x0e, 0x75, 0x26, 0x05, 0x69, 0x43, 0x35, 0x1f, 0x56, 0x31, 0x30, + 0xd2, 0x8c, 0xae, 0x89, 0x50, 0x72, 0x81, 0x6e, 0x8c, 0x32, 0x03, 0x33, 0xec, 0x78, 0x0b, 0xd0, 0x38, 0xf2, 0x9f, + 0xd2, 0x8d, 0x78, 0x04, 0x59, 0xb5, 0x25, 0x24, 0xae, 0x4b, 0x3a, 0x17, 0x3a, 0x85, 0x3c, 0x4e, 0x20, 0x28, 0x4b, + 0xf0, 0x3b, 0xa4, 0x07, 0xd1, 0x02, 0x1d, 0xb2, 0xba, 0xe5, 0xc1, 0x79, 0xbc, 0x4c, 0xe4, 0x51, 0x13, 0xf3, 0x72, + 0x5a, 0x5a, 0xa1, 0x6e, 0x75, 0xbd, 0x44, 0xd4, 0xc8, 0xbd, 0xa4, 0x69, 0xc9, 0x40, 0x87, 0xa7, 0xa5, 0x46, 0x85, + 0xe6, 0x82, 0x57, 0x9f, 0xa4, 0x38, 0x62, 0x86, 0x76, 0x99, 0x18, 0xd1, 0x55, 0x41, 0xa7, 0x12, 0x42, 0x94, 0xdd, + 0x28, 0x2b, 0x02, 0x38, 0xd3, 0xaa, 0xf7, 0x1f, 0xaf, 0x43, 0x24, 0x6c, 0x89, 0xdb, 0x2f, 0xef, 0x83, 0xd4, 0x1b, + 0x9a, 0xb4, 0x99, 0x55, 0xe5, 0xeb, 0xf1, 0x30, 0xc8, 0x17, 0x9b, 0x0e, 0xc1, 0xcc, 0x0b, 0xc7, 0x01, 0xbb, 0xf0, + 0x86, 0xdf, 0x61, 0x9d, 0xd7, 0xc3, 0xe0, 0x15, 0x54, 0xc8, 0xd4, 0xfe, 0xe3, 0x35, 0x91, 0xee, 0x3a, 0x84, 0x9d, + 0xd1, 0x16, 0xa8, 0x7e, 0x87, 0xa7, 0x5c, 0x62, 0x31, 0xb5, 0x46, 0x60, 0x89, 0xdc, 0x52, 0x1c, 0xdb, 0x32, 0x64, + 0x3c, 0xe5, 0x0f, 0xec, 0x4d, 0x85, 0x9f, 0x5a, 0x80, 0x2b, 0x12, 0x27, 0x58, 0xde, 0x99, 0x32, 0xb0, 0x44, 0x56, + 0xdf, 0x45, 0x2b, 0x01, 0x29, 0x9f, 0x00, 0x0a, 0x51, 0x79, 0xfa, 0x7e, 0x70, 0x22, 0xab, 0x85, 0x50, 0x76, 0x4e, + 0xfd, 0xc2, 0xaf, 0x4c, 0x55, 0x8a, 0x04, 0x50, 0x8b, 0x5b, 0xb5, 0x7f, 0xb2, 0x2f, 0xd7, 0xee, 0x0f, 0xba, 0x67, + 0xd2, 0xe0, 0xb0, 0x57, 0x71, 0x6f, 0xbe, 0x2c, 0x1e, 0xb2, 0x2b, 0x05, 0x6e, 0xc9, 0x19, 0x94, 0xc0, 0x1c, 0x95, + 0x9b, 0x6c, 0x90, 0x1f, 0x48, 0x99, 0x58, 0x10, 0x28, 0xda, 0x3d, 0x02, 0x3f, 0x46, 0x7a, 0x37, 0x5f, 0x42, 0xb2, + 0xcc, 0x14, 0xbd, 0x0d, 0xf8, 0xbf, 0xc5, 0x94, 0xa0, 0xa4, 0x9b, 0x85, 0x49, 0x14, 0xab, 0x30, 0xcc, 0x6a, 0xde, + 0x24, 0x45, 0xca, 0xd7, 0x86, 0x03, 0xae, 0x25, 0xab, 0x30, 0x61, 0xfb, 0xd5, 0xa6, 0xd2, 0xb8, 0x07, 0x7a, 0xf1, + 0x43, 0xe1, 0x83, 0xa9, 0x20, 0xad, 0x1c, 0xc0, 0xe6, 0x7c, 0x54, 0x97, 0x8f, 0x7d, 0xe3, 0x2f, 0x91, 0x31, 0xf4, + 0x8c, 0x6b, 0xcf, 0xf8, 0x31, 0xbc, 0xca, 0x6a, 0x17, 0x2f, 0xcf, 0x25, 0x67, 0xb0, 0x9e, 0x06, 0x11, 0x98, 0xca, + 0x97, 0x0a, 0xdf, 0xe2, 0x36, 0x23, 0x17, 0x5e, 0x3c, 0x65, 0x22, 0x85, 0x9b, 0x78, 0x2b, 0x64, 0x07, 0xba, 0x34, + 0x2d, 0x10, 0x9e, 0x6c, 0x8f, 0x9b, 0xd6, 0xf9, 0xd6, 0x28, 0x8d, 0x83, 0x3f, 0xb3, 0x3b, 0x60, 0xb3, 0x92, 0x34, + 0x5a, 0x80, 0xcc, 0xca, 0x9b, 0x72, 0x1d, 0x84, 0xa1, 0xb1, 0xdd, 0x3e, 0xf7, 0xe9, 0x13, 0x93, 0xb2, 0x8a, 0xa5, + 0xd1, 0x74, 0x1a, 0x30, 0x4d, 0xca, 0x3e, 0x96, 0x7f, 0xe6, 0x74, 0xcf, 0x16, 0x91, 0xab, 0xf5, 0xac, 0xe9, 0x60, 0x89, 0x11, 0xb3, 0x9c, 0x1b, 0x04, 0xc4, 0x45, 0xc6, 0x55, 0xc8, 0x90, 0x6b, 0xe2, 0x5c, 0x14, 0x07, 0xd7, 0x1c, 0x47, 0xcb, 0x61, 0xc0, 0x4c, 0x3c, 0x0d, 0xf0, 0xc9, 0xf5, 0x70, 0x39, 0x1c, 0x06, 0x94, 0x2e, 0x0c, 0xe2, 0xaf, 0x45, 0x09, 0xca, 0x45, 0x33, 0xbd, 0x07, 0x83, 0xb2, 0xd2, 0x2a, 0xf8, 0x60, 0x33, 0x09, 0x37, 0x07, 0xfa, 0x40, - 0x0a, 0x32, 0xd0, 0xfa, 0x99, 0x76, 0x55, 0xb8, 0xb1, 0xb0, 0x44, 0xed, 0x35, 0xb0, 0x74, 0xee, 0xa5, 0xfa, 0x1e, - 0x67, 0x58, 0xf1, 0xc2, 0xb1, 0xf2, 0x8a, 0xf6, 0xae, 0x6a, 0xa8, 0x64, 0xfa, 0xc5, 0xb3, 0xcb, 0xa9, 0x86, 0xfa, - 0xda, 0xf7, 0xa6, 0x61, 0x94, 0xa4, 0xfe, 0x48, 0xbd, 0xea, 0xbd, 0xf6, 0xb5, 0xcb, 0x79, 0xaa, 0xe9, 0x57, 0xc6, - 0xb7, 0x72, 0x1e, 0x30, 0x81, 0x29, 0x31, 0x0d, 0xd8, 0x86, 0x3a, 0xf2, 0xe9, 0xd9, 0x56, 0x4f, 0x60, 0x64, 0xac, + 0x0a, 0x32, 0xd0, 0xcd, 0x33, 0xed, 0xaa, 0x70, 0x63, 0x61, 0x89, 0xda, 0xab, 0x61, 0xe9, 0xdc, 0x4b, 0xf5, 0x3d, + 0xce, 0xb0, 0xe2, 0x85, 0x63, 0xe5, 0x15, 0xed, 0x5d, 0xd5, 0x50, 0xc9, 0xf4, 0x8b, 0x67, 0x97, 0x53, 0x0d, 0xf5, + 0xb5, 0xef, 0x4d, 0xc3, 0x28, 0x49, 0xfd, 0x91, 0x7a, 0xd5, 0x7b, 0xed, 0x6b, 0x97, 0xf3, 0x54, 0xd3, 0xaf, 0x8c, + 0x6f, 0xe5, 0x3c, 0x60, 0x02, 0x53, 0x62, 0x1a, 0xb0, 0x86, 0x3a, 0xf2, 0xe9, 0xd9, 0x56, 0x4f, 0x60, 0x64, 0xac, 0xf3, 0xad, 0x0b, 0xb5, 0x2a, 0x19, 0xc5, 0x30, 0x55, 0x24, 0x64, 0x14, 0xfb, 0x56, 0xef, 0x91, 0x10, 0xe6, 0x9b, 0xe5, 0x1a, 0x99, 0x86, 0xb4, 0x20, 0xbe, 0x18, 0x04, 0x5f, 0x78, 0x8e, 0xd2, 0xf3, 0x9e, 0xec, 0xf5, 0x50, 0x22, 0xe3, 0x83, 0x6f, 0xca, 0x1c, 0xc8, 0xe3, 0x75, 0x9a, 0x81, 0xc9, 0x61, 0x18, 0xa5, 0x0a, 0x44, 0x76, 0x83, 0x0f, @@ -3695,3994 +3695,3997 @@ constexpr uint8_t INDEX_GZ[] PROGMEM = { 0xba, 0x25, 0xf4, 0xaa, 0x09, 0x76, 0x37, 0x16, 0x31, 0xf3, 0xb9, 0x18, 0x45, 0x30, 0x60, 0x95, 0xa3, 0x07, 0xc1, 0x9a, 0x72, 0xde, 0x2a, 0x05, 0x8b, 0x6b, 0x24, 0x18, 0x80, 0xb9, 0x38, 0x8f, 0x30, 0xc8, 0xae, 0x81, 0x91, 0x84, 0x08, 0x66, 0x62, 0x8c, 0x46, 0x24, 0x27, 0x91, 0xf3, 0xc3, 0xc5, 0x32, 0xc5, 0xc8, 0xf4, 0x00, 0x00, 0xcb, 0x54, - 0x05, 0x2f, 0x8c, 0x80, 0xeb, 0x8b, 0x0b, 0x4f, 0xa6, 0x2a, 0xfe, 0xb8, 0x5e, 0xc6, 0xa5, 0x33, 0x80, 0xe3, 0x70, - 0x18, 0xa8, 0xd7, 0x81, 0xc7, 0x98, 0x0f, 0x63, 0x64, 0x14, 0x69, 0x5d, 0xb4, 0x11, 0xda, 0x3f, 0x34, 0x20, 0x90, - 0x11, 0xf5, 0xd3, 0xd3, 0x82, 0xc6, 0xc1, 0x42, 0xb4, 0xea, 0xd2, 0x30, 0x07, 0x20, 0xa3, 0x3c, 0x85, 0xb9, 0x73, - 0xe1, 0x52, 0x2f, 0x4c, 0xeb, 0xd4, 0x0b, 0x95, 0xec, 0xea, 0x06, 0x38, 0x0d, 0x83, 0xec, 0xba, 0x70, 0x74, 0x2d, - 0xc6, 0x0b, 0x5b, 0x92, 0xca, 0x15, 0xb4, 0x74, 0x73, 0xb9, 0x3d, 0xdb, 0x22, 0xf6, 0xe7, 0x5e, 0x7c, 0x47, 0xe6, - 0x6f, 0x86, 0x6c, 0x23, 0xa7, 0xab, 0x0a, 0xd1, 0x03, 0x9a, 0x00, 0x22, 0x0d, 0xaa, 0xf2, 0x75, 0x5e, 0xc6, 0xf8, - 0x68, 0x73, 0x1b, 0x20, 0xf8, 0xd6, 0xb5, 0xfa, 0x9c, 0x59, 0x24, 0x7f, 0xa4, 0x26, 0x3d, 0x2d, 0xd9, 0x30, 0xbc, - 0xa4, 0x3c, 0xbc, 0xb0, 0xbc, 0xd1, 0x70, 0x30, 0x44, 0x29, 0x08, 0x6e, 0x1c, 0x19, 0x26, 0xc1, 0x6c, 0x5e, 0x51, - 0x7a, 0xf7, 0xbb, 0x2e, 0x07, 0x83, 0xe5, 0x08, 0x61, 0x39, 0x1a, 0x44, 0xb3, 0x9e, 0x58, 0x11, 0xe0, 0x45, 0x80, + 0x05, 0x2f, 0x8c, 0x80, 0xeb, 0x8b, 0x0b, 0x4f, 0xa6, 0x2a, 0xfe, 0x78, 0xb3, 0x8c, 0x4b, 0x67, 0x00, 0xc7, 0xe1, + 0x30, 0x50, 0xaf, 0x03, 0x8f, 0x31, 0x1f, 0xc6, 0xc8, 0x28, 0xd2, 0xba, 0x68, 0x23, 0xb4, 0x7f, 0xa8, 0x41, 0x20, + 0x23, 0xea, 0xa7, 0xa7, 0x05, 0xb5, 0x83, 0x85, 0x68, 0xd5, 0xa5, 0x61, 0x0e, 0x40, 0x46, 0x79, 0x0a, 0x73, 0xe7, + 0xc2, 0xa5, 0x5e, 0x98, 0xd6, 0xa9, 0x17, 0x2a, 0xd9, 0xd5, 0x0d, 0x70, 0x1a, 0x06, 0xd9, 0x75, 0xe1, 0xe8, 0x5a, + 0x8c, 0x17, 0xb6, 0x24, 0x95, 0x2b, 0x68, 0xe9, 0xe6, 0x72, 0x7b, 0xb6, 0x45, 0xec, 0xcf, 0xbd, 0xf8, 0x8e, 0xcc, + 0xdf, 0x0c, 0xd9, 0x46, 0x4e, 0x57, 0x15, 0xa2, 0x07, 0x34, 0x01, 0x44, 0x1a, 0x54, 0xe5, 0xeb, 0xbc, 0x8c, 0xf1, + 0xd1, 0xe6, 0x36, 0x40, 0xf0, 0xad, 0x6b, 0xf5, 0x39, 0xb3, 0x48, 0xfe, 0x48, 0x4d, 0x7a, 0x5a, 0xd2, 0x30, 0xbc, + 0xa4, 0x3c, 0xbc, 0xb0, 0xbc, 0xd1, 0x70, 0x30, 0x44, 0x29, 0x08, 0x6e, 0x1c, 0x19, 0x26, 0xc1, 0xac, 0x5f, 0x51, + 0x7a, 0xf7, 0x87, 0x2e, 0x07, 0x83, 0xe5, 0x08, 0x61, 0x39, 0x6a, 0x44, 0xb3, 0x9e, 0x58, 0x11, 0xe0, 0x45, 0x80, 0x0b, 0x89, 0x91, 0x03, 0xa1, 0xfc, 0x98, 0x4a, 0xbe, 0x85, 0x62, 0x38, 0x1a, 0x04, 0x3b, 0x1d, 0x8d, 0xd8, 0x75, 0x23, 0x6c, 0x15, 0x67, 0x27, 0xfb, 0x54, 0x9b, 0x88, 0x22, 0x55, 0x82, 0x69, 0x88, 0x61, 0x84, 0xc5, 0x2c, 0x40, 0x82, 0x70, 0xd7, 0x29, 0x2e, 0x3a, 0xd6, 0x1c, 0xd5, 0xd2, 0xce, 0x69, 0x99, 0xe1, 0xc1, 0x56, 0x6a, 0xff, 0x04, 0x53, 0x7e, 0x02, 0x59, 0x87, 0xa0, 0x58, 0x27, 0xfb, 0xf4, 0xa8, 0x54, 0x4e, 0x44, 0xd1, 0x89, 0x90, 0x41, 0x76, 0x79, 0x07, 0x0f, 0x3a, 0x2a, 0x49, 0xca, 0x16, 0x50, 0xea, 0x65, 0xaa, 0x32, 0xe7, 0x0c, 0x16, 0x8f, 0xbe, 0x07, 0xa1, 0x79, 0x6c, 0x70, 0x89, 0x50, 0x95, 0xb9, 0x77, 0x8b, 0x23, 0x17, 0x6f, 0xbc, 0x5b, 0xcd, 0xe1, 0xaf, 0x8a, - 0xb3, 0x96, 0x94, 0xcf, 0xda, 0xa8, 0x76, 0x43, 0x0e, 0xe0, 0x86, 0x3c, 0x6a, 0x5e, 0xdc, 0x99, 0x58, 0xdc, 0xf1, + 0xb3, 0x96, 0x94, 0xcf, 0xda, 0x68, 0xe3, 0x86, 0x1c, 0xc0, 0x0d, 0x79, 0x54, 0xbf, 0xb8, 0x33, 0xb1, 0xb8, 0xe3, 0x86, 0xc5, 0x1d, 0x6f, 0x59, 0xdc, 0x80, 0x2f, 0xa4, 0x92, 0x4f, 0x5d, 0x8c, 0xbe, 0xd4, 0xf9, 0xe4, 0x71, 0x7e, - 0xa4, 0xcb, 0xcf, 0x19, 0xce, 0x93, 0x99, 0x04, 0x60, 0x4b, 0xbc, 0x61, 0xae, 0x9a, 0xe6, 0x45, 0x9a, 0x88, 0xfa, - 0xc0, 0xf3, 0x53, 0x27, 0xc6, 0x0d, 0x29, 0xbc, 0xb5, 0xa0, 0x3a, 0x5e, 0xd8, 0xa5, 0xd8, 0xd0, 0xd0, 0x66, 0x1b, - 0x46, 0x3a, 0xdb, 0x32, 0xd2, 0x51, 0xe9, 0xe8, 0xf2, 0x61, 0xd3, 0x21, 0x94, 0x07, 0x05, 0x7b, 0x10, 0xfc, 0x2b, - 0x70, 0xcb, 0x94, 0xf7, 0xe1, 0x66, 0x1c, 0x2b, 0xed, 0xa8, 0x85, 0x97, 0x24, 0xab, 0x28, 0x06, 0x03, 0x05, 0xe8, - 0xe6, 0x61, 0x5b, 0x6a, 0xee, 0x87, 0x3c, 0xf6, 0xd9, 0xc6, 0xcd, 0x54, 0xbc, 0x97, 0xb7, 0x54, 0xeb, 0xf0, 0x90, - 0x6a, 0x2c, 0xbc, 0x34, 0x65, 0x31, 0x4e, 0xba, 0x07, 0x49, 0x32, 0xfe, 0x4b, 0xb6, 0x59, 0x03, 0x0e, 0x09, 0x24, - 0xac, 0x8e, 0x18, 0x7a, 0x01, 0x2c, 0x18, 0x69, 0x24, 0x43, 0x7d, 0x2d, 0xc5, 0x51, 0x8d, 0xf3, 0x89, 0xff, 0x11, - 0x8f, 0xab, 0x16, 0x4b, 0x9e, 0xbe, 0xce, 0x91, 0x6e, 0x2d, 0xbc, 0xf1, 0x7b, 0xb0, 0x83, 0xd1, 0x5a, 0x06, 0xf8, - 0xb4, 0xc8, 0x51, 0x53, 0x63, 0xe2, 0x09, 0x47, 0x05, 0x92, 0x44, 0x2c, 0xc9, 0x2d, 0x86, 0x21, 0xd8, 0x80, 0x67, - 0x4e, 0xae, 0xd6, 0xad, 0x6c, 0x7f, 0xea, 0xeb, 0x35, 0xac, 0x09, 0xa8, 0x2d, 0x70, 0xfb, 0xb9, 0xd0, 0x2d, 0x30, - 0x9c, 0x23, 0x1d, 0x14, 0xa5, 0x97, 0x90, 0x0e, 0xdd, 0x16, 0x97, 0xe9, 0x41, 0x0c, 0x54, 0x0b, 0xd4, 0x8a, 0x4f, - 0xa6, 0xf8, 0xcb, 0xb9, 0xca, 0x9e, 0x0c, 0xf1, 0x57, 0xeb, 0x2a, 0x57, 0x62, 0x55, 0xa4, 0x08, 0xd2, 0x98, 0xd5, - 0x7e, 0x69, 0x3f, 0x91, 0xb9, 0xf6, 0x03, 0xb6, 0x0d, 0x5f, 0xe0, 0x47, 0x8f, 0xd7, 0x09, 0x04, 0x28, 0x90, 0xc7, - 0x10, 0x5a, 0xb1, 0x9e, 0x35, 0x96, 0x4f, 0x37, 0x94, 0x0f, 0xf5, 0xdf, 0x99, 0xf0, 0xe3, 0x2e, 0x89, 0x0a, 0x9a, - 0x52, 0x96, 0x81, 0x5c, 0x0f, 0xfd, 0xd0, 0x8b, 0xef, 0xae, 0xe9, 0x16, 0xa2, 0x09, 0x16, 0x3f, 0x97, 0xed, 0x10, - 0x2f, 0x5a, 0xb6, 0x0e, 0x49, 0x25, 0x45, 0xd5, 0x1d, 0x27, 0xf4, 0xee, 0x9f, 0x62, 0x89, 0xbf, 0x2b, 0x5d, 0x63, - 0xf9, 0x82, 0x94, 0x3e, 0x74, 0xfd, 0x78, 0xad, 0xb1, 0x7a, 0x37, 0x95, 0xd1, 0x56, 0x18, 0x48, 0x58, 0x1e, 0xbc, - 0x12, 0xcf, 0xc7, 0x7e, 0x17, 0xcd, 0x3f, 0x86, 0xd1, 0xad, 0xf9, 0x78, 0x9d, 0x9e, 0xaa, 0x73, 0x2f, 0xfe, 0xc8, - 0xc6, 0xe6, 0xc8, 0x8f, 0x47, 0x01, 0x30, 0x8f, 0xc3, 0xc0, 0x0b, 0x3f, 0xf2, 0x47, 0x33, 0x5a, 0xa6, 0x68, 0xd0, - 0x75, 0xef, 0x0d, 0x5a, 0xcc, 0x09, 0x09, 0x12, 0x91, 0xab, 0x6d, 0x98, 0x05, 0xe5, 0xfd, 0x40, 0x5c, 0xeb, 0x0b, - 0x46, 0xb1, 0xa8, 0x65, 0x80, 0x3f, 0x02, 0xd8, 0x98, 0x41, 0x80, 0x07, 0x43, 0xc5, 0xf5, 0x52, 0x0d, 0x79, 0xa8, - 0xa4, 0x55, 0xcb, 0x33, 0x14, 0x5f, 0x63, 0x0f, 0xbf, 0xfe, 0x73, 0x50, 0xf2, 0x90, 0xcf, 0xe5, 0xbd, 0x7c, 0xde, - 0x08, 0xa1, 0xd4, 0x24, 0xc7, 0xc2, 0x07, 0x7c, 0x9c, 0x33, 0x98, 0x9b, 0x3f, 0x2d, 0x37, 0xf6, 0x92, 0x64, 0x39, - 0x67, 0x63, 0x52, 0x89, 0x9d, 0x16, 0x40, 0x95, 0xef, 0x21, 0x32, 0x60, 0x7f, 0x5f, 0xb6, 0x8e, 0x0f, 0x5e, 0x81, - 0x81, 0x1f, 0x30, 0x94, 0xd1, 0x64, 0xa2, 0x16, 0xa2, 0x80, 0x7b, 0x9a, 0x39, 0x07, 0x7f, 0x5f, 0xbe, 0x39, 0xb3, - 0xdf, 0xe4, 0x8d, 0x43, 0x60, 0x8c, 0x85, 0xb5, 0x12, 0xe7, 0x8b, 0x25, 0x78, 0xc5, 0x88, 0x26, 0x5e, 0xb8, 0x79, - 0x38, 0x97, 0xa5, 0x2d, 0xbe, 0x60, 0x6c, 0x0c, 0x0c, 0xb7, 0x51, 0x2b, 0xbd, 0x0e, 0xd8, 0x0d, 0xcb, 0x2d, 0xa1, - 0xea, 0x1f, 0x6b, 0x68, 0x81, 0xa1, 0x5a, 0xb9, 0xee, 0x91, 0x73, 0x75, 0xd2, 0x90, 0x06, 0x38, 0x06, 0x3e, 0x72, - 0xf9, 0x88, 0x55, 0x8e, 0xd4, 0xc0, 0x50, 0x25, 0x00, 0x36, 0x42, 0x76, 0xba, 0xa1, 0xbc, 0x0b, 0x88, 0x7a, 0x03, - 0x6c, 0x86, 0xa3, 0x77, 0x21, 0xb5, 0x05, 0x9f, 0xa7, 0x00, 0x4e, 0x9e, 0x56, 0x48, 0x4d, 0x36, 0xcd, 0x58, 0x93, - 0xa8, 0x4d, 0x25, 0x21, 0x8d, 0x70, 0x0e, 0x40, 0x2f, 0x19, 0x21, 0xae, 0x6a, 0x5c, 0x1b, 0xa5, 0x3c, 0xf2, 0x21, - 0x26, 0x7e, 0x0f, 0x59, 0x92, 0x6c, 0x9c, 0xb0, 0x7c, 0xd1, 0x0d, 0xb5, 0xa8, 0x5d, 0x9e, 0x8f, 0xa2, 0xdc, 0xb0, - 0x0d, 0x60, 0x09, 0x70, 0x80, 0xd5, 0x6f, 0x21, 0x79, 0xb9, 0x9e, 0x73, 0xf3, 0xce, 0x78, 0x3a, 0x54, 0xb9, 0xe9, - 0xdd, 0xa6, 0xf7, 0x2b, 0x95, 0x03, 0x55, 0x22, 0xd3, 0x8d, 0xa0, 0x69, 0x25, 0xd4, 0x5b, 0x93, 0x2a, 0x61, 0x07, - 0x02, 0xa6, 0x0a, 0x7e, 0x65, 0x93, 0x09, 0x1b, 0xa5, 0x89, 0x2e, 0x64, 0x4c, 0x79, 0xb0, 0x75, 0x70, 0xb2, 0xdd, - 0x73, 0xd5, 0x1f, 0x21, 0xe4, 0x8c, 0x88, 0x49, 0xc8, 0x01, 0x12, 0x77, 0xa6, 0xe6, 0x69, 0xa2, 0x1e, 0xcb, 0x53, - 0xc4, 0xbf, 0x02, 0x52, 0xe8, 0x86, 0x72, 0x04, 0x8d, 0xd3, 0x9f, 0x62, 0x5f, 0x44, 0xb9, 0x11, 0xe8, 0x76, 0x54, - 0xb4, 0xed, 0xf8, 0xae, 0x9d, 0x37, 0x87, 0x8e, 0x9d, 0xa9, 0x06, 0xb8, 0x3a, 0x7f, 0xac, 0x6c, 0x63, 0x22, 0x50, - 0xae, 0x7a, 0xfe, 0xf6, 0xd5, 0x9f, 0xce, 0x5e, 0xef, 0x8a, 0x11, 0xb0, 0xcb, 0x36, 0x74, 0xb9, 0x0c, 0xb7, 0x74, - 0xfa, 0xf3, 0x8f, 0x0f, 0xeb, 0xb6, 0xe5, 0xbc, 0x70, 0x54, 0x83, 0xac, 0xd3, 0x25, 0xbc, 0x38, 0x8a, 0x6e, 0x58, - 0xfc, 0xd9, 0xd3, 0x20, 0x77, 0xde, 0x0c, 0xee, 0xdb, 0x9f, 0xce, 0x7e, 0xdc, 0x19, 0xd4, 0x23, 0xc7, 0x06, 0xdc, - 0x9e, 0x46, 0x8b, 0x07, 0x8c, 0xae, 0xad, 0x1a, 0xea, 0x28, 0x88, 0x12, 0xb6, 0x01, 0x82, 0x57, 0xe7, 0x6f, 0xdf, - 0xe3, 0x74, 0x15, 0x2c, 0x08, 0x75, 0xf5, 0x79, 0x83, 0xff, 0xe9, 0xdd, 0xd9, 0xfb, 0xf7, 0xaa, 0x81, 0xc9, 0xba, - 0x13, 0xb9, 0x77, 0xbe, 0x89, 0xef, 0xa1, 0x38, 0x8d, 0x7b, 0x9d, 0xa8, 0x1a, 0x5d, 0xa4, 0xcb, 0xa3, 0xa1, 0xb2, - 0xda, 0x36, 0xe7, 0xd4, 0x8e, 0x7f, 0x99, 0x6e, 0xbf, 0x3b, 0x8d, 0xab, 0x01, 0x1f, 0x6d, 0x27, 0xa9, 0xa5, 0x92, - 0xb9, 0x1f, 0x5e, 0x37, 0x94, 0x7a, 0xb7, 0x0d, 0xa5, 0x70, 0x7d, 0xac, 0xe1, 0xc7, 0x65, 0x34, 0x97, 0xd8, 0x11, - 0x76, 0x7b, 0xff, 0x74, 0x49, 0x77, 0xb8, 0xcf, 0x00, 0x9a, 0x27, 0x5b, 0xa9, 0x42, 0xdd, 0x50, 0xcc, 0x2f, 0x5e, - 0xf9, 0xdc, 0x8e, 0x02, 0xb0, 0xc9, 0x67, 0xb2, 0x1a, 0xb2, 0xcc, 0xaa, 0x72, 0x8f, 0x1a, 0xb7, 0x72, 0x2b, 0xa0, - 0x66, 0xa4, 0xba, 0xe1, 0x34, 0x65, 0xe1, 0x8d, 0xc1, 0xd0, 0xdd, 0x1c, 0x46, 0x69, 0x1a, 0xcd, 0xbb, 0x8e, 0xbd, - 0xb8, 0x55, 0x95, 0x9e, 0x10, 0x76, 0x70, 0x3b, 0xfc, 0xee, 0xbf, 0xff, 0x55, 0x41, 0xf3, 0x54, 0x7e, 0x9d, 0xb2, - 0xf9, 0x82, 0xc5, 0x5e, 0xba, 0x8c, 0x59, 0xa6, 0xfc, 0xfb, 0x7f, 0x5e, 0x55, 0x2e, 0xf6, 0x3d, 0xb9, 0x0d, 0xb1, - 0xf4, 0x72, 0x93, 0xeb, 0x20, 0x5a, 0xed, 0x15, 0x1e, 0x77, 0xf7, 0x54, 0x9e, 0xf9, 0xd3, 0x59, 0x5e, 0xfb, 0x34, - 0xdd, 0x32, 0x36, 0x01, 0x3d, 0xe9, 0x03, 0x94, 0xf3, 0x68, 0xd5, 0xfd, 0xf7, 0xbf, 0x72, 0x81, 0xcd, 0xbd, 0xbb, - 0xae, 0x19, 0xd0, 0xf2, 0x8a, 0x36, 0xd7, 0xa9, 0x2d, 0x31, 0xbc, 0xaf, 0x2d, 0x70, 0xad, 0x90, 0x76, 0x65, 0x5d, - 0x37, 0xb7, 0x65, 0x4c, 0xdf, 0xf9, 0xd3, 0xd9, 0xe7, 0x0e, 0x0a, 0x26, 0xf4, 0xde, 0x51, 0x41, 0xa5, 0x2f, 0x30, - 0xac, 0x41, 0x77, 0xf7, 0x05, 0xfb, 0xcc, 0x71, 0xdd, 0x37, 0xa4, 0x2f, 0x31, 0x1a, 0x2e, 0xb9, 0x7d, 0x3f, 0x18, - 0xe4, 0xc9, 0x6a, 0xe5, 0xf6, 0xe0, 0x33, 0x78, 0x5a, 0x2b, 0xe1, 0xec, 0x45, 0xd7, 0xd6, 0x29, 0x98, 0xcf, 0x0e, - 0x13, 0x82, 0xd6, 0xef, 0x0d, 0xd3, 0xb1, 0x19, 0x5f, 0x93, 0x13, 0x5b, 0xed, 0xdb, 0x35, 0x64, 0x0d, 0xa5, 0x98, - 0xe8, 0x34, 0xd7, 0x1a, 0x1a, 0xcd, 0xe0, 0xac, 0x62, 0x6f, 0x41, 0x4a, 0x02, 0x05, 0x35, 0x26, 0x20, 0x74, 0xa9, - 0xdc, 0xa2, 0x6f, 0xbc, 0xe0, 0x66, 0xb7, 0x0b, 0xd5, 0x66, 0x0a, 0x86, 0xa4, 0xf9, 0x3f, 0x47, 0xbc, 0x91, 0x2e, - 0x3f, 0x98, 0x76, 0xaf, 0xbc, 0x94, 0xc5, 0xd7, 0x33, 0xf0, 0xf6, 0x15, 0xd2, 0x03, 0x88, 0xa3, 0xbb, 0x0d, 0x29, - 0x97, 0xd8, 0xd2, 0x06, 0x34, 0x5a, 0x60, 0xb8, 0x5f, 0x87, 0xbb, 0xbf, 0x10, 0xe6, 0xee, 0x9e, 0x81, 0x3f, 0xe6, - 0x6f, 0x86, 0xbd, 0xb7, 0x51, 0xa6, 0xff, 0xc7, 0xde, 0xff, 0x8d, 0xd8, 0x7b, 0xeb, 0x77, 0x7e, 0xcd, 0xc2, 0xfe, - 0x1f, 0xc0, 0xf2, 0x5d, 0xe6, 0x9e, 0x71, 0x4c, 0xaf, 0x69, 0x9e, 0xab, 0xc5, 0xa5, 0xc3, 0x8b, 0x78, 0xb5, 0xa6, - 0x60, 0xe5, 0xc1, 0xd6, 0xb8, 0xe5, 0xa0, 0x87, 0xc8, 0x7e, 0xcb, 0x51, 0xfe, 0xed, 0x11, 0x7d, 0x42, 0x19, 0xaa, + 0xa4, 0xcb, 0xcf, 0x19, 0xce, 0x93, 0x99, 0x04, 0x60, 0x4b, 0xdc, 0x30, 0x57, 0x75, 0xf3, 0x22, 0x4d, 0xc4, 0xe6, + 0xc0, 0xf3, 0x53, 0x27, 0xc6, 0x0d, 0x29, 0xbc, 0xb5, 0xa0, 0x3a, 0x5e, 0xd8, 0xa5, 0xd8, 0xd0, 0xd0, 0x66, 0x0d, + 0x23, 0x9d, 0x6d, 0x19, 0xe9, 0xa8, 0x74, 0x74, 0xf9, 0xb0, 0xe9, 0x10, 0xca, 0x83, 0x82, 0x3d, 0x08, 0xfe, 0x15, + 0xb8, 0x65, 0xca, 0xfb, 0xb0, 0x19, 0xc7, 0x4a, 0x3b, 0x6a, 0xe1, 0x25, 0xc9, 0x2a, 0x8a, 0xc1, 0x40, 0x01, 0xba, + 0x79, 0xd8, 0x96, 0x9a, 0xfb, 0x21, 0x8f, 0x7d, 0xd6, 0xb8, 0x99, 0x8a, 0xf7, 0xf2, 0x96, 0x6a, 0x1d, 0x1e, 0x52, + 0x8d, 0x85, 0x97, 0xa6, 0x2c, 0xc6, 0x49, 0xf7, 0x20, 0x49, 0xc6, 0x7f, 0xc8, 0x36, 0xab, 0xc1, 0x21, 0x81, 0x84, + 0xd5, 0x11, 0x43, 0x2f, 0x80, 0x05, 0x23, 0x8d, 0x64, 0xa8, 0xaf, 0xa5, 0x38, 0xaa, 0x71, 0x3e, 0xf1, 0x3f, 0xe1, + 0x71, 0xd5, 0x62, 0xc9, 0xd3, 0xd7, 0x39, 0xd2, 0xad, 0x85, 0x37, 0x7e, 0x0f, 0x76, 0x30, 0x5a, 0xcb, 0x00, 0x9f, + 0x16, 0x39, 0x6a, 0x6a, 0x4c, 0x3c, 0xe1, 0xa8, 0x40, 0x92, 0x88, 0x25, 0xb9, 0xc5, 0x30, 0x04, 0x1b, 0xf0, 0xcc, + 0xc9, 0xd5, 0xba, 0x95, 0xed, 0x4f, 0x7d, 0x7d, 0x03, 0x6b, 0x02, 0x6a, 0x0b, 0xdc, 0x7e, 0x2e, 0x74, 0x0b, 0x0c, + 0xe7, 0x48, 0x07, 0x45, 0xe9, 0x25, 0xa4, 0x43, 0xb7, 0xc5, 0x65, 0x7a, 0x10, 0x03, 0xd5, 0x02, 0xb5, 0xe2, 0x93, + 0x29, 0xfe, 0x72, 0xae, 0xb2, 0x27, 0x43, 0xfc, 0xd5, 0xba, 0xca, 0x95, 0x58, 0x15, 0x29, 0x82, 0x34, 0x66, 0xb5, + 0x5f, 0xda, 0x4f, 0x64, 0xae, 0xfd, 0x80, 0x6d, 0xc3, 0x17, 0xf8, 0xd1, 0xe3, 0x75, 0x02, 0x01, 0x0a, 0xe4, 0x31, + 0x84, 0x56, 0xac, 0x67, 0xb5, 0xe5, 0xd3, 0x86, 0xf2, 0xa1, 0xfe, 0x07, 0x13, 0x7e, 0xdc, 0x25, 0x51, 0x41, 0x53, + 0xca, 0x32, 0x90, 0xeb, 0xa1, 0x1f, 0x7a, 0xf1, 0xdd, 0x35, 0xdd, 0x42, 0x34, 0xc1, 0xe2, 0xe7, 0xb2, 0x1d, 0xe2, + 0x45, 0xcb, 0xd6, 0x21, 0xa9, 0xa4, 0xa8, 0xba, 0xe3, 0x84, 0xde, 0xfd, 0x73, 0x2c, 0xf1, 0x77, 0xa5, 0x6b, 0x2c, + 0x5f, 0x90, 0xd2, 0x87, 0xae, 0x1f, 0xaf, 0x35, 0xb6, 0xd9, 0x4d, 0x65, 0xb4, 0x15, 0x06, 0x12, 0x96, 0x07, 0xaf, + 0xc4, 0xf3, 0xb1, 0xdf, 0x45, 0xf3, 0x8f, 0x61, 0x74, 0x6b, 0x3e, 0x5e, 0xa7, 0xa7, 0xea, 0xdc, 0x8b, 0x3f, 0xb2, + 0xb1, 0x39, 0xf2, 0xe3, 0x51, 0x00, 0xcc, 0xe3, 0x30, 0xf0, 0xc2, 0x8f, 0xfc, 0xd1, 0x8c, 0x96, 0x29, 0x1a, 0x74, + 0xdd, 0x7b, 0x83, 0x16, 0x73, 0x42, 0x82, 0x44, 0xe4, 0x6a, 0x6b, 0x66, 0x41, 0x79, 0x3f, 0x10, 0xd7, 0xfa, 0x82, + 0x51, 0x2c, 0x6a, 0x19, 0xe0, 0x8f, 0x00, 0x36, 0x66, 0x10, 0xe0, 0xc1, 0x50, 0x71, 0xbd, 0x54, 0x43, 0x1e, 0x2a, + 0x69, 0xd5, 0xf2, 0x0c, 0xc5, 0xd7, 0xd8, 0xc3, 0x6f, 0xff, 0x1c, 0x94, 0x3c, 0xe4, 0x73, 0x79, 0x2f, 0x9f, 0x37, + 0x42, 0x28, 0x35, 0xc9, 0xb1, 0xf0, 0x01, 0x1f, 0xe7, 0x0c, 0x66, 0xf3, 0xa7, 0xe5, 0xc6, 0x5e, 0x92, 0x2c, 0xe7, + 0x6c, 0x4c, 0x2a, 0xb1, 0xd3, 0x02, 0xa8, 0xf2, 0x3d, 0x44, 0x06, 0xec, 0x6f, 0xcb, 0xd6, 0xf1, 0xc1, 0x2b, 0x30, + 0xf0, 0x03, 0x86, 0x32, 0x9a, 0x4c, 0xd4, 0x42, 0x14, 0x70, 0x4f, 0x33, 0xe7, 0xe0, 0x6f, 0xcb, 0x37, 0x67, 0xf6, + 0x9b, 0xbc, 0x71, 0x08, 0x8c, 0xb1, 0xb0, 0x56, 0xe2, 0x7c, 0xb1, 0x04, 0xaf, 0x18, 0xd1, 0xc4, 0x0b, 0x9b, 0x87, + 0x73, 0x59, 0xda, 0xe2, 0x0b, 0xc6, 0xc6, 0xc0, 0x70, 0x1b, 0x1b, 0xa5, 0xd7, 0x01, 0xbb, 0x61, 0xb9, 0x25, 0xd4, + 0xe6, 0xc7, 0x6a, 0x5a, 0x60, 0xa8, 0x56, 0xae, 0x7b, 0xe4, 0x5c, 0x9d, 0x34, 0xa4, 0x01, 0x8e, 0x81, 0x8f, 0x5c, + 0x3e, 0x62, 0x95, 0x23, 0x35, 0x30, 0x54, 0x09, 0x80, 0x46, 0xc8, 0x4e, 0x1b, 0xca, 0xbb, 0x80, 0xa8, 0x1b, 0x60, + 0x33, 0x1c, 0xbd, 0x0b, 0xa9, 0x2d, 0xf8, 0x3c, 0x05, 0x70, 0xf2, 0xb4, 0x42, 0x6a, 0xd2, 0x34, 0x63, 0x75, 0xa2, + 0x36, 0x95, 0x84, 0x34, 0xc2, 0x39, 0x00, 0xbd, 0x64, 0x84, 0xb8, 0xaa, 0x76, 0x6d, 0x94, 0xf2, 0xc8, 0x87, 0x98, + 0xf8, 0x3d, 0x64, 0x49, 0xd2, 0x38, 0x61, 0xf9, 0xa2, 0x1b, 0x6a, 0x51, 0xbb, 0x3c, 0x1f, 0x45, 0xb9, 0x61, 0x1b, + 0xc0, 0x12, 0xe0, 0x00, 0xab, 0xdf, 0x42, 0xf2, 0x72, 0x3d, 0xe7, 0xe6, 0x9d, 0xf1, 0x74, 0xa8, 0x72, 0xd3, 0xbb, + 0xa6, 0xf7, 0x2b, 0x95, 0x03, 0x55, 0x22, 0xd3, 0xb5, 0xa0, 0x69, 0x25, 0xd4, 0xbb, 0x21, 0x55, 0xc2, 0x0e, 0x04, + 0x4c, 0x15, 0xfc, 0xca, 0x26, 0x13, 0x36, 0x4a, 0x13, 0x5d, 0xc8, 0x98, 0xf2, 0x60, 0xeb, 0xe0, 0x64, 0xbb, 0xe7, + 0xaa, 0x3f, 0x41, 0xc8, 0x19, 0x11, 0x93, 0x90, 0x03, 0x24, 0xee, 0x4c, 0xf5, 0xd3, 0x44, 0x3d, 0x96, 0xa7, 0x88, + 0x7f, 0x05, 0xa4, 0xd0, 0x35, 0xe5, 0x08, 0x1a, 0xa7, 0x3f, 0xc5, 0xbe, 0x88, 0x72, 0x23, 0xd0, 0xed, 0xa8, 0x68, + 0xdb, 0xf1, 0x5d, 0x3b, 0x6f, 0x0e, 0x1d, 0x3b, 0x53, 0x0d, 0x70, 0x75, 0xfe, 0x58, 0xd9, 0xc6, 0x44, 0xa0, 0x5c, + 0xf5, 0xfc, 0xed, 0xab, 0x3f, 0x9f, 0xbd, 0xde, 0x15, 0x23, 0x60, 0x97, 0x6d, 0xe8, 0x72, 0x19, 0x6e, 0xe9, 0xf4, + 0x97, 0x9f, 0x1e, 0xd6, 0x6d, 0xcb, 0x79, 0xe1, 0xa8, 0x06, 0x59, 0xa7, 0x4b, 0x78, 0x71, 0x14, 0xdd, 0xb0, 0xf8, + 0xb3, 0xa7, 0x41, 0xee, 0xbc, 0x1e, 0xdc, 0xb7, 0x3f, 0x9f, 0xfd, 0xb4, 0x33, 0xa8, 0x47, 0x8e, 0x0d, 0xb8, 0x3d, + 0x8d, 0x16, 0x0f, 0x18, 0x5d, 0x5b, 0x35, 0xd4, 0x51, 0x10, 0x25, 0xac, 0x01, 0x82, 0x57, 0xe7, 0x6f, 0xdf, 0xe3, + 0x74, 0x15, 0x2c, 0x08, 0x75, 0xf5, 0x79, 0x83, 0xff, 0xf9, 0xdd, 0xd9, 0xfb, 0xf7, 0xaa, 0x81, 0xc9, 0xba, 0x13, + 0xb9, 0x77, 0xbe, 0x89, 0xef, 0xa1, 0x38, 0xb5, 0x7b, 0x9d, 0xa8, 0x1a, 0x5d, 0xa4, 0xcb, 0xa3, 0xa1, 0xb2, 0x8d, + 0x6d, 0xce, 0xa9, 0x1d, 0xff, 0x32, 0xdd, 0x7e, 0x77, 0x1a, 0x57, 0x0d, 0x3e, 0xda, 0x4e, 0x52, 0x4b, 0x25, 0x73, + 0x3f, 0xbc, 0xae, 0x29, 0xf5, 0x6e, 0x6b, 0x4a, 0xe1, 0xfa, 0xb8, 0x81, 0x1f, 0x97, 0xd1, 0x5c, 0x62, 0x47, 0xd8, + 0xed, 0xfd, 0xd3, 0x25, 0xdd, 0xe1, 0x3e, 0x03, 0x68, 0x9e, 0x6c, 0xa5, 0x0a, 0x75, 0x4d, 0x31, 0xbf, 0x78, 0xe5, + 0x73, 0x3b, 0x0a, 0xc0, 0x26, 0x9f, 0xc9, 0x6a, 0xc8, 0x32, 0xab, 0xca, 0x3d, 0x6a, 0xdc, 0xca, 0xad, 0x80, 0x9a, + 0x91, 0xea, 0x86, 0xd3, 0x94, 0x85, 0x37, 0x06, 0x43, 0x77, 0x73, 0x18, 0xa5, 0x69, 0x34, 0xef, 0x3a, 0xf6, 0xe2, + 0x56, 0x55, 0x7a, 0x42, 0xd8, 0xc1, 0xed, 0xf0, 0xbb, 0xff, 0xfa, 0x67, 0x05, 0xcd, 0x53, 0xf9, 0x75, 0xca, 0xe6, + 0x0b, 0x16, 0x7b, 0xe9, 0x32, 0x66, 0x99, 0xf2, 0xaf, 0xff, 0x79, 0x55, 0xb9, 0xd8, 0xf7, 0xe4, 0x36, 0xc4, 0xd2, + 0xcb, 0x4d, 0xae, 0x83, 0x68, 0xb5, 0x57, 0x78, 0xdc, 0xdd, 0x53, 0x79, 0xe6, 0x4f, 0x67, 0x79, 0xed, 0xd3, 0x74, + 0xcb, 0xd8, 0x04, 0xf4, 0xa4, 0x0f, 0x50, 0xce, 0xa3, 0x55, 0xf7, 0x5f, 0xff, 0xcc, 0x05, 0x36, 0xf7, 0xee, 0xba, + 0x7a, 0x40, 0xcb, 0x2b, 0x5a, 0x5f, 0x67, 0x63, 0x89, 0xe1, 0xfd, 0xc6, 0x02, 0x6f, 0x14, 0xd2, 0xae, 0xdc, 0xd4, + 0xcd, 0x6d, 0x19, 0xd3, 0x77, 0xfe, 0x74, 0xf6, 0xb9, 0x83, 0x82, 0x09, 0xbd, 0x77, 0x54, 0x50, 0xe9, 0x0b, 0x0c, + 0x6b, 0xd0, 0xdd, 0x7d, 0xc1, 0x3e, 0x73, 0x5c, 0xf7, 0x0d, 0xe9, 0x4b, 0x8c, 0x86, 0x4b, 0x6e, 0xdf, 0x0f, 0x06, + 0x79, 0xb2, 0x5a, 0xb9, 0x3d, 0xf8, 0x0c, 0x9e, 0x6e, 0x94, 0x70, 0xf6, 0xa2, 0x6b, 0xeb, 0x14, 0xcc, 0x67, 0x87, + 0x09, 0x41, 0xeb, 0xf7, 0x9a, 0xe9, 0x68, 0xc6, 0xd7, 0xe4, 0xc4, 0xb6, 0xf1, 0xed, 0x0d, 0x64, 0x0d, 0xa5, 0x98, + 0xe8, 0x34, 0xd7, 0x1a, 0x1a, 0xf5, 0xe0, 0xac, 0x62, 0x6f, 0x41, 0x4a, 0x02, 0x05, 0x35, 0x26, 0x20, 0x74, 0xa9, + 0xdc, 0xa2, 0x6f, 0xbc, 0xe0, 0x66, 0xb7, 0x0b, 0x55, 0x33, 0x05, 0x43, 0xd2, 0xfc, 0xef, 0x23, 0xde, 0x48, 0x97, + 0x1f, 0x4c, 0xbb, 0x57, 0x5e, 0xca, 0xe2, 0xeb, 0x19, 0x78, 0xfb, 0x0a, 0xe9, 0x01, 0xc4, 0xd1, 0xdd, 0x86, 0x94, + 0x4b, 0x6c, 0x69, 0x0d, 0x1a, 0x2d, 0x30, 0xdc, 0x6f, 0xc3, 0xdd, 0x5f, 0x08, 0x73, 0x77, 0xcf, 0xc0, 0x1f, 0xf3, + 0x77, 0xc3, 0xde, 0xdb, 0x28, 0xd3, 0xff, 0x63, 0xef, 0xff, 0x44, 0xec, 0xbd, 0xf5, 0x3b, 0xbf, 0x65, 0x61, 0xff, + 0x0f, 0x60, 0xf9, 0x2e, 0x73, 0xcf, 0x38, 0xa6, 0xd7, 0x34, 0xcf, 0xd5, 0xe2, 0xd2, 0xe1, 0x45, 0xbc, 0xba, 0xa1, + 0x60, 0xe5, 0xc1, 0xd6, 0xb8, 0xe5, 0xa0, 0x87, 0xc8, 0x7e, 0xcb, 0x51, 0xfe, 0xfd, 0x11, 0x7d, 0x42, 0x19, 0xaa, 0x24, 0x4c, 0xdf, 0x3d, 0x33, 0x92, 0xd2, 0x48, 0xbc, 0x95, 0x77, 0xb7, 0x0b, 0xde, 0x11, 0xc0, 0x7e, 0xb3, 0xf2, - 0xee, 0x9a, 0x80, 0xdd, 0x88, 0x5e, 0xab, 0x1f, 0x3b, 0x05, 0x2f, 0x9f, 0x2e, 0xba, 0xf8, 0x18, 0x83, 0x84, 0xa5, - 0xa7, 0x50, 0xe8, 0x3e, 0x5e, 0xef, 0x55, 0x2b, 0x66, 0x03, 0xf0, 0x7f, 0x96, 0x00, 0x8f, 0x4a, 0x80, 0xfb, 0xc9, - 0x75, 0x14, 0x3e, 0x04, 0xf2, 0x9f, 0x40, 0xf8, 0xf3, 0xab, 0x41, 0xc7, 0xcf, 0xd5, 0x60, 0xc7, 0xd2, 0x2a, 0xf0, - 0x58, 0x58, 0x85, 0xbe, 0xd7, 0x2c, 0xab, 0xaf, 0x10, 0x5a, 0xa4, 0xb1, 0x8c, 0x08, 0xad, 0x02, 0x7a, 0x15, 0x05, - 0x74, 0x5c, 0x15, 0x92, 0xeb, 0x87, 0x93, 0xd8, 0x8b, 0xd9, 0x78, 0xf3, 0x15, 0xa0, 0x64, 0x9d, 0x7c, 0x67, 0x25, - 0xcb, 0xc5, 0x22, 0x8a, 0xd3, 0xe4, 0x1a, 0xe3, 0xb4, 0xcc, 0x7d, 0xb8, 0x50, 0x40, 0x46, 0xb1, 0x3c, 0x6a, 0xef, - 0x59, 0x93, 0x7c, 0xdb, 0x60, 0x6e, 0x39, 0xd9, 0x06, 0xf7, 0xbe, 0x31, 0xb8, 0xff, 0xce, 0x4c, 0xd2, 0x5f, 0xcc, - 0xac, 0x34, 0xf6, 0xe7, 0x9a, 0x6e, 0x38, 0xb6, 0xae, 0x0b, 0xf9, 0xca, 0xcc, 0xed, 0xef, 0x51, 0xb4, 0xe1, 0x99, - 0x0e, 0x51, 0x0b, 0xd1, 0xa3, 0x05, 0x6c, 0xe5, 0x5e, 0x2e, 0x27, 0x13, 0x16, 0x6b, 0x22, 0x2c, 0x23, 0xc4, 0x85, - 0x25, 0x63, 0x40, 0xf0, 0x73, 0xfc, 0xe0, 0xb3, 0x15, 0xe4, 0x7f, 0x2a, 0xc2, 0xaa, 0x83, 0xaf, 0x27, 0x99, 0x93, - 0x43, 0x6e, 0xb9, 0xb4, 0xdd, 0xd2, 0xc6, 0xcf, 0x0e, 0x8c, 0x19, 0x04, 0x63, 0x2a, 0xdc, 0xe3, 0x31, 0xce, 0x9f, - 0x1f, 0xa6, 0x1d, 0xfc, 0x02, 0x74, 0x00, 0x87, 0x37, 0x70, 0x73, 0xbf, 0x28, 0x65, 0x94, 0x77, 0x38, 0x73, 0xfb, - 0xc1, 0x73, 0x97, 0xf4, 0x3c, 0x68, 0xb7, 0xf7, 0x6a, 0xe6, 0xc5, 0xaf, 0xa2, 0x31, 0x43, 0x40, 0x87, 0x69, 0x04, - 0xde, 0x9a, 0x52, 0x18, 0x1e, 0x8c, 0xc2, 0x63, 0x96, 0x22, 0xf3, 0xec, 0x43, 0xd1, 0xb5, 0x5c, 0xe4, 0x3e, 0x7f, - 0xbc, 0x6f, 0xc0, 0x49, 0x6b, 0x5e, 0x69, 0xb1, 0x68, 0x7c, 0xa9, 0x1b, 0x5f, 0xc9, 0xbb, 0xf5, 0x95, 0x17, 0xc7, - 0x3e, 0x8b, 0x15, 0xed, 0xbb, 0x5f, 0x74, 0x79, 0xd3, 0x96, 0x14, 0x3a, 0x5c, 0xcb, 0xac, 0x60, 0x34, 0xba, 0x89, - 0xcf, 0x82, 0xb1, 0xab, 0x8e, 0xa8, 0x61, 0xae, 0xbc, 0x69, 0x77, 0x6c, 0xdb, 0xe6, 0x0a, 0x53, 0x87, 0x7e, 0x82, - 0xc2, 0x14, 0x7e, 0xc2, 0x43, 0x49, 0xbc, 0xd8, 0x21, 0x2e, 0xa2, 0x46, 0xce, 0x1a, 0x21, 0x7c, 0x47, 0xf1, 0x7c, + 0xee, 0xea, 0x80, 0x6d, 0x44, 0xaf, 0xd5, 0x8f, 0x9d, 0x82, 0x97, 0x4f, 0x17, 0x5d, 0x7c, 0x8c, 0x41, 0xc2, 0xd2, + 0x53, 0x28, 0x74, 0x1f, 0xaf, 0xf7, 0xaa, 0x15, 0xb3, 0x01, 0xf8, 0x3f, 0x4b, 0x80, 0x47, 0x25, 0xc0, 0xfd, 0xe4, + 0x3a, 0x0a, 0x1f, 0x02, 0xf9, 0xcf, 0x20, 0xfc, 0xf9, 0xcd, 0xa0, 0xe3, 0xe7, 0x36, 0x60, 0xc7, 0xd2, 0x2a, 0xf0, + 0x58, 0x58, 0x85, 0xbe, 0x57, 0x2f, 0xab, 0xaf, 0x10, 0x5a, 0xa4, 0xb1, 0x8c, 0x08, 0xad, 0x02, 0x7a, 0x15, 0x05, + 0x74, 0x5c, 0x15, 0x92, 0xeb, 0x87, 0x93, 0xd8, 0x8b, 0xd9, 0xb8, 0xf9, 0x0a, 0x50, 0xb2, 0x4e, 0xbe, 0xb3, 0x92, + 0xe5, 0x62, 0x11, 0xc5, 0x69, 0x72, 0x8d, 0x71, 0x5a, 0xe6, 0x3e, 0x5c, 0x28, 0x20, 0xa3, 0x58, 0x1e, 0xb5, 0xf7, + 0xac, 0x4e, 0xbe, 0x6d, 0x30, 0xb7, 0x9c, 0x6c, 0x83, 0x7b, 0xdf, 0x18, 0xdc, 0x7f, 0x67, 0x26, 0xe9, 0x2f, 0x66, + 0x56, 0x1a, 0xfb, 0x73, 0x4d, 0x37, 0x1c, 0x5b, 0xd7, 0x85, 0x7c, 0x65, 0xe6, 0xf6, 0xf7, 0x28, 0xda, 0xf0, 0x4c, + 0x87, 0xa8, 0x85, 0xe8, 0xd1, 0x02, 0xb6, 0x72, 0x2f, 0x97, 0x93, 0x09, 0x8b, 0x35, 0x11, 0x96, 0x11, 0xe2, 0xc2, + 0x92, 0x31, 0x20, 0xf8, 0x39, 0x7e, 0xf0, 0xd9, 0x0a, 0xf2, 0x3f, 0x15, 0x61, 0xd5, 0xc1, 0xd7, 0x93, 0xcc, 0xc9, + 0x21, 0xb7, 0x5c, 0xda, 0x6e, 0x69, 0xe3, 0x67, 0x07, 0xc6, 0x0c, 0x82, 0x31, 0x15, 0xee, 0xf1, 0x18, 0xe7, 0xcf, + 0x0f, 0xd3, 0x0e, 0x7e, 0x01, 0x3a, 0x80, 0xc3, 0x1b, 0xb8, 0xb9, 0x5f, 0x94, 0x32, 0xca, 0x3b, 0x9c, 0xb9, 0xfd, + 0xe0, 0xb9, 0x4b, 0x7a, 0x1e, 0xb4, 0xdb, 0x7b, 0x35, 0xf3, 0xe2, 0x57, 0xd1, 0x98, 0x21, 0xa0, 0xc3, 0x34, 0x02, + 0x6f, 0x4d, 0x29, 0x0c, 0x0f, 0x46, 0xe1, 0x31, 0x4b, 0x91, 0x79, 0xf6, 0xa1, 0xe8, 0x5a, 0x2e, 0x72, 0x9f, 0x3f, + 0xde, 0x37, 0xe0, 0xa4, 0xd5, 0xaf, 0xb4, 0x58, 0x34, 0xbe, 0xd4, 0xb5, 0xaf, 0xe4, 0xdd, 0xfa, 0xca, 0x8b, 0x63, + 0x9f, 0xc5, 0x8a, 0xf6, 0xdd, 0xaf, 0xba, 0xbc, 0x69, 0x4b, 0x0a, 0x1d, 0xae, 0x65, 0x56, 0x30, 0x1a, 0xdd, 0xc4, + 0x67, 0xc1, 0xd8, 0x55, 0x47, 0xd4, 0x30, 0x57, 0xde, 0xb4, 0x3b, 0xb6, 0x6d, 0x73, 0x85, 0xa9, 0x43, 0x3f, 0x41, + 0x61, 0x0a, 0x3f, 0xe1, 0xa1, 0x24, 0x5e, 0xec, 0x10, 0x17, 0xb1, 0x41, 0xce, 0x6a, 0x21, 0x7c, 0x47, 0xf1, 0x7c, 0x1e, 0x02, 0x1b, 0x8f, 0xfb, 0x23, 0x40, 0x73, 0x04, 0x58, 0x05, 0x4c, 0x15, 0x80, 0x0e, 0x1f, 0x02, 0xd0, 0x85, - 0x3f, 0xf7, 0xc3, 0x69, 0xb2, 0x11, 0x22, 0x54, 0x9b, 0x96, 0xe0, 0x49, 0xa9, 0x85, 0xaa, 0xe0, 0x1a, 0xce, 0xa2, - 0x00, 0xf2, 0x10, 0xa9, 0xcc, 0x9a, 0x5a, 0xca, 0x0b, 0xdb, 0xb6, 0x0d, 0xf3, 0x00, 0x32, 0xfe, 0x1d, 0x1e, 0xd9, - 0x86, 0x09, 0x7f, 0x59, 0x96, 0xd5, 0x20, 0x8f, 0xed, 0xcd, 0xfd, 0xd0, 0xa4, 0xc7, 0x96, 0xbd, 0x1b, 0xbc, 0xf7, - 0x5a, 0xf5, 0x26, 0x5c, 0x37, 0x36, 0xcc, 0x5d, 0x47, 0xb5, 0xa1, 0x9b, 0x94, 0x6d, 0xdd, 0x2c, 0x0a, 0xb8, 0xc4, - 0xe3, 0x61, 0x54, 0x88, 0xd1, 0xb0, 0xfc, 0x16, 0xd9, 0xd2, 0xb8, 0x9a, 0xc7, 0x42, 0xfd, 0x9e, 0x83, 0xd5, 0x55, - 0x5e, 0x45, 0xcb, 0x60, 0x8c, 0xe6, 0x50, 0x60, 0xbb, 0xac, 0x14, 0x56, 0xa1, 0x95, 0x64, 0x53, 0x90, 0x4b, 0x1c, - 0x13, 0xad, 0xbd, 0x47, 0xe2, 0x14, 0xc5, 0xda, 0x53, 0x9c, 0xe2, 0xcb, 0xa6, 0x2d, 0x78, 0xf5, 0x14, 0xe2, 0x09, - 0xed, 0xd0, 0x80, 0xef, 0x0b, 0xa8, 0x1f, 0xec, 0x52, 0x5f, 0xac, 0xdb, 0xd5, 0x53, 0x0a, 0x3a, 0xeb, 0x7d, 0xfa, - 0xb4, 0x37, 0xfa, 0xf4, 0x69, 0xaf, 0x96, 0xa9, 0x63, 0xf3, 0x08, 0x69, 0x63, 0x30, 0x1e, 0x62, 0x04, 0xe2, 0x06, - 0x11, 0xd0, 0xdf, 0x43, 0x79, 0xd7, 0xe3, 0xd1, 0xaa, 0xe8, 0x69, 0x64, 0xf0, 0x0f, 0xd2, 0x63, 0x90, 0x55, 0x26, - 0x65, 0xe6, 0x7a, 0x24, 0xe6, 0xf9, 0xf4, 0x89, 0x1f, 0x37, 0x63, 0xec, 0x8e, 0xf2, 0x22, 0x47, 0x35, 0x96, 0x6e, - 0x90, 0x3f, 0xaa, 0x08, 0xf2, 0x92, 0x63, 0xcc, 0x02, 0xe2, 0x95, 0x17, 0x87, 0x32, 0xc0, 0x3f, 0x46, 0x0a, 0xff, - 0xac, 0xc2, 0x23, 0xa2, 0x8e, 0xab, 0xab, 0x31, 0x71, 0x99, 0xb6, 0x24, 0x1c, 0x28, 0x2c, 0xdd, 0xa4, 0x0e, 0x2e, - 0x04, 0xb6, 0xc7, 0xb4, 0xa8, 0x62, 0x80, 0xe8, 0x5f, 0x8d, 0x27, 0x77, 0x2c, 0x86, 0xf5, 0xce, 0x5b, 0x75, 0x97, - 0xe2, 0xe1, 0x8c, 0x4c, 0xe2, 0xbb, 0x93, 0xdc, 0x6f, 0x79, 0x41, 0x5e, 0x87, 0x53, 0xf7, 0xdb, 0x58, 0x5b, 0x18, - 0xa9, 0xa1, 0x0a, 0x32, 0xa2, 0xea, 0xc6, 0xbc, 0x29, 0xc0, 0x6a, 0x6f, 0xce, 0xc3, 0xcd, 0x68, 0x62, 0x2b, 0x5c, - 0x4f, 0xd0, 0x57, 0x21, 0x1c, 0xdd, 0x61, 0x00, 0xe5, 0xe2, 0x3d, 0x81, 0x72, 0xcd, 0xb3, 0xef, 0x8d, 0xe5, 0x57, - 0xb0, 0xe0, 0xaa, 0x31, 0xd1, 0x0d, 0xf2, 0x01, 0x98, 0x7e, 0x49, 0x73, 0x7f, 0x8a, 0xa9, 0x3c, 0x97, 0xc2, 0xbd, - 0x0a, 0x07, 0x80, 0xeb, 0x8a, 0x03, 0x40, 0xc3, 0x7c, 0x2a, 0x31, 0x4b, 0x16, 0x51, 0x08, 0x77, 0xc5, 0xeb, 0xc2, - 0xc3, 0xeb, 0xba, 0xee, 0xe1, 0xd5, 0xd0, 0x14, 0xdf, 0x50, 0x3b, 0x50, 0x49, 0x5f, 0xfc, 0xa5, 0x62, 0xa1, 0x2f, - 0x48, 0x3d, 0x66, 0x29, 0x3f, 0xdb, 0xe4, 0xd9, 0xfd, 0xfd, 0xfd, 0x9e, 0xdd, 0xe7, 0x3b, 0x79, 0x76, 0x7f, 0xff, + 0x3f, 0xf7, 0xc3, 0x69, 0xd2, 0x08, 0x11, 0xaa, 0x4d, 0x4b, 0xf0, 0xa4, 0xd4, 0x42, 0x55, 0x70, 0x0d, 0x67, 0x51, + 0x00, 0x79, 0x88, 0x54, 0x66, 0x4d, 0x2d, 0xe5, 0x85, 0x6d, 0xdb, 0x86, 0x79, 0x00, 0x19, 0xff, 0x0e, 0x8f, 0x6c, + 0xc3, 0x84, 0xbf, 0x2c, 0xcb, 0xaa, 0x91, 0xc7, 0xf6, 0xe6, 0x7e, 0x68, 0xd2, 0x63, 0xcb, 0xde, 0x0d, 0xde, 0x7b, + 0xad, 0x7a, 0x13, 0xae, 0x1b, 0x1b, 0xe6, 0xae, 0xa3, 0xda, 0xd0, 0x4d, 0xca, 0xb6, 0x6e, 0x16, 0x05, 0x5c, 0xe2, + 0xf1, 0x30, 0x2a, 0xc4, 0x68, 0x58, 0x7e, 0x8b, 0x6c, 0x69, 0x5c, 0xcd, 0x63, 0xa1, 0x7e, 0xcf, 0xc1, 0xea, 0x2a, + 0xaf, 0xa2, 0x65, 0x30, 0x46, 0x73, 0x28, 0xb0, 0x5d, 0x56, 0x0a, 0xab, 0xd0, 0x4a, 0xb2, 0x29, 0xc8, 0x25, 0x8e, + 0x89, 0xd6, 0xde, 0x23, 0x71, 0x8a, 0x62, 0xed, 0x29, 0x4e, 0xf1, 0x65, 0xdd, 0x16, 0xbc, 0x7a, 0x0a, 0xf1, 0x84, + 0x76, 0x68, 0xc0, 0xf7, 0x05, 0xd4, 0x0f, 0x76, 0xa9, 0x2f, 0xd6, 0xed, 0xea, 0x29, 0x05, 0x9d, 0xf5, 0x3e, 0x7d, + 0xda, 0x1b, 0x7d, 0xfa, 0xb4, 0xb7, 0x91, 0xa9, 0xa3, 0x79, 0x84, 0xb4, 0x31, 0x18, 0x0f, 0x31, 0x02, 0x71, 0x83, + 0x08, 0xe8, 0xef, 0xa1, 0xbc, 0xeb, 0xf1, 0x68, 0x55, 0xf4, 0x34, 0x32, 0xf8, 0x07, 0xe9, 0x31, 0xc8, 0x2a, 0x93, + 0x32, 0x73, 0x3d, 0x12, 0xf3, 0x7c, 0xfa, 0xc4, 0x8f, 0x9b, 0x31, 0x76, 0x47, 0x79, 0x91, 0xa3, 0x1a, 0x4b, 0x37, + 0xc8, 0x1f, 0x55, 0x04, 0x79, 0xc9, 0x31, 0x66, 0x01, 0xf1, 0xca, 0x8b, 0x43, 0x19, 0xe0, 0x9f, 0x22, 0x85, 0x7f, + 0x56, 0xe1, 0x11, 0x51, 0xc7, 0xd5, 0xd5, 0x98, 0xb8, 0x4c, 0x5b, 0x12, 0x0e, 0x14, 0x96, 0x6e, 0x52, 0x07, 0x17, + 0x02, 0xdb, 0x63, 0x5a, 0x54, 0x31, 0x40, 0xf4, 0xaf, 0xc6, 0x93, 0x3b, 0x16, 0xc3, 0x7a, 0xe7, 0xad, 0xba, 0x4b, + 0xf1, 0x70, 0x46, 0x26, 0xf1, 0xdd, 0x49, 0xee, 0xb7, 0xbc, 0x20, 0xaf, 0xc3, 0xa9, 0xfb, 0x6d, 0xac, 0x2d, 0x8c, + 0xd4, 0x50, 0x05, 0x19, 0x51, 0x75, 0x63, 0x5e, 0x17, 0x60, 0xb5, 0x37, 0xe7, 0xe1, 0x66, 0x34, 0xb1, 0x15, 0xae, + 0x27, 0xe8, 0xab, 0x10, 0x8e, 0xee, 0x30, 0x80, 0x72, 0xf1, 0x9e, 0x40, 0xb9, 0xe6, 0xd9, 0xf7, 0xc6, 0xf2, 0x2b, + 0x58, 0x70, 0xd5, 0x98, 0xe8, 0x06, 0xf9, 0x00, 0x4c, 0xbf, 0xa4, 0xb9, 0x3f, 0xc5, 0x54, 0x9e, 0x4b, 0xe1, 0x5e, + 0x85, 0x03, 0xc0, 0x75, 0xc5, 0x01, 0xa0, 0x66, 0x3e, 0x95, 0x98, 0x25, 0x8b, 0x28, 0x84, 0xbb, 0xe2, 0x75, 0xe1, + 0xe1, 0x75, 0xbd, 0xe9, 0xe1, 0x55, 0xd3, 0x14, 0xdf, 0x50, 0x3b, 0x50, 0x49, 0x5f, 0xfc, 0x57, 0xc5, 0x42, 0x5f, + 0x90, 0x7a, 0xcc, 0x52, 0x7e, 0xd6, 0xe4, 0xd9, 0xfd, 0xfd, 0xfd, 0x9e, 0xdd, 0xe7, 0x3b, 0x79, 0x76, 0x7f, 0xff, 0xc5, 0x3d, 0xbb, 0xcf, 0x64, 0xcf, 0x6e, 0x20, 0xc1, 0x67, 0x6c, 0x27, 0x47, 0x5a, 0xe1, 0xd2, 0x12, 0xad, 0x12, - 0xd7, 0xe1, 0x9a, 0xb5, 0x64, 0x34, 0x63, 0x60, 0xaa, 0xc0, 0x59, 0xdd, 0x20, 0x9a, 0x82, 0xbf, 0xeb, 0x66, 0x8f, - 0xd6, 0x2f, 0xe5, 0xcf, 0x1a, 0x44, 0x53, 0x55, 0xca, 0xd3, 0x16, 0x8a, 0x3c, 0x6d, 0x10, 0x9b, 0xee, 0xef, 0xb7, - 0xce, 0xcb, 0x4b, 0xa7, 0xd7, 0x76, 0x20, 0xce, 0x29, 0x68, 0x9f, 0xb1, 0xc0, 0xee, 0xb5, 0xdb, 0x50, 0xb0, 0x92, - 0x0a, 0x5a, 0x50, 0xe0, 0x4b, 0x05, 0x87, 0x50, 0x30, 0x92, 0x0a, 0x8e, 0xa0, 0x60, 0x2c, 0x15, 0x1c, 0x43, 0xc1, - 0x8d, 0x9a, 0x5d, 0x86, 0xb9, 0xdf, 0xfa, 0xb1, 0x7e, 0x55, 0x4a, 0xd1, 0x99, 0x9b, 0x4a, 0x88, 0x2a, 0xc7, 0x86, - 0xc8, 0x17, 0x61, 0x1e, 0xe8, 0x9c, 0x47, 0x1b, 0x7c, 0x35, 0x00, 0xcc, 0x0b, 0x96, 0x23, 0x06, 0xd8, 0xdd, 0x50, - 0xcd, 0xb6, 0x78, 0xad, 0x76, 0x73, 0x3f, 0x6f, 0xdb, 0x68, 0x09, 0xbf, 0xe9, 0x2e, 0x46, 0xf1, 0x10, 0x95, 0x0f, - 0x9f, 0xcf, 0xf2, 0xe0, 0xd1, 0x4b, 0xb7, 0x08, 0x86, 0xd3, 0x86, 0x14, 0x3a, 0x9c, 0x57, 0x63, 0x1a, 0xd8, 0xcb, - 0x40, 0xac, 0x13, 0x71, 0x8a, 0xc4, 0x07, 0x14, 0x74, 0x86, 0xef, 0x79, 0x05, 0x0f, 0xc7, 0x43, 0xad, 0x13, 0xf4, - 0xf3, 0x3c, 0x82, 0x35, 0xe9, 0x52, 0x97, 0x46, 0xea, 0x4d, 0xbb, 0x33, 0x83, 0x0c, 0xa9, 0xba, 0x53, 0x48, 0x49, - 0x72, 0x3a, 0xee, 0x2e, 0x8c, 0xd5, 0x8c, 0x85, 0xdd, 0x09, 0x77, 0x3b, 0x84, 0xf5, 0x27, 0x4f, 0x92, 0xb9, 0x2e, - 0x5c, 0xa0, 0x70, 0x4f, 0x14, 0x6f, 0x09, 0x4a, 0x33, 0xdf, 0x4a, 0x85, 0xf7, 0x8e, 0x26, 0x1b, 0x59, 0x7d, 0x09, - 0x5f, 0x8b, 0xd7, 0x6c, 0xb8, 0x9c, 0x2a, 0xe7, 0xd1, 0xf4, 0x5e, 0xbf, 0x0a, 0xf9, 0x15, 0x40, 0xa9, 0x92, 0x35, - 0xa9, 0x29, 0xb6, 0x37, 0xff, 0x16, 0x3d, 0x66, 0xe5, 0xfa, 0x29, 0xc0, 0xa6, 0xa4, 0xc4, 0x36, 0xc0, 0x77, 0x60, - 0xb6, 0x25, 0xcf, 0x85, 0x73, 0x98, 0x3f, 0xe9, 0xf9, 0xc2, 0x93, 0xe0, 0xe9, 0xff, 0xc0, 0x92, 0xc4, 0x9b, 0x32, - 0x19, 0xb5, 0x94, 0x3a, 0x07, 0x2c, 0x98, 0xab, 0x93, 0x71, 0x02, 0x81, 0xb1, 0xf7, 0x6b, 0xfe, 0x28, 0xe0, 0x32, + 0xd7, 0xe1, 0x9a, 0xb5, 0x64, 0x34, 0x63, 0x60, 0xaa, 0xc0, 0x59, 0xdd, 0x20, 0x9a, 0x82, 0xbf, 0x6b, 0xb3, 0x47, + 0xeb, 0x97, 0xf2, 0x67, 0x0d, 0xa2, 0xa9, 0x2a, 0xe5, 0x69, 0x0b, 0x45, 0x9e, 0x36, 0x88, 0x4d, 0xf7, 0xb7, 0x5b, + 0xe7, 0xe5, 0xa5, 0xd3, 0x6b, 0x3b, 0x10, 0xe7, 0x14, 0xb4, 0xcf, 0x58, 0x60, 0xf7, 0xda, 0x6d, 0x28, 0x58, 0x49, + 0x05, 0x2d, 0x28, 0xf0, 0xa5, 0x82, 0x43, 0x28, 0x18, 0x49, 0x05, 0x47, 0x50, 0x30, 0x96, 0x0a, 0x8e, 0xa1, 0xe0, + 0x46, 0xcd, 0x2e, 0xc3, 0xdc, 0x6f, 0xfd, 0x58, 0xbf, 0x2a, 0xa5, 0xe8, 0xcc, 0x4d, 0x25, 0x44, 0x95, 0x63, 0x43, + 0xe4, 0x8b, 0x30, 0x0f, 0x74, 0xce, 0xa3, 0x0d, 0xbe, 0x1a, 0x00, 0xe6, 0x05, 0xcb, 0x11, 0x03, 0xec, 0x6e, 0xa8, + 0x66, 0x5b, 0xbc, 0x56, 0xbb, 0xb9, 0x9f, 0xb7, 0x6d, 0xb4, 0x84, 0xdf, 0x74, 0x17, 0xa3, 0x78, 0x88, 0xca, 0x87, + 0xcf, 0x67, 0x79, 0xf0, 0xe8, 0xa5, 0x5b, 0x04, 0xc3, 0x69, 0x43, 0x0a, 0x1d, 0xce, 0xab, 0x31, 0x0d, 0xec, 0x65, + 0x20, 0xd6, 0x89, 0x38, 0x45, 0xe2, 0x03, 0x0a, 0x3a, 0xc3, 0xf7, 0xbc, 0x82, 0x87, 0xe3, 0xa1, 0xd6, 0x09, 0xfa, + 0x79, 0x1e, 0xc1, 0x9a, 0x74, 0xa9, 0x4b, 0x23, 0xf5, 0xa6, 0xdd, 0x99, 0x41, 0x86, 0x54, 0xdd, 0x29, 0xa4, 0x24, + 0x39, 0x1d, 0x77, 0x17, 0xc6, 0x6a, 0xc6, 0xc2, 0xee, 0x84, 0xbb, 0x1d, 0xc2, 0xfa, 0x93, 0x27, 0xc9, 0x5c, 0x17, + 0x2e, 0x50, 0xb8, 0x27, 0x8a, 0xb7, 0x04, 0xa5, 0x99, 0x6f, 0xa5, 0xc2, 0x7b, 0x47, 0x93, 0x8d, 0xac, 0xbe, 0x84, + 0xaf, 0xc5, 0x6b, 0x36, 0x5c, 0x4e, 0x95, 0xf3, 0x68, 0x7a, 0xaf, 0x5f, 0x85, 0xfc, 0x0a, 0xa0, 0x54, 0xc9, 0x9a, + 0xd4, 0x14, 0xdb, 0x9b, 0x7f, 0x8b, 0x1e, 0xb3, 0x72, 0xfd, 0x14, 0x60, 0x53, 0x52, 0x62, 0x1b, 0xe0, 0x3b, 0x30, + 0xdb, 0x92, 0xe7, 0xc2, 0x39, 0xcc, 0x9f, 0xf4, 0x7c, 0xe1, 0x49, 0xf0, 0xf4, 0x7f, 0x64, 0x49, 0xe2, 0x4d, 0x99, + 0x8c, 0x5a, 0x4a, 0x9d, 0x03, 0x16, 0xcc, 0xd5, 0xc9, 0x38, 0x81, 0xc0, 0xd8, 0xfb, 0x1b, 0xfe, 0x28, 0xe0, 0x32, 0x0b, 0x7e, 0x5a, 0xb0, 0x68, 0x85, 0xf3, 0x86, 0x6f, 0xc1, 0xf2, 0x94, 0xfd, 0x28, 0x00, 0x89, 0xdc, 0xb0, 0xa0, 0x5a, 0x98, 0x7a, 0xd3, 0x6a, 0x11, 0xad, 0x75, 0x56, 0x42, 0x7b, 0x7a, 0xe9, 0x51, 0xe0, 0xc2, 0xcf, 0xb0, 0xcb, - 0x0f, 0xa2, 0xe9, 0x6f, 0x6a, 0x94, 0xbf, 0xc5, 0x99, 0xe2, 0x87, 0xd0, 0x08, 0xd3, 0x81, 0x85, 0x73, 0xac, 0x58, + 0x0f, 0xa2, 0xe9, 0xef, 0x6a, 0x94, 0xbf, 0xc5, 0x99, 0xe2, 0xc7, 0xd0, 0x08, 0xd3, 0x81, 0x85, 0x73, 0xac, 0x58, 0x30, 0x85, 0xdd, 0x30, 0x9d, 0x99, 0x18, 0x58, 0x4e, 0x6b, 0x85, 0xba, 0x61, 0xe1, 0xda, 0xae, 0xab, 0xe1, 0x34, 0xbb, 0xf1, 0x74, 0xe8, 0x69, 0x4e, 0xeb, 0xd8, 0x10, 0x7f, 0x2c, 0xfb, 0x50, 0xcf, 0xb0, 0x07, 0x65, 0xec, 0xdf, 0xac, 0x27, 0x51, 0x98, 0x9a, 0x13, 0x6f, 0xee, 0x07, 0x77, 0xdd, 0x79, 0x14, 0x46, 0xc9, 0xc2, 0x1b, 0xb1, 0x9e, 0xc4, 0x8f, 0x62, 0xa0, 0x66, 0x1e, 0x2b, 0xd0, 0xb1, 0x5a, 0x31, 0x9b, 0x53, 0xeb, 0x3c, 0x0e, 0xf3, 0x24, 0x60, 0xb7, 0x19, 0xff, 0x7c, 0xa9, 0x32, 0x55, 0xc5, 0x2d, 0x47, 0x2d, 0x80, 0x65, 0xe6, 0x41, 0x9e, 0x21, 0xb5, 0x41, - 0x8f, 0x4b, 0x1d, 0xbb, 0x56, 0xeb, 0x30, 0x66, 0x73, 0xc5, 0x3a, 0xdc, 0xd8, 0x79, 0x1c, 0xad, 0xfa, 0x00, 0x2d, - 0x36, 0x36, 0x13, 0x16, 0x4c, 0xf0, 0x8d, 0x89, 0x71, 0xa5, 0x44, 0x3f, 0x26, 0xda, 0x15, 0x40, 0x6f, 0x6c, 0xde, - 0x83, 0xd7, 0xdd, 0x96, 0x62, 0x4b, 0xfc, 0xf4, 0xb1, 0xbd, 0x90, 0xfa, 0x92, 0xe7, 0x4f, 0x5f, 0x63, 0x75, 0x47, - 0xb1, 0x7b, 0xa0, 0x3f, 0x9e, 0x04, 0xd1, 0xaa, 0x3b, 0xf3, 0xc7, 0x63, 0x16, 0xf6, 0x10, 0xe6, 0xbc, 0x90, 0x05, - 0x81, 0xbf, 0x48, 0xfc, 0xa4, 0x37, 0xf7, 0x6e, 0x79, 0xaf, 0x07, 0x9b, 0x7a, 0x6d, 0xf3, 0x5e, 0xdb, 0x3b, 0xf7, - 0x2a, 0x75, 0x03, 0x31, 0xac, 0xa8, 0x1f, 0x0e, 0xda, 0xa1, 0x62, 0x57, 0xc6, 0xb9, 0x73, 0xaf, 0x8b, 0x98, 0xad, - 0xe7, 0x5e, 0x3c, 0xf5, 0xc3, 0xae, 0x9d, 0x59, 0x37, 0x6b, 0xda, 0x18, 0x8f, 0x3a, 0x9d, 0x4e, 0x66, 0x8d, 0xc5, - 0x93, 0x3d, 0x1e, 0x67, 0xd6, 0x48, 0x3c, 0x4d, 0x26, 0xb6, 0x3d, 0x99, 0x64, 0x96, 0x2f, 0x0a, 0xda, 0xad, 0xd1, - 0xb8, 0xdd, 0xca, 0xac, 0x95, 0x54, 0x23, 0xb3, 0x18, 0x7f, 0x8a, 0xd9, 0xb8, 0x87, 0x1b, 0x89, 0xfb, 0x3f, 0x1f, - 0xdb, 0x76, 0x86, 0x18, 0xe0, 0xb2, 0x84, 0x9b, 0xd0, 0x74, 0xe5, 0x6a, 0xbd, 0x73, 0x4d, 0xa5, 0xf8, 0xdc, 0x68, - 0xd4, 0x58, 0x6f, 0xec, 0xc5, 0x1f, 0xaf, 0x14, 0x69, 0x14, 0x9e, 0x47, 0xd5, 0xd6, 0x62, 0x1a, 0xcc, 0xdb, 0x2e, - 0x24, 0xec, 0xe8, 0x0d, 0xa3, 0x18, 0xce, 0x6c, 0xec, 0x8d, 0xfd, 0x65, 0xd2, 0x75, 0x5a, 0x8b, 0x5b, 0x51, 0xc4, - 0xf7, 0x7a, 0x51, 0x80, 0x67, 0xaf, 0x9b, 0x44, 0x81, 0x3f, 0x16, 0x45, 0x9b, 0xce, 0x92, 0xd3, 0xd2, 0x7b, 0xc8, - 0xbf, 0xfa, 0x18, 0x74, 0xd9, 0x0b, 0x02, 0xc5, 0x6a, 0x27, 0x0a, 0xf3, 0x12, 0x34, 0x97, 0x53, 0xec, 0x84, 0xe6, - 0x05, 0x43, 0xd3, 0x3a, 0x07, 0x8b, 0xdb, 0x7c, 0xcf, 0x3b, 0x47, 0x8b, 0xdb, 0xec, 0xeb, 0x39, 0x1b, 0xfb, 0x9e, - 0xa2, 0x15, 0xbb, 0xc9, 0xb1, 0xc1, 0xa4, 0x4e, 0x5f, 0x6f, 0xd8, 0xa6, 0xe2, 0x58, 0x40, 0x62, 0xa3, 0x3d, 0x7f, - 0x0e, 0x72, 0x18, 0x2f, 0x4c, 0xb3, 0x6c, 0x70, 0x95, 0x65, 0xbd, 0x73, 0x5f, 0xbb, 0xfc, 0xab, 0x46, 0xb4, 0x90, - 0x4c, 0x50, 0x33, 0xfd, 0xca, 0x38, 0x63, 0xb2, 0xbb, 0x0c, 0x90, 0x31, 0x74, 0x95, 0x91, 0x2b, 0x13, 0xbd, 0xad, - 0x57, 0xa6, 0x49, 0xce, 0xab, 0x93, 0xf7, 0x4d, 0xb9, 0x0a, 0x52, 0x20, 0xa8, 0x70, 0xc6, 0xdc, 0x73, 0xc9, 0xf7, - 0x06, 0x98, 0x1e, 0xac, 0x4c, 0x51, 0x85, 0x5e, 0x6f, 0xe2, 0x3d, 0x2f, 0xee, 0xe7, 0x3d, 0xff, 0x96, 0xee, 0xc2, - 0x7b, 0x5e, 0x7c, 0x71, 0xde, 0xf3, 0x75, 0x3d, 0xaa, 0xd0, 0x45, 0xe4, 0xaa, 0xb9, 0xc1, 0x24, 0x90, 0xa6, 0x98, - 0xe2, 0xf5, 0xbf, 0x4e, 0x7f, 0x6d, 0x78, 0x17, 0xd1, 0x1b, 0x12, 0x05, 0xce, 0xa7, 0x82, 0x98, 0xf5, 0x6d, 0xe8, - 0xfe, 0x29, 0x96, 0x9f, 0x27, 0x13, 0xf7, 0x75, 0x24, 0x15, 0xe4, 0x4f, 0xdc, 0x97, 0xa4, 0x14, 0x5b, 0x99, 0xde, - 0xe4, 0xde, 0x3e, 0x90, 0x7d, 0x1a, 0x42, 0xb3, 0x92, 0x6b, 0xf7, 0x38, 0xf7, 0xb9, 0xeb, 0x95, 0x41, 0xd0, 0x72, - 0x27, 0x57, 0x11, 0x80, 0xab, 0x66, 0x19, 0x35, 0x65, 0x42, 0x06, 0xf0, 0xf2, 0xee, 0xfb, 0xb1, 0x76, 0x11, 0xe9, - 0x99, 0x9f, 0xbc, 0xad, 0x86, 0xbf, 0x12, 0x7a, 0x2e, 0x79, 0x38, 0x19, 0xf7, 0x9b, 0x93, 0xa2, 0xdc, 0xe2, 0x6b, - 0x6a, 0x7e, 0x5a, 0x1a, 0x69, 0x57, 0x6e, 0xd8, 0xa3, 0x98, 0xdf, 0x35, 0x62, 0xcc, 0xc3, 0xc4, 0xac, 0x39, 0x97, + 0x8f, 0x4b, 0x1d, 0xbb, 0x56, 0xeb, 0x30, 0x66, 0x73, 0xc5, 0x3a, 0x6c, 0xec, 0x3c, 0x8e, 0x56, 0x7d, 0x80, 0x16, + 0x1b, 0x9b, 0x09, 0x0b, 0x26, 0xf8, 0xc6, 0xc4, 0xb8, 0x52, 0xa2, 0x1f, 0x13, 0xed, 0x0a, 0xa0, 0x37, 0x36, 0xef, + 0xc1, 0xeb, 0x6e, 0x4b, 0xb1, 0x25, 0x7e, 0xfa, 0xd8, 0x5e, 0x48, 0x7d, 0xc9, 0xf3, 0xa7, 0xaf, 0xb1, 0xba, 0xa3, + 0xd8, 0x3d, 0xd0, 0x1f, 0x4f, 0x82, 0x68, 0xd5, 0x9d, 0xf9, 0xe3, 0x31, 0x0b, 0x7b, 0x08, 0x73, 0x5e, 0xc8, 0x82, + 0xc0, 0x5f, 0x24, 0x7e, 0xd2, 0x9b, 0x7b, 0xb7, 0xbc, 0xd7, 0x83, 0xa6, 0x5e, 0xdb, 0xbc, 0xd7, 0xf6, 0xce, 0xbd, + 0x4a, 0xdd, 0x40, 0x0c, 0x2b, 0xea, 0x87, 0x83, 0x76, 0xa8, 0xd8, 0x95, 0x71, 0xee, 0xdc, 0xeb, 0x22, 0x66, 0xeb, + 0xb9, 0x17, 0x4f, 0xfd, 0xb0, 0x6b, 0x67, 0xd6, 0xcd, 0x9a, 0x36, 0xc6, 0xa3, 0x4e, 0xa7, 0x93, 0x59, 0x63, 0xf1, + 0x64, 0x8f, 0xc7, 0x99, 0x35, 0x12, 0x4f, 0x93, 0x89, 0x6d, 0x4f, 0x26, 0x99, 0xe5, 0x8b, 0x82, 0x76, 0x6b, 0x34, + 0x6e, 0xb7, 0x32, 0x6b, 0x25, 0xd5, 0xc8, 0x2c, 0xc6, 0x9f, 0x62, 0x36, 0xee, 0xe1, 0x46, 0xe2, 0xfe, 0xcf, 0xc7, + 0xb6, 0x9d, 0x21, 0x06, 0xb8, 0x2c, 0xe1, 0x26, 0x34, 0x5d, 0xb9, 0x5a, 0xef, 0x5c, 0x53, 0x29, 0x3e, 0x37, 0x1a, + 0xd5, 0xd6, 0x1b, 0x7b, 0xf1, 0xc7, 0x2b, 0x45, 0x1a, 0x85, 0xe7, 0x51, 0xb5, 0xb5, 0x98, 0x06, 0xf3, 0xb6, 0x0b, + 0x09, 0x3b, 0x7a, 0xc3, 0x28, 0x86, 0x33, 0x1b, 0x7b, 0x63, 0x7f, 0x99, 0x74, 0x9d, 0xd6, 0xe2, 0x56, 0x14, 0xf1, + 0xbd, 0x5e, 0x14, 0xe0, 0xd9, 0xeb, 0x26, 0x51, 0xe0, 0x8f, 0x45, 0x51, 0xd3, 0x59, 0x72, 0x5a, 0x7a, 0x0f, 0xf9, + 0x57, 0x1f, 0x83, 0x2e, 0x7b, 0x41, 0xa0, 0x58, 0xed, 0x44, 0x61, 0x5e, 0x82, 0xe6, 0x72, 0x8a, 0x9d, 0xd0, 0xbc, + 0x60, 0x68, 0x5a, 0xe7, 0x60, 0x71, 0x9b, 0xef, 0x79, 0xe7, 0x68, 0x71, 0x9b, 0x7d, 0x3d, 0x67, 0x63, 0xdf, 0x53, + 0xb4, 0x62, 0x37, 0x39, 0x36, 0x98, 0xd4, 0xe9, 0xeb, 0x86, 0x6d, 0x2a, 0x8e, 0x05, 0x24, 0x36, 0xda, 0xf3, 0xe7, + 0x20, 0x87, 0xf1, 0xc2, 0x34, 0xcb, 0x06, 0x57, 0x59, 0xd6, 0x3b, 0xf7, 0xb5, 0xcb, 0xff, 0xd6, 0x88, 0x16, 0x92, + 0x09, 0x6a, 0xa6, 0x5f, 0x19, 0x67, 0x4c, 0x76, 0x97, 0x01, 0x32, 0x86, 0xae, 0x32, 0x72, 0x65, 0xa2, 0xb7, 0x9b, + 0x95, 0x69, 0x92, 0xf3, 0xea, 0xe4, 0x7d, 0x53, 0xae, 0x82, 0x14, 0x08, 0x2a, 0x9c, 0x31, 0xf7, 0x5c, 0xf2, 0xbd, + 0x01, 0xa6, 0x07, 0x2b, 0x53, 0x54, 0xa1, 0xd7, 0x4d, 0xbc, 0xe7, 0xc5, 0xfd, 0xbc, 0xe7, 0x5f, 0xd3, 0x5d, 0x78, + 0xcf, 0x8b, 0x2f, 0xce, 0x7b, 0xbe, 0xde, 0x8c, 0x2a, 0x74, 0x11, 0xb9, 0x6a, 0x6e, 0x30, 0x09, 0xa4, 0x29, 0xa6, + 0x78, 0xfd, 0xaf, 0xd3, 0xdf, 0x1a, 0xde, 0x45, 0xf4, 0x86, 0x44, 0x81, 0xf3, 0xa9, 0x20, 0x66, 0x7d, 0x1b, 0xba, + 0x7f, 0x8e, 0xe5, 0xe7, 0xc9, 0xc4, 0x7d, 0x1d, 0x49, 0x05, 0xf9, 0x13, 0xf7, 0x25, 0x29, 0xc5, 0x56, 0xa6, 0x37, + 0xb9, 0xb7, 0x0f, 0x64, 0x9f, 0x86, 0xd0, 0xac, 0xe4, 0xda, 0x3d, 0xce, 0x7d, 0xee, 0x7a, 0x65, 0x10, 0xb4, 0xdc, + 0xc9, 0x55, 0x04, 0xe0, 0xda, 0xb0, 0x8c, 0x9a, 0x32, 0x21, 0x03, 0x78, 0x79, 0xf7, 0xfd, 0x58, 0xbb, 0x88, 0xf4, + 0xcc, 0x4f, 0xde, 0x56, 0xc3, 0x5f, 0x09, 0x3d, 0x97, 0x3c, 0x9c, 0x8c, 0xfb, 0xcd, 0x49, 0x51, 0x6e, 0xf1, 0x35, + 0x35, 0x3f, 0x2d, 0x8d, 0xb4, 0x2b, 0x37, 0xec, 0x51, 0xcc, 0xef, 0x0d, 0x62, 0xcc, 0xc3, 0xc4, 0xac, 0x39, 0x97, 0xb7, 0xc6, 0x67, 0x88, 0x1a, 0x3a, 0xa6, 0xe6, 0xfe, 0x38, 0xcb, 0xf4, 0x9e, 0x98, 0x08, 0x89, 0xd0, 0xb2, 0xfb, 0x98, 0xb8, 0xa4, 0x10, 0x02, 0x71, 0x89, 0x0f, 0x59, 0x33, 0x5f, 0x80, 0x7f, 0x00, 0xb7, 0x7d, 0xe6, 0x73, 0xa6, - 0x2a, 0x34, 0x7d, 0xe4, 0x37, 0x22, 0x0d, 0x08, 0x0c, 0xda, 0x65, 0x6f, 0xab, 0xd2, 0x82, 0xd4, 0x1d, 0x5b, 0x69, - 0x72, 0xd0, 0xc1, 0x01, 0x62, 0xfc, 0x0a, 0xb1, 0x10, 0xa1, 0x1d, 0x5e, 0x07, 0x1f, 0x32, 0x35, 0xe7, 0xfd, 0x70, - 0xfb, 0xf5, 0x4f, 0xf6, 0xa1, 0x41, 0xbf, 0xa2, 0x74, 0xbb, 0xc7, 0x2f, 0x13, 0x58, 0x89, 0x64, 0x65, 0x58, 0xc9, - 0x4a, 0x79, 0xb6, 0x16, 0xf1, 0xb1, 0x53, 0x6f, 0x61, 0x82, 0x96, 0x07, 0x71, 0x2f, 0xc7, 0x78, 0x52, 0x28, 0xee, - 0xde, 0x32, 0x01, 0xdc, 0x88, 0x72, 0x14, 0xc4, 0x3f, 0xbd, 0xd1, 0x32, 0x4e, 0xa2, 0xb8, 0xbb, 0x88, 0xfc, 0x30, - 0x65, 0x71, 0x46, 0x82, 0x15, 0x9c, 0x1f, 0x31, 0x3d, 0x57, 0xeb, 0x68, 0xe1, 0x8d, 0xfc, 0xf4, 0xae, 0x6b, 0x73, - 0x96, 0xc2, 0xee, 0x71, 0xee, 0xc0, 0x6e, 0xac, 0xdf, 0xe5, 0xb3, 0xf9, 0x1c, 0x19, 0xbf, 0xb8, 0xce, 0xce, 0xc8, - 0xdb, 0xbc, 0x27, 0xbd, 0xa5, 0x08, 0xe1, 0xc0, 0x7e, 0x78, 0xb1, 0x39, 0x05, 0x2c, 0x0f, 0x4b, 0x6d, 0x8f, 0xd9, - 0xd4, 0x40, 0xac, 0x0d, 0x66, 0x86, 0xe2, 0x8f, 0x75, 0xa8, 0x2b, 0x76, 0x73, 0x31, 0x70, 0x3c, 0xfa, 0x2e, 0x90, - 0x75, 0xbd, 0x49, 0xca, 0x62, 0x63, 0x97, 0x9a, 0x43, 0x36, 0x89, 0x62, 0x46, 0xd9, 0xe4, 0x9c, 0xce, 0xe2, 0x76, - 0xf7, 0xee, 0xb7, 0x0f, 0xbf, 0xb9, 0x9f, 0x30, 0x4a, 0x35, 0xd1, 0x99, 0x7e, 0x4f, 0x6f, 0x75, 0x7a, 0x06, 0xac, - 0x21, 0xcd, 0xfc, 0x88, 0xa4, 0x20, 0x10, 0x09, 0xac, 0x31, 0x69, 0xc7, 0x22, 0xe2, 0x34, 0x2f, 0x66, 0x81, 0x97, - 0xfa, 0x37, 0x82, 0x67, 0x6c, 0x1f, 0x2d, 0x6e, 0xc5, 0x1a, 0x23, 0xc1, 0x7b, 0xc0, 0x22, 0x55, 0x40, 0x11, 0x8b, - 0x54, 0x2d, 0xc6, 0x45, 0xea, 0xd5, 0x46, 0x23, 0xe2, 0x58, 0x57, 0x28, 0xfd, 0xe1, 0xe2, 0x56, 0x26, 0xd1, 0x45, - 0xb3, 0x9c, 0x52, 0x57, 0x13, 0x90, 0xcc, 0xfd, 0xf1, 0x38, 0x60, 0x59, 0x69, 0xa1, 0xcb, 0x6b, 0x29, 0x4d, 0x4e, - 0x3e, 0x0f, 0xde, 0x30, 0x89, 0x82, 0x65, 0xca, 0x9a, 0xa7, 0x4b, 0x48, 0x74, 0x8b, 0xc9, 0xc1, 0xdf, 0x65, 0x58, - 0x0f, 0x81, 0xdd, 0x86, 0x6d, 0x62, 0xf7, 0x20, 0xdf, 0xa0, 0xd9, 0x2e, 0x83, 0x0e, 0xaf, 0x72, 0xa0, 0x8d, 0x86, - 0x81, 0x18, 0x40, 0x96, 0x08, 0x7b, 0x2b, 0x96, 0xc3, 0xcb, 0xf2, 0x9c, 0x6b, 0x79, 0x51, 0x56, 0x1e, 0xcc, 0x6f, - 0x73, 0xc6, 0x5e, 0x34, 0x9f, 0xb1, 0x17, 0xe2, 0x8c, 0x6d, 0xdf, 0x99, 0x8f, 0x26, 0x0e, 0xfc, 0xd7, 0x2b, 0x06, - 0xd4, 0xb5, 0x95, 0xf6, 0xe2, 0x56, 0x71, 0x16, 0xb7, 0x8a, 0xd9, 0x5a, 0xdc, 0x2a, 0xd8, 0x35, 0xba, 0xb7, 0x18, - 0x56, 0x4b, 0x37, 0x6c, 0x05, 0x0a, 0xe1, 0x8f, 0x5d, 0x7a, 0xe5, 0x1c, 0xc0, 0x3b, 0x68, 0x75, 0x58, 0x7f, 0xd7, - 0xda, 0x7e, 0xd4, 0xe9, 0x2c, 0x09, 0xa4, 0xad, 0x5b, 0xa9, 0x37, 0x1c, 0x82, 0x28, 0x33, 0x1a, 0x2d, 0x93, 0x7f, - 0x72, 0xf8, 0xf9, 0x24, 0x6e, 0x45, 0x04, 0x95, 0x7e, 0x44, 0x53, 0x50, 0x14, 0xde, 0x30, 0xd1, 0xc3, 0x3a, 0x5f, - 0xa7, 0x2e, 0x25, 0x47, 0x6c, 0x59, 0x07, 0x0d, 0x9b, 0xbc, 0x79, 0xa2, 0x7f, 0xb3, 0x55, 0xda, 0x8c, 0x62, 0x3e, - 0x63, 0x5a, 0xb6, 0x4e, 0xc7, 0xc3, 0x67, 0x83, 0xaf, 0xa6, 0xdd, 0x69, 0x06, 0xf7, 0x52, 0x7c, 0xe9, 0x4a, 0x10, - 0x15, 0x4e, 0xb7, 0x78, 0x28, 0x8e, 0xed, 0xbd, 0x6e, 0xda, 0x23, 0xb5, 0x5e, 0xb7, 0x10, 0x84, 0xa2, 0xee, 0x8e, - 0x58, 0xfe, 0xd1, 0x8b, 0x03, 0xf8, 0x8f, 0xb8, 0xfa, 0xbf, 0xa5, 0x4d, 0x8c, 0xfa, 0xeb, 0xb4, 0xc4, 0xa8, 0x13, - 0xab, 0x84, 0x8c, 0xf8, 0xee, 0xf5, 0x27, 0x93, 0x87, 0x35, 0xd8, 0xb9, 0x36, 0x79, 0x86, 0x55, 0x6b, 0xbf, 0x8c, - 0xa2, 0x80, 0x79, 0x61, 0xbd, 0xba, 0x98, 0x1e, 0x72, 0xf3, 0x4f, 0x5d, 0x68, 0x24, 0xee, 0x11, 0xe4, 0x94, 0xa0, - 0x62, 0x1b, 0xba, 0x4a, 0x9c, 0x6f, 0xba, 0x4a, 0xbc, 0xbb, 0xff, 0x2a, 0xf1, 0xc7, 0x9d, 0xae, 0x12, 0xef, 0xbe, - 0xf8, 0x55, 0xe2, 0xbc, 0x7e, 0x95, 0x38, 0x8f, 0x84, 0x3b, 0xb0, 0xf1, 0x66, 0xc9, 0x7f, 0x7e, 0x20, 0x7b, 0xdf, - 0x77, 0x91, 0x7b, 0x68, 0x53, 0xc2, 0xc3, 0x8b, 0x5f, 0x7d, 0xb1, 0xc0, 0x8d, 0xf8, 0x0e, 0xbd, 0xe3, 0x8a, 0xab, - 0x05, 0xc7, 0xec, 0xf8, 0x1d, 0xa9, 0x38, 0x88, 0xc2, 0xe9, 0x4f, 0x60, 0xef, 0x0d, 0xe2, 0xc0, 0x58, 0x7a, 0xe1, - 0x27, 0x3f, 0x45, 0x8b, 0xe5, 0x02, 0x15, 0x55, 0x1f, 0xfc, 0xc4, 0x1f, 0x06, 0x2c, 0x8f, 0x30, 0x49, 0x5a, 0x57, - 0x2e, 0x5b, 0x07, 0xc5, 0xab, 0xf8, 0xe9, 0xdd, 0x8a, 0x9f, 0xe8, 0x62, 0xcb, 0x7f, 0x93, 0x9b, 0xa0, 0xda, 0x7c, - 0x11, 0x11, 0x16, 0x62, 0x12, 0xd0, 0x0f, 0xbf, 0x8c, 0x9c, 0x8b, 0x58, 0x5e, 0xa5, 0x51, 0x0a, 0xf7, 0x8d, 0x8d, - 0xfd, 0xb0, 0x6a, 0x3f, 0x6f, 0x96, 0xba, 0x91, 0x27, 0xe0, 0xa8, 0x8b, 0xf3, 0xe7, 0xd1, 0x32, 0x61, 0xe3, 0x68, - 0x15, 0xaa, 0x46, 0xc8, 0xf5, 0xaa, 0x11, 0xca, 0xd4, 0xf3, 0x36, 0x65, 0x85, 0xa3, 0x6a, 0x2d, 0x60, 0x0e, 0x4d, - 0xd2, 0x60, 0x9b, 0x38, 0x44, 0x55, 0x84, 0x6c, 0xea, 0xed, 0x69, 0x5a, 0xe4, 0x3e, 0xac, 0xa5, 0xf0, 0x3c, 0x89, - 0x2c, 0x2e, 0x15, 0x4e, 0xb4, 0x50, 0x08, 0x17, 0x45, 0x14, 0xec, 0x86, 0x85, 0xe3, 0x6f, 0x28, 0x42, 0x64, 0xf1, - 0x16, 0x74, 0x55, 0xd9, 0x92, 0xaf, 0x07, 0x8f, 0x09, 0x4d, 0x8f, 0xaf, 0xa4, 0x69, 0x7c, 0x7b, 0xc3, 0xe2, 0xc0, - 0xbb, 0xd3, 0xf4, 0x2c, 0x0a, 0x7f, 0x80, 0x09, 0x78, 0x1d, 0xad, 0x42, 0xb9, 0x02, 0xa6, 0x6a, 0x6f, 0xd8, 0x4b, - 0x8d, 0xd1, 0xcb, 0x21, 0x66, 0x87, 0x04, 0x81, 0x6f, 0x2d, 0xbc, 0x29, 0xfb, 0x8b, 0x41, 0xff, 0xfe, 0x55, 0xcf, - 0x8c, 0x77, 0x51, 0xfe, 0xa1, 0x9f, 0x17, 0x3b, 0x7c, 0xe6, 0xc9, 0x93, 0xbd, 0xcd, 0xc3, 0xd6, 0x46, 0x01, 0xf3, - 0x62, 0x01, 0x45, 0x43, 0x6b, 0x7d, 0xe3, 0x29, 0x00, 0x28, 0x2e, 0xa2, 0xe5, 0x68, 0x86, 0x7e, 0xbb, 0x5f, 0x6e, - 0xbc, 0x29, 0xf4, 0xc9, 0x92, 0x4b, 0xfb, 0x2a, 0x1f, 0x7a, 0xa5, 0xa8, 0x98, 0x05, 0xfc, 0xfe, 0x19, 0xa4, 0xdf, - 0xfa, 0x0f, 0x4e, 0x43, 0x7d, 0xd7, 0xe4, 0x21, 0xbf, 0x1e, 0xb4, 0x79, 0x7b, 0x3e, 0x44, 0xe5, 0xa1, 0xc0, 0xd6, - 0x42, 0x49, 0xd7, 0x8c, 0x64, 0xb2, 0xea, 0xa4, 0xc9, 0x49, 0x64, 0x36, 0xe5, 0xc7, 0x11, 0x5f, 0x61, 0x56, 0xc9, - 0x6a, 0xc4, 0x60, 0x1c, 0x5b, 0x55, 0x90, 0x0c, 0xf7, 0xa6, 0x60, 0x88, 0xbe, 0xaa, 0xef, 0xe6, 0x7e, 0x68, 0x60, - 0x0e, 0xd8, 0xfa, 0x1b, 0xef, 0x16, 0xb2, 0x20, 0x02, 0x72, 0xab, 0xbe, 0x82, 0x42, 0x43, 0x8e, 0x16, 0xe4, 0x8d, - 0xc7, 0x9a, 0xda, 0x38, 0x13, 0x42, 0x1b, 0x38, 0xf8, 0x4a, 0x51, 0x14, 0x25, 0xbf, 0x46, 0x28, 0xf9, 0x3d, 0x02, - 0xcb, 0xf1, 0x3a, 0x00, 0xda, 0x92, 0x6c, 0x71, 0x4b, 0x25, 0x70, 0x33, 0x40, 0xfb, 0x69, 0x51, 0xc0, 0x13, 0xfd, - 0x80, 0x71, 0x0b, 0x15, 0x88, 0x0b, 0x3d, 0xa8, 0xbe, 0xbd, 0x18, 0xf2, 0x01, 0x76, 0x15, 0xbc, 0xb0, 0xe3, 0x5b, - 0x2e, 0x09, 0x56, 0x6c, 0x7a, 0x1c, 0xf4, 0x58, 0x73, 0x46, 0x98, 0x50, 0xc2, 0x82, 0xa0, 0x75, 0xa8, 0x24, 0x78, - 0x34, 0x58, 0x03, 0x6e, 0xc4, 0x7b, 0xd1, 0x6d, 0x3a, 0x67, 0xe1, 0x52, 0x35, 0xc0, 0xea, 0x04, 0x33, 0xf4, 0x40, - 0x9d, 0xd7, 0xc4, 0x6c, 0x01, 0xb6, 0x69, 0x6e, 0x39, 0x23, 0x5a, 0x28, 0x4c, 0x55, 0x3c, 0x63, 0xc4, 0x03, 0xe0, - 0x24, 0x1c, 0xb7, 0x55, 0x29, 0x04, 0x5f, 0xd2, 0xa8, 0x8c, 0xcd, 0x79, 0xc8, 0x2b, 0xe4, 0x14, 0xc8, 0x46, 0x8c, - 0x8b, 0x8b, 0xc4, 0xb4, 0x6b, 0x5e, 0x75, 0xd1, 0x72, 0x8d, 0x8c, 0x57, 0x11, 0x14, 0xc5, 0x7a, 0xbd, 0x1b, 0x0e, - 0x27, 0xa4, 0x25, 0xd8, 0xd8, 0xcf, 0xa8, 0xd6, 0xcf, 0x86, 0x41, 0x7f, 0x64, 0x77, 0x44, 0x48, 0x68, 0xaa, 0x3e, - 0xb2, 0x3b, 0x30, 0x0e, 0x3f, 0x03, 0x69, 0x8a, 0xba, 0x05, 0x5d, 0x1b, 0x90, 0xe8, 0x77, 0x04, 0xa9, 0x2a, 0xb6, - 0x1c, 0x20, 0x3b, 0xdb, 0x82, 0xc5, 0x29, 0x1c, 0xa9, 0x91, 0xf4, 0xc4, 0x21, 0xe6, 0x11, 0x0b, 0xb4, 0xc6, 0x39, - 0x36, 0x1b, 0x8e, 0x86, 0xfe, 0xcc, 0xb1, 0xed, 0xfd, 0x5a, 0x7d, 0x10, 0x64, 0x37, 0xd5, 0xd6, 0x8d, 0xd4, 0x75, - 0x6c, 0xd3, 0x7f, 0x66, 0xb5, 0x7a, 0x35, 0x1a, 0x2d, 0x65, 0x92, 0x1a, 0xa0, 0xf8, 0xab, 0xff, 0x78, 0xad, 0xd5, - 0x0e, 0xa4, 0x5e, 0x8d, 0x00, 0x80, 0xb0, 0x65, 0x5c, 0xfe, 0x35, 0xa8, 0x93, 0x7e, 0xca, 0x63, 0x45, 0x59, 0xcd, - 0x07, 0x90, 0x0b, 0x51, 0x83, 0x63, 0xf4, 0x07, 0xe5, 0xb9, 0xa2, 0xd1, 0xf1, 0xd1, 0xf5, 0x41, 0x4f, 0x60, 0x14, - 0x11, 0x22, 0x47, 0xee, 0xa0, 0xf2, 0xc5, 0xa4, 0x8a, 0xe1, 0x78, 0xd6, 0x35, 0x56, 0x68, 0xf4, 0xb6, 0x72, 0x0b, - 0xd8, 0xff, 0x06, 0xf2, 0x69, 0x0d, 0x21, 0xc6, 0x23, 0xd4, 0x80, 0xcc, 0xa9, 0xf7, 0x76, 0x08, 0xe1, 0x79, 0xe5, - 0xee, 0xca, 0x44, 0x72, 0xf7, 0xce, 0x90, 0xe8, 0xa0, 0x0e, 0x2d, 0xef, 0xaf, 0x99, 0xdc, 0x3d, 0xb0, 0x4b, 0x16, - 0x8e, 0xcb, 0x1d, 0x56, 0xe8, 0xd7, 0xee, 0xdd, 0x95, 0x30, 0x0a, 0xa4, 0x14, 0x8e, 0x1a, 0x30, 0x4a, 0x16, 0x85, - 0xb8, 0xf9, 0xe9, 0xb8, 0xf9, 0x3b, 0x71, 0x31, 0xd8, 0x80, 0xf2, 0x81, 0xe4, 0xcd, 0x24, 0xa1, 0x38, 0xe4, 0xad, - 0xc4, 0x08, 0x5a, 0x9a, 0x60, 0x44, 0x37, 0xee, 0xc4, 0x54, 0xb8, 0x2b, 0x16, 0x6d, 0x7c, 0x9e, 0x89, 0x6a, 0x57, - 0xa9, 0xb5, 0x7f, 0xbf, 0xd4, 0x3a, 0xbd, 0x4f, 0x6a, 0x4d, 0xd1, 0x61, 0xb8, 0x3d, 0xa8, 0x88, 0x92, 0x23, 0x98, - 0x73, 0x39, 0xce, 0x50, 0x49, 0xd4, 0x8d, 0xc1, 0x64, 0x1a, 0xac, 0x48, 0xa9, 0x37, 0x72, 0x40, 0x44, 0xf1, 0xb7, - 0x74, 0x41, 0x11, 0x0a, 0x75, 0x59, 0x36, 0x7e, 0x5e, 0xc8, 0xc6, 0xe9, 0x56, 0x53, 0xc4, 0x05, 0x11, 0xdc, 0xbf, - 0x14, 0x73, 0x27, 0xbf, 0x1d, 0x14, 0xb1, 0x77, 0x0a, 0x48, 0xa5, 0x68, 0x32, 0xc5, 0x45, 0x43, 0x8a, 0x51, 0x24, - 0x6e, 0x19, 0xe5, 0x50, 0x45, 0xe5, 0xaa, 0x45, 0x30, 0x99, 0xa2, 0x1c, 0xa4, 0xee, 0x08, 0x72, 0x5e, 0x2c, 0x6f, - 0x9b, 0x72, 0x34, 0x11, 0xf9, 0xb5, 0xb4, 0x49, 0xf2, 0xb0, 0x1f, 0x34, 0xc1, 0x42, 0x4c, 0x5f, 0xd1, 0x6b, 0xe7, - 0x36, 0x10, 0x08, 0x64, 0x43, 0x94, 0xa2, 0xfb, 0xa5, 0xf3, 0x94, 0x2d, 0xb9, 0x50, 0x5d, 0x3b, 0x48, 0xdd, 0x49, - 0x13, 0x2c, 0xcb, 0x23, 0x70, 0xae, 0xaf, 0x24, 0x09, 0x42, 0xd7, 0x56, 0xec, 0x5e, 0x03, 0x03, 0x80, 0xf4, 0xbf, - 0xfa, 0xcc, 0x59, 0x01, 0x90, 0x44, 0x2a, 0xb6, 0xac, 0xf3, 0xc7, 0x43, 0x6c, 0x92, 0x25, 0x3b, 0x56, 0xad, 0x7f, - 0x93, 0xe4, 0x3d, 0x6b, 0x1e, 0x13, 0xa4, 0x2c, 0xce, 0xe7, 0x35, 0xba, 0x02, 0x0e, 0xbe, 0xcb, 0xe2, 0x65, 0x88, - 0x49, 0x70, 0xcd, 0x34, 0xf6, 0x46, 0x1f, 0xd7, 0xd2, 0xf7, 0xb8, 0x48, 0x14, 0xc4, 0xc5, 0x65, 0xa5, 0x42, 0xcf, - 0xc3, 0x9c, 0x51, 0xac, 0x6b, 0xb5, 0x12, 0x49, 0x50, 0xd3, 0x7d, 0x64, 0xb7, 0xbd, 0x17, 0x93, 0x83, 0x8a, 0xfc, - 0xb4, 0x75, 0x58, 0x96, 0xae, 0xe7, 0x70, 0xcc, 0xa3, 0x5f, 0x78, 0xf4, 0xa4, 0xdf, 0xff, 0xd3, 0x09, 0xff, 0x66, - 0x65, 0x8d, 0x3e, 0x07, 0x04, 0x68, 0x5f, 0x52, 0x4c, 0xcb, 0x6a, 0x9a, 0x5a, 0xc9, 0x26, 0xb0, 0x26, 0x7e, 0x10, - 0x98, 0x01, 0xb8, 0x31, 0xac, 0x3f, 0x6b, 0x78, 0xd8, 0xcf, 0x12, 0xb2, 0x15, 0x7e, 0x46, 0x3f, 0xe5, 0x9d, 0x92, - 0xce, 0x96, 0xf3, 0xe1, 0x5a, 0x16, 0x94, 0x4b, 0xf2, 0xf3, 0xba, 0xcc, 0x5c, 0xfe, 0xec, 0x64, 0x32, 0x29, 0x4b, - 0x8d, 0x6d, 0xe5, 0x00, 0x25, 0xbf, 0x8f, 0x6c, 0xdb, 0xae, 0xce, 0xef, 0xa6, 0x83, 0x42, 0x07, 0xc3, 0x44, 0x21, - 0x7c, 0xe7, 0xfe, 0x3d, 0xf5, 0x3b, 0x41, 0x4b, 0x5d, 0x6d, 0x3a, 0x8f, 0xb4, 0xd5, 0xfe, 0x2b, 0x40, 0x41, 0xd4, - 0x70, 0xdf, 0xf1, 0xaf, 0xef, 0x95, 0x2d, 0x3d, 0x55, 0x0f, 0xf0, 0xc3, 0x1a, 0xdf, 0xb3, 0xd7, 0x77, 0x68, 0xba, - 0x69, 0x7b, 0x67, 0x56, 0x41, 0x76, 0x4b, 0x36, 0x4b, 0x3d, 0xb2, 0x54, 0xf2, 0x53, 0x36, 0x4f, 0xba, 0x23, 0x86, - 0x0a, 0x52, 0x4b, 0xa2, 0xb6, 0x68, 0xd5, 0x63, 0x4e, 0xc1, 0x8e, 0xcb, 0x11, 0x78, 0xd8, 0x56, 0x50, 0x59, 0x55, - 0xd3, 0xac, 0x89, 0x8f, 0x20, 0x15, 0x5b, 0xd7, 0x15, 0x4e, 0xb8, 0x4d, 0x0f, 0xed, 0x3f, 0x94, 0xea, 0x29, 0xc0, - 0x9d, 0xae, 0x85, 0xb5, 0x09, 0x29, 0x4f, 0xf0, 0xef, 0x5c, 0x39, 0xf7, 0x62, 0x71, 0x5b, 0x36, 0xee, 0xea, 0x80, - 0xba, 0xa9, 0x20, 0x65, 0x04, 0x75, 0x13, 0xea, 0xcb, 0x4d, 0x80, 0x26, 0xb2, 0x75, 0x0b, 0x58, 0xd0, 0x88, 0x29, - 0xa8, 0xe8, 0x08, 0x73, 0x50, 0xf1, 0x3a, 0x0b, 0x3b, 0xaf, 0x90, 0xef, 0xe3, 0x2f, 0xc8, 0x52, 0x0e, 0xe9, 0x4e, - 0xfe, 0x60, 0x3c, 0xef, 0xa0, 0x72, 0xaf, 0xb4, 0x55, 0xd1, 0x54, 0x06, 0xf7, 0x80, 0xb8, 0x91, 0x2a, 0xcb, 0x38, - 0x30, 0x29, 0x71, 0xbd, 0xa6, 0xaf, 0xeb, 0xe3, 0xde, 0xdc, 0xbd, 0x73, 0x08, 0x7a, 0x8d, 0xfa, 0x54, 0xed, 0xa4, - 0xda, 0xab, 0xea, 0xb0, 0x05, 0x9c, 0xb0, 0x02, 0xe0, 0x33, 0xab, 0xa0, 0xd1, 0x90, 0x52, 0xc1, 0x7d, 0x34, 0xe8, - 0xfc, 0xad, 0x8c, 0xac, 0xc5, 0x38, 0xb1, 0xbb, 0xe6, 0x2a, 0xd4, 0xb7, 0xd0, 0x0c, 0xc2, 0xdc, 0x71, 0xec, 0x84, - 0xcf, 0x26, 0xec, 0x18, 0x19, 0x5d, 0x39, 0xb8, 0x83, 0xf0, 0x94, 0x9a, 0x94, 0xfc, 0x84, 0x4e, 0x29, 0xea, 0x12, - 0xfe, 0xd8, 0x28, 0xbc, 0xbf, 0x28, 0x49, 0xe3, 0x79, 0xd0, 0x89, 0x96, 0xbe, 0x53, 0xed, 0xb9, 0x1f, 0xee, 0x5e, - 0xd7, 0xbb, 0xdd, 0xb9, 0x2e, 0x30, 0x87, 0x3b, 0x57, 0x06, 0xee, 0x12, 0x2b, 0x5f, 0xa4, 0xee, 0x1f, 0x25, 0xe5, - 0x81, 0x1c, 0x30, 0x51, 0xc5, 0x56, 0x74, 0xa3, 0xff, 0x71, 0xe9, 0x0e, 0x4e, 0x4e, 0x6f, 0xe7, 0x81, 0x72, 0xc3, - 0xe2, 0x04, 0x12, 0x4a, 0xa8, 0x8e, 0x65, 0xab, 0x0a, 0x1a, 0xf4, 0xfb, 0xe1, 0xd4, 0x55, 0x7f, 0xbe, 0x78, 0x63, - 0x76, 0xd4, 0x53, 0x30, 0xc7, 0xb8, 0x99, 0x22, 0x8b, 0x7b, 0xee, 0xdd, 0xb1, 0xf8, 0xba, 0xc5, 0x3d, 0x7e, 0x88, - 0xb9, 0xc5, 0x32, 0xa5, 0xa5, 0xee, 0x90, 0x12, 0x5e, 0xb9, 0xf1, 0xd9, 0xea, 0x65, 0x74, 0xeb, 0xaa, 0x80, 0x58, - 0x9d, 0x56, 0x47, 0x71, 0x5a, 0x07, 0xd6, 0x51, 0x47, 0xed, 0x7f, 0xa5, 0x28, 0x27, 0x63, 0x36, 0x49, 0xfa, 0x28, - 0x8e, 0x39, 0x41, 0x7e, 0x90, 0x7e, 0x2b, 0x8a, 0x35, 0x0a, 0x12, 0xd3, 0x51, 0xd6, 0xfc, 0x51, 0x51, 0x00, 0x19, - 0x75, 0x95, 0x47, 0x93, 0xd6, 0xe4, 0x60, 0xf2, 0xa2, 0xc7, 0x8b, 0xb3, 0xaf, 0x4a, 0xd5, 0x0d, 0xfa, 0xb7, 0x25, - 0x35, 0x4b, 0xd2, 0x38, 0xfa, 0xc8, 0x38, 0x2f, 0xa9, 0xe4, 0x82, 0xa2, 0x6a, 0xd3, 0x56, 0xfd, 0x4b, 0x4e, 0x67, - 0x38, 0x9a, 0xb4, 0x8a, 0xea, 0x08, 0xe3, 0x7e, 0x0e, 0xe4, 0xc9, 0xbe, 0x00, 0xfd, 0x44, 0x9e, 0x26, 0xc7, 0x6c, - 0x9a, 0x28, 0x47, 0xe5, 0x63, 0x9c, 0x8a, 0xf1, 0x9d, 0x40, 0xc6, 0xb5, 0xc2, 0x7b, 0x31, 0xc1, 0x66, 0xae, 0xfa, - 0x83, 0xd3, 0xea, 0x18, 0x8e, 0x73, 0x64, 0x1d, 0x75, 0x46, 0xb6, 0x71, 0x60, 0x1d, 0x98, 0x6d, 0xeb, 0xc8, 0xe8, - 0x98, 0x1d, 0xa3, 0xf3, 0x5d, 0x67, 0x64, 0x1e, 0x58, 0x07, 0x86, 0x6d, 0x76, 0xa0, 0xd0, 0xec, 0x98, 0x9d, 0x1b, - 0xf3, 0xa0, 0x33, 0xb2, 0xb1, 0xb4, 0x65, 0x1d, 0x1e, 0x9a, 0x8e, 0x6d, 0x1d, 0x1e, 0x1a, 0x87, 0xd6, 0xd1, 0x91, - 0xe9, 0xb4, 0xad, 0xa3, 0xa3, 0xf3, 0xc3, 0x8e, 0xd5, 0x86, 0x77, 0xed, 0xf6, 0xa8, 0x6d, 0x39, 0x8e, 0x09, 0x7f, - 0x19, 0x1d, 0xab, 0x45, 0x3f, 0x1c, 0xc7, 0x6a, 0x3b, 0x86, 0x1d, 0x1c, 0xb6, 0xac, 0xa3, 0x17, 0x06, 0xfe, 0x8d, - 0xd5, 0x0c, 0xfc, 0x0b, 0xba, 0x31, 0x5e, 0x58, 0xad, 0x23, 0xfa, 0x85, 0x1d, 0xde, 0x1c, 0x74, 0xfe, 0xa6, 0xee, - 0x6f, 0x1c, 0x83, 0x43, 0x63, 0xe8, 0x1c, 0x5a, 0xed, 0xb6, 0x71, 0xe0, 0x58, 0x9d, 0xf6, 0xcc, 0x3c, 0x68, 0x59, - 0x47, 0xc7, 0x23, 0xd3, 0xb1, 0x8e, 0x8f, 0x0d, 0xdb, 0x6c, 0x5b, 0x2d, 0xc3, 0xb1, 0x0e, 0xda, 0xf8, 0xa3, 0x6d, - 0xb5, 0x6e, 0x8e, 0x5f, 0x58, 0x47, 0x87, 0xb3, 0x23, 0xeb, 0xe0, 0xc3, 0x41, 0xc7, 0x6a, 0xb5, 0x67, 0xed, 0x23, - 0xab, 0x75, 0x7c, 0x73, 0x64, 0x1d, 0xcc, 0xcc, 0xd6, 0xd1, 0xd6, 0x96, 0x4e, 0xcb, 0x82, 0x39, 0xc2, 0xd7, 0xf0, - 0xc2, 0xe0, 0x2f, 0xe0, 0xcf, 0x0c, 0xdb, 0xfe, 0x8e, 0xdd, 0x24, 0xf5, 0xa6, 0x2f, 0xac, 0xce, 0xf1, 0x88, 0xaa, - 0x43, 0x81, 0x29, 0x6a, 0x40, 0x93, 0x1b, 0x93, 0x3e, 0x8b, 0xdd, 0x99, 0xa2, 0x23, 0xf1, 0x87, 0x7f, 0xec, 0xc6, - 0x84, 0x0f, 0xd3, 0x77, 0xff, 0xa3, 0xfd, 0xe4, 0x4b, 0x7e, 0xb2, 0x3f, 0xa5, 0xad, 0x3f, 0xed, 0x7f, 0x75, 0x02, - 0x87, 0xbb, 0x3f, 0x30, 0x7e, 0xd9, 0xa4, 0x94, 0xfc, 0xc7, 0xfd, 0x4a, 0xc9, 0x97, 0xcb, 0x5d, 0x94, 0x92, 0xff, - 0xf8, 0xe2, 0x4a, 0xc9, 0x5f, 0xaa, 0xbe, 0x35, 0x6f, 0xaa, 0x59, 0xa8, 0xff, 0xb8, 0xae, 0x8a, 0x1c, 0x12, 0x4f, - 0xbb, 0xfc, 0x71, 0x79, 0x05, 0xf1, 0xe3, 0xdf, 0x44, 0xee, 0xcb, 0x65, 0xc9, 0xe0, 0x33, 0x02, 0x1c, 0xfb, 0x26, - 0x22, 0x1c, 0xfb, 0x61, 0xe9, 0x82, 0x95, 0x19, 0x67, 0x73, 0xfc, 0xb1, 0x39, 0xf3, 0x82, 0x49, 0xce, 0x22, 0x41, - 0x49, 0x0f, 0x8b, 0xc1, 0x6f, 0x1e, 0xc8, 0x33, 0xdc, 0x64, 0x96, 0xf3, 0x30, 0x01, 0x8b, 0x60, 0xb0, 0xe4, 0x98, - 0xc4, 0x59, 0xa5, 0xb1, 0x25, 0x22, 0xee, 0x5f, 0x73, 0x8f, 0xe2, 0x8d, 0xef, 0xd1, 0x00, 0xb8, 0xb9, 0x77, 0xa7, - 0xde, 0xaf, 0x02, 0x96, 0x75, 0xc2, 0x40, 0x1a, 0xb8, 0xfd, 0xa6, 0xf7, 0x65, 0x33, 0xdc, 0x8a, 0xe1, 0xf5, 0x66, - 0x48, 0x01, 0x92, 0x6a, 0x7b, 0xa7, 0x6c, 0xc6, 0x7b, 0xdf, 0x30, 0x1b, 0x3e, 0x5f, 0x6a, 0xbe, 0xc5, 0x86, 0x38, - 0xef, 0xb8, 0x3a, 0x55, 0xeb, 0x12, 0x9f, 0xd6, 0x3c, 0x21, 0xc5, 0x05, 0xb5, 0x30, 0x34, 0x2e, 0x38, 0x55, 0x5b, - 0x41, 0x7e, 0xc7, 0x96, 0xde, 0x95, 0xfa, 0x94, 0x8d, 0x93, 0x9f, 0xad, 0xf1, 0x5e, 0xe1, 0xff, 0x02, 0x9c, 0x28, - 0xe7, 0x78, 0x86, 0x91, 0x3c, 0xcf, 0x6b, 0xa9, 0x5f, 0x92, 0x46, 0x64, 0x33, 0x67, 0x5d, 0xe7, 0x45, 0x37, 0xba, - 0x25, 0x38, 0x6c, 0x2e, 0xb8, 0x20, 0xfc, 0x3c, 0x39, 0x01, 0x64, 0xe4, 0xa8, 0x81, 0x7e, 0x0e, 0xdb, 0x3a, 0x13, - 0xf5, 0x1e, 0xc1, 0x26, 0xe6, 0x9e, 0x80, 0x8a, 0x1c, 0xd2, 0x74, 0x3d, 0x09, 0x22, 0x2f, 0xed, 0x22, 0x9b, 0x26, - 0xb1, 0xbc, 0x2d, 0xf4, 0x58, 0xe8, 0x6d, 0x31, 0xa6, 0x93, 0x3b, 0xe6, 0x9d, 0xa0, 0xe7, 0xc3, 0x36, 0xfb, 0xbb, - 0xdc, 0xe1, 0x6c, 0x5d, 0x32, 0x47, 0x71, 0x0e, 0x8f, 0x0d, 0xe7, 0xc8, 0xb0, 0x8e, 0x0f, 0xf5, 0x4c, 0x1c, 0x38, - 0xb9, 0xcb, 0xd2, 0x84, 0x80, 0x03, 0x44, 0x0e, 0xa6, 0x1f, 0xfa, 0xa9, 0xef, 0x05, 0x19, 0xf0, 0xc3, 0xe5, 0x4b, - 0xca, 0x3f, 0x96, 0x49, 0x0a, 0x63, 0x14, 0x4c, 0x2f, 0x3a, 0x7f, 0x98, 0x43, 0x96, 0xae, 0x18, 0x0b, 0x37, 0x18, - 0xc6, 0x54, 0x7d, 0x49, 0x7e, 0x3b, 0xcb, 0xfa, 0x8c, 0xac, 0xd6, 0x86, 0x69, 0xc8, 0xf7, 0x87, 0x70, 0x7c, 0xc8, - 0x06, 0xc6, 0x77, 0x9b, 0x10, 0xee, 0xcf, 0xf7, 0x23, 0xdc, 0x94, 0xed, 0x82, 0x70, 0x7f, 0xfe, 0xe2, 0x08, 0xf7, - 0x3b, 0x19, 0xe1, 0x96, 0xfc, 0x07, 0x0b, 0x0d, 0xd3, 0x7b, 0x7c, 0xd6, 0xc0, 0x45, 0xf6, 0xb9, 0xba, 0x4f, 0x0c, - 0xbc, 0xaa, 0x17, 0xd9, 0x6b, 0xff, 0xbc, 0x94, 0x2d, 0xa8, 0x51, 0x00, 0x8a, 0x79, 0x1d, 0x7d, 0x74, 0x5d, 0xf6, - 0xc1, 0xd5, 0x4d, 0x84, 0x61, 0x80, 0x3e, 0xbf, 0x0f, 0xd3, 0xc0, 0x7a, 0xc7, 0xef, 0x91, 0xa0, 0xd0, 0x7d, 0x13, - 0xc5, 0x73, 0x0f, 0x53, 0x8c, 0xa8, 0x3a, 0xb8, 0xd3, 0xc1, 0x83, 0x0d, 0x81, 0x40, 0x46, 0x51, 0x38, 0xce, 0xb5, - 0x92, 0xcc, 0xbd, 0x24, 0x8e, 0x5b, 0xbd, 0x63, 0x5e, 0xac, 0x1a, 0xf4, 0x1a, 0x16, 0xf7, 0x59, 0xdb, 0x7e, 0xd6, - 0x3a, 0x78, 0x76, 0x64, 0xc3, 0xff, 0x0e, 0x6b, 0x67, 0x06, 0xaf, 0x38, 0x8f, 0xc2, 0x74, 0x56, 0xd4, 0xdc, 0x54, - 0x6d, 0xc5, 0xd8, 0xc7, 0xa2, 0xd6, 0x71, 0x73, 0xa5, 0xb1, 0x77, 0x57, 0xd4, 0x69, 0xac, 0x31, 0x8b, 0x96, 0x12, - 0x58, 0x0d, 0xd0, 0xf8, 0xe1, 0x12, 0xe4, 0xec, 0x52, 0x0d, 0xf9, 0x35, 0x1f, 0x6e, 0x31, 0x2e, 0xd6, 0xce, 0xae, - 0x44, 0x0e, 0x05, 0xb5, 0x27, 0xd2, 0xea, 0xdd, 0x3b, 0x83, 0x5c, 0x45, 0x69, 0x63, 0xce, 0x29, 0xcc, 0x6c, 0x08, - 0x19, 0xa7, 0x98, 0x58, 0x20, 0x8f, 0x16, 0x28, 0x8d, 0x97, 0xe1, 0x48, 0xc3, 0x9f, 0xde, 0x30, 0xd1, 0xfc, 0xfd, - 0xd8, 0xe2, 0x1f, 0xd6, 0x71, 0xd5, 0xbc, 0xbe, 0x5d, 0x24, 0x9d, 0x4f, 0xc4, 0xaa, 0x78, 0xcf, 0x52, 0x23, 0x46, - 0x3d, 0x36, 0x2d, 0xad, 0xe9, 0x7a, 0xcf, 0xf2, 0x86, 0xcf, 0x52, 0x23, 0x7c, 0x0e, 0xba, 0x4f, 0xd7, 0x7e, 0xf2, - 0x84, 0x6a, 0xed, 0xb9, 0x62, 0x58, 0xa7, 0xa3, 0x22, 0x33, 0x85, 0xe2, 0x4d, 0x23, 0x4a, 0x4e, 0xd1, 0x1d, 0x19, - 0xd1, 0xf3, 0xe7, 0x7d, 0xd7, 0xd1, 0x87, 0x31, 0xf3, 0x3e, 0x66, 0x22, 0xdc, 0x77, 0x88, 0xf9, 0x69, 0xcf, 0x77, - 0x33, 0x34, 0xd2, 0x1b, 0x5d, 0x69, 0x17, 0x70, 0x67, 0xb2, 0x85, 0x3b, 0x02, 0xc7, 0x5e, 0x90, 0xbb, 0x9e, 0x0c, - 0x0a, 0x3c, 0x61, 0xf0, 0x23, 0xea, 0xe4, 0xb7, 0xae, 0xb6, 0x65, 0x5b, 0xb6, 0x9a, 0x37, 0x9c, 0xf8, 0x53, 0x77, - 0x1d, 0xa5, 0x5e, 0x77, 0xcf, 0x31, 0x82, 0x68, 0x0a, 0x7e, 0x74, 0xa9, 0x9f, 0x06, 0xac, 0xab, 0xaa, 0xe0, 0x50, - 0x37, 0xa7, 0x7b, 0x79, 0xc6, 0xbd, 0x1b, 0xbc, 0x18, 0xd2, 0x96, 0xc7, 0x77, 0xc2, 0x15, 0x17, 0x83, 0xa5, 0xff, - 0x00, 0xc4, 0x50, 0x53, 0x35, 0x90, 0x0d, 0xb0, 0x38, 0x31, 0x65, 0x6f, 0xa1, 0xae, 0x02, 0x6d, 0x74, 0x95, 0x0f, - 0x62, 0x12, 0x7b, 0x73, 0xc8, 0xab, 0xbb, 0xce, 0x0c, 0x8e, 0x69, 0x55, 0x8e, 0x6a, 0x15, 0xe7, 0xc5, 0x91, 0xa1, - 0xb4, 0x1c, 0x43, 0xb1, 0x01, 0xdd, 0xaa, 0x99, 0xb1, 0xce, 0xae, 0x7a, 0xf7, 0x19, 0x3c, 0x10, 0x7e, 0x79, 0x44, - 0xe3, 0x20, 0x53, 0x07, 0xae, 0x4a, 0x4a, 0x29, 0x49, 0x8e, 0x26, 0x65, 0xd0, 0xf4, 0x49, 0xe9, 0x79, 0xc1, 0x6e, - 0x53, 0x1d, 0x34, 0x47, 0xa2, 0x8a, 0xaf, 0xaf, 0xd1, 0x61, 0xd8, 0x0f, 0x15, 0xff, 0xd3, 0x27, 0xcd, 0x07, 0x67, - 0x26, 0x57, 0x9a, 0x1f, 0x78, 0xd6, 0x4b, 0x13, 0xe6, 0x17, 0x6a, 0x7a, 0x9c, 0x2c, 0xf0, 0x34, 0x84, 0x7f, 0x8b, - 0x62, 0xf1, 0x83, 0x9b, 0x49, 0x58, 0x81, 0x17, 0x4e, 0x01, 0xa5, 0x79, 0xe1, 0xb4, 0x66, 0x8e, 0x45, 0x3e, 0xcf, - 0x95, 0xd2, 0xa2, 0xab, 0xc2, 0x54, 0x2a, 0x79, 0x79, 0x77, 0xe1, 0x4d, 0x7f, 0xf4, 0xe6, 0x4c, 0x53, 0x81, 0xca, - 0xa1, 0x8b, 0x6e, 0xa1, 0xc9, 0x7d, 0xee, 0x3e, 0x3d, 0x99, 0xb3, 0xd4, 0x23, 0x35, 0x10, 0x5c, 0x7e, 0x81, 0x1d, - 0x50, 0x38, 0xa1, 0xe1, 0x01, 0x2f, 0x5c, 0xca, 0xa5, 0x45, 0x74, 0xc2, 0x50, 0x38, 0x9d, 0x32, 0xd1, 0xe2, 0xd3, - 0x75, 0x0c, 0x72, 0x38, 0x18, 0x79, 0x98, 0x4f, 0xc7, 0x0d, 0x23, 0xb5, 0xff, 0x34, 0xf7, 0xcd, 0xdc, 0xb4, 0x08, - 0x81, 0x1f, 0x7e, 0xbc, 0x8c, 0x59, 0xf0, 0x4f, 0xf7, 0x29, 0x10, 0xee, 0xa7, 0x57, 0xaa, 0xde, 0x4b, 0xad, 0x59, - 0xcc, 0x26, 0xee, 0x53, 0xb8, 0x90, 0x76, 0xd1, 0x3c, 0x16, 0xb8, 0xf6, 0xe7, 0xb7, 0xf3, 0xc0, 0xc0, 0xeb, 0x3d, - 0xc1, 0xa2, 0xb6, 0x5b, 0x45, 0x5c, 0xf3, 0xf6, 0x4e, 0x97, 0xfa, 0x3e, 0xbf, 0xad, 0xc3, 0x0d, 0x70, 0x5d, 0xba, - 0x63, 0x3b, 0x3d, 0xbc, 0x3f, 0x0f, 0x03, 0x6f, 0xf4, 0xb1, 0x47, 0x6f, 0x4a, 0x0f, 0x26, 0x50, 0xeb, 0x91, 0xb7, - 0xe8, 0x22, 0x79, 0x95, 0x0b, 0xc1, 0x7b, 0x9a, 0x4a, 0x73, 0xce, 0xae, 0x71, 0x2f, 0xe3, 0x56, 0x5e, 0xe3, 0x97, - 0xf1, 0x53, 0xab, 0x99, 0x9f, 0x32, 0xf1, 0x29, 0x7c, 0xc8, 0x32, 0x71, 0x51, 0xa7, 0x2b, 0x2a, 0x5e, 0xac, 0xad, - 0xb6, 0xe2, 0x74, 0xbe, 0x3b, 0xbc, 0x71, 0xec, 0x59, 0xcb, 0xb1, 0x3a, 0x1f, 0x9c, 0xce, 0xac, 0x6d, 0x1d, 0x07, - 0x66, 0xdb, 0x3a, 0x86, 0x3f, 0x1f, 0x8e, 0xad, 0xce, 0xcc, 0x6c, 0x59, 0x07, 0x1f, 0x9c, 0x56, 0x60, 0x76, 0xac, - 0x63, 0xf8, 0x73, 0x4e, 0xad, 0xe0, 0x02, 0x44, 0xf7, 0x9d, 0xa7, 0x25, 0x2c, 0x20, 0xfd, 0xce, 0x75, 0xb2, 0x46, - 0x89, 0xbc, 0x35, 0xe8, 0x75, 0x17, 0x18, 0x45, 0x42, 0xe4, 0xaf, 0x09, 0x7b, 0x5a, 0xe8, 0x32, 0x4a, 0x2a, 0x2b, - 0xcc, 0xdb, 0x84, 0x1f, 0xba, 0xc8, 0xe6, 0xd9, 0x78, 0x8c, 0x78, 0x9b, 0xe6, 0x0c, 0x96, 0xba, 0xc8, 0x08, 0x8c, - 0xcf, 0x3f, 0x2f, 0x30, 0xfe, 0xba, 0x48, 0xc3, 0x2c, 0x61, 0x25, 0xf0, 0x3d, 0xb7, 0xc2, 0x68, 0x85, 0xb6, 0x15, - 0xf7, 0x01, 0x8e, 0xde, 0xfc, 0x4c, 0x58, 0x76, 0x7d, 0xd9, 0xbe, 0xa5, 0xcc, 0xd7, 0x9f, 0xd5, 0x0f, 0x0f, 0x0b, - 0x21, 0x67, 0x9f, 0x1c, 0xfb, 0x71, 0x0e, 0x9e, 0x84, 0xa2, 0x9d, 0xe6, 0xd4, 0x9f, 0xba, 0x41, 0xc1, 0x91, 0x58, - 0x7c, 0xe3, 0x05, 0x92, 0x21, 0x9b, 0xd4, 0x72, 0x2f, 0xc7, 0xfc, 0x4f, 0x9e, 0x14, 0xc0, 0x99, 0x15, 0xb8, 0x4f, - 0x9c, 0x43, 0x20, 0xbb, 0x87, 0xac, 0xbd, 0xd5, 0xa6, 0x92, 0x6e, 0x3a, 0xdb, 0x7c, 0xab, 0x8b, 0x4c, 0x47, 0xc2, - 0x6e, 0x4a, 0x58, 0x6c, 0x6c, 0x34, 0xec, 0xac, 0xd9, 0x6b, 0x40, 0xaa, 0xb8, 0xca, 0x55, 0x47, 0xd5, 0x7b, 0xa1, - 0x30, 0x3f, 0x08, 0xb7, 0x24, 0x79, 0xe3, 0x77, 0x31, 0x15, 0xa6, 0x66, 0xcb, 0x38, 0xee, 0x71, 0x10, 0xff, 0x4f, - 0x0f, 0x02, 0x9d, 0x35, 0xc1, 0x5e, 0xa2, 0x72, 0x5a, 0x4b, 0xce, 0x7b, 0x39, 0x5d, 0x25, 0x82, 0xca, 0x92, 0x53, - 0x15, 0x8a, 0xd4, 0xae, 0x8a, 0x8e, 0x61, 0x6a, 0x6e, 0x2c, 0x9a, 0x53, 0x8b, 0xa2, 0xc0, 0xf0, 0x31, 0xa1, 0xa6, - 0x70, 0x1c, 0xd5, 0x9f, 0x3c, 0xd9, 0x48, 0x84, 0xc8, 0x38, 0x27, 0x61, 0xa9, 0x60, 0xd0, 0x35, 0x55, 0xc6, 0x6f, - 0xaa, 0x8c, 0x62, 0xf2, 0x7e, 0x11, 0x6b, 0x08, 0x1b, 0x57, 0xda, 0x7b, 0xf8, 0x73, 0xc8, 0xbc, 0xd4, 0xe2, 0xca, - 0x52, 0x4d, 0x22, 0xee, 0x86, 0xc3, 0xda, 0x60, 0xdd, 0xca, 0xd3, 0x34, 0xf0, 0x34, 0x28, 0x8f, 0xd7, 0x7f, 0x5e, - 0xf2, 0xa8, 0x0e, 0xd0, 0xc7, 0x27, 0xbb, 0x88, 0xc3, 0xf9, 0x36, 0xf5, 0x28, 0x0e, 0x9a, 0x4c, 0x72, 0xa3, 0xd4, - 0x23, 0x7b, 0x0e, 0x1f, 0x43, 0xd7, 0x34, 0x47, 0xe4, 0x92, 0x22, 0x3f, 0xf4, 0xdf, 0x5e, 0x7c, 0xa3, 0xf0, 0xfd, - 0x4f, 0xd6, 0x02, 0x78, 0x91, 0xa1, 0x78, 0x33, 0x2e, 0xc5, 0x9b, 0x51, 0x78, 0x26, 0x63, 0xc8, 0xb9, 0x9a, 0xed, - 0xd3, 0x0c, 0xa2, 0x00, 0x9a, 0x6c, 0x28, 0xe6, 0xcb, 0x20, 0xf5, 0x17, 0x5e, 0x9c, 0xee, 0x63, 0xb0, 0x19, 0x0c, - 0x5e, 0xb3, 0x29, 0x1e, 0x04, 0x99, 0x61, 0x88, 0xec, 0x20, 0x69, 0x28, 0xec, 0x30, 0x26, 0x7e, 0x90, 0x9b, 0x61, - 0x88, 0x0f, 0x78, 0xa3, 0x11, 0x5b, 0xa4, 0x6e, 0x29, 0xa8, 0x4d, 0x34, 0x4a, 0x59, 0x6a, 0x26, 0x69, 0xcc, 0xbc, - 0xb9, 0x9a, 0x07, 0xb9, 0xaa, 0xf7, 0x97, 0x2c, 0x87, 0x10, 0xa5, 0x47, 0x84, 0xdb, 0xa2, 0x01, 0x82, 0x41, 0x04, - 0x80, 0x08, 0x41, 0x66, 0x68, 0x0a, 0xcf, 0xa3, 0x69, 0x65, 0x47, 0x15, 0x9c, 0xcb, 0x29, 0x26, 0x09, 0xa3, 0x9b, - 0x0c, 0x48, 0x8b, 0x47, 0x51, 0x70, 0xcd, 0x63, 0x58, 0xe4, 0xd9, 0x66, 0xd4, 0xfe, 0x09, 0xbf, 0xde, 0x2a, 0x18, - 0xbe, 0x45, 0x3d, 0xb4, 0x21, 0x0d, 0xda, 0xa6, 0xe8, 0x16, 0xfb, 0xbc, 0x32, 0x90, 0x26, 0xea, 0x19, 0x33, 0x59, - 0x12, 0x2c, 0x17, 0xc0, 0x08, 0x95, 0x0c, 0x66, 0x66, 0x4e, 0x3f, 0x77, 0xa7, 0x44, 0xa8, 0x90, 0x57, 0xfa, 0xf4, - 0xe9, 0xfd, 0xe0, 0xdf, 0xff, 0x82, 0x74, 0x9b, 0x33, 0x47, 0xc4, 0x94, 0xb8, 0x94, 0x6b, 0x71, 0xee, 0xd3, 0x18, - 0xa0, 0xb1, 0x14, 0x1b, 0x8b, 0x68, 0x7f, 0x62, 0x6b, 0x65, 0x83, 0x2b, 0x11, 0xa7, 0x0e, 0x12, 0xf5, 0xea, 0x22, - 0xf2, 0xc5, 0x00, 0x96, 0x77, 0x20, 0x62, 0xa2, 0x28, 0x7f, 0xbf, 0x7d, 0x79, 0xac, 0x14, 0xe1, 0x13, 0x9b, 0x2c, - 0x7a, 0x68, 0x0f, 0xf5, 0x4f, 0x3c, 0x05, 0x99, 0x16, 0x64, 0x3f, 0x92, 0xee, 0x3e, 0x0c, 0x73, 0x16, 0xcd, 0x99, - 0xe5, 0x47, 0xfb, 0x2b, 0x36, 0x34, 0xbd, 0x85, 0x4f, 0x76, 0x39, 0x28, 0x77, 0x53, 0x88, 0xf3, 0xcb, 0xcd, 0x5d, - 0x88, 0xbf, 0xce, 0x8a, 0xa9, 0x8c, 0x2a, 0x81, 0xd0, 0x5a, 0x85, 0x1e, 0xf0, 0x80, 0x07, 0x19, 0x13, 0x35, 0xfb, - 0x27, 0xfb, 0x5e, 0xbf, 0x9c, 0x79, 0xc6, 0x12, 0x19, 0x54, 0xcb, 0x44, 0xe0, 0x94, 0x12, 0xc8, 0x88, 0x5c, 0x31, - 0xc5, 0x83, 0x19, 0x4d, 0x26, 0x72, 0xb6, 0x18, 0xab, 0x0c, 0x5e, 0x3e, 0x69, 0xc5, 0x96, 0x8e, 0x16, 0xf4, 0xa5, - 0xfa, 0x27, 0xf2, 0x9f, 0x6a, 0x17, 0xd3, 0x44, 0xc1, 0x98, 0xe1, 0xb8, 0xd7, 0xb2, 0xce, 0xe4, 0x33, 0xf6, 0x88, - 0x2a, 0x71, 0x3c, 0x52, 0xcd, 0x71, 0xb8, 0x81, 0x73, 0xd9, 0x73, 0x5d, 0x42, 0x73, 0x55, 0x6c, 0x07, 0x93, 0xd8, - 0x90, 0x4d, 0x16, 0x06, 0x9b, 0x42, 0x43, 0x93, 0xdc, 0x65, 0xb1, 0x51, 0x75, 0x38, 0x75, 0x18, 0xf7, 0x3d, 0xb1, - 0xfd, 0x4a, 0x1b, 0x14, 0x36, 0x1e, 0x5f, 0x77, 0xc0, 0xef, 0xa2, 0x9f, 0x0a, 0x9a, 0x57, 0xbe, 0x26, 0x8c, 0x6e, - 0x06, 0xde, 0x5d, 0x24, 0x99, 0x31, 0xf1, 0x88, 0x26, 0xe7, 0x58, 0x7a, 0x21, 0x3c, 0x89, 0x6b, 0x07, 0x0d, 0x49, - 0x18, 0x64, 0xdd, 0xac, 0x1f, 0xb6, 0x82, 0xfe, 0x06, 0xec, 0xbe, 0xb3, 0x26, 0xd7, 0x2d, 0x0f, 0x06, 0x91, 0x67, - 0x56, 0x9c, 0xc3, 0xd2, 0x4b, 0x44, 0x0b, 0xd9, 0xc9, 0x3e, 0x8c, 0x0f, 0xa2, 0xb0, 0x94, 0x18, 0x27, 0x5f, 0x87, - 0x50, 0x2f, 0x5e, 0x42, 0xa6, 0x58, 0xdf, 0x8f, 0x05, 0xcf, 0x87, 0x17, 0x4b, 0x29, 0x97, 0xbc, 0x54, 0xa5, 0xce, - 0xcb, 0xd8, 0xcd, 0x4c, 0xe0, 0xfd, 0x29, 0x6a, 0x3f, 0x2c, 0x31, 0x3f, 0x6d, 0xd6, 0x4b, 0x99, 0x08, 0x72, 0x70, - 0x9e, 0x6e, 0x88, 0x83, 0xb0, 0xa9, 0x0a, 0xf1, 0xb3, 0x5b, 0x2a, 0x14, 0xfb, 0x78, 0x5b, 0xad, 0x82, 0x73, 0x2a, - 0xaa, 0x79, 0x9a, 0xfa, 0x08, 0x77, 0x7c, 0xad, 0x36, 0x96, 0x62, 0x74, 0x86, 0xd4, 0x85, 0xaa, 0x42, 0x16, 0xef, - 0x2d, 0x16, 0x54, 0x59, 0xef, 0x9d, 0xec, 0xd3, 0xb5, 0xb4, 0x4f, 0x3b, 0xac, 0x7f, 0x02, 0xa6, 0xdc, 0xb4, 0xe8, - 0xde, 0x62, 0xc1, 0x97, 0x94, 0x7e, 0xd1, 0x9b, 0xfd, 0x59, 0x3a, 0x0f, 0xfa, 0xff, 0x0b, 0x3a, 0x5f, 0xcc, 0x86, - 0x37, 0x7a, 0x03, 0x00}; + 0x2a, 0x34, 0x7d, 0xe4, 0x37, 0x22, 0x0d, 0x08, 0x0c, 0xda, 0x65, 0x6f, 0xab, 0xd2, 0x82, 0x6c, 0x3a, 0xb6, 0xd2, + 0xe4, 0xa0, 0x83, 0x03, 0xc4, 0xf8, 0x15, 0x62, 0x21, 0x42, 0x3b, 0xbc, 0x0e, 0x3e, 0x64, 0x6a, 0xce, 0xfb, 0xe1, + 0xf6, 0xeb, 0x9f, 0xec, 0x43, 0x83, 0x7e, 0x45, 0xe9, 0x76, 0x8f, 0x5f, 0x26, 0xb0, 0x12, 0xc9, 0xca, 0xb0, 0x92, + 0x95, 0xf2, 0x6c, 0x2d, 0xe2, 0x63, 0xa7, 0xde, 0xc2, 0x04, 0x2d, 0x0f, 0xe2, 0x5e, 0x8e, 0xf1, 0xa4, 0x50, 0xdc, + 0xbd, 0x65, 0x02, 0xb8, 0x11, 0xe5, 0x28, 0x88, 0x7f, 0x7a, 0xa3, 0x65, 0x9c, 0x44, 0x71, 0x77, 0x11, 0xf9, 0x61, + 0xca, 0xe2, 0x8c, 0x04, 0x2b, 0x38, 0x3f, 0x62, 0x7a, 0xae, 0xd6, 0xd1, 0xc2, 0x1b, 0xf9, 0xe9, 0x5d, 0xd7, 0xe6, + 0x2c, 0x85, 0xdd, 0xe3, 0xdc, 0x81, 0x5d, 0x5b, 0xbf, 0xcb, 0x67, 0xf3, 0x39, 0x32, 0x7e, 0xf1, 0x26, 0x3b, 0x23, + 0x6f, 0xf3, 0x9e, 0xf4, 0x96, 0x22, 0x84, 0x03, 0xfb, 0xe1, 0xc5, 0xe6, 0x14, 0xb0, 0x3c, 0x2c, 0xb5, 0x3d, 0x66, + 0x53, 0x03, 0xb1, 0x36, 0x98, 0x19, 0x8a, 0x3f, 0xd6, 0xa1, 0xae, 0xd8, 0xf5, 0xc5, 0xc0, 0xf1, 0xe8, 0xbb, 0x40, + 0xd6, 0xf5, 0x26, 0x29, 0x8b, 0x8d, 0x5d, 0x6a, 0x0e, 0xd9, 0x24, 0x8a, 0x19, 0x65, 0x93, 0x73, 0x3a, 0x8b, 0xdb, + 0xdd, 0xbb, 0xdf, 0x3e, 0xfc, 0xfa, 0x7e, 0xc2, 0x28, 0xd5, 0x44, 0x67, 0xfa, 0x3d, 0xbd, 0x6d, 0xd2, 0x33, 0x60, + 0x0d, 0x69, 0xe6, 0x47, 0x24, 0x05, 0x81, 0x48, 0x60, 0xb5, 0x49, 0x3b, 0x16, 0x11, 0xa7, 0x79, 0x31, 0x0b, 0xbc, + 0xd4, 0xbf, 0x11, 0x3c, 0x63, 0xfb, 0x68, 0x71, 0x2b, 0xd6, 0x18, 0x09, 0xde, 0x03, 0x16, 0xa9, 0x02, 0x8a, 0x58, + 0xa4, 0x6a, 0x31, 0x2e, 0x52, 0x6f, 0x63, 0x34, 0x22, 0x8e, 0x75, 0x85, 0xd2, 0x1f, 0x2e, 0x6e, 0x65, 0x12, 0x5d, + 0x34, 0xcb, 0x29, 0x75, 0x35, 0x01, 0xc9, 0xdc, 0x1f, 0x8f, 0x03, 0x96, 0x95, 0x16, 0xba, 0xbc, 0x96, 0xd2, 0xe4, + 0xe4, 0xf3, 0xe0, 0x0d, 0x93, 0x28, 0x58, 0xa6, 0xac, 0x7e, 0xba, 0x84, 0x44, 0xb7, 0x98, 0x1c, 0xfc, 0x5d, 0x86, + 0xf5, 0x10, 0xd8, 0x6d, 0xd8, 0x26, 0x76, 0x0f, 0xf2, 0x0d, 0x9a, 0xed, 0x32, 0xe8, 0xf0, 0x2a, 0x07, 0xda, 0xa8, + 0x19, 0x88, 0x01, 0x64, 0x89, 0xb0, 0xb7, 0x62, 0x39, 0xbc, 0x2c, 0xcf, 0xb9, 0x96, 0x17, 0x65, 0xe5, 0xc1, 0xfc, + 0x3e, 0x67, 0xec, 0x45, 0xfd, 0x19, 0x7b, 0x21, 0xce, 0xd8, 0xf6, 0x9d, 0xf9, 0x68, 0xe2, 0xc0, 0x7f, 0xbd, 0x62, + 0x40, 0x5d, 0x5b, 0x69, 0x2f, 0x6e, 0x15, 0x67, 0x71, 0xab, 0x98, 0xad, 0xc5, 0xad, 0x82, 0x5d, 0xa3, 0x7b, 0x8b, + 0x61, 0xb5, 0x74, 0xc3, 0x56, 0xa0, 0x10, 0xfe, 0xd8, 0xa5, 0x57, 0xce, 0x01, 0xbc, 0x83, 0x56, 0x87, 0x9b, 0xef, + 0x5a, 0xdb, 0x8f, 0x3a, 0x9d, 0x25, 0x81, 0xb4, 0x75, 0x2b, 0xf5, 0x86, 0x43, 0x10, 0x65, 0x46, 0xa3, 0x65, 0xf2, + 0x0f, 0x0e, 0x3f, 0x9f, 0xc4, 0xad, 0x88, 0xa0, 0xd2, 0x8f, 0x68, 0x0a, 0x8a, 0xc2, 0x1b, 0x26, 0x7a, 0x58, 0xe7, + 0xeb, 0xd4, 0xa5, 0xe4, 0x88, 0x2d, 0xeb, 0xa0, 0x66, 0x93, 0xd7, 0x4f, 0xf4, 0xef, 0xb6, 0x4a, 0xcd, 0x28, 0xe6, + 0x33, 0xa6, 0x65, 0xeb, 0x74, 0x3c, 0x7c, 0x36, 0xf8, 0x6a, 0xda, 0x9d, 0x7a, 0x70, 0x2f, 0xc5, 0x97, 0xae, 0x04, + 0x51, 0xe1, 0x74, 0x8b, 0x87, 0xe2, 0xd8, 0xde, 0x6b, 0xd3, 0x1e, 0xd9, 0xe8, 0x75, 0x0b, 0x41, 0x28, 0xea, 0xee, + 0x88, 0xe5, 0x1f, 0xbd, 0x38, 0x80, 0xff, 0x88, 0xab, 0xff, 0x6b, 0x5a, 0xc7, 0xa8, 0xbf, 0x4e, 0x4b, 0x8c, 0x3a, + 0xb1, 0x4a, 0xc8, 0x88, 0xef, 0x5e, 0x7f, 0x32, 0x79, 0x58, 0x83, 0x9d, 0x6b, 0x93, 0x67, 0x58, 0xb5, 0xf6, 0xcb, + 0x28, 0x0a, 0x98, 0x17, 0x6e, 0x56, 0x17, 0xd3, 0x43, 0x6e, 0xfe, 0xa9, 0x0b, 0x8d, 0xc4, 0x3d, 0x82, 0x9c, 0x12, + 0x54, 0x6c, 0x43, 0x57, 0x89, 0xf3, 0xa6, 0xab, 0xc4, 0xbb, 0xfb, 0xaf, 0x12, 0x3f, 0xec, 0x74, 0x95, 0x78, 0xf7, + 0xc5, 0xaf, 0x12, 0xe7, 0x9b, 0x57, 0x89, 0xf3, 0x48, 0xb8, 0x03, 0x1b, 0x6f, 0x96, 0xfc, 0xe7, 0x07, 0xb2, 0xf7, + 0x7d, 0x17, 0xb9, 0x87, 0x36, 0x25, 0x3c, 0xbc, 0xf8, 0xcd, 0x17, 0x0b, 0xdc, 0x88, 0xef, 0xd0, 0x3b, 0xae, 0xb8, + 0x5a, 0x70, 0xcc, 0x8e, 0xdf, 0x91, 0x8a, 0x83, 0x28, 0x9c, 0xfe, 0x0c, 0xf6, 0xde, 0x20, 0x0e, 0x8c, 0xa5, 0x17, + 0x7e, 0xf2, 0x73, 0xb4, 0x58, 0x2e, 0x50, 0x51, 0xf5, 0xc1, 0x4f, 0xfc, 0x61, 0xc0, 0xf2, 0x08, 0x93, 0xa4, 0x75, + 0xe5, 0xb2, 0x75, 0x50, 0xbc, 0x8a, 0x9f, 0xde, 0xad, 0xf8, 0x89, 0x2e, 0xb6, 0xfc, 0x37, 0xb9, 0x09, 0xaa, 0xf5, + 0x17, 0x11, 0x61, 0x21, 0x26, 0x01, 0xfd, 0xf0, 0xcb, 0xc8, 0xb9, 0x88, 0xe5, 0x55, 0x1a, 0xa5, 0x70, 0xdf, 0x68, + 0xec, 0x87, 0x55, 0xfb, 0x79, 0xb3, 0xd4, 0x8d, 0x3c, 0x01, 0xc7, 0xa6, 0x38, 0x7f, 0x1e, 0x2d, 0x13, 0x36, 0x8e, + 0x56, 0xa1, 0x6a, 0x84, 0x5c, 0xaf, 0x1a, 0xa1, 0x4c, 0x3d, 0x6f, 0x53, 0x56, 0x38, 0xaa, 0xd6, 0x02, 0xe6, 0xd0, + 0x24, 0x0d, 0xb6, 0x89, 0x43, 0x54, 0x45, 0xc8, 0xa6, 0xde, 0x9e, 0xa6, 0x45, 0xee, 0xc3, 0x5a, 0x0a, 0xcf, 0x93, + 0xc8, 0xe2, 0x52, 0xe1, 0x44, 0x0b, 0x85, 0x70, 0x51, 0x44, 0xc1, 0xae, 0x59, 0x38, 0xfe, 0x86, 0x22, 0x44, 0x16, + 0x6f, 0x41, 0x57, 0x95, 0x2d, 0xf9, 0x7a, 0xf0, 0x98, 0xd0, 0xf4, 0xf8, 0x4a, 0x9a, 0xc6, 0xb7, 0x37, 0x2c, 0x0e, + 0xbc, 0x3b, 0x4d, 0xcf, 0xa2, 0xf0, 0x47, 0x98, 0x80, 0xd7, 0xd1, 0x2a, 0x94, 0x2b, 0x60, 0xaa, 0xf6, 0x9a, 0xbd, + 0x54, 0x1b, 0xbd, 0x1c, 0x62, 0x76, 0x48, 0x10, 0xf8, 0xd6, 0xc2, 0x9b, 0xb2, 0xff, 0x32, 0xe8, 0xdf, 0xff, 0xd6, + 0x33, 0xe3, 0x5d, 0x94, 0x7f, 0xe8, 0x97, 0xc5, 0x0e, 0x9f, 0x79, 0xf2, 0x64, 0xaf, 0x79, 0xd8, 0xda, 0x28, 0x60, + 0x5e, 0x2c, 0xa0, 0xa8, 0x69, 0xad, 0x37, 0x9e, 0x02, 0x80, 0xe2, 0x22, 0x5a, 0x8e, 0x66, 0xe8, 0xb7, 0xfb, 0xe5, + 0xc6, 0x9b, 0x42, 0x9f, 0x2c, 0xb9, 0xb4, 0xaf, 0xf2, 0xa1, 0x57, 0x8a, 0x8a, 0x59, 0xc0, 0xef, 0x9f, 0x41, 0xfa, + 0xad, 0x7f, 0xe3, 0x34, 0x6c, 0xee, 0x9a, 0x3c, 0xe4, 0xd7, 0x83, 0x36, 0x6f, 0xcf, 0x87, 0xa8, 0x3c, 0x14, 0xd8, + 0x5a, 0x28, 0xe9, 0xea, 0x91, 0x4c, 0x56, 0x9d, 0x34, 0x39, 0x89, 0x4c, 0x53, 0x7e, 0x1c, 0xf1, 0x15, 0x66, 0x95, + 0xac, 0x46, 0x0c, 0xc6, 0xb1, 0x55, 0x05, 0xc9, 0x70, 0x6f, 0x0a, 0x86, 0xe8, 0xab, 0xfa, 0x6e, 0xee, 0x87, 0x06, + 0xe6, 0x80, 0xdd, 0x7c, 0xe3, 0xdd, 0x42, 0x16, 0x44, 0x40, 0x6e, 0xd5, 0x57, 0x50, 0x68, 0xc8, 0xd1, 0x82, 0xbc, + 0xf1, 0x58, 0x53, 0x6b, 0x67, 0x42, 0x68, 0x03, 0x07, 0x5f, 0x29, 0x8a, 0xa2, 0xe4, 0xd7, 0x08, 0x25, 0xbf, 0x47, + 0x60, 0x39, 0x5e, 0x07, 0x40, 0x5b, 0x92, 0x2d, 0x6e, 0xa9, 0x04, 0x6e, 0x06, 0x68, 0x3f, 0x2d, 0x0a, 0x78, 0xa2, + 0x1f, 0x30, 0x6e, 0xa1, 0x02, 0x71, 0xa1, 0x07, 0xd5, 0xb7, 0x17, 0x43, 0x3e, 0xc0, 0xae, 0x82, 0x17, 0x76, 0x7c, + 0xcb, 0x25, 0xc1, 0x8a, 0x4d, 0x8f, 0x83, 0x1e, 0xab, 0xcf, 0x08, 0x13, 0x4a, 0x58, 0x10, 0xb4, 0x0e, 0x95, 0x04, + 0x8f, 0x06, 0xab, 0xc1, 0x8d, 0x78, 0x2f, 0xba, 0x4d, 0xe7, 0x2c, 0x5c, 0xaa, 0x06, 0x58, 0x9d, 0x60, 0x86, 0x1e, + 0xa8, 0xf3, 0x9a, 0x98, 0x2d, 0xc0, 0x36, 0xf5, 0x2d, 0x67, 0x44, 0x0b, 0x85, 0xa9, 0x8a, 0x67, 0x8c, 0x78, 0x00, + 0x9c, 0x84, 0xe3, 0xb6, 0x2a, 0x85, 0xe0, 0x4b, 0x1a, 0x95, 0xb1, 0x39, 0x0f, 0x79, 0x85, 0x9c, 0x02, 0xd9, 0x88, + 0x71, 0x71, 0x91, 0x98, 0x76, 0xcd, 0xab, 0x2e, 0x5a, 0xae, 0x91, 0xf1, 0x2a, 0x82, 0xa2, 0x58, 0xdf, 0xec, 0x86, + 0xc3, 0x09, 0x69, 0x09, 0x1a, 0xfb, 0x19, 0x6d, 0xf4, 0xd3, 0x30, 0xe8, 0x8f, 0xec, 0x8e, 0x08, 0x09, 0x4d, 0xd5, + 0x47, 0x76, 0x07, 0xc6, 0xe1, 0x67, 0x20, 0x4d, 0x51, 0xb7, 0xa0, 0x6b, 0x03, 0x12, 0xfd, 0x8e, 0x20, 0x55, 0xc5, + 0x96, 0x03, 0x64, 0x67, 0x5b, 0xb0, 0x38, 0x85, 0x23, 0x35, 0x92, 0x9e, 0x38, 0xc4, 0x3c, 0x62, 0x81, 0x56, 0x3b, + 0xc7, 0x66, 0xcd, 0xd1, 0xd0, 0x9f, 0x39, 0xb6, 0xbd, 0xbf, 0x51, 0x1f, 0x04, 0xd9, 0x75, 0xb5, 0x75, 0x23, 0x75, + 0x1d, 0xdb, 0xf4, 0x9f, 0x59, 0xad, 0xde, 0x06, 0x8d, 0x96, 0x32, 0x49, 0x0d, 0x50, 0xfc, 0xd5, 0x7f, 0xbc, 0xd6, + 0x36, 0x0e, 0xa4, 0x5e, 0x8d, 0x00, 0x80, 0xb0, 0x65, 0x5c, 0xfe, 0x35, 0xd8, 0x24, 0xfd, 0x94, 0xc7, 0x8a, 0xb2, + 0x9a, 0x0f, 0x20, 0x17, 0xa2, 0x06, 0xc7, 0xe8, 0x4f, 0xca, 0x73, 0x45, 0xa3, 0xe3, 0xa3, 0xeb, 0x83, 0x9e, 0xc0, + 0x28, 0x22, 0x44, 0x8e, 0xdc, 0x41, 0xe5, 0x8b, 0x49, 0x15, 0xc3, 0xf1, 0xac, 0x6b, 0xac, 0xd0, 0xe8, 0x6d, 0xe5, + 0x16, 0xb0, 0xff, 0x06, 0xf2, 0x69, 0x0d, 0x21, 0xc6, 0x23, 0xd4, 0x80, 0xcc, 0xa9, 0xf7, 0x76, 0x08, 0xe1, 0x79, + 0xe5, 0xee, 0xca, 0x44, 0x72, 0xf7, 0xce, 0x90, 0xe8, 0xa0, 0x0e, 0x2d, 0xef, 0xaf, 0x9e, 0xdc, 0x3d, 0xb0, 0x4b, + 0x16, 0x8e, 0xcb, 0x1d, 0x56, 0xe8, 0xd7, 0xee, 0xdd, 0x95, 0x30, 0x0a, 0xa4, 0x14, 0x8e, 0x6a, 0x30, 0x4a, 0x16, + 0x85, 0xb8, 0xf9, 0xe9, 0xb8, 0xf9, 0x3b, 0x71, 0x31, 0xd8, 0x80, 0xf2, 0x81, 0xe4, 0xcd, 0x24, 0xa1, 0x38, 0xe4, + 0xad, 0xc4, 0x08, 0x5a, 0x9a, 0x60, 0x44, 0x1b, 0x77, 0x62, 0x2a, 0xdc, 0x15, 0x8b, 0x36, 0x3e, 0xcf, 0x44, 0xb5, + 0xab, 0xd4, 0xda, 0xbf, 0x5f, 0x6a, 0x9d, 0xde, 0x27, 0xb5, 0xa6, 0xe8, 0x30, 0xdc, 0x1e, 0x54, 0x44, 0xc9, 0x11, + 0xcc, 0xb9, 0x1c, 0x67, 0xa8, 0x24, 0xea, 0xc6, 0x60, 0x32, 0x35, 0x56, 0xa4, 0xd4, 0x1b, 0x39, 0x20, 0xa2, 0xf8, + 0x5b, 0xba, 0xa0, 0x08, 0x85, 0xba, 0x2c, 0x1b, 0x3f, 0x2f, 0x64, 0xe3, 0x74, 0xab, 0x29, 0xe2, 0x82, 0x08, 0xee, + 0x5f, 0x8a, 0xb9, 0x93, 0xdf, 0x0e, 0x8a, 0xd8, 0x3b, 0x05, 0xa4, 0x52, 0x34, 0x99, 0xe2, 0xa2, 0x21, 0xc5, 0x28, + 0x12, 0xb7, 0x8c, 0x72, 0xa8, 0xa2, 0x72, 0xd5, 0x22, 0x98, 0x4c, 0x51, 0x0e, 0x52, 0x77, 0x04, 0x39, 0x2f, 0x96, + 0xb7, 0x4d, 0x39, 0x9a, 0x88, 0xfc, 0x5a, 0xda, 0x24, 0x79, 0xd8, 0x0f, 0x9a, 0x60, 0x21, 0xa6, 0xaf, 0xe8, 0xb5, + 0x73, 0x1b, 0x08, 0x04, 0xb2, 0x26, 0x4a, 0xd1, 0xfd, 0xd2, 0x79, 0xca, 0x96, 0x5c, 0xa8, 0xae, 0x1d, 0xa4, 0xee, + 0xa4, 0x09, 0x96, 0xe5, 0x11, 0x38, 0xd7, 0x57, 0x92, 0x04, 0xa1, 0x6b, 0x2b, 0x76, 0xaf, 0x86, 0x01, 0x40, 0xfa, + 0x5f, 0x7d, 0xe6, 0xac, 0x00, 0x48, 0x22, 0x15, 0x5b, 0xd6, 0xf9, 0xe3, 0x21, 0x36, 0xc9, 0x92, 0x1d, 0xab, 0x6e, + 0x7e, 0x93, 0xe4, 0x3d, 0x6b, 0x1e, 0x13, 0xa4, 0x2c, 0xce, 0xe7, 0x35, 0xba, 0x02, 0x0e, 0xbe, 0xcb, 0xe2, 0x65, + 0x88, 0x49, 0x70, 0xcd, 0x34, 0xf6, 0x46, 0x1f, 0xd7, 0xd2, 0xf7, 0xb8, 0x48, 0x14, 0xc4, 0xc5, 0x65, 0xa5, 0x42, + 0xcf, 0xc3, 0x9c, 0x51, 0xac, 0x6b, 0xb5, 0x12, 0x49, 0x50, 0xd3, 0x7d, 0x64, 0xb7, 0xbd, 0x17, 0x93, 0x83, 0x8a, + 0xfc, 0xb4, 0x75, 0x58, 0x96, 0xae, 0xe7, 0x70, 0xcc, 0xa3, 0x5f, 0x79, 0xf4, 0xa4, 0x3f, 0xfe, 0xd3, 0x09, 0xff, + 0x66, 0x65, 0x8d, 0x3e, 0x07, 0x04, 0x68, 0x5f, 0x52, 0x4c, 0xcb, 0x6a, 0x9a, 0x8d, 0x92, 0x26, 0xb0, 0x26, 0x7e, + 0x10, 0x98, 0x01, 0xb8, 0x31, 0xac, 0x3f, 0x6b, 0x78, 0xd8, 0xcf, 0x12, 0xb2, 0x15, 0x7e, 0x46, 0x3f, 0xe5, 0x9d, + 0x92, 0xce, 0x96, 0xf3, 0xe1, 0x5a, 0x16, 0x94, 0x4b, 0xf2, 0xf3, 0x4d, 0x99, 0xb9, 0xfc, 0xd9, 0xc9, 0x64, 0x52, + 0x96, 0x1a, 0xdb, 0xca, 0x01, 0x4a, 0x7e, 0x1f, 0xd9, 0xb6, 0x5d, 0x9d, 0xdf, 0xa6, 0x83, 0x42, 0x07, 0xc3, 0x44, + 0x21, 0x7c, 0xe7, 0xfe, 0x3d, 0xf5, 0x07, 0x41, 0x4b, 0x5d, 0x35, 0x9d, 0x47, 0xda, 0x6a, 0xff, 0x11, 0xa0, 0x20, + 0x6a, 0xb8, 0xef, 0xf8, 0x6f, 0xee, 0x95, 0x2d, 0x3d, 0x55, 0x0f, 0xf0, 0xc3, 0x1a, 0xdf, 0xb3, 0xd7, 0x77, 0x68, + 0xda, 0xb4, 0xbd, 0x33, 0xab, 0x20, 0xbb, 0x25, 0x9b, 0xa5, 0x1e, 0x59, 0x2a, 0xf9, 0x29, 0x9b, 0x27, 0xdd, 0x11, + 0x43, 0x05, 0xa9, 0x25, 0x51, 0x5b, 0xb4, 0xea, 0x31, 0xa7, 0x60, 0xc7, 0xe5, 0x08, 0x3c, 0x6c, 0x2b, 0xa8, 0xac, + 0xda, 0xd0, 0xac, 0x89, 0x8f, 0x20, 0x15, 0x5b, 0x6f, 0x2a, 0x9c, 0x70, 0x9b, 0x1e, 0xda, 0x7f, 0x2a, 0xd5, 0x53, + 0x80, 0x3b, 0x5d, 0x0b, 0x6b, 0x13, 0x52, 0x9e, 0xe0, 0xdf, 0xb9, 0x72, 0xee, 0xc5, 0xe2, 0xb6, 0x6c, 0xdc, 0xd5, + 0x01, 0x75, 0x53, 0x41, 0xca, 0x08, 0xea, 0x3a, 0xd4, 0x97, 0x9b, 0x00, 0x4d, 0x64, 0xeb, 0x16, 0xb0, 0xa0, 0x11, + 0x53, 0x50, 0xd1, 0x11, 0xe6, 0xa0, 0xe2, 0x75, 0x16, 0x76, 0x5e, 0x21, 0xdf, 0xc7, 0x5f, 0x90, 0xa5, 0x1c, 0xd2, + 0x9d, 0xfc, 0xc9, 0x78, 0xde, 0x41, 0xe5, 0x5e, 0x69, 0xab, 0xa2, 0xa9, 0x0c, 0xee, 0x01, 0x71, 0x23, 0x55, 0x96, + 0x71, 0x60, 0x52, 0xe2, 0x7a, 0x4d, 0x5f, 0x6f, 0x8e, 0xbb, 0xb9, 0x7b, 0xe7, 0x10, 0xf4, 0x1a, 0x9b, 0x53, 0xb5, + 0x93, 0x6a, 0xaf, 0xaa, 0xc3, 0x16, 0x70, 0xc2, 0x0a, 0x80, 0xcf, 0xac, 0x82, 0x46, 0x43, 0x4a, 0x05, 0xf7, 0xd1, + 0xa0, 0xf3, 0xb7, 0x32, 0xb2, 0x16, 0xe3, 0xc4, 0xee, 0xea, 0xab, 0x50, 0xdf, 0x42, 0x33, 0x08, 0x73, 0xc7, 0xb1, + 0x13, 0x3e, 0x9b, 0xb0, 0x63, 0x64, 0x74, 0xe5, 0xe0, 0x0e, 0xc2, 0x53, 0x6a, 0x52, 0xf2, 0x13, 0x3a, 0xa5, 0xa8, + 0x4b, 0xf8, 0xa1, 0x56, 0x78, 0x7f, 0x51, 0x92, 0xc6, 0xf3, 0xa0, 0x13, 0x2d, 0x7d, 0xa7, 0xda, 0x73, 0x3f, 0xdc, + 0xbd, 0xae, 0x77, 0xbb, 0x73, 0x5d, 0x60, 0x0e, 0x77, 0xae, 0x0c, 0xdc, 0x25, 0x56, 0xbe, 0x48, 0xdd, 0x1f, 0x24, + 0xe5, 0x81, 0x1c, 0x30, 0x51, 0xc5, 0x56, 0x74, 0xa3, 0xff, 0x69, 0xe9, 0x0e, 0x4e, 0x4e, 0x6f, 0xe7, 0x81, 0x72, + 0xc3, 0xe2, 0x04, 0x12, 0x4a, 0xa8, 0x8e, 0x65, 0xab, 0x0a, 0x1a, 0xf4, 0xfb, 0xe1, 0xd4, 0x55, 0x7f, 0xb9, 0x78, + 0x63, 0x76, 0xd4, 0x53, 0x30, 0xc7, 0xb8, 0x99, 0x22, 0x8b, 0x7b, 0xee, 0xdd, 0xb1, 0xf8, 0xba, 0xc5, 0x3d, 0x7e, + 0x88, 0xb9, 0xc5, 0x32, 0xa5, 0xa5, 0xee, 0x90, 0x12, 0x5e, 0xb9, 0xf1, 0xd9, 0xea, 0x65, 0x74, 0xeb, 0xaa, 0x80, + 0x58, 0x9d, 0x56, 0x47, 0x71, 0x5a, 0x07, 0xd6, 0x51, 0x47, 0xed, 0x7f, 0xa5, 0x28, 0x27, 0x63, 0x36, 0x49, 0xfa, + 0x28, 0x8e, 0x39, 0x41, 0x7e, 0x90, 0x7e, 0x2b, 0x8a, 0x35, 0x0a, 0x12, 0xd3, 0x51, 0xd6, 0xfc, 0x51, 0x51, 0x00, + 0x19, 0x75, 0x95, 0x47, 0x93, 0xd6, 0xe4, 0x60, 0xf2, 0xa2, 0xc7, 0x8b, 0xb3, 0xaf, 0x4a, 0xd5, 0x0d, 0xfa, 0xb7, + 0x25, 0x35, 0x4b, 0xd2, 0x38, 0xfa, 0xc8, 0x38, 0x2f, 0xa9, 0xe4, 0x82, 0xa2, 0x6a, 0xd3, 0xd6, 0xe6, 0x97, 0x9c, + 0xce, 0x70, 0x34, 0x69, 0x15, 0xd5, 0x11, 0xc6, 0xfd, 0x1c, 0xc8, 0x93, 0x7d, 0x01, 0xfa, 0x89, 0x3c, 0x4d, 0x8e, + 0x59, 0x37, 0x51, 0x8e, 0xca, 0xc7, 0x38, 0x15, 0xe3, 0x3b, 0x81, 0x8c, 0x6b, 0x85, 0xf7, 0x62, 0x82, 0xcd, 0x5c, + 0xf5, 0x47, 0xa7, 0xd5, 0x31, 0x1c, 0xe7, 0xc8, 0x3a, 0xea, 0x8c, 0x6c, 0xe3, 0xc0, 0x3a, 0x30, 0xdb, 0xd6, 0x91, + 0xd1, 0x31, 0x3b, 0x46, 0xe7, 0xbb, 0xce, 0xc8, 0x3c, 0xb0, 0x0e, 0x0c, 0xdb, 0xec, 0x40, 0xa1, 0xd9, 0x31, 0x3b, + 0x37, 0xe6, 0x41, 0x67, 0x64, 0x63, 0x69, 0xcb, 0x3a, 0x3c, 0x34, 0x1d, 0xdb, 0x3a, 0x3c, 0x34, 0x0e, 0xad, 0xa3, + 0x23, 0xd3, 0x69, 0x5b, 0x47, 0x47, 0xe7, 0x87, 0x1d, 0xab, 0x0d, 0xef, 0xda, 0xed, 0x51, 0xdb, 0x72, 0x1c, 0x13, + 0xfe, 0x32, 0x3a, 0x56, 0x8b, 0x7e, 0x38, 0x8e, 0xd5, 0x76, 0x0c, 0x3b, 0x38, 0x6c, 0x59, 0x47, 0x2f, 0x0c, 0xfc, + 0x1b, 0xab, 0x19, 0xf8, 0x17, 0x74, 0x63, 0xbc, 0xb0, 0x5a, 0x47, 0xf4, 0x0b, 0x3b, 0xbc, 0x39, 0xe8, 0xfc, 0x55, + 0xdd, 0x6f, 0x1c, 0x83, 0x43, 0x63, 0xe8, 0x1c, 0x5a, 0xed, 0xb6, 0x71, 0xe0, 0x58, 0x9d, 0xf6, 0xcc, 0x3c, 0x68, + 0x59, 0x47, 0xc7, 0x23, 0xd3, 0xb1, 0x8e, 0x8f, 0x0d, 0xdb, 0x6c, 0x5b, 0x2d, 0xc3, 0xb1, 0x0e, 0xda, 0xf8, 0xa3, + 0x6d, 0xb5, 0x6e, 0x8e, 0x5f, 0x58, 0x47, 0x87, 0xb3, 0x23, 0xeb, 0xe0, 0xc3, 0x41, 0xc7, 0x6a, 0xb5, 0x67, 0xed, + 0x23, 0xab, 0x75, 0x7c, 0x73, 0x64, 0x1d, 0xcc, 0xcc, 0xd6, 0xd1, 0xd6, 0x96, 0x4e, 0xcb, 0x82, 0x39, 0xc2, 0xd7, + 0xf0, 0xc2, 0xe0, 0x2f, 0xe0, 0xcf, 0x0c, 0xdb, 0xfe, 0x81, 0xdd, 0x24, 0x9b, 0x4d, 0x5f, 0x58, 0x9d, 0xe3, 0x11, + 0x55, 0x87, 0x02, 0x53, 0xd4, 0x80, 0x26, 0x37, 0x26, 0x7d, 0x16, 0xbb, 0x33, 0x45, 0x47, 0xe2, 0x0f, 0xff, 0xd8, + 0x8d, 0x09, 0x1f, 0xa6, 0xef, 0xfe, 0x5b, 0xfb, 0xc9, 0x97, 0xfc, 0x64, 0x7f, 0x4a, 0x5b, 0x7f, 0xda, 0xff, 0xea, + 0x04, 0x0e, 0x77, 0x7f, 0x60, 0xfc, 0xda, 0xa4, 0x94, 0xfc, 0xfb, 0xfd, 0x4a, 0xc9, 0x97, 0xcb, 0x5d, 0x94, 0x92, + 0x7f, 0xff, 0xe2, 0x4a, 0xc9, 0x5f, 0xab, 0xbe, 0x35, 0x6f, 0xaa, 0x59, 0xa8, 0x7f, 0x58, 0x57, 0x45, 0x0e, 0x89, + 0xa7, 0x5d, 0xfe, 0xb4, 0xbc, 0x82, 0xf8, 0xf1, 0x6f, 0x22, 0xf7, 0xe5, 0xb2, 0x64, 0xf0, 0x19, 0x01, 0x8e, 0x7d, + 0x13, 0x11, 0x8e, 0xfd, 0xb0, 0x74, 0xc1, 0xca, 0x8c, 0xb3, 0x39, 0xfe, 0xd8, 0x9c, 0x79, 0xc1, 0x24, 0x67, 0x91, + 0xa0, 0xa4, 0x87, 0xc5, 0xe0, 0x37, 0x0f, 0xe4, 0x19, 0x6e, 0x32, 0xcb, 0x79, 0x98, 0x80, 0x45, 0x30, 0x58, 0x72, + 0x4c, 0xe2, 0xac, 0xd2, 0xd8, 0x12, 0x11, 0xf7, 0xaf, 0xb9, 0x47, 0x71, 0xe3, 0x7b, 0x34, 0x00, 0xae, 0xef, 0xdd, + 0xd9, 0xec, 0x57, 0x01, 0xcb, 0x3a, 0x61, 0x20, 0x0d, 0xdc, 0x7e, 0xdd, 0xfb, 0xb2, 0x19, 0x6e, 0xc5, 0xf0, 0xba, + 0x19, 0x52, 0x80, 0xa4, 0xda, 0xde, 0x29, 0x9b, 0xf1, 0xde, 0x37, 0xcc, 0x9a, 0xcf, 0x97, 0x9a, 0x6f, 0xb1, 0x21, + 0xce, 0x3b, 0xae, 0x4e, 0xd5, 0xba, 0xc4, 0xa7, 0xd5, 0x4f, 0x48, 0x71, 0x41, 0x2d, 0x0c, 0x8d, 0x0b, 0x4e, 0xd5, + 0x56, 0x90, 0xdf, 0xb1, 0xa5, 0x77, 0xa5, 0x3e, 0x65, 0xe3, 0xe4, 0x67, 0x6b, 0xbc, 0x57, 0xf8, 0xbf, 0x02, 0x27, + 0xca, 0x39, 0x9e, 0x61, 0x24, 0xcf, 0xf3, 0x5a, 0xea, 0x97, 0xa4, 0x11, 0xd9, 0xcc, 0x59, 0x6f, 0xf2, 0xa2, 0x8d, + 0x6e, 0x09, 0x0e, 0x9b, 0x0b, 0x2e, 0x08, 0x3f, 0x4f, 0x4e, 0x00, 0x19, 0x39, 0x6a, 0xa0, 0x9f, 0xc3, 0xb6, 0xce, + 0x44, 0xbd, 0x47, 0xb0, 0x89, 0xb9, 0x27, 0xa0, 0x22, 0x87, 0x34, 0x5d, 0x4f, 0x82, 0xc8, 0x4b, 0xbb, 0xc8, 0xa6, + 0x49, 0x2c, 0x6f, 0x0b, 0x3d, 0x16, 0x7a, 0x5b, 0x8c, 0xe9, 0xe4, 0x8e, 0x79, 0x27, 0xe8, 0xf9, 0xb0, 0xcd, 0xfe, + 0x2e, 0x77, 0x38, 0x5b, 0x97, 0xcc, 0x51, 0x9c, 0xc3, 0x63, 0xc3, 0x39, 0x32, 0xac, 0xe3, 0x43, 0x3d, 0x13, 0x07, + 0x4e, 0xee, 0xb2, 0x34, 0x21, 0xe0, 0x00, 0x91, 0x83, 0xe9, 0x87, 0x7e, 0xea, 0x7b, 0x41, 0x06, 0xfc, 0x70, 0xf9, + 0x92, 0xf2, 0xf7, 0x65, 0x92, 0xc2, 0x18, 0x05, 0xd3, 0x8b, 0xce, 0x1f, 0xe6, 0x90, 0xa5, 0x2b, 0xc6, 0xc2, 0x06, + 0xc3, 0x98, 0xaa, 0x2f, 0xc9, 0xef, 0x67, 0x59, 0x9f, 0x91, 0xd5, 0xda, 0x30, 0x0d, 0xf9, 0xfe, 0x10, 0x8e, 0x0f, + 0xd9, 0xc0, 0xf8, 0xae, 0x09, 0xe1, 0xfe, 0x72, 0x3f, 0xc2, 0x4d, 0xd9, 0x2e, 0x08, 0xf7, 0x97, 0x2f, 0x8e, 0x70, + 0xbf, 0x93, 0x11, 0x6e, 0xc9, 0x7f, 0xb0, 0xd0, 0x30, 0xbd, 0xc7, 0x67, 0x0d, 0x5c, 0x64, 0x9f, 0xab, 0xfb, 0xc4, + 0xc0, 0xab, 0x7a, 0x91, 0xbd, 0xf6, 0x2f, 0x4b, 0xd9, 0x82, 0x1a, 0x05, 0xa0, 0x98, 0xd7, 0xd1, 0x47, 0xd7, 0x65, + 0x1f, 0x5c, 0xdd, 0x44, 0x18, 0x06, 0xe8, 0xf3, 0xfb, 0x30, 0x0d, 0xac, 0x77, 0xfc, 0x1e, 0x09, 0x0a, 0xdd, 0x37, + 0x51, 0x3c, 0xf7, 0x30, 0xc5, 0x88, 0xaa, 0x83, 0x3b, 0x1d, 0x3c, 0xd8, 0x10, 0x08, 0x64, 0x14, 0x85, 0xe3, 0x5c, + 0x2b, 0xc9, 0xdc, 0x4b, 0xe2, 0xb8, 0xd5, 0x3b, 0xe6, 0xc5, 0xaa, 0x41, 0xaf, 0x61, 0x71, 0x9f, 0xb5, 0xed, 0x67, + 0xad, 0x83, 0x67, 0x47, 0x36, 0xfc, 0xef, 0xb0, 0x76, 0x66, 0xf0, 0x8a, 0xf3, 0x28, 0x4c, 0x67, 0x45, 0xcd, 0xa6, + 0x6a, 0x2b, 0xc6, 0x3e, 0x16, 0xb5, 0x8e, 0xeb, 0x2b, 0x8d, 0xbd, 0xbb, 0xa2, 0x4e, 0x6d, 0x8d, 0x59, 0xb4, 0x94, + 0xc0, 0xaa, 0x81, 0xc6, 0x0f, 0x97, 0x20, 0x67, 0x97, 0x6a, 0xc8, 0xaf, 0xf9, 0x70, 0x8b, 0x71, 0xb1, 0x76, 0x76, + 0x25, 0x72, 0x28, 0xa8, 0x3d, 0x91, 0x56, 0xef, 0xde, 0x19, 0xe4, 0x2a, 0x4a, 0x1b, 0x73, 0x4e, 0x61, 0x66, 0x43, + 0xc8, 0x38, 0xc5, 0xc4, 0x02, 0x79, 0xb4, 0x40, 0x69, 0xbc, 0x0c, 0x47, 0x1a, 0xfe, 0xf4, 0x86, 0x89, 0xe6, 0xef, + 0xc7, 0x16, 0xff, 0xb0, 0x8e, 0xab, 0xe6, 0xf5, 0xed, 0x22, 0xe9, 0x7c, 0x22, 0x56, 0xc5, 0x7b, 0x96, 0x1a, 0x31, + 0xea, 0xb1, 0x69, 0x69, 0x4d, 0xd7, 0x7b, 0x96, 0x37, 0x7c, 0x96, 0x1a, 0xe1, 0x73, 0xd0, 0x7d, 0xba, 0xf6, 0x93, + 0x27, 0x54, 0x6b, 0xcf, 0x15, 0xc3, 0x3a, 0x1d, 0x15, 0x99, 0x29, 0x14, 0x6f, 0x1a, 0x51, 0x72, 0x8a, 0xee, 0xc8, + 0x88, 0x9e, 0x3f, 0xef, 0xbb, 0x8e, 0x3e, 0x8c, 0x99, 0xf7, 0x31, 0x13, 0xe1, 0xbe, 0x43, 0xcc, 0x4f, 0x7b, 0xbe, + 0x9b, 0xa1, 0x91, 0x5e, 0xeb, 0x4a, 0xbb, 0x80, 0x3b, 0x93, 0x2d, 0xdc, 0x11, 0x38, 0xf6, 0x82, 0xdc, 0xf5, 0x64, + 0x50, 0xe0, 0x09, 0x83, 0x1f, 0x51, 0xe7, 0x7a, 0xe6, 0x25, 0x3f, 0x24, 0x51, 0xf8, 0xcb, 0x02, 0x82, 0x1f, 0x17, + 0x16, 0x45, 0xe2, 0x32, 0xd6, 0xb6, 0x6c, 0xcb, 0x56, 0xf3, 0xfe, 0x26, 0xfe, 0xd4, 0x5d, 0x47, 0xa9, 0xd7, 0xdd, + 0x73, 0x8c, 0x20, 0x9a, 0x82, 0x7b, 0x5d, 0xea, 0xa7, 0x01, 0xeb, 0xaa, 0x2a, 0xf8, 0xd9, 0xcd, 0xe9, 0xba, 0x9e, + 0x71, 0xa7, 0x07, 0x2f, 0x86, 0x6c, 0xe6, 0xf1, 0x9d, 0xf0, 0xd0, 0xc5, 0x18, 0xea, 0x3f, 0x02, 0x8d, 0xd4, 0x54, + 0x0d, 0x44, 0x06, 0x2c, 0x4e, 0x4c, 0xd9, 0x89, 0xa8, 0xab, 0x40, 0x1b, 0x5d, 0xe5, 0x63, 0x9b, 0xc4, 0xde, 0x1c, + 0xd2, 0xed, 0xae, 0x33, 0x83, 0x23, 0x60, 0x95, 0x63, 0x60, 0xc5, 0x79, 0x71, 0x64, 0x28, 0x2d, 0xc7, 0x50, 0x6c, + 0xc0, 0xc2, 0x6a, 0x66, 0xac, 0xb3, 0xab, 0xde, 0x7d, 0x76, 0x10, 0x84, 0x76, 0x1e, 0xd1, 0x38, 0xc8, 0x02, 0x82, + 0x6b, 0x98, 0x52, 0xca, 0x9d, 0xa3, 0x49, 0x89, 0x35, 0x7d, 0xd2, 0x85, 0x5e, 0xb0, 0xdb, 0x54, 0x07, 0x85, 0x92, + 0xa8, 0xe2, 0xeb, 0x6b, 0xf4, 0x23, 0xf6, 0x43, 0xc5, 0xff, 0xf4, 0x49, 0xf3, 0xc1, 0xc7, 0xc9, 0x95, 0xe6, 0x07, + 0x9e, 0xf5, 0xd2, 0x84, 0xf9, 0x85, 0xf6, 0x1e, 0x27, 0x0b, 0x1c, 0x10, 0xe1, 0xdf, 0xa2, 0x58, 0xfc, 0xe0, 0xd6, + 0x13, 0x56, 0xe0, 0x85, 0x53, 0xc0, 0x74, 0x5e, 0x38, 0xdd, 0xb0, 0xd2, 0x22, 0x57, 0xe8, 0x4a, 0x69, 0xd1, 0x55, + 0x61, 0x41, 0x95, 0xbc, 0xbc, 0xbb, 0xf0, 0xa6, 0x3f, 0x79, 0x73, 0xa6, 0xa9, 0x40, 0xfc, 0xd0, 0x73, 0xb7, 0x50, + 0xf0, 0x3e, 0x77, 0x9f, 0x9e, 0xcc, 0x59, 0xea, 0x91, 0x76, 0x08, 0xee, 0xc4, 0xc0, 0x25, 0x28, 0x9c, 0xfe, 0xf0, + 0x38, 0x18, 0x2e, 0xa5, 0xd8, 0x22, 0xf2, 0x61, 0x28, 0x9c, 0x7c, 0x99, 0x68, 0x08, 0xea, 0x3a, 0x06, 0xf9, 0x21, + 0x8c, 0x3c, 0x4c, 0xb3, 0xe3, 0x86, 0x91, 0xda, 0x7f, 0x9a, 0xbb, 0x6c, 0x36, 0x2d, 0x42, 0xe0, 0x87, 0x1f, 0x2f, + 0x63, 0x16, 0xfc, 0xc3, 0x7d, 0x0a, 0xf4, 0xfc, 0xe9, 0x95, 0xaa, 0xf7, 0x52, 0x6b, 0x16, 0xb3, 0x89, 0xfb, 0x14, + 0xee, 0xa9, 0x5d, 0xb4, 0x9a, 0x05, 0x66, 0xfe, 0xf9, 0xed, 0x3c, 0x30, 0xf0, 0xd6, 0x4f, 0xb0, 0xa8, 0xed, 0x56, + 0x11, 0xee, 0xbc, 0xbd, 0xd3, 0x5d, 0xbf, 0xcf, 0x2f, 0xf1, 0x70, 0x31, 0x5c, 0x97, 0xae, 0xde, 0x4e, 0x0f, 0xaf, + 0xd5, 0xc3, 0xc0, 0x1b, 0x7d, 0xec, 0xd1, 0x9b, 0xd2, 0x83, 0x09, 0x44, 0x7c, 0xe4, 0x2d, 0xba, 0x48, 0x75, 0xe5, + 0x42, 0x70, 0xaa, 0xa6, 0xd2, 0x9c, 0xe1, 0xab, 0xdd, 0xcb, 0xb8, 0x95, 0xd7, 0xf8, 0x65, 0xfc, 0xd4, 0x6a, 0xe6, + 0xa7, 0x4c, 0x7c, 0x0a, 0x1f, 0xb2, 0x4c, 0xdc, 0xdf, 0xe9, 0xe6, 0x8a, 0xf7, 0x6d, 0xab, 0xad, 0x38, 0x9d, 0xef, + 0x0e, 0x6f, 0x1c, 0x7b, 0xd6, 0x72, 0xac, 0xce, 0x07, 0xa7, 0x33, 0x6b, 0x5b, 0xc7, 0x81, 0xd9, 0xb6, 0x8e, 0xe1, + 0xcf, 0x87, 0x63, 0xab, 0x33, 0x33, 0x5b, 0xd6, 0xc1, 0x07, 0xa7, 0x15, 0x98, 0x1d, 0xeb, 0x18, 0xfe, 0x9c, 0x53, + 0x2b, 0xb8, 0x17, 0xd1, 0x35, 0xe8, 0x69, 0x09, 0x39, 0x48, 0xbf, 0x73, 0x55, 0xad, 0x51, 0xa2, 0x7a, 0x35, 0xea, + 0xde, 0x05, 0x06, 0x97, 0x10, 0x69, 0x6d, 0x30, 0xf4, 0x90, 0x16, 0xba, 0x8c, 0xd2, 0xcd, 0x0a, 0xc3, 0x37, 0xe1, + 0xa1, 0x5e, 0xe4, 0x3f, 0x95, 0x4e, 0x10, 0xaf, 0xdb, 0x4b, 0x68, 0xbb, 0x4b, 0x61, 0xe5, 0xb4, 0xca, 0xb1, 0x6b, + 0xc8, 0x7c, 0xac, 0x1b, 0xa0, 0x3a, 0x06, 0xc4, 0x54, 0x84, 0x83, 0xd2, 0x6a, 0xd1, 0x96, 0xc0, 0x66, 0x09, 0x4b, + 0xa9, 0x48, 0x13, 0x2d, 0x81, 0xd8, 0xe8, 0xba, 0x48, 0x58, 0x8c, 0x1d, 0xf3, 0x1a, 0x8c, 0x67, 0x56, 0xae, 0x7d, + 0xd5, 0xab, 0xe2, 0x4b, 0xf0, 0x8a, 0xb7, 0xc2, 0x68, 0x85, 0x56, 0x1f, 0xf7, 0xcd, 0x1d, 0xc6, 0x19, 0x60, 0xc2, + 0xe6, 0xac, 0x0c, 0x2c, 0x0f, 0x9d, 0x5d, 0xfd, 0xe0, 0x06, 0x82, 0x7e, 0xd0, 0x07, 0xa5, 0x44, 0xdd, 0x9f, 0xd5, + 0x0f, 0x8f, 0x62, 0x21, 0x27, 0xcb, 0x1c, 0xfb, 0x71, 0x0e, 0x9e, 0x44, 0x51, 0x9c, 0xfa, 0x4c, 0xa5, 0xba, 0x41, + 0xb1, 0x9c, 0x58, 0x7c, 0xe3, 0x05, 0x92, 0xdd, 0x9d, 0xd4, 0x72, 0x2f, 0x27, 0x54, 0x4f, 0x9e, 0x14, 0xc0, 0x99, + 0x15, 0xb8, 0x4f, 0x9c, 0x43, 0xe0, 0x12, 0x0e, 0x59, 0x7b, 0xab, 0x09, 0x28, 0x5d, 0xcc, 0xb6, 0xb9, 0x82, 0x17, + 0x89, 0x99, 0x84, 0x99, 0x97, 0x30, 0x30, 0x69, 0xb4, 0x43, 0xdd, 0x30, 0x2f, 0x81, 0xcc, 0x76, 0x95, 0x9b, 0x99, + 0xaa, 0xf7, 0x42, 0x61, 0x2d, 0x11, 0x6e, 0xc9, 0x49, 0xc7, 0xaf, 0x8e, 0x2a, 0x4c, 0xcd, 0x96, 0x71, 0xdc, 0xe3, + 0xcf, 0xfe, 0xef, 0x1e, 0x04, 0xfa, 0x96, 0x82, 0x79, 0x47, 0x05, 0x8b, 0x94, 0x7c, 0x0d, 0x73, 0x7a, 0x4f, 0x84, + 0x9e, 0x25, 0xa7, 0x2a, 0x14, 0xa9, 0x5d, 0x15, 0xfd, 0xd8, 0xd4, 0xdc, 0xb6, 0x35, 0xa7, 0x62, 0x45, 0x81, 0xe1, + 0x63, 0xfe, 0x4f, 0xe1, 0xe7, 0xaa, 0x3f, 0x79, 0xd2, 0x48, 0x1c, 0xc9, 0x96, 0x28, 0x61, 0xa9, 0xb8, 0x4f, 0x68, + 0xaa, 0x8c, 0x77, 0x55, 0x19, 0xf5, 0xe5, 0xfd, 0x22, 0x36, 0x13, 0x26, 0xb9, 0xb4, 0xf7, 0xf0, 0xe7, 0x90, 0x79, + 0xa9, 0xc5, 0x75, 0xbb, 0x9a, 0xc4, 0x74, 0x18, 0x80, 0x36, 0x32, 0x42, 0x21, 0xf9, 0x30, 0x07, 0x8f, 0xd7, 0x7f, + 0x59, 0xf2, 0x20, 0x14, 0xd0, 0xc7, 0xa7, 0x4f, 0x76, 0x11, 0x37, 0xf4, 0x6d, 0xea, 0x51, 0xdc, 0x36, 0x99, 0x17, + 0x88, 0x52, 0x8f, 0xec, 0x4f, 0x7c, 0x0c, 0xb5, 0x53, 0x1f, 0x41, 0x4c, 0x8a, 0x54, 0xd1, 0x7f, 0x7b, 0xf1, 0x8d, + 0xc2, 0x0f, 0x00, 0x59, 0x37, 0xe0, 0xc5, 0x8b, 0xe2, 0xe3, 0xb8, 0x14, 0x1f, 0x47, 0xe1, 0x99, 0x97, 0x21, 0x47, + 0x6c, 0xb6, 0x4f, 0x53, 0x88, 0x02, 0x73, 0xb2, 0xf9, 0x98, 0x2f, 0x83, 0xd4, 0x5f, 0x78, 0x71, 0xba, 0x8f, 0xc1, + 0x71, 0x30, 0xd8, 0x4e, 0x53, 0xfc, 0x0a, 0x32, 0x1b, 0x11, 0xd9, 0x4c, 0xd2, 0x50, 0xd8, 0x8d, 0x4c, 0xfc, 0x20, + 0x37, 0x1b, 0x11, 0x1f, 0xf0, 0x46, 0x23, 0xb6, 0x48, 0xdd, 0x52, 0x10, 0x9e, 0x68, 0x94, 0xb2, 0xd4, 0x4c, 0xd2, + 0x98, 0x79, 0x73, 0x35, 0x0f, 0xca, 0xb5, 0xd9, 0x5f, 0xb2, 0x1c, 0x42, 0x54, 0x21, 0x11, 0x1e, 0x8c, 0x06, 0x08, + 0x06, 0x1c, 0x00, 0x22, 0x04, 0xc5, 0xa1, 0x29, 0x3c, 0x8f, 0xa6, 0x95, 0x2d, 0x55, 0xb0, 0x54, 0xa7, 0x98, 0xd4, + 0x8c, 0x6e, 0x5e, 0x20, 0xdd, 0x1e, 0x45, 0xc1, 0x35, 0x8f, 0xb9, 0x91, 0x67, 0xc7, 0x51, 0xfb, 0x27, 0xfc, 0x3a, + 0xae, 0x60, 0xb8, 0x19, 0xf5, 0xd0, 0x86, 0xb4, 0x6d, 0x4d, 0xd1, 0x38, 0xf6, 0x79, 0x65, 0xa0, 0x99, 0xd4, 0x33, + 0x66, 0xde, 0x24, 0x58, 0x2e, 0x80, 0x64, 0x95, 0x0c, 0x7c, 0x66, 0x4e, 0x3f, 0x77, 0xff, 0x44, 0xa8, 0x90, 0xaa, + 0x7d, 0xfa, 0xf4, 0x7e, 0xf0, 0xaf, 0x7f, 0x42, 0x7a, 0xd0, 0x99, 0x23, 0x62, 0x60, 0x5c, 0xca, 0xb5, 0x38, 0x5b, + 0x6c, 0x0c, 0xd0, 0xb8, 0x8b, 0x8d, 0x45, 0x74, 0x42, 0xb1, 0xb7, 0xb2, 0xc1, 0x95, 0x88, 0xab, 0x07, 0x89, 0x85, + 0x75, 0x11, 0xa9, 0x63, 0x00, 0xcb, 0x3b, 0x10, 0x31, 0x5c, 0x94, 0xbf, 0xdd, 0xbe, 0x3c, 0x56, 0x8a, 0x70, 0x8f, + 0x75, 0x16, 0x48, 0xb4, 0x87, 0xfa, 0x27, 0x9e, 0x82, 0xdc, 0x14, 0xf2, 0x45, 0x49, 0x77, 0x1f, 0x86, 0x39, 0x8b, + 0xe6, 0xcc, 0xf2, 0xa3, 0xfd, 0x15, 0x1b, 0x9a, 0xde, 0xc2, 0x27, 0x3b, 0x22, 0x94, 0x13, 0x2a, 0xc4, 0x92, 0xe6, + 0xe6, 0x39, 0xc4, 0xf8, 0x67, 0xc5, 0x54, 0x46, 0x95, 0xc0, 0x6d, 0xad, 0x42, 0x6f, 0x79, 0xc0, 0x83, 0xa2, 0x89, + 0x9a, 0xfd, 0x93, 0x7d, 0xaf, 0x5f, 0xce, 0x94, 0x63, 0x89, 0x8c, 0xaf, 0x65, 0x2a, 0x70, 0x4a, 0x09, 0x6f, 0x44, + 0x6e, 0x9b, 0xe2, 0xc1, 0x8c, 0x26, 0x13, 0x39, 0xbb, 0x8d, 0x55, 0x06, 0x2f, 0x9f, 0xb4, 0x62, 0x4b, 0x47, 0x0b, + 0xfa, 0xd2, 0xe6, 0x27, 0xf2, 0x9f, 0x6a, 0x17, 0xd3, 0x5a, 0xc1, 0x98, 0xe1, 0xbc, 0x6f, 0x64, 0xc9, 0xc9, 0x67, + 0xec, 0x11, 0x55, 0xe2, 0x88, 0xa4, 0x9a, 0x93, 0xb1, 0x81, 0xa5, 0xda, 0x73, 0x5d, 0xc2, 0x73, 0x55, 0x74, 0x07, + 0x93, 0x58, 0x93, 0xfd, 0x16, 0x06, 0x9b, 0x42, 0x43, 0x93, 0xdc, 0x7b, 0xb1, 0x51, 0x75, 0x38, 0x9b, 0x30, 0xee, + 0x7b, 0x62, 0xfb, 0x95, 0x36, 0x28, 0x6c, 0x3c, 0xbe, 0xee, 0x80, 0xe0, 0x45, 0x3f, 0x15, 0x3c, 0xaf, 0x7c, 0x4d, + 0x28, 0xdd, 0x0c, 0xbc, 0xbb, 0x48, 0x32, 0xbb, 0xe2, 0x11, 0x58, 0xce, 0xb1, 0xf4, 0x42, 0x78, 0x3e, 0x6f, 0x1c, + 0x34, 0xa4, 0x61, 0x90, 0x25, 0x74, 0xf3, 0xb0, 0x15, 0x04, 0x38, 0x60, 0xf7, 0x9d, 0x35, 0xb9, 0x6e, 0x79, 0x30, + 0x88, 0x3c, 0xb3, 0xe2, 0x1c, 0x96, 0x5e, 0x22, 0x5a, 0xc8, 0x4e, 0xf6, 0x61, 0x7c, 0x94, 0xf7, 0x50, 0x30, 0x79, + 0xc2, 0xbe, 0x10, 0x6f, 0xbd, 0x7e, 0xd3, 0xad, 0xb7, 0xca, 0xa3, 0x94, 0x59, 0x2f, 0x5f, 0x87, 0xd8, 0x36, 0x5e, + 0x42, 0xb6, 0x67, 0xdf, 0x8f, 0x39, 0x61, 0x90, 0xbe, 0x92, 0x07, 0xc3, 0x2c, 0xd5, 0xd3, 0xb7, 0x06, 0x89, 0xa1, + 0x8c, 0xbb, 0x1f, 0x96, 0x98, 0x6e, 0x37, 0xeb, 0xa5, 0x4c, 0xc4, 0x6c, 0x38, 0x4f, 0x1b, 0xc2, 0x3a, 0x34, 0x55, + 0x21, 0x3e, 0x7c, 0x4b, 0x85, 0x62, 0x9b, 0x6f, 0xab, 0x55, 0x70, 0x56, 0x45, 0x35, 0x4f, 0x53, 0x1f, 0xe1, 0x81, + 0xd8, 0xa8, 0x8d, 0xa5, 0x18, 0x6c, 0x22, 0x75, 0xa1, 0xaa, 0x50, 0x2d, 0x78, 0x8b, 0x05, 0x55, 0xd6, 0x7b, 0x27, + 0xfb, 0x74, 0x9d, 0xee, 0xd3, 0x06, 0xec, 0x9f, 0x80, 0x65, 0x3a, 0xed, 0x09, 0x6f, 0xb1, 0xe0, 0x2b, 0x4e, 0xbf, + 0xe8, 0xcd, 0xfe, 0x2c, 0x9d, 0x07, 0xfd, 0xff, 0x05, 0x64, 0x23, 0xa6, 0xdb, 0x06, 0x7b, 0x03, 0x00}; #else // Brotli (default, smaller) -constexpr uint8_t INDEX_BR[] PROGMEM = { - 0x5b, 0x36, 0x7a, 0x53, 0xc2, 0x36, 0x06, 0x5a, 0x1f, 0xd4, 0x4e, 0x00, 0xb3, 0xd6, 0xea, 0xff, 0x0a, 0xab, 0x51, - 0x94, 0xb1, 0xe6, 0xb0, 0x2e, 0x61, 0xbb, 0x1a, 0x70, 0x3b, 0xd8, 0x06, 0xfd, 0x7d, 0x2f, 0x1a, 0x00, 0x55, 0x35, - 0xe3, 0xa8, 0x1c, 0x62, 0xca, 0xd3, 0xb4, 0x00, 0xdb, 0x5e, 0x43, 0xa7, 0x14, 0x08, 0xa4, 0x51, 0x99, 0x96, 0xb6, - 0xf5, 0x0e, 0x99, 0x80, 0x52, 0x31, 0xe8, 0x10, 0x27, 0x1c, 0x04, 0x11, 0x58, 0xc5, 0x1c, 0xcc, 0xd4, 0x74, 0x4c, - 0x33, 0x11, 0xbb, 0xdb, 0xb0, 0xb4, 0x0a, 0x87, 0x13, 0x12, 0xb4, 0x8f, 0xe7, 0xd1, 0xc8, 0x85, 0x26, 0x07, 0xab, - 0x2e, 0x7a, 0x6e, 0x93, 0xb9, 0x4c, 0xb4, 0x84, 0x5b, 0x59, 0xda, 0xde, 0x2f, 0x74, 0x88, 0x3c, 0x53, 0x3b, 0xfd, - 0x28, 0xf8, 0x60, 0x4b, 0xf2, 0xc2, 0x03, 0xda, 0x33, 0x59, 0xe4, 0x2a, 0x48, 0x26, 0xde, 0x20, 0x6e, 0xcb, 0x83, - 0xef, 0x81, 0x7a, 0x6b, 0x36, 0x9a, 0x3f, 0x38, 0x8e, 0xfd, 0xf9, 0x7e, 0x73, 0xab, 0x6e, 0x85, 0xf0, 0x62, 0xe0, - 0x60, 0x60, 0x23, 0x1b, 0x11, 0xaf, 0x39, 0x32, 0xed, 0x79, 0x20, 0x27, 0x04, 0x1f, 0x6b, 0xf4, 0xd9, 0x97, 0x2f, - 0x38, 0xe4, 0x5b, 0x75, 0xbb, 0xb4, 0xfe, 0xd5, 0xfb, 0x5f, 0xa5, 0xc2, 0x5c, 0x88, 0xc6, 0xdf, 0xd0, 0x1e, 0x10, - 0xa7, 0x44, 0x3b, 0xdd, 0xb8, 0xbd, 0x2c, 0x7a, 0x10, 0x61, 0xbe, 0x8b, 0xbb, 0x3c, 0x10, 0x0e, 0x5f, 0x06, 0xc6, - 0xe0, 0xf2, 0xe6, 0x88, 0xa8, 0xb8, 0xa2, 0xab, 0xf7, 0x6b, 0xab, 0xe9, 0xfb, 0xbd, 0xa6, 0xfe, 0xd7, 0xef, 0x23, - 0xd3, 0x86, 0xa9, 0xdc, 0x57, 0x3a, 0xf7, 0xb8, 0x49, 0x6e, 0x45, 0x76, 0xda, 0xa6, 0x21, 0xc3, 0x2b, 0x0f, 0xac, - 0xc9, 0x85, 0x03, 0x60, 0xdd, 0x28, 0x5d, 0xe5, 0x3b, 0xfb, 0xf3, 0xbb, 0xdc, 0xc2, 0x94, 0xab, 0x90, 0x2b, 0x05, - 0xc8, 0x64, 0x3f, 0x3f, 0xc8, 0x1a, 0xe3, 0xef, 0x17, 0x88, 0x77, 0x9f, 0x18, 0x8d, 0xda, 0xd2, 0xc0, 0xb8, 0x5b, - 0x99, 0x6e, 0xd9, 0x12, 0xaa, 0xdc, 0x2f, 0xeb, 0xff, 0xff, 0xfb, 0x4e, 0xff, 0xbf, 0x7e, 0xed, 0x55, 0x22, 0xe6, - 0x8a, 0x96, 0x64, 0x4c, 0xdf, 0x4b, 0x2c, 0xef, 0x43, 0x42, 0x69, 0x18, 0x37, 0x29, 0x19, 0x40, 0x1f, 0x19, 0x8a, - 0x6b, 0x84, 0xb5, 0x31, 0x6a, 0x1d, 0xd9, 0x57, 0x5b, 0x04, 0xbb, 0xd6, 0xde, 0xfa, 0x52, 0xfd, 0xbe, 0x7e, 0x1f, - 0xe7, 0x5d, 0xc3, 0x72, 0xc9, 0x69, 0x3a, 0x6f, 0xf7, 0x1e, 0x6d, 0x29, 0x74, 0xce, 0x4b, 0xde, 0x98, 0xe5, 0x62, - 0x40, 0x34, 0x7a, 0xba, 0x6d, 0x0c, 0x30, 0x01, 0xd0, 0x92, 0x98, 0x29, 0xed, 0xfb, 0xea, 0xeb, 0x7f, 0xfd, 0x56, - 0x3a, 0x29, 0x0a, 0x1f, 0xd9, 0xb7, 0x84, 0x3a, 0x26, 0xfa, 0xd6, 0xce, 0x76, 0x3a, 0x32, 0x19, 0x0a, 0xb2, 0x94, - 0xc7, 0x80, 0xbe, 0x24, 0x74, 0x27, 0x0a, 0xff, 0x5f, 0x5f, 0xd3, 0xfa, 0xfa, 0x95, 0x08, 0xbb, 0xcc, 0xa5, 0x4e, - 0x51, 0xa8, 0x7b, 0x7f, 0x4f, 0x49, 0xce, 0xb2, 0xec, 0x9e, 0x0a, 0x41, 0xcb, 0xb4, 0x8d, 0x17, 0xe1, 0x00, 0xdc, - 0x13, 0x13, 0x59, 0xf7, 0x30, 0x55, 0xeb, 0xbf, 0x9f, 0x97, 0x0d, 0x67, 0x15, 0x30, 0x14, 0x61, 0x0c, 0x48, 0xaa, - 0xd8, 0x41, 0x5a, 0x75, 0x4b, 0xd3, 0x16, 0xa5, 0xc1, 0xd4, 0xc8, 0x9c, 0x84, 0x1c, 0x78, 0x01, 0xe8, 0x24, 0x45, - 0xca, 0xff, 0x73, 0xbe, 0x2d, 0xad, 0xfe, 0x74, 0x75, 0x79, 0xa2, 0xba, 0x1f, 0x6a, 0x09, 0x10, 0xc6, 0x50, 0xb3, - 0x6c, 0x4a, 0xe7, 0x9f, 0x5d, 0xbf, 0x26, 0x78, 0xa6, 0x37, 0x07, 0xce, 0xa5, 0x55, 0xaf, 0xef, 0x06, 0xd4, 0x72, - 0x4f, 0x48, 0xdf, 0x15, 0x1b, 0xec, 0xd7, 0x48, 0xa6, 0xe5, 0xbd, 0xf3, 0x85, 0x88, 0xaa, 0x1c, 0x80, 0x11, 0xb4, - 0x51, 0x68, 0xa0, 0xbc, 0xbd, 0xf6, 0xaa, 0x6f, 0x55, 0x94, 0x8e, 0x0d, 0x8c, 0xc0, 0x32, 0xcb, 0xaf, 0x77, 0x27, - 0x94, 0x5c, 0xad, 0xbd, 0x6f, 0x92, 0xf0, 0x18, 0x70, 0x5a, 0x6c, 0x58, 0x38, 0x2f, 0xd9, 0x90, 0xa8, 0xf9, 0x9a, - 0xad, 0xc0, 0x05, 0x0a, 0xeb, 0x98, 0xcb, 0xaa, 0x7a, 0x8b, 0x0a, 0x89, 0xf8, 0x75, 0xfb, 0x7e, 0xc6, 0x7d, 0x4c, - 0x37, 0xc3, 0x0c, 0x86, 0x8d, 0x82, 0x27, 0x18, 0xd7, 0x33, 0xb5, 0x4c, 0x5f, 0x6f, 0xa7, 0x52, 0x3e, 0xdd, 0x19, - 0x97, 0x57, 0x40, 0x5b, 0x29, 0xed, 0xd9, 0x0b, 0xb1, 0xcb, 0x58, 0x04, 0x08, 0x0d, 0x20, 0x51, 0xe9, 0x1b, 0x22, - 0xf6, 0x9a, 0x6f, 0xe8, 0xe9, 0xbc, 0x01, 0x2c, 0x70, 0xf8, 0x86, 0x54, 0x72, 0xa5, 0x38, 0x22, 0xd0, 0xcb, 0x95, - 0xe1, 0xc1, 0xb5, 0xb9, 0x59, 0x01, 0xae, 0xd5, 0x5a, 0x4a, 0xfa, 0x13, 0x37, 0x9b, 0xd6, 0xff, 0x3e, 0x2f, 0xce, - 0xdb, 0xec, 0x44, 0xba, 0xfe, 0x92, 0xe3, 0xe4, 0x4a, 0xe9, 0xd9, 0x82, 0x09, 0x25, 0xcc, 0xb0, 0x86, 0x01, 0x93, - 0xa6, 0xd5, 0x3e, 0x3c, 0x24, 0x1f, 0x50, 0xab, 0x6f, 0xf8, 0x83, 0xe5, 0xff, 0xef, 0xad, 0xb4, 0xdc, 0xfe, 0x88, - 0x74, 0x20, 0x44, 0xf6, 0x00, 0xe4, 0x38, 0xc3, 0x91, 0xf5, 0xfb, 0xce, 0xcc, 0x2a, 0x50, 0x0d, 0x90, 0x6c, 0x59, - 0xbf, 0xd6, 0x62, 0x53, 0x71, 0xcd, 0xbb, 0xcc, 0xef, 0xa2, 0x33, 0x7e, 0x18, 0x56, 0x64, 0x44, 0x66, 0x23, 0x5d, - 0x0d, 0xcb, 0x36, 0xb2, 0x9c, 0x06, 0xf6, 0xbe, 0xf7, 0x7f, 0x14, 0xfe, 0xff, 0x91, 0xc5, 0x89, 0x88, 0x2c, 0x50, - 0x91, 0x59, 0xc5, 0x39, 0x99, 0x05, 0xcc, 0xa8, 0x0a, 0xe0, 0x8c, 0x0a, 0x60, 0x1f, 0x1d, 0x90, 0x63, 0x41, 0xd0, - 0x68, 0x9a, 0x6c, 0xf6, 0xd1, 0x90, 0x2d, 0x6f, 0x77, 0x5a, 0xac, 0x38, 0x94, 0xeb, 0x19, 0xd9, 0x96, 0xfc, 0x6e, - 0x07, 0xca, 0x9a, 0xa5, 0xb4, 0xd2, 0xd1, 0x7d, 0x73, 0x6f, 0x76, 0xca, 0xa9, 0x23, 0x50, 0x69, 0xd5, 0x56, 0x08, - 0xd7, 0xcc, 0xec, 0x06, 0x76, 0x3f, 0xe7, 0x97, 0x36, 0x1f, 0x37, 0xc5, 0xa4, 0x98, 0x94, 0xe0, 0x80, 0xe4, 0x19, - 0x7b, 0xc6, 0x12, 0x0a, 0xc7, 0xf2, 0xde, 0xa9, 0x3f, 0x86, 0xdf, 0x43, 0x69, 0x61, 0x30, 0x75, 0xa6, 0x69, 0xfa, - 0xef, 0xb6, 0x31, 0xab, 0x2f, 0xd7, 0xca, 0xcc, 0x2d, 0xcd, 0x10, 0x42, 0x0a, 0xa0, 0xa2, 0xeb, 0xff, 0x36, 0xbe, - 0xd6, 0x57, 0x8e, 0xda, 0xfa, 0x75, 0xd4, 0xdd, 0x7e, 0x5c, 0x67, 0x08, 0x10, 0x02, 0xed, 0x3c, 0x64, 0x4a, 0x75, - 0xdb, 0x71, 0x9e, 0x3d, 0x4d, 0x08, 0x21, 0x04, 0xe2, 0xa8, 0xe2, 0xf8, 0x5f, 0x23, 0x9d, 0x4a, 0x1b, 0x02, 0x0d, - 0xa4, 0x48, 0xfd, 0x1b, 0xeb, 0x6f, 0x0d, 0xdb, 0xf7, 0xf4, 0x10, 0x9d, 0xd8, 0xd3, 0x00, 0xa1, 0x36, 0x49, 0xbe, - 0xd8, 0x9a, 0xa7, 0xb5, 0x6d, 0x74, 0x2c, 0x43, 0x2d, 0x7f, 0xdf, 0x4c, 0xbf, 0x6d, 0x30, 0x06, 0x2c, 0x33, 0x25, - 0x81, 0x19, 0xf2, 0xb9, 0x6e, 0x04, 0xf7, 0xcf, 0x4c, 0x26, 0xce, 0x1e, 0x07, 0x80, 0xd3, 0xb1, 0x47, 0x5b, 0x88, - 0x19, 0x28, 0xc7, 0xb9, 0x83, 0x9b, 0x5b, 0x23, 0x98, 0xf6, 0xf6, 0x50, 0xb2, 0xdd, 0x47, 0x21, 0xd6, 0x20, 0x5a, - 0x78, 0x61, 0x76, 0xf4, 0x81, 0xc9, 0xdb, 0x4e, 0x15, 0xbd, 0xa5, 0x5b, 0x7e, 0xc2, 0x1c, 0x81, 0x66, 0xf4, 0x92, - 0x5f, 0x04, 0xf0, 0xbe, 0x7d, 0x0f, 0x45, 0x99, 0x04, 0xca, 0xfe, 0xfa, 0xc9, 0x96, 0x91, 0x9d, 0x9f, 0xb8, 0x6b, - 0x7b, 0xc3, 0xe6, 0xe0, 0x21, 0x83, 0x27, 0x75, 0x98, 0xd9, 0x7e, 0x29, 0x3f, 0xe1, 0x6a, 0x19, 0xe6, 0x36, 0xe6, - 0xf3, 0xfd, 0x14, 0x5d, 0xa1, 0x21, 0x63, 0x41, 0xea, 0xb9, 0xf7, 0x87, 0xc6, 0xf2, 0xc7, 0x64, 0x59, 0x06, 0x6e, - 0xcb, 0x89, 0xc7, 0x92, 0xfa, 0xc5, 0x52, 0x4d, 0xdf, 0x93, 0x26, 0x01, 0x80, 0xac, 0xb5, 0x0d, 0xfd, 0x08, 0x57, - 0x71, 0x7d, 0xad, 0x5c, 0x14, 0xe3, 0x9a, 0x6f, 0x87, 0x09, 0x06, 0x92, 0xf5, 0x13, 0x28, 0x6e, 0x7b, 0xf7, 0x6f, - 0xcf, 0x6e, 0x6d, 0x59, 0x64, 0xd4, 0x74, 0x38, 0xbb, 0x0f, 0x9d, 0x69, 0x03, 0x8a, 0xb8, 0xc3, 0xdd, 0xa7, 0x63, - 0x4d, 0x41, 0x62, 0xc3, 0x21, 0x83, 0xe7, 0x02, 0x1d, 0xcb, 0x3e, 0xa9, 0xa5, 0x24, 0x6b, 0x72, 0xe6, 0xd2, 0x10, - 0x66, 0xa3, 0x73, 0x76, 0x1b, 0x4b, 0x07, 0xdc, 0x96, 0x33, 0xda, 0x45, 0x26, 0xf7, 0xbc, 0x89, 0xef, 0x68, 0xcc, - 0x2f, 0xbb, 0xc0, 0xde, 0xfa, 0x20, 0xdb, 0x42, 0xd0, 0x6c, 0xcc, 0xec, 0xc0, 0x37, 0xde, 0x77, 0xd3, 0x21, 0x09, - 0x12, 0x4d, 0xdf, 0xb7, 0xa0, 0x79, 0x31, 0xfa, 0xb8, 0xee, 0x61, 0xab, 0x50, 0xdf, 0xfe, 0x88, 0x87, 0x19, 0x9e, - 0x78, 0x39, 0x23, 0x5f, 0xef, 0xdd, 0xe6, 0x65, 0xe7, 0xd1, 0x17, 0x31, 0xbe, 0x3d, 0xbb, 0x7d, 0xb9, 0x79, 0x64, - 0xf7, 0x11, 0xd6, 0xbe, 0x1b, 0x12, 0x76, 0x35, 0xbf, 0x77, 0x1b, 0x7e, 0xf4, 0xda, 0x4b, 0x35, 0xab, 0xc9, 0x7f, - 0xfa, 0xfe, 0x13, 0xda, 0xa8, 0x1d, 0xdc, 0xc3, 0xfd, 0x05, 0xc2, 0xb3, 0xa6, 0x38, 0x17, 0x58, 0x73, 0x18, 0x7f, - 0xb6, 0x66, 0xc9, 0x3f, 0x3b, 0x22, 0xf4, 0x39, 0xcc, 0xd7, 0x39, 0x6f, 0xcf, 0x62, 0x6a, 0xfd, 0x4a, 0x44, 0x61, - 0x22, 0x2a, 0x83, 0x26, 0x28, 0xca, 0x2b, 0x27, 0x7d, 0xc7, 0x45, 0x49, 0x20, 0x91, 0xdd, 0x52, 0x2b, 0xa5, 0x75, - 0xe1, 0xe4, 0xde, 0xef, 0x00, 0x02, 0xfd, 0x39, 0xb6, 0x2c, 0xbb, 0xe4, 0xf5, 0x4b, 0x49, 0x7d, 0xc7, 0x3c, 0xcd, - 0x3d, 0x00, 0x91, 0x0a, 0xdd, 0x2c, 0x65, 0x61, 0x89, 0x12, 0x79, 0x36, 0x9e, 0xeb, 0xbc, 0xca, 0xd0, 0x43, 0xb7, - 0xef, 0xdb, 0x25, 0xf2, 0xf0, 0x7c, 0x86, 0x03, 0x7c, 0xc7, 0x9c, 0x53, 0xbd, 0xc2, 0x84, 0xbc, 0x72, 0x56, 0xdc, - 0x8f, 0xa1, 0xd4, 0x68, 0x22, 0x56, 0xe4, 0x66, 0x34, 0xc8, 0x7b, 0x46, 0x6e, 0xaa, 0xfd, 0x52, 0x5b, 0x33, 0x33, - 0xd7, 0xb7, 0xad, 0x96, 0x71, 0x86, 0x72, 0x2f, 0xaa, 0x3a, 0xaa, 0xef, 0x49, 0x20, 0x3d, 0xcb, 0x19, 0xd7, 0x37, - 0x6f, 0x56, 0x94, 0x5f, 0x2b, 0x95, 0x50, 0xf6, 0x0d, 0x35, 0x79, 0xcd, 0xec, 0x4b, 0xea, 0x1a, 0xe6, 0x29, 0xd2, - 0x76, 0x3a, 0xf2, 0x5c, 0x14, 0x32, 0x68, 0x05, 0x7b, 0x9e, 0x3c, 0xba, 0xf6, 0x65, 0x5e, 0xe2, 0x2f, 0x2b, 0xcb, - 0x88, 0x04, 0x68, 0xe4, 0x5f, 0x10, 0xcd, 0x0c, 0x54, 0xc5, 0xd3, 0x64, 0xe1, 0x24, 0x68, 0x71, 0xa2, 0x0d, 0x9e, - 0xfd, 0x7e, 0xdb, 0xec, 0x83, 0x12, 0x2e, 0xaa, 0xd4, 0x7d, 0x4f, 0x13, 0xf2, 0xf0, 0x73, 0x91, 0xac, 0x65, 0xa0, - 0xd7, 0x60, 0x9e, 0x26, 0x20, 0x15, 0x96, 0xc9, 0x58, 0xe8, 0x82, 0x9a, 0xd1, 0x4d, 0x93, 0xe9, 0x11, 0x5f, 0xde, - 0xe2, 0xa2, 0xb8, 0xd5, 0x6a, 0xcb, 0x37, 0xb3, 0xb4, 0x63, 0xda, 0x02, 0xda, 0x1f, 0x3a, 0x81, 0x27, 0x6c, 0x1e, - 0x0b, 0xef, 0x26, 0x27, 0x23, 0x8c, 0x3f, 0xf7, 0x72, 0xa4, 0x1d, 0x6a, 0x77, 0x42, 0xf4, 0xea, 0x40, 0xe0, 0xbe, - 0x50, 0xb6, 0x62, 0xec, 0x4d, 0x12, 0x51, 0xdd, 0x7f, 0x40, 0xb7, 0xf6, 0xbf, 0x59, 0xbb, 0xf4, 0x45, 0xd0, 0x58, - 0x09, 0x47, 0xd2, 0xfa, 0x6d, 0xa8, 0x3d, 0x44, 0x00, 0x4e, 0xaf, 0x82, 0x19, 0x76, 0xdb, 0x80, 0xd9, 0x71, 0x51, - 0x8e, 0xfb, 0x7c, 0xc2, 0x32, 0x3f, 0xd0, 0x32, 0xba, 0xa9, 0xfd, 0x71, 0xfc, 0x21, 0x63, 0x96, 0xb6, 0x30, 0xb5, - 0xaf, 0x1a, 0x7f, 0x69, 0xd0, 0x48, 0xc5, 0x79, 0xed, 0xd0, 0xc0, 0x4f, 0xec, 0x0b, 0x96, 0x83, 0x23, 0x41, 0x2f, - 0xf3, 0xc1, 0x33, 0x97, 0x6a, 0xb0, 0xbc, 0x39, 0x72, 0xa0, 0x10, 0xb6, 0x14, 0xa1, 0x6c, 0xb0, 0xb5, 0xd2, 0x8a, - 0x55, 0x4f, 0x18, 0x9b, 0xf7, 0xa7, 0xb7, 0x26, 0xb2, 0x29, 0x5b, 0x38, 0x4c, 0xf5, 0x85, 0x82, 0x64, 0x7f, 0x16, - 0x77, 0x59, 0x26, 0xc0, 0x41, 0xc2, 0xf0, 0x82, 0x8e, 0x24, 0xdf, 0x07, 0x6f, 0xfa, 0x2c, 0x58, 0x18, 0x6d, 0x9b, - 0x1f, 0x6d, 0xbd, 0x17, 0xcf, 0x2c, 0x88, 0x6f, 0x16, 0x14, 0xdb, 0xb2, 0xe2, 0x08, 0x4b, 0x35, 0x6c, 0x42, 0x68, - 0x05, 0x11, 0xa8, 0xa9, 0x3f, 0xd7, 0x83, 0x6d, 0xd6, 0xbe, 0x6a, 0x55, 0xa5, 0xb3, 0x24, 0xd5, 0x38, 0x82, 0x42, - 0x0e, 0x06, 0x45, 0x10, 0xba, 0xb3, 0x7b, 0xf0, 0x13, 0x07, 0xe3, 0x42, 0xe0, 0x86, 0x96, 0xa7, 0xae, 0x5b, 0xc7, - 0x2d, 0x03, 0x53, 0x7d, 0xb9, 0xd2, 0x3c, 0xee, 0xa1, 0x75, 0xb0, 0x6b, 0x3b, 0x92, 0xcf, 0xb5, 0x78, 0x42, 0xdf, - 0x9d, 0xe8, 0x04, 0x86, 0x87, 0x23, 0x4f, 0xbc, 0x3c, 0x90, 0x80, 0x87, 0x00, 0xb3, 0xf2, 0xdd, 0x0f, 0xa1, 0x7a, - 0x7d, 0x11, 0x50, 0x40, 0x7c, 0x55, 0x6e, 0xfb, 0x03, 0x04, 0x2b, 0xea, 0x58, 0x00, 0x27, 0x2a, 0x4e, 0xc9, 0x01, - 0x09, 0xc6, 0x11, 0xea, 0x1b, 0xa0, 0x9b, 0x0f, 0x95, 0xf2, 0x2f, 0x5c, 0x4f, 0xbc, 0x28, 0xcf, 0xfa, 0xf9, 0x96, - 0x7c, 0xba, 0x6a, 0x51, 0x70, 0xaa, 0xb6, 0x49, 0xa6, 0x50, 0xba, 0xc1, 0x61, 0x4a, 0x11, 0xa7, 0x89, 0x44, 0x2d, - 0x04, 0x60, 0x5b, 0x45, 0xb4, 0xf4, 0xeb, 0x75, 0xd8, 0xd1, 0x52, 0xd8, 0xb2, 0xe0, 0x0b, 0xf5, 0x43, 0x8d, 0xb0, - 0xd8, 0xa8, 0xed, 0x5c, 0x6a, 0xfe, 0xf3, 0x35, 0xc9, 0x86, 0xd6, 0x2e, 0x7b, 0x0b, 0x41, 0x4d, 0xe1, 0x02, 0xb5, - 0xe5, 0x45, 0x32, 0xb5, 0x83, 0x98, 0xfd, 0xc8, 0x44, 0x72, 0x62, 0x79, 0x65, 0x6f, 0x2a, 0x5b, 0xd7, 0xa6, 0xdd, - 0x37, 0x25, 0x18, 0x7e, 0xd4, 0x52, 0x7a, 0x36, 0xec, 0xfc, 0x5a, 0x59, 0x7a, 0x0c, 0x6b, 0x67, 0x4a, 0x20, 0x70, - 0xf9, 0x17, 0xa7, 0xed, 0x02, 0x93, 0x5b, 0x3d, 0xa6, 0xf4, 0x52, 0x8f, 0xf9, 0xee, 0x75, 0x48, 0x95, 0x2c, 0x04, - 0xc9, 0x61, 0xa0, 0xbf, 0x16, 0x13, 0xa5, 0x40, 0x0b, 0x89, 0x50, 0x6e, 0x23, 0x09, 0x70, 0xff, 0x5e, 0x95, 0x31, - 0xc0, 0xb6, 0x0e, 0x3f, 0x4b, 0xb3, 0xab, 0xe7, 0x62, 0x40, 0xd8, 0x18, 0x3d, 0x4c, 0x9d, 0x11, 0xc2, 0x49, 0x53, - 0x7b, 0xe7, 0x2a, 0x12, 0xc9, 0x51, 0x8a, 0x58, 0x6c, 0x9c, 0x95, 0x2e, 0x35, 0xc2, 0x5a, 0x18, 0xcb, 0xb1, 0x02, - 0x92, 0x33, 0xb7, 0x59, 0x79, 0x7c, 0xdb, 0x1c, 0x24, 0xc0, 0x3d, 0xe2, 0xe2, 0x1f, 0x9c, 0x04, 0xc8, 0x35, 0x41, - 0x82, 0x84, 0xf6, 0xd9, 0x80, 0x4c, 0x18, 0x90, 0x91, 0x31, 0x89, 0x6e, 0x04, 0x92, 0x7b, 0x4d, 0xa7, 0xfa, 0x18, - 0x60, 0x2a, 0x27, 0xab, 0x09, 0x44, 0x42, 0x1c, 0x6f, 0x6a, 0xc3, 0x4e, 0x60, 0x2c, 0x03, 0xec, 0xb8, 0x74, 0x54, - 0x72, 0x2e, 0x0e, 0x30, 0xac, 0x9a, 0xf9, 0x85, 0x4d, 0x97, 0xc0, 0x10, 0x47, 0xb4, 0x0b, 0x28, 0xc8, 0xa9, 0xeb, - 0x73, 0x0b, 0x46, 0x0e, 0x12, 0xd7, 0xe8, 0x4e, 0x8b, 0x64, 0x14, 0x91, 0x97, 0xc4, 0xb8, 0xf8, 0x75, 0x4c, 0x93, - 0xb8, 0x58, 0x4f, 0x73, 0x9f, 0x8a, 0xde, 0xb9, 0x0d, 0xfe, 0x71, 0xa3, 0x27, 0x5d, 0x27, 0x2c, 0x01, 0x58, 0x8f, - 0x94, 0x64, 0xc0, 0xa5, 0x9a, 0x9e, 0xfc, 0x3a, 0x48, 0x09, 0xbc, 0x72, 0xd0, 0x39, 0xc4, 0xf1, 0x85, 0x12, 0xd1, - 0xe0, 0xdf, 0xe4, 0xc8, 0x23, 0xf0, 0xeb, 0x67, 0xcc, 0x30, 0xcd, 0x12, 0xd8, 0x63, 0xe5, 0x99, 0x88, 0xf9, 0xab, - 0x46, 0xd2, 0x48, 0x58, 0xf1, 0xb4, 0xd5, 0xd2, 0x54, 0xf8, 0xd8, 0x08, 0x05, 0xc2, 0x1e, 0x80, 0xa6, 0x00, 0xde, - 0x1b, 0x12, 0xf3, 0xe5, 0xa9, 0xa6, 0x24, 0xe5, 0x29, 0x3a, 0x9b, 0xb3, 0x60, 0xfa, 0x2c, 0x2c, 0xa0, 0x9b, 0x63, - 0xca, 0xc3, 0x4d, 0x2c, 0xf3, 0x32, 0xcc, 0x94, 0xcc, 0xa8, 0x25, 0x98, 0x08, 0x93, 0xe1, 0x75, 0x72, 0x01, 0xcb, - 0x7b, 0x9b, 0x66, 0xc6, 0x2d, 0xa3, 0x57, 0xae, 0x4f, 0xa0, 0x79, 0xdc, 0x93, 0x65, 0x91, 0xa6, 0x0c, 0x57, 0x38, - 0x00, 0xe9, 0xaf, 0x98, 0xc7, 0xc2, 0x29, 0x35, 0x2b, 0xb9, 0x71, 0xc3, 0xc5, 0x42, 0x6a, 0x5c, 0xdd, 0x95, 0x77, - 0x22, 0x04, 0x48, 0x65, 0x4b, 0x27, 0x83, 0x67, 0x40, 0xf1, 0x9e, 0x00, 0x02, 0x11, 0x8d, 0xc2, 0x67, 0x7e, 0x92, - 0xa3, 0x55, 0x4e, 0x10, 0x0b, 0x73, 0x55, 0x3b, 0x2f, 0xde, 0x2a, 0x44, 0x94, 0x0b, 0x8e, 0x36, 0x00, 0x5b, 0xb4, - 0x2f, 0x72, 0x9f, 0x41, 0xc0, 0xb7, 0x8f, 0x33, 0xc3, 0x5c, 0x9a, 0x76, 0x48, 0x95, 0xcf, 0xc6, 0xe1, 0xe7, 0xb2, - 0xc0, 0x33, 0x4e, 0x99, 0xd8, 0xfd, 0x67, 0xab, 0xba, 0x21, 0xea, 0xfc, 0x35, 0x75, 0xc0, 0xa9, 0xb6, 0xd9, 0xd9, - 0x8d, 0x41, 0x97, 0xc5, 0xc9, 0x01, 0x93, 0xb2, 0xb3, 0x75, 0xb0, 0xa2, 0x1c, 0xff, 0x72, 0xf2, 0x03, 0x19, 0x0b, - 0x34, 0x5e, 0x15, 0x44, 0x45, 0x46, 0xa6, 0x03, 0x41, 0xbc, 0x34, 0x7c, 0x2e, 0x06, 0x68, 0x91, 0x79, 0x55, 0x52, - 0xa0, 0xd0, 0xac, 0x46, 0x94, 0x37, 0xd0, 0x20, 0x9b, 0xbb, 0x9a, 0x6a, 0x14, 0x1c, 0x21, 0x8f, 0x5a, 0x6c, 0x4c, - 0x8f, 0xc5, 0x52, 0x4c, 0xcb, 0xb4, 0x39, 0x45, 0x12, 0x81, 0xc5, 0x01, 0x71, 0xfd, 0x59, 0xa9, 0xb1, 0x41, 0x94, - 0x79, 0xde, 0x8c, 0x30, 0xe8, 0x6e, 0xe8, 0x49, 0x36, 0x31, 0xd6, 0x4a, 0x10, 0x39, 0x75, 0xd0, 0xd8, 0x8f, 0x5d, - 0x9f, 0xc8, 0xdb, 0x5d, 0x53, 0x4c, 0x75, 0xb9, 0x3f, 0x51, 0x4c, 0x0c, 0x2d, 0xed, 0xfa, 0x79, 0x06, 0x51, 0x3f, - 0x3d, 0x78, 0x9a, 0x72, 0xb6, 0xf6, 0x18, 0x1e, 0xaa, 0xce, 0x25, 0x79, 0xbf, 0xd4, 0x0d, 0x01, 0xf9, 0xb5, 0xc0, - 0xea, 0x91, 0x93, 0x88, 0x22, 0x10, 0xf6, 0xb3, 0xfe, 0x96, 0x30, 0xfa, 0x9b, 0x81, 0x25, 0xbb, 0x1e, 0x6c, 0x4b, - 0x5d, 0x62, 0x2c, 0x6b, 0x65, 0xcd, 0x29, 0x30, 0x5c, 0xba, 0x54, 0x4e, 0x1e, 0x48, 0x3c, 0x54, 0x0e, 0x16, 0xd3, - 0xe7, 0xe9, 0xc2, 0x01, 0x23, 0x85, 0xec, 0xfd, 0x34, 0xc8, 0x8b, 0xd3, 0x8b, 0x24, 0xb5, 0x18, 0x31, 0x76, 0xa9, - 0xb6, 0xb1, 0xf4, 0x08, 0xab, 0xb6, 0x36, 0xf0, 0xfc, 0xc4, 0x76, 0xbb, 0xdd, 0xb0, 0x53, 0x41, 0x56, 0x52, 0x27, - 0x72, 0x33, 0x8b, 0xce, 0x8f, 0x26, 0x91, 0xca, 0x13, 0x46, 0x01, 0x79, 0x39, 0x63, 0x5b, 0x20, 0x8b, 0x6e, 0x8a, - 0x5e, 0x18, 0xe3, 0xd6, 0xb3, 0x5c, 0x7d, 0xeb, 0x37, 0x38, 0x14, 0x92, 0x32, 0x35, 0xcd, 0x94, 0x7b, 0x9d, 0xcd, - 0x77, 0x55, 0x44, 0x8b, 0x72, 0xd6, 0x5c, 0x9b, 0x5f, 0x24, 0xab, 0xbd, 0x14, 0xd9, 0xd2, 0xe9, 0x30, 0x7b, 0x97, - 0x8a, 0xd4, 0x92, 0x04, 0xaa, 0x1d, 0xc7, 0x66, 0x1c, 0xe7, 0x18, 0xf4, 0x56, 0x30, 0xb3, 0x86, 0x97, 0x80, 0xec, - 0x22, 0x85, 0x45, 0x56, 0x5a, 0xbc, 0xd1, 0x2d, 0x25, 0xef, 0x07, 0x89, 0xca, 0xd2, 0xc3, 0x30, 0x93, 0xbe, 0x14, - 0xf6, 0x80, 0x14, 0x8f, 0x50, 0x45, 0x98, 0xbb, 0xdf, 0x45, 0x50, 0xa0, 0x3a, 0xc3, 0x13, 0x98, 0xf6, 0x7c, 0x94, - 0x8e, 0x0b, 0x98, 0x9f, 0xcd, 0x44, 0xbb, 0x7a, 0xb5, 0x06, 0x2c, 0xbc, 0xfa, 0x90, 0xe2, 0x3a, 0xa5, 0xb7, 0xf2, - 0x55, 0xf8, 0x1c, 0x2b, 0xcf, 0x02, 0x1d, 0x2b, 0x15, 0x86, 0xd9, 0x5c, 0x18, 0x23, 0x49, 0x9a, 0x0f, 0xe7, 0xde, - 0xa0, 0x9b, 0x21, 0x28, 0x11, 0x53, 0xdc, 0x10, 0x62, 0x31, 0xcc, 0x58, 0x03, 0xca, 0xdd, 0xa2, 0x99, 0x93, 0xac, - 0xb9, 0x93, 0x49, 0xce, 0x7c, 0xf7, 0x95, 0x5e, 0xa5, 0x94, 0x10, 0x4d, 0xc7, 0x57, 0x39, 0x59, 0x3e, 0x46, 0xc3, - 0x2c, 0xae, 0x1c, 0x13, 0xb4, 0x4e, 0xe2, 0x84, 0xa2, 0x70, 0x48, 0x50, 0x5b, 0x61, 0xba, 0x53, 0x23, 0x9c, 0x0a, - 0x7a, 0x3f, 0xe9, 0xe6, 0x0e, 0xba, 0x13, 0xdb, 0x50, 0xd1, 0x9a, 0x86, 0x0a, 0x62, 0xdb, 0xbf, 0xf7, 0x33, 0x3a, - 0x74, 0xfc, 0x56, 0x34, 0xa6, 0x42, 0xa0, 0x66, 0x0e, 0x97, 0xe7, 0xbe, 0x98, 0x14, 0xe2, 0x4a, 0x5a, 0x9e, 0x08, - 0x92, 0xb4, 0x8f, 0x8d, 0x79, 0xb1, 0xb7, 0x83, 0xc2, 0x34, 0xac, 0xcb, 0x06, 0x44, 0xad, 0x17, 0x2a, 0xf3, 0xeb, - 0xb6, 0x8c, 0x3b, 0x4d, 0x18, 0xaf, 0x9b, 0x81, 0x98, 0xd7, 0xa0, 0x8d, 0x18, 0x8c, 0x55, 0x3b, 0x0e, 0x40, 0x39, - 0x3a, 0x2d, 0x1b, 0xeb, 0x4b, 0xab, 0x46, 0x6f, 0x68, 0x0c, 0x6c, 0xd7, 0x62, 0x91, 0x04, 0xa4, 0x30, 0x66, 0xdd, - 0x8d, 0x49, 0xa7, 0x0a, 0xea, 0x81, 0xec, 0x59, 0x9f, 0x75, 0x3c, 0x4b, 0x4c, 0xf8, 0x25, 0x03, 0x47, 0xf3, 0xe9, - 0xa4, 0x97, 0xae, 0x89, 0x8e, 0x68, 0x6d, 0x21, 0xfe, 0xd4, 0xe0, 0xd6, 0xe2, 0xa5, 0x0e, 0x7c, 0x05, 0xa0, 0x16, - 0x99, 0x0a, 0x21, 0x51, 0x25, 0x15, 0x57, 0x65, 0x3c, 0xd8, 0x94, 0xeb, 0x2a, 0xac, 0x7c, 0x72, 0xef, 0x7a, 0x07, - 0x7f, 0xb6, 0x07, 0x4b, 0xeb, 0x0e, 0xf3, 0xc1, 0xc9, 0x5f, 0x45, 0x48, 0x11, 0xf6, 0xcc, 0xd0, 0xc1, 0xc6, 0x3c, - 0x73, 0xe4, 0xe3, 0xb5, 0x1d, 0xf1, 0xad, 0x0b, 0x6f, 0x98, 0xe4, 0xee, 0x3d, 0x72, 0x19, 0xda, 0x52, 0x40, 0xd4, - 0x6d, 0x6e, 0xfb, 0x83, 0x74, 0xfc, 0x49, 0x4a, 0xf1, 0xef, 0x5d, 0x05, 0x51, 0xbb, 0x68, 0x21, 0x79, 0xa7, 0xe7, - 0xc0, 0x1a, 0x8c, 0x26, 0x8d, 0x11, 0x4c, 0xef, 0x01, 0x97, 0x8a, 0xe2, 0xfc, 0xd1, 0x49, 0x98, 0x70, 0xe2, 0x19, - 0xe0, 0x2f, 0x8d, 0x49, 0xd8, 0x16, 0x01, 0x77, 0xbb, 0x18, 0xff, 0xa2, 0xdd, 0x84, 0x41, 0xde, 0x5d, 0xdf, 0x91, - 0x7e, 0xc4, 0x3d, 0x6c, 0x2e, 0xfb, 0x5b, 0x5e, 0x8a, 0x56, 0xa2, 0x8a, 0x10, 0xa6, 0x46, 0x42, 0x43, 0x9d, 0x23, - 0x81, 0x38, 0xa6, 0x89, 0x35, 0xcc, 0xf6, 0x93, 0xf5, 0x61, 0x97, 0x0a, 0xa1, 0x50, 0x04, 0xa2, 0x33, 0x84, 0x1b, - 0x75, 0x9e, 0x30, 0xc0, 0x3b, 0x04, 0xa0, 0x25, 0xe8, 0xc7, 0x10, 0x9f, 0x5b, 0x27, 0x84, 0xe6, 0x62, 0x9e, 0x3e, - 0x66, 0x0a, 0x4a, 0x52, 0x7d, 0x2d, 0x6f, 0xb3, 0xe6, 0x05, 0x67, 0x2a, 0x2e, 0xa0, 0x68, 0x2b, 0x9e, 0xfa, 0xef, - 0x98, 0x8a, 0x3e, 0x8a, 0x4e, 0x6d, 0xcc, 0x69, 0x9e, 0x30, 0xe7, 0x88, 0x7e, 0xa0, 0xee, 0xc6, 0xf5, 0x6e, 0xc3, - 0x9d, 0xca, 0x12, 0xca, 0x32, 0xf4, 0xba, 0x65, 0xba, 0x94, 0xe4, 0x70, 0x8e, 0xf3, 0xfc, 0x57, 0x0c, 0x71, 0xff, - 0x35, 0xc7, 0xa7, 0xf7, 0x59, 0xda, 0x65, 0x7e, 0xf4, 0xe0, 0xa5, 0x05, 0x66, 0x76, 0xc6, 0x6e, 0x1e, 0xf6, 0x58, - 0x47, 0x02, 0x3b, 0xe2, 0x18, 0xea, 0x1a, 0x67, 0xbc, 0xde, 0x8a, 0x78, 0xa0, 0xb6, 0x1e, 0x6c, 0xbc, 0xa7, 0x34, - 0x4c, 0xb7, 0xa4, 0x8b, 0xac, 0x6b, 0xa2, 0xb2, 0xdf, 0x1f, 0x22, 0xbb, 0xa7, 0xc7, 0x93, 0x3a, 0x69, 0x53, 0x54, - 0x2c, 0x81, 0xce, 0x8d, 0x43, 0xff, 0xe4, 0x2c, 0xcc, 0x63, 0xe8, 0x98, 0xc9, 0x38, 0x5b, 0x67, 0x8c, 0xe7, 0xf6, - 0x33, 0x89, 0xb4, 0x93, 0x81, 0xdf, 0x29, 0x92, 0x9f, 0x7f, 0x58, 0x80, 0x46, 0x14, 0x82, 0xda, 0xed, 0x07, 0x0a, - 0xc5, 0xb1, 0xef, 0x7f, 0x84, 0xb5, 0x7d, 0x8f, 0xd8, 0x85, 0x5d, 0x2c, 0x01, 0xc4, 0x6e, 0x6c, 0xec, 0xff, 0x75, - 0x77, 0xab, 0x91, 0x0d, 0xab, 0x0f, 0x24, 0xd4, 0x57, 0x7b, 0xe6, 0xd9, 0x35, 0xcf, 0x8d, 0x08, 0xce, 0x44, 0x47, - 0xa0, 0x5e, 0xb5, 0xb9, 0xfe, 0x9b, 0xa4, 0xbb, 0x88, 0x22, 0x08, 0x56, 0xd6, 0x20, 0x6b, 0x36, 0xb2, 0xa0, 0x54, - 0xdc, 0x91, 0x1b, 0x7b, 0xbc, 0xc7, 0xf7, 0x76, 0x12, 0x4d, 0x19, 0x25, 0xd7, 0xaa, 0x29, 0x27, 0x0e, 0x63, 0x77, - 0x6f, 0x3c, 0x6b, 0x35, 0x5e, 0x28, 0xfe, 0xa6, 0x06, 0x61, 0xc5, 0x8d, 0xe3, 0x66, 0x83, 0x70, 0xff, 0x6c, 0x51, - 0xa7, 0x6b, 0x91, 0xf1, 0xbc, 0x5a, 0xaf, 0x7d, 0xb6, 0x1b, 0xa9, 0x26, 0xb5, 0x27, 0x34, 0x37, 0x62, 0x9b, 0x77, - 0xdc, 0x6d, 0xc0, 0xe7, 0xc0, 0xc5, 0xd4, 0x91, 0x78, 0xef, 0x51, 0x2f, 0x59, 0xb3, 0xd7, 0x5b, 0x13, 0x1e, 0x1c, - 0x7a, 0x65, 0xb7, 0x7a, 0x22, 0x3e, 0x9f, 0x56, 0xff, 0x74, 0x7f, 0x27, 0x3f, 0xbb, 0x97, 0xb7, 0x6a, 0xca, 0x51, - 0xfa, 0xd4, 0xc4, 0x26, 0x7b, 0x3d, 0x6b, 0x1c, 0xde, 0x6a, 0xdc, 0xee, 0xa4, 0x1b, 0x4d, 0xfb, 0x2f, 0x97, 0x32, - 0x5b, 0xd0, 0x2c, 0x0f, 0x7e, 0x0e, 0x16, 0xdf, 0xb3, 0xd0, 0x7b, 0x15, 0x31, 0xa7, 0x6c, 0x70, 0x48, 0x55, 0x33, - 0xfd, 0x30, 0xde, 0x89, 0x26, 0x6e, 0x3d, 0xda, 0x25, 0xee, 0xa2, 0x46, 0x9c, 0xe4, 0x01, 0xd6, 0x5c, 0xef, 0x0a, - 0xa6, 0xd4, 0x2e, 0xff, 0x04, 0xd2, 0xe2, 0xa5, 0x09, 0x9b, 0xb4, 0xa9, 0xb5, 0x4d, 0x1b, 0xae, 0x02, 0xcb, 0x3f, - 0x16, 0xdb, 0x7c, 0x57, 0xf5, 0x9b, 0x6e, 0xae, 0xf2, 0xf5, 0xcf, 0xe0, 0xc7, 0x6a, 0xaf, 0xe5, 0xa7, 0xff, 0xe1, - 0xfb, 0xff, 0x6a, 0xdc, 0x09, 0x66, 0xbb, 0x39, 0x0b, 0xbe, 0x6a, 0x70, 0x91, 0x65, 0xc3, 0xe7, 0x49, 0xf9, 0xff, - 0xea, 0x7a, 0xf7, 0x8f, 0xab, 0x7f, 0xbf, 0x68, 0x06, 0xf3, 0x11, 0x57, 0x9e, 0x49, 0x5d, 0x9c, 0xfd, 0x37, 0xc4, - 0xd5, 0xd3, 0x7b, 0x1f, 0xb1, 0xc6, 0x15, 0xfb, 0xcd, 0x6e, 0xb2, 0x6c, 0x16, 0x55, 0x96, 0xf0, 0xd4, 0xab, 0x9a, - 0x5d, 0x41, 0x90, 0xcf, 0x7b, 0x9d, 0xcd, 0x47, 0x87, 0x8f, 0x35, 0xa6, 0x7d, 0xa6, 0xdc, 0xfb, 0xfc, 0xc2, 0xf3, - 0x8b, 0x59, 0x39, 0x9e, 0xf7, 0xbc, 0xf4, 0x6a, 0xae, 0xec, 0xc7, 0x9a, 0xe4, 0x4c, 0x67, 0x70, 0xfe, 0x59, 0x39, - 0xbf, 0xf8, 0x7c, 0x7f, 0x76, 0x5f, 0xbe, 0xcc, 0x50, 0xc3, 0x31, 0xcf, 0xac, 0xf9, 0x88, 0x77, 0xa2, 0x56, 0x67, - 0xf1, 0xad, 0xd9, 0xcd, 0xf1, 0x64, 0xc5, 0x11, 0xae, 0xd0, 0x63, 0x3b, 0xac, 0xfc, 0x5d, 0x4f, 0x0d, 0x71, 0xc1, - 0xd0, 0x04, 0x12, 0x0f, 0xfd, 0xdf, 0xc1, 0xd0, 0x0f, 0xbd, 0x97, 0x9f, 0x4d, 0x1a, 0x0e, 0x80, 0xb0, 0xab, 0x96, - 0xc2, 0x6b, 0xa5, 0x3a, 0x62, 0x1b, 0x75, 0x54, 0xf8, 0x40, 0x45, 0x3b, 0x47, 0xdf, 0x5b, 0xde, 0xeb, 0x77, 0xa1, - 0xf4, 0xe0, 0xfa, 0xe1, 0xf1, 0x6b, 0xb1, 0x2e, 0x03, 0xc8, 0x16, 0xc1, 0xe9, 0x86, 0x77, 0xdb, 0xdb, 0x86, 0xe1, - 0x14, 0xaa, 0x3a, 0x51, 0x59, 0x53, 0xeb, 0xcc, 0xbe, 0x8f, 0x16, 0xdd, 0x00, 0x3c, 0x1f, 0xef, 0x13, 0x42, 0xf6, - 0x2e, 0xd2, 0x73, 0xd2, 0x09, 0x23, 0x10, 0xd8, 0x22, 0x0a, 0x6c, 0x1a, 0xe4, 0x7d, 0x7c, 0x87, 0x6e, 0x48, 0xcb, - 0xcf, 0x68, 0xae, 0x3e, 0x77, 0x31, 0xa5, 0xec, 0xe1, 0x6a, 0x6e, 0xb5, 0x4f, 0x01, 0x24, 0xb1, 0x4d, 0x08, 0x58, - 0xfe, 0x93, 0xc7, 0x4d, 0xc3, 0xd6, 0x9b, 0x74, 0xe9, 0x85, 0xd4, 0x9d, 0x9a, 0x34, 0x65, 0x84, 0x91, 0xe9, 0x85, - 0xe7, 0x15, 0x9d, 0x70, 0x97, 0x63, 0xa2, 0x57, 0x8c, 0x8c, 0x59, 0x71, 0xa7, 0xde, 0xb6, 0xbc, 0xfe, 0x5c, 0x07, - 0x41, 0x48, 0x37, 0x36, 0xa4, 0xcb, 0xcc, 0xdd, 0x2b, 0xb8, 0x99, 0x11, 0xb0, 0x79, 0x09, 0x75, 0xc6, 0xef, 0x99, - 0x24, 0xd1, 0x9d, 0x35, 0x4d, 0x9c, 0xf1, 0xb7, 0xd5, 0x6c, 0xa1, 0x8a, 0xc4, 0x0b, 0x73, 0x03, 0x12, 0x84, 0xfa, - 0x86, 0x5a, 0x61, 0xd5, 0x37, 0x98, 0xb4, 0x1f, 0x38, 0x39, 0xcf, 0x34, 0x83, 0x4e, 0xe3, 0x7d, 0x1d, 0x33, 0x26, - 0xba, 0x9a, 0xa2, 0x50, 0x18, 0x6d, 0xe7, 0xcf, 0xe2, 0x66, 0x96, 0xf5, 0x60, 0x02, 0xf0, 0x36, 0x0f, 0xa0, 0x9f, - 0xd7, 0x0a, 0x66, 0xf2, 0x06, 0x3f, 0xfc, 0x12, 0x06, 0x64, 0x7c, 0xe8, 0x41, 0xce, 0xe6, 0x27, 0x05, 0xfd, 0xc7, - 0x20, 0x5e, 0x77, 0x96, 0xb3, 0x5b, 0x42, 0x6b, 0x29, 0xd6, 0x8a, 0x71, 0x96, 0x91, 0xeb, 0xd4, 0x6f, 0xea, 0x2a, - 0x99, 0xaf, 0xed, 0xea, 0x72, 0xf1, 0x6d, 0x4f, 0xee, 0x30, 0x38, 0x05, 0xbc, 0xa0, 0xa1, 0x20, 0x66, 0x2a, 0x3f, - 0xaf, 0xce, 0x6e, 0x28, 0xf5, 0x41, 0x32, 0x7d, 0xae, 0xf0, 0xaa, 0x1a, 0xff, 0x85, 0x45, 0x5f, 0x6a, 0x25, 0xf4, - 0x17, 0x60, 0xf8, 0xd0, 0xb6, 0xcd, 0x84, 0x67, 0x67, 0x0b, 0xf7, 0x34, 0xf9, 0x54, 0x73, 0xb7, 0xda, 0x88, 0x39, - 0xcf, 0x01, 0x81, 0xb4, 0x50, 0x6a, 0xfc, 0x89, 0xd3, 0x12, 0x24, 0xb9, 0xf1, 0xb3, 0x85, 0x32, 0x3a, 0xba, 0xe2, - 0x14, 0xa7, 0x8b, 0xd7, 0x82, 0x55, 0xd4, 0xfe, 0xb6, 0xa9, 0xe3, 0x8b, 0xef, 0xd2, 0xb1, 0x6d, 0xb9, 0x7f, 0x37, - 0x7d, 0xdb, 0xa0, 0x38, 0x13, 0x36, 0x06, 0x7f, 0xe9, 0xbe, 0x6d, 0x0e, 0x34, 0x7c, 0x88, 0x3e, 0x77, 0xaf, 0xfe, - 0xb8, 0x26, 0x5d, 0x81, 0x6e, 0x89, 0xa7, 0x67, 0x3a, 0x28, 0x9a, 0x87, 0x92, 0x8b, 0x17, 0x25, 0xb0, 0x56, 0x30, - 0x9a, 0xce, 0x78, 0x11, 0x9c, 0x14, 0xda, 0xe7, 0x0d, 0x93, 0x9f, 0x16, 0x5c, 0xfb, 0x51, 0x49, 0xef, 0x14, 0x6a, - 0x8c, 0xef, 0xaf, 0x9b, 0x6c, 0xbd, 0x6b, 0x5e, 0x4b, 0xaa, 0x28, 0x8c, 0x7e, 0x87, 0x01, 0xf2, 0xd5, 0x97, 0x25, - 0x32, 0xfa, 0xf6, 0x4e, 0xdd, 0x6a, 0xbe, 0x77, 0x39, 0xd3, 0x19, 0x81, 0x3f, 0xdf, 0x8c, 0x59, 0xd3, 0x74, 0x33, - 0x46, 0x36, 0x02, 0x73, 0x17, 0x10, 0xa5, 0x89, 0x34, 0x28, 0x2b, 0xc8, 0x77, 0x57, 0xc3, 0x56, 0xa9, 0xcd, 0xde, - 0x9f, 0xfd, 0xdf, 0x4f, 0x0b, 0xee, 0x91, 0xf4, 0xe2, 0x8f, 0x1e, 0xaf, 0x63, 0x21, 0xcd, 0xbd, 0x50, 0xe3, 0x67, - 0xed, 0x71, 0xca, 0x6d, 0x3c, 0x40, 0xb3, 0x86, 0x0e, 0x0d, 0xdb, 0x87, 0x74, 0x5c, 0x1c, 0x5f, 0x63, 0xe8, 0xfb, - 0x06, 0x52, 0xc2, 0xd4, 0xe4, 0x3d, 0xa5, 0x15, 0x75, 0x60, 0x04, 0x15, 0xe5, 0x85, 0x72, 0x62, 0xd6, 0x56, 0xf4, - 0x6e, 0xc3, 0xc4, 0xd9, 0xa0, 0xfe, 0x66, 0xcb, 0xcb, 0x1e, 0x7b, 0x1f, 0xf0, 0xf5, 0x4c, 0x01, 0xc7, 0x38, 0xa1, - 0x46, 0xb0, 0x1d, 0xa4, 0xc8, 0x22, 0xf0, 0x09, 0x33, 0x6a, 0x08, 0x65, 0xcd, 0xd4, 0x96, 0xf2, 0x24, 0x48, 0xaf, - 0x2b, 0x2b, 0x5d, 0xd8, 0xe5, 0xdb, 0x2a, 0xba, 0x39, 0xbd, 0x83, 0x3d, 0xf9, 0x5e, 0xf3, 0xde, 0x7a, 0xa9, 0xd4, - 0x6a, 0xda, 0x90, 0xb0, 0x90, 0xb5, 0xc0, 0xae, 0x5b, 0xe7, 0x47, 0xd7, 0x31, 0x66, 0xf1, 0x08, 0x3e, 0x23, 0xd8, - 0x0b, 0x72, 0x53, 0xe7, 0xd6, 0xce, 0x60, 0x61, 0x42, 0xbe, 0xcf, 0xcf, 0x12, 0xb0, 0xb4, 0x63, 0xd3, 0x74, 0x70, - 0x3e, 0xa4, 0xef, 0xa1, 0x77, 0x4b, 0x01, 0x59, 0x38, 0x35, 0x7b, 0x57, 0x8b, 0x02, 0x4f, 0x1c, 0x92, 0x1a, 0xd0, - 0xf7, 0xec, 0x75, 0xfc, 0x46, 0x9f, 0x58, 0x24, 0x52, 0xd3, 0xdb, 0xf8, 0x9b, 0x67, 0xdf, 0xe8, 0xab, 0x53, 0xcf, - 0x84, 0xaf, 0x3e, 0x94, 0x5a, 0xcd, 0x86, 0x06, 0xec, 0x30, 0xd3, 0xc6, 0xb9, 0x0e, 0x4a, 0x7d, 0x33, 0x61, 0x27, - 0xe4, 0x65, 0x71, 0xe3, 0x28, 0x0d, 0xc1, 0x40, 0x7a, 0x11, 0x8f, 0xa2, 0xfc, 0xbe, 0xea, 0xc9, 0xcc, 0xfa, 0x08, - 0x4f, 0x2f, 0x1f, 0xfe, 0xf1, 0xb5, 0x2a, 0xbe, 0xbc, 0xb7, 0x1b, 0xf9, 0xc9, 0x4e, 0x41, 0xe2, 0xd0, 0x54, 0xec, - 0x01, 0x4d, 0x0e, 0x1c, 0x02, 0x71, 0x43, 0x79, 0xec, 0xc3, 0xfe, 0x5c, 0xf3, 0x0d, 0x01, 0x35, 0x56, 0x12, 0x54, - 0x4b, 0x67, 0xfe, 0x72, 0x25, 0x17, 0x2e, 0xde, 0x5d, 0x3c, 0xb7, 0x28, 0x7b, 0xff, 0xf3, 0x96, 0xfe, 0xd1, 0x7e, - 0x6f, 0x28, 0xb7, 0x97, 0x33, 0xff, 0xef, 0x35, 0x85, 0x0b, 0x81, 0x07, 0x24, 0xa9, 0x85, 0xe4, 0x4a, 0x87, 0x8f, - 0xdf, 0x1c, 0xea, 0xbc, 0xc7, 0x74, 0x5f, 0x61, 0x56, 0x17, 0x44, 0xc1, 0xc7, 0x07, 0xa8, 0x6c, 0x03, 0xc5, 0x08, - 0xe1, 0x02, 0x46, 0x1f, 0xee, 0xeb, 0x46, 0x2d, 0x02, 0x69, 0x57, 0xae, 0xfe, 0x78, 0x61, 0xe0, 0x95, 0xc3, 0x6f, - 0x2d, 0xb9, 0x2f, 0x25, 0xbe, 0xd0, 0xd8, 0x9e, 0x5c, 0x4b, 0xe3, 0x89, 0x8c, 0x8a, 0x50, 0x55, 0x91, 0x7c, 0xce, - 0xbd, 0x5a, 0x7d, 0x31, 0x8c, 0x7c, 0x26, 0x30, 0xd7, 0x9b, 0x36, 0x76, 0x9c, 0x54, 0x97, 0xfc, 0xa7, 0x1e, 0x60, - 0x30, 0xd8, 0x97, 0x6d, 0xd3, 0xf4, 0x7e, 0xe7, 0xa4, 0xe1, 0x09, 0x92, 0xc0, 0x1a, 0x0c, 0x5c, 0x95, 0x24, 0x48, - 0x6f, 0xcc, 0x8a, 0x2e, 0x4d, 0xc9, 0x7b, 0xea, 0x29, 0xd9, 0x88, 0xe4, 0x21, 0xa0, 0x23, 0xc1, 0x45, 0xff, 0x51, - 0x6b, 0x23, 0x5c, 0x6b, 0xf9, 0x39, 0x9f, 0x6c, 0xe8, 0x74, 0xb3, 0x2b, 0xb2, 0xa3, 0x0f, 0xa3, 0x3c, 0x9c, 0x3b, - 0x19, 0xe6, 0x61, 0x24, 0xb0, 0x92, 0xb9, 0x79, 0xdc, 0x00, 0xf1, 0x4d, 0x96, 0x64, 0xb7, 0xe4, 0x7f, 0xfc, 0x29, - 0xaf, 0x23, 0xa4, 0x64, 0x5b, 0xdf, 0x55, 0x34, 0x3a, 0x85, 0x93, 0x5c, 0xa7, 0x65, 0x79, 0x21, 0x9c, 0x50, 0x20, - 0x6c, 0x69, 0xd4, 0x48, 0x7e, 0xff, 0xfe, 0xc7, 0x10, 0x2c, 0xfa, 0xf8, 0xa6, 0x99, 0x75, 0x5b, 0x81, 0x31, 0x82, - 0x46, 0x9d, 0x99, 0xdd, 0xe8, 0xf4, 0x26, 0x23, 0x11, 0x28, 0x49, 0x63, 0x8a, 0xb4, 0x87, 0xc3, 0xdd, 0xe6, 0xab, - 0x3f, 0xb2, 0x1d, 0x4b, 0xaa, 0xb6, 0x59, 0xa8, 0x2d, 0x40, 0x80, 0x51, 0xbf, 0x37, 0x10, 0x4d, 0x34, 0x05, 0x05, - 0x2b, 0x6f, 0xe8, 0xdc, 0x8e, 0x6e, 0xcd, 0x6e, 0x41, 0xfb, 0x55, 0xfd, 0x19, 0xa1, 0x83, 0xdb, 0x4a, 0x7a, 0x4e, - 0x4a, 0x55, 0xa4, 0x2e, 0x38, 0xa7, 0x20, 0xb1, 0xb5, 0x0d, 0xb4, 0x7d, 0x6a, 0x4c, 0xe4, 0xfd, 0x45, 0xc5, 0x55, - 0x44, 0x00, 0x02, 0x84, 0x97, 0xe5, 0x5d, 0xc2, 0x27, 0xa3, 0x04, 0x00, 0xd3, 0xe3, 0xd2, 0x4b, 0xc6, 0x52, 0xd1, - 0xf0, 0xb0, 0x55, 0x50, 0x6d, 0xbb, 0x40, 0xe5, 0x80, 0x0b, 0xac, 0xac, 0xc3, 0x3c, 0x13, 0x52, 0x35, 0x29, 0x2e, - 0xba, 0x99, 0x5d, 0xa4, 0x3c, 0xdd, 0xa7, 0xa9, 0x24, 0x6c, 0x5a, 0x7f, 0x67, 0x7c, 0x19, 0x87, 0x68, 0x96, 0xbe, - 0x38, 0x6e, 0x3c, 0x5a, 0xde, 0x8e, 0xa6, 0x03, 0xd3, 0xda, 0x79, 0x12, 0x01, 0xca, 0x4e, 0x95, 0x70, 0xf5, 0x3c, - 0x04, 0x45, 0xa8, 0xf1, 0x43, 0xb7, 0x41, 0xc1, 0x6f, 0xe4, 0xe7, 0xa7, 0x86, 0x02, 0x84, 0xf1, 0xd2, 0x01, 0x0f, - 0xdd, 0xe4, 0xc5, 0x96, 0xb2, 0x73, 0xc0, 0xd8, 0x1b, 0xd1, 0x0b, 0x48, 0x6b, 0x62, 0xea, 0x4e, 0x72, 0x14, 0x5d, - 0x9c, 0x53, 0x5e, 0xc5, 0x2d, 0xd3, 0x65, 0xe9, 0x63, 0xea, 0x9d, 0x08, 0x9f, 0x13, 0x2b, 0x84, 0xff, 0x1d, 0x91, - 0xc3, 0xac, 0x94, 0x69, 0x81, 0x11, 0x6b, 0x89, 0x17, 0x38, 0xdf, 0x09, 0xd1, 0x4c, 0xd5, 0x4c, 0x57, 0x84, 0x79, - 0xaa, 0xaf, 0xf5, 0x9e, 0x3c, 0xc9, 0x1e, 0xa8, 0xf2, 0x61, 0xaf, 0xbb, 0x24, 0x98, 0xd7, 0xb2, 0xa9, 0xb7, 0x61, - 0xa2, 0xb0, 0x0f, 0x16, 0xf2, 0xb8, 0x6a, 0x08, 0x38, 0x3d, 0xf5, 0xab, 0x6f, 0xf5, 0x61, 0xdd, 0xb4, 0x2b, 0x04, - 0x9f, 0x93, 0x44, 0x84, 0x9f, 0xdb, 0x25, 0xce, 0xca, 0xab, 0xeb, 0xec, 0xb3, 0x58, 0xad, 0x41, 0xe6, 0xe5, 0x29, - 0xd1, 0xb6, 0xff, 0xd9, 0x41, 0x79, 0xde, 0x4d, 0x12, 0x3c, 0x4c, 0x47, 0x14, 0x33, 0x71, 0x8e, 0xa4, 0x21, 0x13, - 0xcf, 0xf9, 0xe2, 0x8b, 0x1a, 0xbd, 0x9f, 0x13, 0x4a, 0xc7, 0xa4, 0xf9, 0x8d, 0x0a, 0xa1, 0x0b, 0x09, 0x1d, 0x3f, - 0x74, 0xf9, 0xba, 0xb0, 0x76, 0xf3, 0x89, 0x88, 0xf5, 0x1f, 0xdc, 0x88, 0xa2, 0xd2, 0x79, 0x2c, 0x96, 0x40, 0x32, - 0xc6, 0x4f, 0xf4, 0x1b, 0x33, 0x4f, 0xba, 0x7a, 0xf8, 0x0c, 0x1b, 0x0d, 0xc7, 0x41, 0x9c, 0x03, 0x9e, 0xbf, 0x0c, - 0x7b, 0x5b, 0x1f, 0x15, 0xbf, 0x7f, 0x7d, 0x40, 0x54, 0x6b, 0xb8, 0xa2, 0xf4, 0x67, 0x1c, 0xe2, 0x12, 0xc9, 0x40, - 0x8b, 0x19, 0x7e, 0x21, 0xd2, 0xea, 0x7f, 0x45, 0xce, 0x3d, 0x0e, 0xec, 0x84, 0xfc, 0x17, 0xb7, 0xbd, 0x07, 0x5d, - 0x15, 0x42, 0xde, 0x8e, 0xa8, 0x91, 0x22, 0x0e, 0xef, 0xee, 0xcd, 0xd7, 0xd6, 0x22, 0xe7, 0xc0, 0xac, 0xdd, 0x4d, - 0x99, 0x85, 0xbb, 0x48, 0x6d, 0x31, 0x6d, 0x9a, 0x6d, 0x82, 0x97, 0x61, 0x27, 0x9d, 0x2c, 0x3e, 0xb5, 0x81, 0x50, - 0x55, 0x04, 0x48, 0x25, 0x0b, 0xfd, 0x0b, 0x94, 0xae, 0x5a, 0x2c, 0x43, 0x4b, 0x25, 0xe7, 0xba, 0x12, 0x4b, 0x3f, - 0xa1, 0xc0, 0x20, 0xfd, 0xe2, 0x56, 0x69, 0x3a, 0x2b, 0xa4, 0x88, 0x44, 0x8f, 0xd7, 0x96, 0xdd, 0x5d, 0xa8, 0x3c, - 0x92, 0xee, 0x33, 0x39, 0xc0, 0xf5, 0x4e, 0xaa, 0x8e, 0x95, 0x04, 0xed, 0x40, 0xf7, 0x00, 0xa5, 0x7e, 0x6a, 0x3d, - 0x44, 0x5a, 0x21, 0xf0, 0x12, 0x6e, 0xcc, 0x90, 0x68, 0x1f, 0xa8, 0x87, 0xc4, 0x04, 0xa0, 0x29, 0x38, 0xc1, 0x96, - 0x42, 0xdb, 0xb9, 0x91, 0x5c, 0xa1, 0x80, 0x95, 0xb1, 0x46, 0x35, 0x66, 0x1e, 0x5a, 0x61, 0x22, 0x8e, 0xb3, 0xcd, - 0xc8, 0x43, 0x1c, 0xa9, 0x43, 0xb4, 0xfd, 0x42, 0xe2, 0xa0, 0xc5, 0x99, 0x33, 0x8d, 0x5c, 0x21, 0x1c, 0x9f, 0x82, - 0x34, 0x8c, 0x60, 0xc3, 0xf5, 0x51, 0x6d, 0xac, 0x32, 0x21, 0x72, 0xab, 0x6e, 0x24, 0xed, 0xd7, 0xf1, 0x3b, 0x97, - 0x58, 0xc9, 0xb2, 0xe2, 0x9b, 0xa6, 0xd4, 0x33, 0xe5, 0xd5, 0x67, 0x56, 0x26, 0xbd, 0xdc, 0x47, 0x79, 0xc4, 0x5b, - 0xb0, 0xb8, 0x29, 0xf9, 0x21, 0xa6, 0xa0, 0x06, 0xf0, 0x68, 0x5c, 0x3b, 0x5c, 0x41, 0xb1, 0x18, 0x18, 0x69, 0x3a, - 0xad, 0x1c, 0xf3, 0xa5, 0x9a, 0x0d, 0x0c, 0xf3, 0x18, 0x4f, 0x2c, 0x74, 0x62, 0x7f, 0xcd, 0xf3, 0xb9, 0x45, 0x23, - 0x4f, 0xd3, 0x06, 0x59, 0x7e, 0x9b, 0xd6, 0x6b, 0x95, 0xe3, 0xf1, 0xb6, 0x02, 0x88, 0xdf, 0x41, 0x59, 0x50, 0x0c, - 0x15, 0x4d, 0x8a, 0x19, 0x0c, 0x97, 0xc6, 0x0b, 0x32, 0x01, 0xba, 0x0c, 0x33, 0x6b, 0x8b, 0xaa, 0xbc, 0x3d, 0x16, - 0xc3, 0x7d, 0x50, 0xaa, 0x48, 0x7e, 0xa5, 0x9a, 0x2d, 0x8f, 0x2a, 0xfc, 0xc7, 0x50, 0x1d, 0x43, 0xa4, 0x9d, 0xfc, - 0xab, 0xa3, 0x46, 0xc7, 0x7d, 0x71, 0xc8, 0x8e, 0x3d, 0x3f, 0x63, 0x20, 0x42, 0x4e, 0xba, 0x15, 0x6e, 0xf1, 0x49, - 0x7a, 0xd4, 0x08, 0xf4, 0x15, 0x04, 0x57, 0x6b, 0xde, 0x9d, 0x51, 0x61, 0xab, 0x51, 0x8e, 0x82, 0x32, 0x0c, 0x51, - 0x0d, 0x4f, 0xd1, 0x38, 0xf0, 0xb2, 0xc0, 0x01, 0x13, 0x01, 0x28, 0xe1, 0xb9, 0x24, 0xba, 0xe8, 0x7f, 0x4b, 0x73, - 0xf4, 0x86, 0x15, 0xec, 0xc8, 0x7c, 0xe9, 0x40, 0x8a, 0x80, 0x90, 0x4a, 0xb9, 0xaa, 0x7f, 0x30, 0x10, 0x8e, 0x87, - 0x91, 0xc1, 0xe4, 0x67, 0xc8, 0x87, 0xf2, 0x66, 0x86, 0x97, 0x47, 0x6e, 0x20, 0x4d, 0x4c, 0xa9, 0xc7, 0xb4, 0x46, - 0x6a, 0xb7, 0xdb, 0xc1, 0x55, 0x2a, 0xdd, 0xa0, 0xc6, 0x17, 0x45, 0x30, 0xfa, 0x97, 0x1b, 0x08, 0x3f, 0xfc, 0x2f, - 0x6e, 0x4b, 0xb0, 0x29, 0x7a, 0x8b, 0x03, 0x90, 0xf6, 0x6d, 0xa9, 0xba, 0x1e, 0x20, 0xc6, 0x96, 0x05, 0xfc, 0xe7, - 0xe0, 0x14, 0x11, 0x4b, 0x67, 0x2c, 0x66, 0xab, 0x53, 0x18, 0x72, 0xff, 0x8b, 0xa1, 0x23, 0x08, 0xfb, 0xd7, 0x19, - 0x76, 0x0c, 0x33, 0x40, 0x26, 0x7b, 0x90, 0x8a, 0x38, 0x52, 0x4c, 0x63, 0x1e, 0xad, 0x05, 0x8e, 0x14, 0x69, 0xf1, - 0x3e, 0x2a, 0x15, 0xdd, 0x97, 0x3c, 0x70, 0x40, 0x55, 0x0e, 0xe1, 0xb7, 0x16, 0x7d, 0x2b, 0x21, 0xf3, 0xba, 0xc9, - 0x02, 0x50, 0x17, 0x63, 0x31, 0xd6, 0x13, 0x92, 0x91, 0x9f, 0xb4, 0xa3, 0xc7, 0x68, 0x68, 0xf2, 0xf1, 0xa9, 0xee, - 0xb8, 0xe9, 0x9e, 0xbf, 0x51, 0x43, 0xb1, 0x7f, 0x2f, 0x13, 0x7d, 0x23, 0x8b, 0x64, 0xef, 0xec, 0xb3, 0xd9, 0x19, - 0xe6, 0xa6, 0xa7, 0xc6, 0x48, 0x37, 0xab, 0x3b, 0x9b, 0x2d, 0x53, 0x3b, 0x72, 0x07, 0x34, 0x67, 0x7c, 0x9d, 0xde, - 0x40, 0x1c, 0xef, 0x85, 0xc4, 0xcd, 0x74, 0xc4, 0x94, 0x7e, 0xdc, 0x88, 0x80, 0x1a, 0x45, 0x07, 0x1b, 0x99, 0xf6, - 0x6f, 0x81, 0x9c, 0x4d, 0xd0, 0x41, 0x15, 0x54, 0x5b, 0xcc, 0x4c, 0x0b, 0x39, 0x34, 0xd2, 0x82, 0x82, 0x95, 0xc6, - 0x60, 0xb0, 0x52, 0x95, 0x64, 0x2f, 0x4a, 0x2c, 0x3d, 0xcb, 0x59, 0xe8, 0x50, 0x36, 0x1d, 0x3c, 0x5f, 0x0a, 0x97, - 0x44, 0xbc, 0xac, 0x85, 0x61, 0xda, 0x6c, 0xa5, 0xa5, 0x65, 0x45, 0x25, 0xec, 0xe4, 0xfa, 0xbc, 0x94, 0x85, 0x79, - 0xa2, 0xb6, 0xf1, 0x8c, 0xdc, 0xcc, 0x50, 0xbe, 0x52, 0xfd, 0x30, 0xbd, 0x5f, 0xf8, 0x77, 0x90, 0x40, 0x9f, 0x22, - 0x60, 0x05, 0x5d, 0x12, 0x6c, 0xa4, 0xf4, 0xcf, 0x17, 0x97, 0x35, 0x0a, 0x7f, 0x7a, 0x01, 0xaf, 0xd2, 0xbd, 0x6e, - 0x87, 0xa1, 0xc8, 0xd7, 0x9e, 0x7c, 0x35, 0x9d, 0xfc, 0x91, 0xc2, 0xe1, 0xba, 0x92, 0x9e, 0x4b, 0x6f, 0x16, 0xf4, - 0x73, 0xeb, 0xd5, 0x57, 0xea, 0x2d, 0x54, 0x4f, 0x93, 0x88, 0xdd, 0x2d, 0xbd, 0x8f, 0x9e, 0x8d, 0x42, 0x68, 0x56, - 0xde, 0x7c, 0xff, 0x90, 0xb4, 0x4c, 0xd4, 0x2a, 0x17, 0x77, 0xb3, 0x81, 0xd0, 0xd6, 0x38, 0x47, 0xd8, 0xbb, 0x71, - 0xee, 0x5f, 0x5a, 0x92, 0x00, 0x55, 0x4c, 0x28, 0xbe, 0xa3, 0xd3, 0x40, 0xee, 0x83, 0x3b, 0x3a, 0x7e, 0xbb, 0xf3, - 0xb5, 0x9a, 0x06, 0xf6, 0x08, 0x53, 0x0f, 0xa2, 0xbf, 0xc1, 0xaa, 0x97, 0x5e, 0x2f, 0x17, 0xd8, 0x9c, 0x97, 0x3c, - 0xb0, 0x54, 0x90, 0x95, 0x57, 0xee, 0xc0, 0xa4, 0xf6, 0x08, 0x02, 0xe8, 0x2f, 0x1b, 0x37, 0xf7, 0x77, 0x22, 0x15, - 0xc1, 0x5d, 0x76, 0x9c, 0x8c, 0xd6, 0xf4, 0x3b, 0x3b, 0x8e, 0x05, 0x63, 0xa7, 0x97, 0x09, 0xab, 0x30, 0xd0, 0x8a, - 0xa3, 0xf5, 0x55, 0xf2, 0x8f, 0x2a, 0xc3, 0xac, 0x15, 0x05, 0xec, 0xfd, 0x52, 0x85, 0xf5, 0x41, 0x29, 0xaa, 0x8b, - 0x78, 0x42, 0xc7, 0x93, 0x66, 0xc3, 0x01, 0x8b, 0xa1, 0x45, 0x8c, 0x2d, 0xf6, 0x48, 0x87, 0xcd, 0x38, 0xa9, 0x77, - 0x7c, 0x56, 0xe1, 0xbc, 0x71, 0x1c, 0xb7, 0xc1, 0x6b, 0x8d, 0xca, 0xf2, 0x05, 0x6e, 0xe0, 0x17, 0xaf, 0x54, 0x8f, - 0x7f, 0xf8, 0xf6, 0xba, 0xb8, 0xa8, 0x3a, 0x0c, 0x6d, 0xf1, 0xa7, 0x0d, 0x69, 0x4c, 0x1a, 0xf6, 0x70, 0xfd, 0x4a, - 0x9a, 0x7c, 0xc1, 0xa8, 0xef, 0x90, 0x8d, 0xd9, 0xba, 0x5f, 0x00, 0x8f, 0x79, 0xef, 0x7a, 0xa9, 0x5f, 0x4a, 0x52, - 0x35, 0xa2, 0x15, 0x35, 0xf1, 0xcd, 0x1a, 0x37, 0xc9, 0x5a, 0x90, 0x84, 0xb6, 0x47, 0xed, 0x88, 0x0f, 0xf1, 0xfb, - 0xb7, 0x29, 0x54, 0x81, 0x78, 0x6f, 0x76, 0x5d, 0x06, 0xbb, 0xd5, 0xb3, 0x94, 0x84, 0x95, 0x1b, 0x30, 0x35, 0xd5, - 0xd2, 0x6c, 0x58, 0x85, 0x7c, 0x0e, 0x4c, 0xd2, 0x9a, 0x74, 0x4e, 0x69, 0x21, 0xd4, 0x32, 0xec, 0x9f, 0x92, 0x45, - 0xc4, 0xc7, 0x32, 0xf8, 0xbc, 0x90, 0x53, 0x77, 0xd0, 0x88, 0x2c, 0x46, 0xad, 0xdc, 0x82, 0xdd, 0x8e, 0xd6, 0x3e, - 0x55, 0x09, 0x88, 0xf5, 0xbb, 0x66, 0xe3, 0x6c, 0x14, 0xd8, 0x43, 0xe8, 0x07, 0xdf, 0xf1, 0x36, 0xcb, 0x5d, 0x60, - 0x4a, 0x79, 0xa4, 0xda, 0x52, 0xfa, 0x8c, 0x17, 0x3f, 0xf2, 0x1e, 0x5a, 0xca, 0xdd, 0x7d, 0xc5, 0x1f, 0x3f, 0x5d, - 0xe7, 0xb5, 0x98, 0x4e, 0xe2, 0x5c, 0x9a, 0x63, 0x49, 0xd9, 0x23, 0xc7, 0x71, 0x71, 0xf7, 0x29, 0x14, 0x9a, 0x51, - 0x11, 0x86, 0x34, 0x12, 0x94, 0x9f, 0x29, 0xae, 0xb8, 0xf6, 0x09, 0xad, 0x25, 0x02, 0x64, 0xf0, 0xfd, 0xa3, 0x4c, - 0x57, 0xee, 0xf3, 0x00, 0x7f, 0xb7, 0x68, 0x95, 0xb0, 0x66, 0x51, 0x84, 0x6d, 0x02, 0x92, 0xf5, 0xdd, 0x61, 0x87, - 0xec, 0xec, 0x86, 0x08, 0x02, 0x75, 0xd7, 0x21, 0x40, 0x18, 0xaf, 0x11, 0xca, 0xe5, 0x5f, 0x2b, 0xe3, 0x76, 0x25, - 0x13, 0xea, 0x30, 0xca, 0x2e, 0x28, 0xf0, 0xde, 0x8b, 0x7e, 0xe9, 0x6d, 0x95, 0x1b, 0x5a, 0x4e, 0xd6, 0x47, 0x2f, - 0x3f, 0x29, 0xbb, 0x24, 0x7f, 0xc8, 0x68, 0x01, 0x48, 0xd9, 0x98, 0x66, 0xe3, 0x98, 0xae, 0x5a, 0xb7, 0xcc, 0x47, - 0xd9, 0x06, 0xc3, 0x76, 0x88, 0x51, 0x3c, 0x68, 0xd5, 0x90, 0x5c, 0xb1, 0x69, 0x8f, 0x7b, 0xe9, 0xc1, 0x6d, 0xf7, - 0x7e, 0x38, 0x38, 0x17, 0xf2, 0x88, 0xd9, 0x4b, 0x98, 0x8f, 0x1a, 0xbb, 0x42, 0xa6, 0x19, 0x0e, 0xe2, 0x20, 0x07, - 0xd9, 0x76, 0xdd, 0x05, 0x53, 0x8e, 0x69, 0x71, 0xbb, 0xc5, 0x46, 0x36, 0x90, 0x11, 0x5a, 0xb0, 0xc9, 0xf8, 0x50, - 0x2c, 0xd3, 0xa1, 0x74, 0xdf, 0x63, 0x02, 0xc6, 0x5a, 0x0f, 0x13, 0xbf, 0x66, 0x1d, 0xa2, 0x4b, 0x16, 0xa4, 0xa9, - 0xd1, 0xe3, 0x9b, 0x3e, 0xa5, 0x5b, 0x66, 0x43, 0xc3, 0xf7, 0x3a, 0xcc, 0x1a, 0x0c, 0x2b, 0xf6, 0x89, 0xb2, 0x57, - 0xf5, 0xc7, 0xdb, 0x71, 0x50, 0x4b, 0x39, 0x73, 0x79, 0x9b, 0xd1, 0xbf, 0x99, 0xc3, 0x40, 0xe3, 0x85, 0xc2, 0x7b, - 0x39, 0x81, 0x9a, 0x96, 0x34, 0x29, 0x78, 0xbb, 0xbc, 0x5a, 0x6d, 0xc2, 0xb8, 0x7f, 0xf7, 0xa1, 0x54, 0x5c, 0xff, - 0xee, 0x7d, 0xdd, 0x55, 0xbd, 0x2f, 0x6f, 0x94, 0x4b, 0x3a, 0xaf, 0xd6, 0x57, 0x10, 0xa0, 0xd2, 0x5b, 0x99, 0xb2, - 0x21, 0xdb, 0x83, 0xd7, 0x75, 0xbd, 0x77, 0x55, 0x7e, 0xdc, 0x81, 0xdd, 0x6d, 0x72, 0x16, 0x6b, 0x0b, 0xea, 0xa9, - 0xd3, 0xb8, 0x7b, 0x29, 0xe5, 0x86, 0x83, 0x53, 0x8a, 0xba, 0xc5, 0x37, 0xbc, 0x3f, 0x67, 0xd7, 0xa9, 0x4b, 0xbd, - 0xd5, 0x6f, 0xd7, 0xbf, 0xf2, 0xd4, 0x58, 0xe5, 0xa0, 0x86, 0xf5, 0xab, 0xf6, 0x35, 0x99, 0x5d, 0x83, 0x99, 0x31, - 0x48, 0xe1, 0x72, 0xae, 0x3e, 0x1b, 0x1c, 0x85, 0x79, 0x4e, 0xa8, 0x60, 0x0b, 0x42, 0xfd, 0xf8, 0x25, 0x31, 0x95, - 0xcc, 0x3f, 0x1c, 0x57, 0x46, 0x3f, 0x08, 0x7d, 0xbb, 0x6a, 0xbd, 0x0c, 0x75, 0x4e, 0x91, 0x8f, 0xb9, 0x9a, 0xe0, - 0x97, 0xd4, 0xc1, 0xd1, 0x2c, 0xfc, 0x53, 0x1d, 0xb6, 0x3b, 0x9c, 0x8f, 0x1e, 0x68, 0x5c, 0xed, 0x1b, 0xf0, 0x46, - 0xb4, 0xb3, 0xb0, 0xe3, 0xdd, 0xa7, 0x69, 0xac, 0xc3, 0xe1, 0xcc, 0xb0, 0xa4, 0x8c, 0x38, 0x0c, 0x98, 0x87, 0x6e, - 0xc9, 0x76, 0xb9, 0x6e, 0x93, 0x83, 0x94, 0xf5, 0x1e, 0x4a, 0x31, 0x8f, 0xe6, 0xb9, 0x69, 0xef, 0x79, 0x2f, 0xba, - 0xc2, 0x70, 0x79, 0x30, 0x32, 0x1f, 0xb3, 0x42, 0xaa, 0xae, 0x53, 0xd7, 0x71, 0xa6, 0x35, 0x46, 0xe4, 0x23, 0xc6, - 0xcd, 0xf7, 0x96, 0xb0, 0x68, 0x57, 0xb7, 0x2b, 0x88, 0x33, 0xac, 0xfc, 0x5f, 0x19, 0x9b, 0xa9, 0xa7, 0x0b, 0xb6, - 0xa7, 0x16, 0xfc, 0x79, 0x93, 0xb2, 0xa2, 0x82, 0x1b, 0x1d, 0x41, 0xa9, 0x7f, 0x7c, 0x5e, 0xd4, 0xaa, 0x66, 0x64, - 0xcd, 0x6f, 0x89, 0x77, 0xc6, 0x78, 0x5d, 0xd7, 0x15, 0xb2, 0xdf, 0xc5, 0xa9, 0xd1, 0x87, 0x26, 0x35, 0x8a, 0x64, - 0xfd, 0xa5, 0x68, 0x0e, 0x0c, 0x61, 0x84, 0xc6, 0x9b, 0xb5, 0xce, 0xc9, 0xe0, 0x24, 0xce, 0xaf, 0x3a, 0xb0, 0xde, - 0xce, 0xb1, 0xbd, 0x82, 0x41, 0x10, 0xf8, 0x57, 0x11, 0xa3, 0x55, 0xfd, 0xbc, 0x33, 0x33, 0x54, 0xf5, 0x72, 0x9a, - 0xac, 0x6c, 0xfe, 0x18, 0x53, 0x0d, 0xca, 0x4b, 0xd9, 0x55, 0xf6, 0x89, 0x8c, 0xfa, 0xb1, 0xa0, 0x1e, 0x5d, 0x9e, - 0x33, 0x94, 0xb7, 0x60, 0xcf, 0x52, 0x6f, 0x06, 0x08, 0x91, 0xb6, 0xab, 0x61, 0xc2, 0x31, 0xcc, 0xe9, 0xc8, 0x8a, - 0x55, 0x99, 0xc0, 0x47, 0x11, 0x5f, 0x34, 0xa7, 0x05, 0xce, 0xac, 0x2e, 0x3b, 0xbc, 0x15, 0xa2, 0xa2, 0xb8, 0xe3, - 0x7e, 0x42, 0x6b, 0x3e, 0x0e, 0x33, 0x31, 0x5e, 0xb3, 0x78, 0xde, 0xfd, 0x05, 0x04, 0x4d, 0x9d, 0xd0, 0x60, 0xe1, - 0xdd, 0x0f, 0x05, 0x44, 0xc9, 0x6b, 0x2b, 0x72, 0x92, 0xe1, 0xb7, 0x02, 0xc9, 0x74, 0x84, 0xd0, 0xc2, 0x25, 0x70, - 0xb3, 0xfd, 0x74, 0xdc, 0x05, 0xc7, 0x48, 0x91, 0x58, 0x38, 0x9e, 0x26, 0x6c, 0x3e, 0xb1, 0x26, 0x96, 0xe3, 0xa4, - 0x43, 0xe9, 0x2a, 0x34, 0xd5, 0x2a, 0x06, 0xad, 0xab, 0xfa, 0xc9, 0xde, 0x29, 0x88, 0xdb, 0x96, 0x20, 0xa2, 0x26, - 0xc7, 0x37, 0x1d, 0xb4, 0x3d, 0xb1, 0xc8, 0x1a, 0x65, 0x14, 0xbe, 0xf3, 0x08, 0x65, 0x8c, 0xc0, 0x7d, 0x95, 0x1a, - 0x63, 0x43, 0x59, 0x66, 0x7f, 0x30, 0x7d, 0x33, 0xc1, 0x44, 0x2f, 0xa1, 0xcc, 0x68, 0x95, 0x9c, 0x20, 0xfa, 0x34, - 0x97, 0x72, 0x3c, 0x22, 0xfa, 0x86, 0x85, 0xbf, 0xc4, 0xe2, 0x2a, 0xe6, 0xdd, 0xe5, 0xed, 0x74, 0x6d, 0x91, 0xcf, - 0xd4, 0x16, 0x63, 0x97, 0xcc, 0xa1, 0xf6, 0xb0, 0x21, 0x3b, 0xf1, 0x86, 0x9d, 0x16, 0xa5, 0x7c, 0x3b, 0x4a, 0x11, - 0xf6, 0x5d, 0xd1, 0xbf, 0x5d, 0x6f, 0x0a, 0x73, 0xed, 0x8a, 0xc9, 0xdf, 0xea, 0xeb, 0x19, 0x5a, 0x4f, 0x7c, 0x35, - 0x74, 0x73, 0x58, 0xf3, 0xc7, 0x02, 0xdf, 0x22, 0x2c, 0xb7, 0xb7, 0xd1, 0xc4, 0xb6, 0xce, 0x4b, 0x4f, 0x60, 0xb0, - 0x10, 0x7e, 0x37, 0x4b, 0xf1, 0x80, 0xd5, 0x83, 0xe8, 0x83, 0x02, 0x13, 0x53, 0xb9, 0x7a, 0xb5, 0x62, 0x8f, 0xd0, - 0x1e, 0xc6, 0x3a, 0x91, 0x5a, 0xf9, 0x36, 0xb8, 0x5a, 0xe1, 0x95, 0xbe, 0xde, 0x14, 0xb1, 0x5e, 0x79, 0x58, 0xdb, - 0xea, 0x97, 0xdc, 0xc2, 0xe5, 0xdf, 0xb6, 0x2a, 0x02, 0x7c, 0xc2, 0x55, 0x88, 0x23, 0xd0, 0x77, 0x69, 0x15, 0x05, - 0xf5, 0x97, 0x1c, 0x72, 0xea, 0xfc, 0x88, 0x70, 0x3e, 0x5f, 0x57, 0xf5, 0x1c, 0x47, 0x78, 0xab, 0xfc, 0x0f, 0x96, - 0x26, 0x6f, 0xe2, 0x7a, 0xb4, 0xe7, 0x25, 0x1d, 0x3e, 0xc1, 0xa5, 0x3b, 0x0a, 0x23, 0xbc, 0x94, 0x31, 0x8d, 0x17, - 0xe7, 0x1a, 0x30, 0x7b, 0x83, 0xe4, 0xf5, 0x38, 0x10, 0x95, 0x1c, 0x87, 0x2d, 0x16, 0xb5, 0x9e, 0x14, 0x1a, 0x91, - 0x37, 0xac, 0xca, 0x50, 0x44, 0x4b, 0x62, 0x07, 0x88, 0xfc, 0x58, 0x14, 0x86, 0x0e, 0xd5, 0x22, 0xb4, 0x6b, 0x7c, - 0xbf, 0xa9, 0x8f, 0xd0, 0x12, 0xab, 0x89, 0xf0, 0x61, 0x41, 0xde, 0x03, 0x0c, 0x73, 0x98, 0x24, 0xad, 0xa3, 0x74, - 0x90, 0xe3, 0x2b, 0x41, 0x32, 0x39, 0x30, 0x93, 0xde, 0x41, 0x6c, 0xe7, 0x7c, 0x31, 0x79, 0xbf, 0x11, 0x3f, 0x85, - 0x34, 0x74, 0x95, 0xbd, 0xc6, 0x22, 0xfb, 0x60, 0x82, 0xb5, 0x2f, 0x0f, 0x97, 0x99, 0xd7, 0xe0, 0x27, 0xfc, 0xff, - 0x34, 0x4b, 0x0a, 0xba, 0x0b, 0xb8, 0x98, 0xbd, 0x0f, 0xc3, 0x04, 0x3e, 0x19, 0x4d, 0x54, 0x96, 0xe8, 0x85, 0x05, - 0x4a, 0xd9, 0xef, 0xb7, 0xd0, 0xe0, 0x10, 0xcc, 0x4b, 0xde, 0xf9, 0xaa, 0x5b, 0x29, 0xaf, 0xef, 0xe4, 0xc8, 0xe4, - 0xf3, 0x29, 0xda, 0x1a, 0xb6, 0x56, 0xc0, 0x4b, 0x08, 0x03, 0x41, 0x34, 0xa4, 0xb8, 0xa4, 0xc3, 0x15, 0xc8, 0x1a, - 0xb9, 0x63, 0xd1, 0x4a, 0x19, 0x4e, 0x1b, 0x37, 0x13, 0xb5, 0xfb, 0xb4, 0x10, 0xc3, 0x79, 0x49, 0x87, 0xb1, 0xa4, - 0x0f, 0x4c, 0xb8, 0xc1, 0xb2, 0xad, 0x0a, 0xec, 0x80, 0xe6, 0x79, 0xd1, 0x68, 0x95, 0x9a, 0x0c, 0xa9, 0xa4, 0x93, - 0xf0, 0x11, 0xaf, 0x1d, 0x29, 0x19, 0xeb, 0x44, 0x4e, 0x13, 0x86, 0xb0, 0xfd, 0xd0, 0x48, 0xd4, 0x41, 0x61, 0x4d, - 0x21, 0x38, 0x2f, 0x34, 0xe0, 0xa6, 0x8d, 0xee, 0x07, 0xa8, 0xba, 0xd0, 0x48, 0xb3, 0xd2, 0x16, 0xb9, 0x6e, 0x2c, - 0x0e, 0xca, 0x8f, 0x78, 0x6d, 0x5e, 0x67, 0x55, 0x61, 0x23, 0x91, 0x7b, 0x28, 0x86, 0xb0, 0x19, 0xd3, 0x1f, 0xae, - 0xdc, 0xe3, 0x12, 0xeb, 0xb8, 0xc7, 0x80, 0x2d, 0xaf, 0x90, 0xc6, 0xec, 0x55, 0x72, 0x60, 0x21, 0x03, 0x34, 0xaf, - 0xc4, 0xf0, 0xbe, 0xf5, 0xcb, 0xa1, 0xf1, 0xad, 0x5a, 0x99, 0x4b, 0xcf, 0x84, 0x89, 0x11, 0x1e, 0x08, 0x83, 0x5f, - 0xab, 0x3f, 0x9d, 0xf6, 0xb2, 0x4e, 0x71, 0xbf, 0xca, 0x21, 0x37, 0x27, 0x2c, 0x1a, 0xe8, 0xa0, 0x4c, 0x4e, 0x17, - 0x39, 0x07, 0xf5, 0xcd, 0xdd, 0x82, 0xbc, 0x40, 0x1c, 0x6b, 0x3c, 0x8e, 0x5c, 0xf7, 0x62, 0xde, 0x66, 0x22, 0xd8, - 0x9b, 0x0a, 0xfe, 0x39, 0xc4, 0x29, 0x21, 0x80, 0x35, 0x48, 0x6c, 0xd6, 0xe5, 0x1e, 0xdb, 0xcb, 0xd8, 0xae, 0x13, - 0x99, 0xa2, 0xb2, 0x82, 0xe4, 0xe7, 0x11, 0x76, 0x81, 0x1a, 0x0f, 0x36, 0x49, 0x0f, 0xb2, 0x32, 0x0d, 0x23, 0x96, - 0x6f, 0x57, 0xc5, 0x29, 0xcc, 0x6b, 0xb5, 0x0e, 0x85, 0x20, 0x99, 0xd9, 0x6d, 0x23, 0x9f, 0x33, 0x4f, 0xc2, 0xa0, - 0x63, 0x47, 0x69, 0x83, 0x0a, 0xe5, 0xd8, 0x56, 0xf3, 0x68, 0x82, 0x5e, 0xf6, 0xd6, 0x39, 0x24, 0x73, 0x5b, 0x4e, - 0x0b, 0x56, 0x04, 0x24, 0x1e, 0xd7, 0xe2, 0xa3, 0xa9, 0xbb, 0xa1, 0xce, 0x11, 0x16, 0x39, 0x30, 0xcb, 0x96, 0x89, - 0xa8, 0xc5, 0xa5, 0x67, 0x6e, 0x1a, 0x6c, 0x2a, 0xcb, 0x4c, 0x7a, 0x11, 0xb2, 0x68, 0xa5, 0x89, 0x2d, 0xcc, 0xc5, - 0x38, 0x33, 0x07, 0x96, 0xf6, 0x11, 0x1a, 0x06, 0xcb, 0x48, 0x48, 0x63, 0x4b, 0x96, 0xb7, 0xc8, 0xa9, 0xc0, 0xd1, - 0xfe, 0x67, 0x96, 0x3b, 0x62, 0x2b, 0xf7, 0xd0, 0x82, 0xef, 0xf7, 0x57, 0x51, 0xc3, 0xd0, 0xf6, 0x57, 0xfe, 0xbd, - 0xe4, 0x22, 0xa8, 0x57, 0x90, 0x0f, 0x49, 0x26, 0x15, 0x38, 0x28, 0x0c, 0xd4, 0xc9, 0xb8, 0x11, 0xad, 0x4d, 0x78, - 0x24, 0x87, 0x48, 0x13, 0x79, 0x6d, 0x29, 0x2a, 0x87, 0x22, 0x6b, 0xaf, 0xd4, 0x2a, 0x21, 0x00, 0xbd, 0xf5, 0x4e, - 0xb7, 0x1a, 0x0d, 0x6f, 0xd4, 0x24, 0xca, 0x41, 0x7c, 0x38, 0x0d, 0x4f, 0xda, 0xe8, 0x8a, 0xf3, 0x72, 0xe2, 0x33, - 0x75, 0x77, 0x40, 0xa0, 0x81, 0xb3, 0x80, 0xc3, 0x0b, 0x83, 0x59, 0x5d, 0x55, 0x56, 0xdb, 0x05, 0x09, 0xb2, 0xa9, - 0x7f, 0x41, 0x7f, 0x58, 0x9b, 0x23, 0xb5, 0x49, 0x30, 0x1a, 0x47, 0x93, 0xf5, 0xbf, 0x8b, 0x09, 0xbc, 0xa2, 0x2e, - 0xd0, 0x1e, 0x9f, 0xb6, 0x73, 0x2a, 0x4a, 0xb6, 0xfd, 0xe7, 0x43, 0x39, 0x81, 0xfd, 0x5e, 0x76, 0x62, 0x76, 0x78, - 0x2a, 0x47, 0x3d, 0xbd, 0x4a, 0xc5, 0xd8, 0x43, 0x0c, 0xf5, 0x76, 0x04, 0x2c, 0xeb, 0x1b, 0x6b, 0x96, 0xcd, 0x76, - 0x24, 0x5b, 0x73, 0xf4, 0x72, 0x96, 0x48, 0xee, 0x1e, 0x34, 0x58, 0xce, 0xc4, 0x96, 0xcf, 0xe8, 0x79, 0xbb, 0xb5, - 0xc7, 0xe4, 0xed, 0x2c, 0x3b, 0x82, 0x2f, 0x5a, 0xdf, 0xd8, 0xd5, 0x5e, 0xf4, 0xc8, 0x75, 0xec, 0xc3, 0x04, 0x92, - 0x60, 0xb1, 0x00, 0x17, 0x71, 0xcf, 0x27, 0x67, 0xd6, 0x62, 0x9f, 0x8a, 0xd8, 0x45, 0x5a, 0xa8, 0x9f, 0xd2, 0x32, - 0x22, 0xe9, 0xf0, 0xf2, 0x63, 0xcf, 0x48, 0x1e, 0xd7, 0x51, 0xaa, 0xd4, 0x6f, 0x3d, 0x8e, 0x32, 0xb2, 0xc8, 0x51, - 0x27, 0x5c, 0xc2, 0x63, 0xdb, 0xc9, 0x4e, 0x77, 0xac, 0x5a, 0xc8, 0x3e, 0x80, 0x00, 0x49, 0xc8, 0x96, 0xb4, 0xdd, - 0x45, 0x6a, 0xe4, 0xef, 0x71, 0x31, 0x2c, 0x46, 0x84, 0xf0, 0xb0, 0x6e, 0xf0, 0x4f, 0xba, 0xcc, 0xd4, 0xdb, 0xa9, - 0x7c, 0xf0, 0x82, 0x38, 0x1a, 0x0f, 0x8f, 0xd4, 0x28, 0xb4, 0xfa, 0x64, 0x1c, 0x35, 0x29, 0x9c, 0xa1, 0x95, 0xd3, - 0xf6, 0x6f, 0xa0, 0xb7, 0x53, 0xd3, 0x45, 0xa0, 0x45, 0xe1, 0x8b, 0x47, 0x28, 0x01, 0xb1, 0x44, 0x58, 0x31, 0xa4, - 0x92, 0x30, 0x95, 0x09, 0xb9, 0x64, 0xcc, 0x65, 0xc6, 0x9d, 0x5a, 0x05, 0x77, 0x59, 0x15, 0xf7, 0x58, 0x3d, 0xee, - 0xb3, 0x06, 0x3c, 0xa8, 0x35, 0xe2, 0x21, 0xab, 0xe1, 0x1c, 0xa2, 0x7a, 0x51, 0xbd, 0xac, 0x5e, 0x55, 0xd7, 0xca, - 0x75, 0xaf, 0x43, 0x26, 0x30, 0xfe, 0x25, 0x49, 0xb4, 0xfa, 0x10, 0x2d, 0x4d, 0x79, 0xbe, 0x73, 0xf7, 0xde, 0xfd, - 0x07, 0x0f, 0xc7, 0x05, 0x2a, 0x71, 0x56, 0xf7, 0x6d, 0x59, 0xd2, 0x00, 0xb6, 0x50, 0x3a, 0x7d, 0x4b, 0x49, 0x83, - 0x89, 0x42, 0xdb, 0xf9, 0x93, 0xa5, 0xbf, 0x3c, 0xfe, 0xa0, 0xea, 0xb9, 0x79, 0x56, 0xac, 0xbc, 0xf5, 0x88, 0xd3, - 0x02, 0x2d, 0x9e, 0x5e, 0xb0, 0x90, 0x4e, 0x07, 0x1d, 0xf3, 0x6a, 0x1e, 0xf3, 0x90, 0x51, 0x8f, 0xad, 0x85, 0xa7, - 0x5a, 0xc9, 0xf2, 0x07, 0x1d, 0xc0, 0xb1, 0x38, 0x9e, 0xc9, 0xbe, 0x79, 0x24, 0x66, 0xa2, 0x69, 0xda, 0x6a, 0x42, - 0xd5, 0x3f, 0xdc, 0xba, 0xa1, 0xea, 0xc0, 0xc6, 0xec, 0xf8, 0xed, 0x27, 0x0a, 0xb0, 0x4c, 0x13, 0x98, 0x70, 0xe3, - 0x38, 0x6b, 0x16, 0x2e, 0x66, 0xcb, 0xe6, 0xf9, 0x88, 0x01, 0xea, 0x99, 0x0a, 0xa0, 0x92, 0x1e, 0xf8, 0x67, 0xc7, - 0xfe, 0x74, 0xee, 0x2e, 0x6c, 0x18, 0xfd, 0xce, 0xec, 0x7e, 0xf5, 0x1b, 0x71, 0x02, 0xa9, 0xc7, 0xa2, 0xb2, 0x52, - 0xcd, 0xc4, 0x8b, 0x6a, 0x6f, 0xc4, 0xd1, 0xcd, 0x4c, 0x73, 0x63, 0x6f, 0x6f, 0x82, 0xd1, 0xee, 0xc8, 0x00, 0x88, - 0x40, 0xfd, 0xe7, 0xf4, 0x94, 0xd5, 0xfa, 0x76, 0xfa, 0x4d, 0xca, 0xa6, 0x48, 0x09, 0x3e, 0x2d, 0xfe, 0xe0, 0x9f, - 0xba, 0x6f, 0x5a, 0x6c, 0x85, 0x36, 0xf2, 0xb9, 0xca, 0x89, 0x23, 0x91, 0x27, 0xbe, 0x63, 0xeb, 0xac, 0xf2, 0xb0, - 0xf1, 0x53, 0x58, 0xde, 0x66, 0x5a, 0x5c, 0x8a, 0x63, 0x6d, 0x9c, 0xc3, 0x22, 0x37, 0xff, 0x4c, 0xb5, 0xa3, 0xf1, - 0x8b, 0x7d, 0xfb, 0x2b, 0x53, 0xe5, 0x61, 0x39, 0xbe, 0xf2, 0xef, 0xf2, 0x73, 0xf4, 0x38, 0x2f, 0x72, 0xb5, 0xfe, - 0x3e, 0x35, 0xcb, 0x96, 0x0f, 0x4f, 0x72, 0x5f, 0x71, 0x99, 0xa7, 0xa6, 0xf9, 0xa5, 0xaf, 0x86, 0x3c, 0x77, 0xad, - 0xe9, 0xdd, 0xcd, 0xcf, 0x58, 0xfe, 0x49, 0x35, 0xab, 0xf6, 0xa0, 0x7f, 0x95, 0x3d, 0x3b, 0x9e, 0x8a, 0x11, 0x99, - 0x1a, 0x2b, 0x73, 0x40, 0x75, 0x7f, 0x7e, 0x16, 0x09, 0x6e, 0xfc, 0xa7, 0xc7, 0x25, 0x3d, 0x83, 0xa4, 0xb7, 0x75, - 0xfe, 0x42, 0xe8, 0xa6, 0x9e, 0xf4, 0xe0, 0x10, 0xd4, 0x2b, 0xfe, 0x37, 0x0f, 0xe1, 0x0b, 0x4c, 0x5d, 0x20, 0x10, - 0x6f, 0x60, 0x2a, 0xf4, 0xf3, 0xcb, 0xe8, 0x34, 0xd1, 0xdd, 0xa4, 0x65, 0xaa, 0xa2, 0xa6, 0x94, 0x13, 0x3b, 0x42, - 0xc1, 0xb7, 0x93, 0x91, 0x5e, 0x32, 0xda, 0x3a, 0x7f, 0x2d, 0x74, 0x4b, 0x91, 0xdd, 0x4d, 0xbc, 0x55, 0xe7, 0x3d, - 0x2b, 0xe7, 0xcf, 0xf5, 0x74, 0x7b, 0x82, 0x5d, 0x7d, 0x06, 0x54, 0xcb, 0x02, 0x9c, 0x97, 0x6f, 0x60, 0xda, 0x2f, - 0x02, 0x94, 0xd1, 0x62, 0x35, 0xf4, 0x1b, 0xf5, 0x96, 0xc9, 0xfa, 0xdb, 0x8f, 0x6b, 0x0d, 0xd8, 0x39, 0xfc, 0x73, - 0x03, 0x86, 0xe7, 0x48, 0xf4, 0x1c, 0x41, 0x97, 0xae, 0x31, 0x97, 0x35, 0xda, 0x90, 0x3d, 0xd5, 0xd8, 0xbc, 0x05, - 0x97, 0x82, 0xf0, 0xb6, 0x61, 0x36, 0xcd, 0x7c, 0xac, 0x62, 0xae, 0xce, 0x65, 0x9e, 0x3e, 0x23, 0x21, 0xdf, 0xb7, - 0xac, 0x8d, 0x05, 0x53, 0x10, 0xcc, 0x6c, 0x98, 0x32, 0x96, 0xaf, 0x8b, 0xa6, 0x14, 0x7e, 0x1f, 0xc5, 0xb0, 0xee, - 0x19, 0x8b, 0xa4, 0x60, 0x5d, 0xf9, 0x2f, 0x39, 0xe6, 0x31, 0x8f, 0xa5, 0x83, 0x9e, 0x1b, 0xe6, 0xd1, 0x0c, 0x7c, - 0xcc, 0xa8, 0x4a, 0x9c, 0xc1, 0x8e, 0x17, 0xcf, 0x88, 0x82, 0x16, 0xcd, 0xf5, 0xc7, 0xb6, 0xd6, 0x87, 0x4c, 0xdd, - 0xad, 0x98, 0xcd, 0xcf, 0xf4, 0x6d, 0xb6, 0xe9, 0x80, 0xc9, 0x12, 0x41, 0x98, 0x43, 0xf3, 0x7b, 0xf5, 0xa1, 0xb2, - 0x93, 0x5b, 0xa3, 0xb5, 0x76, 0x46, 0xe3, 0x7c, 0xa8, 0x48, 0x75, 0x0e, 0x7d, 0x91, 0xa8, 0xcb, 0x88, 0x71, 0x93, - 0x2d, 0xbd, 0xdb, 0x2f, 0x4b, 0xe3, 0xbf, 0x96, 0xad, 0x32, 0xe2, 0xa9, 0xb9, 0x02, 0xba, 0xcb, 0x35, 0x5c, 0x56, - 0xc4, 0x64, 0x8a, 0x79, 0xb8, 0x6d, 0xe5, 0x6c, 0x16, 0x72, 0x69, 0x3e, 0x81, 0x33, 0xa0, 0x0a, 0xd7, 0x98, 0x05, - 0xd1, 0x5c, 0xb0, 0x00, 0xd6, 0x02, 0xa7, 0x97, 0x27, 0x73, 0x79, 0xd6, 0x31, 0x4a, 0x8c, 0x65, 0xed, 0x1f, 0x2f, - 0x2f, 0x0d, 0xfa, 0x49, 0x16, 0xf2, 0xcc, 0x77, 0xdc, 0xa9, 0xda, 0xa7, 0x78, 0x3a, 0xfa, 0xdd, 0x4f, 0xf9, 0x7b, - 0x0e, 0xdd, 0x76, 0x49, 0xb6, 0x6f, 0x9f, 0x3a, 0x14, 0xe0, 0x48, 0x17, 0xf2, 0x6b, 0x5b, 0xec, 0xb9, 0x5b, 0xf6, - 0x21, 0xa6, 0x85, 0xb0, 0x31, 0xf3, 0x68, 0x11, 0x70, 0x59, 0xde, 0xbb, 0x51, 0xeb, 0x79, 0x4b, 0x02, 0x2e, 0xf1, - 0x9e, 0x9f, 0xea, 0x68, 0x29, 0x6d, 0x47, 0xee, 0xb3, 0x83, 0x28, 0x67, 0x57, 0x5d, 0x3f, 0x31, 0x37, 0xfe, 0xdb, - 0x9d, 0x3d, 0x53, 0x6b, 0xb5, 0x20, 0x29, 0x97, 0x7e, 0x9e, 0x1f, 0x9a, 0x99, 0x4a, 0x26, 0xf0, 0xf0, 0x02, 0x12, - 0xbf, 0xf0, 0xd3, 0xbf, 0x1f, 0xa8, 0xb2, 0xae, 0x1c, 0x22, 0x3d, 0x03, 0x92, 0xe7, 0xe3, 0xc7, 0xd7, 0x85, 0xc7, - 0x3b, 0xa2, 0x4b, 0xbd, 0x9e, 0xe7, 0x16, 0x12, 0xe7, 0x49, 0x77, 0x4e, 0x5c, 0xad, 0x5c, 0xa0, 0x67, 0xa6, 0x21, - 0xe1, 0x5c, 0xf5, 0x8f, 0x0f, 0x81, 0x7f, 0x05, 0x0e, 0x24, 0xa9, 0xab, 0x0b, 0x15, 0x02, 0x9d, 0xd0, 0x7e, 0xde, - 0x12, 0x48, 0x78, 0xf7, 0x22, 0xd8, 0x62, 0x90, 0x7b, 0x2d, 0xa8, 0xa8, 0x2a, 0x54, 0x30, 0x6f, 0x44, 0x25, 0x78, - 0xe4, 0x1f, 0x30, 0x68, 0x9e, 0x98, 0x99, 0xe1, 0x3c, 0x82, 0x88, 0x24, 0x39, 0xb1, 0x45, 0x7c, 0x00, 0xa0, 0x8e, - 0x77, 0x82, 0xf1, 0x4a, 0xe2, 0x30, 0x42, 0x09, 0x2e, 0xbf, 0x17, 0xad, 0x47, 0x71, 0x67, 0x87, 0x83, 0x7f, 0x41, - 0x9a, 0xc7, 0xed, 0xde, 0x1f, 0x43, 0x7f, 0xf6, 0x01, 0xcd, 0xd0, 0xee, 0x04, 0xf4, 0x71, 0xb7, 0x26, 0xed, 0x7e, - 0x33, 0x3d, 0x13, 0x6d, 0xb7, 0x49, 0x62, 0x73, 0x20, 0x63, 0xde, 0x9e, 0x88, 0x0d, 0x6d, 0xfc, 0x01, 0x7e, 0x6b, - 0x6c, 0x56, 0x5d, 0x26, 0x9e, 0x59, 0x3d, 0x3c, 0x9e, 0x89, 0x27, 0x56, 0xab, 0x8d, 0xd8, 0xb1, 0xfa, 0x3f, 0xd4, - 0xf7, 0xf8, 0x96, 0x55, 0x78, 0x54, 0xfd, 0x17, 0xda, 0x81, 0x87, 0xb0, 0xb1, 0x36, 0x8f, 0x9e, 0x35, 0x6c, 0xf0, - 0x60, 0x75, 0x09, 0x3a, 0xf8, 0xf1, 0x57, 0x06, 0x8f, 0x88, 0xdd, 0x0f, 0x06, 0x2b, 0xab, 0x29, 0xb0, 0x3c, 0xde, - 0x1f, 0xdd, 0xff, 0x3f, 0x6f, 0x1a, 0x1e, 0xba, 0xd6, 0xd3, 0x1a, 0x2c, 0x2a, 0xa1, 0xc2, 0xfc, 0x7f, 0x56, 0x0f, - 0x62, 0xc6, 0x6a, 0x9d, 0x89, 0x29, 0xab, 0x3a, 0x13, 0x13, 0x56, 0xfb, 0xbc, 0x5e, 0x6f, 0x88, 0x1e, 0x2b, 0x5f, - 0x88, 0x31, 0xab, 0xe5, 0x9d, 0xe8, 0xb3, 0xaa, 0xb6, 0x7f, 0x03, 0x31, 0x60, 0xe5, 0x13, 0x31, 0x8c, 0x0c, 0x56, - 0x30, 0xfa, 0x1b, 0x2f, 0x77, 0x32, 0xb4, 0x5b, 0x3d, 0xb7, 0xc6, 0xff, 0x45, 0x27, 0xea, 0xd3, 0xe5, 0xc4, 0x95, - 0x67, 0x12, 0x70, 0xd1, 0xfe, 0x5b, 0x78, 0xbd, 0x09, 0x8f, 0x79, 0x60, 0xa4, 0x62, 0x69, 0x06, 0xc0, 0x38, 0x3f, - 0xfc, 0x4f, 0x77, 0x84, 0xb9, 0x91, 0x04, 0x46, 0x56, 0x29, 0x6b, 0xa3, 0xff, 0x97, 0xae, 0x20, 0x2a, 0x83, 0x6c, - 0xfb, 0xf0, 0xb6, 0x3a, 0x35, 0x7a, 0x0d, 0x6d, 0x18, 0xbd, 0xbc, 0xce, 0x59, 0x17, 0xd0, 0x51, 0x4b, 0x0a, 0xa1, - 0xeb, 0xba, 0x7b, 0x62, 0x7a, 0x5b, 0xbc, 0x23, 0x98, 0x11, 0xd4, 0x44, 0x04, 0x49, 0xd3, 0xfe, 0x4f, 0xce, 0xb6, - 0xe5, 0x58, 0x7b, 0xca, 0x62, 0xf9, 0xf0, 0x7d, 0x75, 0x75, 0x2a, 0x4c, 0x99, 0x09, 0x2e, 0xf3, 0xb0, 0xad, 0xde, - 0x53, 0x3d, 0x8c, 0xa5, 0xdb, 0xd3, 0xf0, 0x12, 0x31, 0x7c, 0x4b, 0xae, 0x5a, 0x10, 0xef, 0x09, 0xe6, 0xd8, 0x2d, - 0x81, 0x58, 0x29, 0x5c, 0x8d, 0xd7, 0x2d, 0x24, 0x8e, 0x19, 0xa9, 0xf1, 0x57, 0xeb, 0xfd, 0xdb, 0xcd, 0x14, 0xa7, - 0xc0, 0xa1, 0xe8, 0x36, 0xe0, 0x1d, 0x11, 0xe5, 0x94, 0x43, 0x96, 0x3c, 0xe2, 0x60, 0x87, 0x13, 0x07, 0x64, 0x99, - 0x26, 0xda, 0xac, 0x95, 0xd7, 0x04, 0xef, 0xea, 0xd1, 0x29, 0x93, 0x63, 0x6b, 0x03, 0xc7, 0x20, 0xf9, 0x17, 0xbc, - 0xea, 0x15, 0xa0, 0x0f, 0xd6, 0x54, 0x5d, 0xe8, 0xdb, 0x97, 0xf3, 0x3b, 0xd5, 0xa6, 0x5d, 0xe1, 0x95, 0xfd, 0x86, - 0xad, 0xce, 0xea, 0x1b, 0x41, 0x6a, 0xdd, 0x6d, 0xa4, 0x21, 0x40, 0xf7, 0x43, 0xbe, 0xbb, 0xa7, 0x23, 0x9c, 0x6c, - 0xed, 0x1c, 0x61, 0xa1, 0x99, 0x11, 0xfa, 0xdb, 0x08, 0xb8, 0x43, 0x01, 0x55, 0xdc, 0xda, 0x33, 0xa3, 0x48, 0x44, - 0xb4, 0x24, 0x15, 0xf1, 0x1e, 0x5c, 0xcf, 0xc6, 0xb0, 0x6c, 0xa4, 0x9d, 0xbd, 0xbe, 0xc9, 0xb6, 0x28, 0x82, 0xb0, - 0x75, 0xbd, 0x23, 0x0c, 0xe4, 0x2f, 0x03, 0xff, 0xd7, 0xea, 0xbd, 0xb4, 0x5a, 0xba, 0xa9, 0x7b, 0x7c, 0x0b, 0x7e, - 0xa8, 0x4b, 0xf9, 0x99, 0xa4, 0x98, 0x9d, 0x26, 0x3e, 0xf5, 0x49, 0x79, 0x8a, 0x97, 0x5d, 0x06, 0x40, 0xe4, 0x7a, - 0x4e, 0xc1, 0x87, 0xbc, 0xdb, 0x4c, 0xa9, 0x8b, 0xcc, 0x63, 0x82, 0x01, 0x66, 0x97, 0xd2, 0xc5, 0x3a, 0x35, 0x5c, - 0x6c, 0xa4, 0x1c, 0x6e, 0x3a, 0x9c, 0x36, 0xf4, 0xaa, 0x18, 0x17, 0x91, 0x1d, 0xdf, 0x35, 0x8d, 0x6f, 0x72, 0x23, - 0xf4, 0xde, 0xb9, 0xe1, 0xa9, 0xcf, 0x18, 0xf3, 0xb7, 0x82, 0x50, 0x19, 0x63, 0x3b, 0x9c, 0xad, 0x28, 0xd5, 0x4a, - 0x7e, 0x39, 0x6c, 0x73, 0xd8, 0xbf, 0xc9, 0xad, 0x6d, 0x0c, 0x18, 0xf1, 0x05, 0xe3, 0xbb, 0x10, 0xbf, 0x2f, 0x58, - 0x23, 0x7a, 0xc1, 0x25, 0xcb, 0x53, 0xb0, 0xf0, 0x50, 0x02, 0x53, 0xb6, 0x87, 0x26, 0xe0, 0xde, 0xe7, 0x26, 0x7e, - 0x3b, 0xe4, 0xe6, 0xd1, 0x87, 0x78, 0x2d, 0x94, 0x2a, 0x11, 0xf6, 0xad, 0x78, 0xd6, 0xa8, 0xe0, 0x99, 0x9b, 0x95, - 0xcc, 0x00, 0x28, 0x04, 0xba, 0xd5, 0x1c, 0xbe, 0xeb, 0x8f, 0x7b, 0x75, 0x80, 0xce, 0x6a, 0x5f, 0xce, 0x84, 0x8c, - 0x91, 0xe7, 0xf4, 0x20, 0xe8, 0xe9, 0xa3, 0x77, 0xbc, 0xbd, 0xac, 0x2a, 0x43, 0x8e, 0xc5, 0xc8, 0xa1, 0x99, 0x3c, - 0x2a, 0x4b, 0x1a, 0xb2, 0xe0, 0x72, 0x2a, 0xd7, 0xa4, 0x5a, 0xf5, 0x25, 0xe9, 0x7d, 0xcd, 0xd9, 0x0e, 0x68, 0xec, - 0x79, 0xbf, 0x85, 0xe0, 0x18, 0x84, 0x90, 0x30, 0x27, 0x36, 0xf7, 0xfe, 0xce, 0x00, 0xe7, 0xdb, 0x97, 0x50, 0x6f, - 0xbe, 0xf9, 0x81, 0x5b, 0xf4, 0x13, 0x8e, 0xa1, 0xb4, 0x38, 0xd5, 0x84, 0xab, 0xa3, 0x37, 0xb2, 0x36, 0x52, 0xd2, - 0x79, 0x1d, 0xbd, 0x1b, 0x1b, 0xc5, 0x78, 0xc7, 0x40, 0x54, 0x44, 0x79, 0xb8, 0x1f, 0x4b, 0xa2, 0x72, 0x73, 0x82, - 0x6b, 0x8a, 0x48, 0x44, 0xe1, 0x20, 0xdd, 0xc5, 0xd5, 0xf8, 0xc2, 0xa1, 0xc6, 0x34, 0x33, 0x07, 0x06, 0x48, 0xaa, - 0x43, 0x5d, 0x9b, 0xdd, 0x37, 0x02, 0xe1, 0x25, 0x17, 0xb0, 0x5c, 0x81, 0xcb, 0x43, 0xfe, 0x22, 0xf5, 0x5d, 0xcc, - 0x3b, 0x6d, 0x42, 0x24, 0xe7, 0xce, 0x80, 0x18, 0x38, 0xa4, 0x08, 0x25, 0xd3, 0x8d, 0x4b, 0x4e, 0xe2, 0xd6, 0x6c, - 0xce, 0x5c, 0x28, 0x19, 0x33, 0xf7, 0x88, 0xfe, 0x83, 0xf7, 0x36, 0x21, 0x5e, 0x70, 0x90, 0x45, 0x25, 0x3a, 0x1b, - 0x46, 0x3a, 0x91, 0xf0, 0x6b, 0x2c, 0xde, 0x60, 0x47, 0x96, 0x06, 0xd1, 0x4d, 0x91, 0x31, 0x84, 0x4d, 0x3b, 0xac, - 0x0e, 0xcd, 0x46, 0x49, 0x42, 0x0e, 0x08, 0xb5, 0xf3, 0x24, 0x27, 0x1d, 0xe1, 0xdd, 0xbb, 0xdc, 0xa6, 0xea, 0xc5, - 0xaf, 0x32, 0xd1, 0xa8, 0x36, 0x11, 0x2b, 0xbf, 0x9e, 0xc8, 0xca, 0xc2, 0x97, 0x02, 0x9a, 0x02, 0x25, 0x49, 0xfe, - 0xf6, 0xd7, 0xb4, 0xcc, 0xd3, 0x6b, 0x1d, 0x64, 0x30, 0x98, 0x7c, 0x25, 0x72, 0x8d, 0x1a, 0xd9, 0xe0, 0x3b, 0xe9, - 0x5c, 0xc6, 0x2a, 0xf7, 0x65, 0x88, 0x78, 0x9a, 0x9b, 0xfe, 0xa5, 0x0f, 0x2a, 0xd4, 0xea, 0x43, 0x23, 0xbb, 0x93, - 0xb6, 0x3e, 0x49, 0xd1, 0x48, 0x56, 0xec, 0xe2, 0x8b, 0x4b, 0x34, 0x5a, 0x32, 0x15, 0x4f, 0xca, 0x22, 0x63, 0x93, - 0x6b, 0xaf, 0x8d, 0x4d, 0xd4, 0x1d, 0x8c, 0x25, 0xb2, 0x34, 0x7c, 0x3b, 0x3d, 0xda, 0x10, 0xb7, 0xf7, 0x50, 0x57, - 0x37, 0x65, 0x1f, 0xde, 0xcd, 0xa8, 0x42, 0x73, 0xb3, 0x4a, 0x9d, 0x71, 0x70, 0x86, 0x5f, 0xaf, 0x50, 0x68, 0x4e, - 0x9f, 0x1a, 0x92, 0xe3, 0xec, 0x6b, 0x69, 0x00, 0xed, 0xab, 0xc9, 0x28, 0x16, 0x61, 0x21, 0xc4, 0x25, 0x1c, 0x80, - 0x5c, 0x3e, 0x4c, 0x97, 0x58, 0x6b, 0x95, 0x2b, 0xe9, 0x95, 0x48, 0x16, 0x28, 0xf1, 0x51, 0x9d, 0x5a, 0xa9, 0xac, - 0xe6, 0x4c, 0x62, 0x06, 0x0a, 0x98, 0xbc, 0x35, 0xbd, 0x91, 0x9e, 0xe5, 0x96, 0xb9, 0x4c, 0x70, 0xe6, 0x42, 0xb4, - 0xe6, 0x87, 0x6f, 0x72, 0x43, 0x56, 0x54, 0xd3, 0x88, 0x14, 0x3f, 0x38, 0x33, 0x10, 0x13, 0x20, 0x96, 0x31, 0xd7, - 0x27, 0x98, 0x60, 0x83, 0xf5, 0x66, 0x4d, 0xd7, 0xb0, 0xa1, 0xfc, 0x74, 0xff, 0xe4, 0x17, 0x63, 0x9e, 0x23, 0x80, - 0x95, 0x99, 0x78, 0x5e, 0xf2, 0xe9, 0x54, 0x29, 0x74, 0x89, 0x28, 0xce, 0x68, 0xd1, 0xd8, 0x99, 0x86, 0x65, 0x6c, - 0x23, 0x43, 0x00, 0xb4, 0x37, 0xf9, 0xf5, 0x65, 0x16, 0x25, 0xb5, 0x32, 0x21, 0xf2, 0x11, 0xd3, 0x8c, 0x3c, 0x39, - 0xd5, 0x21, 0x6b, 0x0d, 0x1e, 0x46, 0x95, 0x48, 0xfe, 0x78, 0x2a, 0xb9, 0x90, 0x44, 0xef, 0x61, 0x3b, 0x54, 0x59, - 0xb2, 0x60, 0xc5, 0x2a, 0x7a, 0xdf, 0xde, 0xee, 0xfa, 0x6b, 0x64, 0xc2, 0x5e, 0x00, 0xa2, 0xd7, 0x1e, 0x1c, 0xe3, - 0x95, 0xc9, 0x27, 0x57, 0x19, 0x2c, 0x28, 0x25, 0x42, 0x4d, 0x38, 0xda, 0x98, 0xcb, 0x32, 0x53, 0x70, 0xd5, 0x23, - 0xd9, 0xb2, 0x94, 0x39, 0xc9, 0xb0, 0xde, 0x06, 0x92, 0xf1, 0x11, 0xb5, 0xfc, 0xb5, 0x60, 0x6f, 0x1d, 0xd4, 0x29, - 0x04, 0x71, 0x92, 0xff, 0xe6, 0xf1, 0xba, 0xc5, 0xf7, 0xcb, 0x4f, 0x9b, 0x2c, 0x46, 0x92, 0xbd, 0x48, 0x53, 0xe9, - 0xbf, 0xd0, 0x2c, 0x0d, 0x0e, 0x4a, 0x4b, 0x6f, 0xcf, 0x05, 0x57, 0x7a, 0x21, 0x8a, 0x59, 0x00, 0x4f, 0x48, 0xa9, - 0x37, 0xba, 0x92, 0x68, 0x9d, 0x61, 0x75, 0x2c, 0xce, 0x6a, 0x11, 0x7a, 0x95, 0x4e, 0x88, 0xc5, 0x53, 0x23, 0xf2, - 0x9b, 0xac, 0x38, 0x47, 0xf7, 0xc6, 0xe3, 0x6b, 0x76, 0xbe, 0x2c, 0x43, 0x65, 0xea, 0x47, 0x88, 0xbe, 0x14, 0x1c, - 0x21, 0x36, 0x12, 0x75, 0x1b, 0x56, 0x8c, 0x10, 0x4c, 0x78, 0x75, 0x62, 0x96, 0x4b, 0xf4, 0xda, 0xfa, 0xe3, 0x7d, - 0xba, 0x67, 0xd5, 0x30, 0x7a, 0x65, 0x3e, 0xfe, 0x25, 0x91, 0xcd, 0x30, 0xfa, 0x13, 0xf8, 0x81, 0xc5, 0x9d, 0x9b, - 0xe9, 0x41, 0x38, 0x31, 0x4f, 0x4a, 0x2a, 0xb3, 0xf9, 0x83, 0xbd, 0xc3, 0x30, 0xba, 0xa0, 0xfb, 0xc1, 0x9b, 0x4e, - 0xad, 0x76, 0xbf, 0x21, 0xba, 0x8a, 0xa7, 0xdd, 0xfd, 0xaa, 0x3f, 0x98, 0x24, 0x0a, 0xd1, 0x8b, 0x06, 0x00, 0x3c, - 0xcd, 0x80, 0x67, 0x92, 0x62, 0x63, 0xf2, 0x66, 0x96, 0xae, 0x9c, 0x13, 0x5f, 0x50, 0xe7, 0xd9, 0x86, 0xac, 0xc9, - 0xbd, 0x24, 0xc8, 0x29, 0x55, 0x6e, 0x0d, 0xca, 0x18, 0x15, 0x55, 0x62, 0x78, 0x9a, 0x46, 0xb0, 0x01, 0x72, 0x4b, - 0x5b, 0x74, 0x3d, 0x23, 0x35, 0xa7, 0x73, 0x48, 0xd5, 0xca, 0x12, 0xdf, 0xa3, 0x3f, 0xca, 0xd0, 0x78, 0x48, 0xc4, - 0x56, 0x5d, 0xd3, 0xdc, 0xaf, 0xeb, 0x5d, 0x3a, 0x6e, 0xf8, 0x9d, 0x89, 0xc2, 0x6f, 0x5e, 0xe0, 0x3d, 0xb7, 0xd0, - 0xd1, 0x06, 0x37, 0x8e, 0xec, 0xe0, 0xcf, 0x60, 0x02, 0x63, 0x3f, 0x6f, 0x86, 0x83, 0xcb, 0x19, 0x9a, 0xaf, 0xa7, - 0xb9, 0xc7, 0xbd, 0x23, 0x6e, 0xd9, 0x5c, 0xe3, 0x7f, 0x73, 0x0b, 0x05, 0xe5, 0xfb, 0xcc, 0xb0, 0xa4, 0xd9, 0xde, - 0x8a, 0xa4, 0xf2, 0x35, 0x03, 0xd8, 0x85, 0x94, 0x9a, 0x0a, 0xb3, 0x69, 0x36, 0x99, 0x60, 0x00, 0x74, 0x91, 0xdf, - 0x5a, 0x2a, 0x88, 0x70, 0x89, 0xc6, 0x80, 0x1b, 0x40, 0x7d, 0x00, 0x86, 0x32, 0xe7, 0x10, 0x1e, 0x82, 0xaf, 0xb0, - 0x91, 0x18, 0xd9, 0x25, 0x18, 0xe3, 0x71, 0xdb, 0xc7, 0xaf, 0xc5, 0x5e, 0xd3, 0xec, 0x94, 0xef, 0x31, 0x36, 0xb1, - 0x79, 0x16, 0x1e, 0xd2, 0x07, 0x39, 0xf3, 0x9d, 0x19, 0x2b, 0xa2, 0x75, 0x7e, 0x2e, 0xec, 0x2f, 0x2d, 0x91, 0x60, - 0xd2, 0x52, 0xdb, 0x1b, 0x90, 0xf2, 0x89, 0x80, 0xa5, 0x34, 0x2f, 0x42, 0x5b, 0x35, 0xc8, 0x03, 0x25, 0xf4, 0x76, - 0x1a, 0xb5, 0xd7, 0x36, 0xeb, 0x85, 0x62, 0x5d, 0x70, 0xdc, 0x6a, 0x01, 0x43, 0x66, 0x38, 0x62, 0x03, 0xd6, 0x81, - 0x2b, 0x99, 0xa9, 0x66, 0xe2, 0x42, 0x9c, 0xd7, 0x99, 0xd1, 0x8c, 0x9f, 0xf3, 0xd4, 0x6e, 0xab, 0xcd, 0x95, 0x38, - 0x43, 0xff, 0xae, 0x5e, 0xbd, 0x99, 0xc6, 0xe8, 0x6e, 0x4c, 0x20, 0xdb, 0x4a, 0x1f, 0x0d, 0x7f, 0x3c, 0x9c, 0xa5, - 0x18, 0x06, 0xdc, 0x9f, 0x53, 0x15, 0xb4, 0xbf, 0x40, 0x79, 0x96, 0x97, 0x36, 0x76, 0x88, 0xd4, 0x64, 0xa0, 0x54, - 0x79, 0xbe, 0x97, 0x6c, 0x95, 0x48, 0xd0, 0x20, 0xb9, 0x91, 0x27, 0xcf, 0xe1, 0x0b, 0x32, 0xe2, 0x8f, 0xc6, 0xef, - 0x2f, 0x20, 0xc2, 0xce, 0x30, 0x93, 0x07, 0x06, 0x33, 0x77, 0x67, 0x03, 0x4f, 0x92, 0xd6, 0x9f, 0x8e, 0x12, 0xcd, - 0x97, 0x9b, 0xbb, 0x92, 0x4c, 0x71, 0x42, 0xf3, 0x93, 0x0f, 0x33, 0x54, 0x14, 0x97, 0x7f, 0xe0, 0x7a, 0xd6, 0x9e, - 0xa4, 0xc0, 0xf5, 0x7e, 0x08, 0x59, 0x11, 0xa9, 0xa8, 0x05, 0xf5, 0xd0, 0x8c, 0xc6, 0xf2, 0x43, 0x73, 0xfb, 0x45, - 0xaf, 0x08, 0x6c, 0xe2, 0x51, 0x8d, 0x9f, 0xf4, 0x5f, 0x3f, 0x30, 0x20, 0xe6, 0xbf, 0x4b, 0x62, 0x45, 0x55, 0xe3, - 0xdc, 0x65, 0x89, 0xaf, 0x60, 0xd5, 0x9f, 0x87, 0x44, 0x11, 0x64, 0xb7, 0x52, 0x84, 0x10, 0x9a, 0x2a, 0x62, 0xda, - 0x03, 0x38, 0xbd, 0x41, 0xdf, 0x51, 0x84, 0x85, 0x0a, 0x37, 0xa6, 0x9f, 0x90, 0x9a, 0x04, 0xa3, 0xd3, 0xd1, 0x40, - 0xe5, 0x80, 0xa4, 0x6f, 0x77, 0xbe, 0xbd, 0x47, 0x26, 0x59, 0xab, 0x5b, 0x99, 0xa4, 0x00, 0x81, 0x36, 0x7c, 0xc8, - 0xed, 0xed, 0x79, 0x9e, 0xe7, 0x2a, 0xab, 0xd7, 0xf1, 0x67, 0x1b, 0x0e, 0x13, 0xdb, 0xb1, 0x35, 0x3c, 0x78, 0xa2, - 0x85, 0x74, 0xfc, 0x45, 0x53, 0x34, 0xe8, 0x1a, 0xf1, 0x11, 0x05, 0xfa, 0x9c, 0x5d, 0x48, 0x1a, 0x37, 0xef, 0xca, - 0x75, 0xe0, 0x32, 0xb8, 0x29, 0x19, 0x1c, 0xd8, 0x9d, 0x0a, 0x99, 0x39, 0x35, 0x17, 0x54, 0x1e, 0x0f, 0xa4, 0xfe, - 0x90, 0xca, 0x8d, 0x59, 0xaa, 0x10, 0x1b, 0x54, 0xa7, 0x06, 0x7c, 0xd9, 0x13, 0x41, 0xcb, 0x13, 0xc8, 0xde, 0xab, - 0x21, 0xc5, 0x98, 0xe4, 0x72, 0x97, 0x03, 0xb6, 0xa1, 0x03, 0xd5, 0xa0, 0xe9, 0x18, 0x40, 0xb8, 0xbb, 0xf8, 0x96, - 0xf4, 0xbf, 0x7d, 0x9c, 0x7e, 0xaa, 0xee, 0x3c, 0x02, 0x4d, 0xb2, 0x56, 0x74, 0xbf, 0xd4, 0xa2, 0x21, 0x48, 0x78, - 0x1b, 0x1e, 0x22, 0xfe, 0xf4, 0x77, 0xe4, 0xd2, 0xc0, 0x5a, 0xd7, 0xa1, 0xff, 0x8e, 0x6c, 0xa6, 0x50, 0xa6, 0x15, - 0x52, 0xea, 0x54, 0x2d, 0x9c, 0x3b, 0x45, 0x59, 0x1a, 0x54, 0x3f, 0x48, 0x65, 0x85, 0x03, 0x09, 0x89, 0x44, 0x45, - 0x19, 0x26, 0x15, 0xaf, 0x69, 0x7d, 0x9f, 0x62, 0x13, 0x9e, 0xdd, 0xad, 0xfc, 0x90, 0x39, 0x88, 0x97, 0xc2, 0x1f, - 0x0f, 0xc6, 0xd7, 0x48, 0x6b, 0xa8, 0x67, 0x87, 0x87, 0x23, 0xcc, 0x51, 0xc4, 0xfa, 0x14, 0x65, 0xa8, 0x04, 0x72, - 0x29, 0xc5, 0x13, 0x86, 0x97, 0x98, 0xa8, 0xe8, 0x1c, 0x72, 0xd0, 0xe6, 0x7c, 0x80, 0x85, 0x87, 0x5e, 0xf0, 0xaf, - 0xe8, 0x21, 0x77, 0xaf, 0x8c, 0x88, 0xa6, 0x32, 0xa5, 0xdb, 0x3a, 0xe5, 0x1e, 0x3a, 0x5b, 0x04, 0xbd, 0x7d, 0xcf, - 0x3f, 0x7d, 0xa7, 0xef, 0xd4, 0xcf, 0x3e, 0xe5, 0x63, 0x7d, 0xca, 0xbf, 0xee, 0xfe, 0x63, 0xdb, 0x21, 0xff, 0x78, - 0xc9, 0xa6, 0x6d, 0x58, 0xd3, 0x6e, 0x4a, 0x54, 0xba, 0x4f, 0x14, 0x66, 0xe2, 0xa5, 0x18, 0xff, 0xb6, 0x28, 0x6b, - 0x7d, 0xb9, 0xb0, 0x82, 0x74, 0x32, 0x9b, 0xf0, 0xf5, 0xaf, 0x0b, 0x47, 0x08, 0x2d, 0x02, 0x3b, 0x49, 0xe9, 0x7c, - 0x92, 0xb5, 0x05, 0x34, 0x97, 0xa4, 0xb3, 0x84, 0x59, 0xc2, 0xd6, 0xf9, 0x04, 0xf4, 0x40, 0xb3, 0xa9, 0x5e, 0xe0, - 0x3a, 0x72, 0x0c, 0xc5, 0xf1, 0x6a, 0xe7, 0xa3, 0xdf, 0xde, 0x8a, 0x6f, 0x31, 0xd8, 0x85, 0xa5, 0x16, 0xd4, 0x8c, - 0x9a, 0x55, 0x0b, 0x38, 0x83, 0xb3, 0x78, 0x16, 0x14, 0xe8, 0xe7, 0x82, 0x21, 0xb8, 0x80, 0xd6, 0x06, 0xfa, 0x60, - 0xda, 0x78, 0x84, 0x45, 0x59, 0xe4, 0x1d, 0xf5, 0xe2, 0x66, 0x5d, 0xf5, 0xde, 0xdf, 0x56, 0x8c, 0x4a, 0xbc, 0xe5, - 0x7f, 0x05, 0x55, 0x22, 0xe9, 0xae, 0x90, 0xc8, 0xbb, 0x2a, 0x3e, 0x5e, 0x9b, 0xd6, 0x07, 0xb1, 0xac, 0xda, 0xb2, - 0xfe, 0x8e, 0xb0, 0x33, 0xe1, 0x71, 0xe8, 0x9e, 0xaa, 0x50, 0x10, 0xd3, 0x38, 0x77, 0xc9, 0xf7, 0x43, 0xbe, 0xea, - 0x7c, 0x37, 0xfc, 0x5b, 0x54, 0xcb, 0x5c, 0xcf, 0x24, 0x02, 0xa2, 0x9c, 0x74, 0xc1, 0xca, 0x61, 0x78, 0xe2, 0x9e, - 0xe6, 0x75, 0x91, 0x9c, 0x17, 0xd4, 0x33, 0x2c, 0x6e, 0xdf, 0xcb, 0x5f, 0x84, 0x6d, 0x8a, 0xca, 0x5f, 0xd8, 0x8c, - 0x3d, 0x1c, 0x51, 0x4d, 0xb7, 0x4f, 0xa8, 0xa0, 0x55, 0xa5, 0x1c, 0x4f, 0xbb, 0xf0, 0x22, 0x72, 0xb6, 0x37, 0xb0, - 0x53, 0xe6, 0xb6, 0x66, 0xdb, 0x1d, 0xe9, 0xef, 0x62, 0x15, 0x45, 0xec, 0x8a, 0xc3, 0x3e, 0xad, 0xa4, 0xeb, 0x8a, - 0x9a, 0xc3, 0x10, 0x8d, 0xe3, 0x0d, 0x14, 0xe5, 0xdb, 0x00, 0x1b, 0x1d, 0x20, 0xf4, 0xdb, 0x06, 0x4f, 0x80, 0x39, - 0xcc, 0xac, 0x36, 0x2e, 0x5e, 0xcd, 0x96, 0x4a, 0xee, 0xa9, 0xea, 0xfd, 0x5b, 0x0e, 0x8c, 0xbd, 0xcd, 0xfe, 0x29, - 0x46, 0x1f, 0xf1, 0xd9, 0xfb, 0xdb, 0x4f, 0x18, 0x82, 0x3c, 0x73, 0xe5, 0x7d, 0xdd, 0x83, 0x98, 0x94, 0xd9, 0x2a, - 0x35, 0x6d, 0x2b, 0x5c, 0x32, 0x08, 0xaf, 0x1d, 0x6d, 0x58, 0xa2, 0x4e, 0x61, 0x7f, 0xad, 0x92, 0xd5, 0x4d, 0x80, - 0xcf, 0x93, 0x5a, 0x62, 0x4e, 0x95, 0xc7, 0xfe, 0xea, 0xc5, 0x49, 0x26, 0x7f, 0x62, 0x02, 0x75, 0x06, 0x97, 0xb0, - 0x29, 0xcb, 0x1f, 0xc4, 0xfe, 0xdd, 0xec, 0x6f, 0x97, 0x46, 0xfe, 0xe6, 0x28, 0xb1, 0x08, 0x29, 0xac, 0x60, 0x5c, - 0xca, 0xd7, 0x4b, 0x3a, 0x80, 0x46, 0x36, 0x29, 0x46, 0x2f, 0x68, 0x5f, 0x7e, 0xee, 0xbe, 0xc2, 0xdc, 0x53, 0x32, - 0x76, 0x71, 0x30, 0xcb, 0xb5, 0x45, 0xe1, 0x48, 0x83, 0x65, 0xf0, 0xa2, 0xb7, 0x3a, 0xc2, 0x47, 0xe6, 0x88, 0x8f, - 0xcf, 0xfb, 0xe5, 0x82, 0x68, 0x51, 0x9a, 0x3f, 0x0e, 0x9e, 0x06, 0x74, 0x5c, 0x6a, 0xdb, 0xf4, 0x1e, 0x39, 0x75, - 0x40, 0xe8, 0x1a, 0x9b, 0x4c, 0x3f, 0x56, 0x28, 0x25, 0x35, 0x4b, 0xdb, 0xe9, 0xb1, 0xb1, 0x53, 0x53, 0xa2, 0xf8, - 0xae, 0xef, 0xba, 0x3b, 0x45, 0xb5, 0xae, 0x9f, 0x72, 0x44, 0x3e, 0xea, 0x82, 0x78, 0x35, 0x72, 0x6d, 0x87, 0x5c, - 0x7d, 0xd9, 0xa9, 0xea, 0x41, 0x5d, 0xec, 0x7f, 0xc8, 0x95, 0x9c, 0x66, 0xe3, 0x5b, 0x6f, 0xd0, 0xea, 0x26, 0x0d, - 0x3d, 0xe4, 0xc0, 0x02, 0x87, 0x14, 0xe1, 0x46, 0x0c, 0x6d, 0x6b, 0x24, 0x78, 0xac, 0x98, 0xc2, 0x83, 0xb8, 0x3f, - 0x8e, 0x4c, 0x80, 0xaa, 0xe8, 0x45, 0xa8, 0x8d, 0x6d, 0x0e, 0x3d, 0x03, 0x5c, 0x0f, 0xe9, 0xaf, 0x82, 0x9c, 0xef, - 0xe0, 0x6e, 0x30, 0x5a, 0x67, 0xcf, 0x8b, 0xf2, 0x81, 0x6a, 0x5c, 0x6f, 0xdb, 0xe1, 0x90, 0x5d, 0x63, 0xb7, 0x8b, - 0xa4, 0x76, 0x59, 0xe8, 0x33, 0x5b, 0x83, 0x91, 0x62, 0x6c, 0xbd, 0x05, 0xbe, 0xd9, 0x96, 0x41, 0x65, 0xd7, 0x7e, - 0x23, 0x29, 0xa1, 0xd1, 0xc5, 0xd0, 0x60, 0xbc, 0x81, 0x40, 0x55, 0xb0, 0x3c, 0x8b, 0x69, 0x2b, 0x61, 0x34, 0x1a, - 0xf7, 0xb4, 0x9f, 0x46, 0xf5, 0xb1, 0xfc, 0x91, 0x6e, 0xa6, 0xdc, 0x48, 0x97, 0x1f, 0xa6, 0xcb, 0x3d, 0x04, 0x53, - 0x61, 0xf9, 0x52, 0xad, 0x24, 0x02, 0x6e, 0xb9, 0x82, 0xd2, 0x60, 0x7f, 0x3f, 0xaf, 0xc0, 0xcc, 0x4b, 0x9e, 0x63, - 0x68, 0x78, 0xb1, 0x51, 0xb1, 0x71, 0xe2, 0xa7, 0xbb, 0x44, 0xcb, 0xa9, 0x19, 0x3c, 0x45, 0x3c, 0x20, 0xd5, 0x6d, - 0x0b, 0x5e, 0x1e, 0x3c, 0x46, 0x23, 0x4b, 0x57, 0xca, 0x2e, 0x93, 0x67, 0xf5, 0x50, 0x8e, 0x2a, 0x71, 0xd0, 0x4b, - 0xa4, 0x51, 0x57, 0xde, 0xfa, 0xc6, 0x4b, 0x60, 0x95, 0xb4, 0x4c, 0x4e, 0xbf, 0xef, 0x88, 0x34, 0x48, 0xb8, 0x94, - 0x42, 0xf1, 0x57, 0x89, 0x90, 0x7a, 0x6f, 0x0d, 0x1d, 0xc3, 0xc0, 0xfd, 0x75, 0x3e, 0xe2, 0xac, 0xf8, 0xec, 0x17, - 0x07, 0xd0, 0xa5, 0x2a, 0x1b, 0xa4, 0x5d, 0xac, 0xdc, 0x99, 0xef, 0xf7, 0xe8, 0x6d, 0x95, 0x62, 0xf1, 0x2d, 0xa3, - 0x9f, 0x58, 0xbc, 0x15, 0x32, 0xd8, 0x3d, 0x3f, 0xc0, 0x83, 0x1d, 0x9a, 0x48, 0x5d, 0x25, 0x04, 0x30, 0x41, 0x27, - 0xbd, 0x9c, 0xbc, 0x42, 0x14, 0xa1, 0x05, 0xee, 0xc9, 0xa1, 0x8d, 0x4a, 0x61, 0xbe, 0x82, 0xf0, 0x8f, 0x72, 0xf9, - 0x1e, 0x03, 0xd3, 0xe0, 0x12, 0x0d, 0xe5, 0x03, 0x22, 0xd2, 0x93, 0x91, 0x14, 0x81, 0x17, 0xf2, 0x3e, 0x11, 0x4c, - 0x5c, 0xa3, 0x75, 0x13, 0xbc, 0xa7, 0xc5, 0xd1, 0x4d, 0xf3, 0xd4, 0xc2, 0x8c, 0xf8, 0x19, 0x13, 0x46, 0xe1, 0x32, - 0xc4, 0x77, 0x16, 0x14, 0x9e, 0x60, 0xa7, 0x1a, 0x54, 0xaf, 0x8f, 0xda, 0xf4, 0x62, 0x37, 0xf8, 0x2b, 0x37, 0x1f, - 0xcf, 0x45, 0x3a, 0xf2, 0x42, 0x5c, 0xf2, 0xdc, 0xf9, 0x01, 0x9a, 0x10, 0x9e, 0xbb, 0x61, 0x77, 0x89, 0x0e, 0xac, - 0x93, 0xc9, 0x86, 0x15, 0x4d, 0xdc, 0x74, 0x02, 0x0c, 0xf2, 0xdc, 0x39, 0xb4, 0x6a, 0xe2, 0xe9, 0x3f, 0x55, 0xb9, - 0x5d, 0xf2, 0xbc, 0xc3, 0xee, 0x9a, 0xda, 0x35, 0x36, 0x06, 0x22, 0xe2, 0x62, 0x34, 0xc7, 0xd2, 0x4b, 0xfc, 0x1c, - 0xee, 0xdc, 0x7b, 0x5c, 0x3f, 0xc5, 0x18, 0x20, 0x1f, 0xde, 0x42, 0xb6, 0x80, 0x9e, 0xc5, 0x79, 0x9f, 0xa1, 0x17, - 0xde, 0xed, 0x25, 0x66, 0x44, 0x72, 0x7f, 0xa6, 0xf5, 0x91, 0x28, 0x47, 0x7a, 0x09, 0x29, 0x4e, 0x70, 0x94, 0xec, - 0x44, 0xc0, 0xfe, 0xbf, 0x10, 0x6d, 0x27, 0x88, 0xf2, 0x6d, 0xc2, 0xcd, 0xdd, 0xed, 0x58, 0xb9, 0x7d, 0xcb, 0x13, - 0x42, 0xa9, 0xf8, 0x84, 0x71, 0x88, 0x69, 0x27, 0x13, 0xbb, 0x23, 0x43, 0x44, 0x0f, 0x4b, 0x70, 0x1d, 0xb8, 0x19, - 0x7e, 0x74, 0xf6, 0x36, 0x8e, 0xe4, 0xe2, 0x73, 0xf5, 0xf3, 0x67, 0xdb, 0x60, 0x71, 0xed, 0xf6, 0xf2, 0x02, 0xc8, - 0x44, 0x3e, 0xea, 0x48, 0xbc, 0xa2, 0x01, 0x1a, 0xa7, 0xd7, 0x80, 0x11, 0xa7, 0x2c, 0x7d, 0x41, 0x87, 0x89, 0xca, - 0x23, 0xf5, 0xa0, 0x11, 0x3f, 0xc2, 0x90, 0xed, 0xb2, 0xac, 0x89, 0xb4, 0x30, 0xda, 0xb7, 0x40, 0xe1, 0x04, 0x58, - 0xb9, 0x0d, 0x0b, 0xf4, 0x6b, 0x21, 0x23, 0xaf, 0x81, 0x86, 0xfa, 0x7c, 0xf3, 0xda, 0xdf, 0x4f, 0xf4, 0x4f, 0x8b, - 0xe6, 0x90, 0x96, 0xd4, 0x23, 0xbf, 0x0f, 0xb6, 0xc7, 0xd6, 0xe2, 0xe7, 0x9d, 0xaf, 0x32, 0xa6, 0x25, 0x18, 0x91, - 0x77, 0x63, 0x08, 0xf9, 0x20, 0xc7, 0x2a, 0x08, 0x25, 0x5f, 0xab, 0x5a, 0x3b, 0xc4, 0x7a, 0xca, 0xdb, 0x14, 0x79, - 0xdb, 0x7c, 0x54, 0x51, 0x58, 0xad, 0xc0, 0xfe, 0xaa, 0xa1, 0xac, 0xc4, 0x0b, 0xfd, 0x57, 0x42, 0xa2, 0x0a, 0x89, - 0x45, 0x07, 0x3d, 0x12, 0xce, 0x3f, 0x08, 0x51, 0xd0, 0xe5, 0x96, 0x6a, 0xd9, 0x6e, 0x5f, 0x1a, 0x0a, 0x57, 0x81, - 0x58, 0x60, 0xb7, 0xf1, 0xbc, 0xad, 0xe3, 0x45, 0x1c, 0x97, 0x99, 0xb5, 0x6f, 0xbc, 0xe2, 0x2b, 0xec, 0x05, 0x81, - 0xfd, 0x1a, 0xce, 0x9a, 0xfc, 0xdf, 0xcf, 0xf0, 0x9a, 0x99, 0xc5, 0xcd, 0xc5, 0xf0, 0x6f, 0x67, 0xb3, 0xfc, 0x62, - 0x78, 0xb3, 0xd9, 0x21, 0xb5, 0x98, 0xd3, 0x68, 0xda, 0x7c, 0x78, 0xfd, 0xf0, 0xf2, 0x20, 0x9d, 0xde, 0xf3, 0x93, - 0xbc, 0x75, 0x76, 0x71, 0x0c, 0x59, 0xf0, 0x09, 0x3f, 0x6b, 0xb0, 0x39, 0xfb, 0xaf, 0xad, 0xd0, 0x1e, 0xd7, 0xde, - 0x33, 0xbb, 0x12, 0xab, 0xd3, 0xd8, 0xeb, 0xed, 0xbe, 0x9a, 0x02, 0xfe, 0x13, 0x81, 0x26, 0xbe, 0xf2, 0xc9, 0xa4, - 0x14, 0x07, 0x40, 0xa0, 0xba, 0x35, 0xf8, 0x7b, 0x18, 0x8c, 0x32, 0x92, 0xf1, 0xf6, 0x13, 0x92, 0xc8, 0xc6, 0xe1, - 0xe9, 0xdc, 0x42, 0xb1, 0x1e, 0xe9, 0xfb, 0x3c, 0xcd, 0xb4, 0x7e, 0x5b, 0x46, 0xd5, 0xa9, 0x81, 0x2c, 0x68, 0x9c, - 0x69, 0x10, 0xac, 0x7f, 0xdb, 0x58, 0x9d, 0x59, 0xf8, 0x66, 0x21, 0xa2, 0x59, 0x16, 0xff, 0x78, 0x4e, 0xb6, 0xd0, - 0xa0, 0xc0, 0x8f, 0x9a, 0x66, 0xde, 0xb5, 0x6e, 0x75, 0x01, 0x21, 0xef, 0x96, 0xa7, 0xf5, 0xa5, 0x3f, 0xff, 0x82, - 0x35, 0xbb, 0xf1, 0x5f, 0x5d, 0x8f, 0xd6, 0x8b, 0x10, 0x25, 0x5b, 0x81, 0x00, 0x71, 0xb1, 0x8d, 0xe3, 0x2d, 0x79, - 0xe3, 0x34, 0x17, 0xc9, 0x3c, 0x7c, 0x75, 0x92, 0x66, 0x05, 0xa1, 0x9a, 0xdf, 0x26, 0xf1, 0x0a, 0xd4, 0x59, 0x89, - 0x8f, 0x8a, 0x77, 0xe3, 0xde, 0xf5, 0xc4, 0xf6, 0xbf, 0xf2, 0x25, 0x14, 0xc4, 0xf7, 0xfb, 0x16, 0xe8, 0x86, 0x9f, - 0x30, 0xc5, 0xdb, 0x8f, 0xd7, 0xe3, 0x37, 0xf9, 0xad, 0xc0, 0x3d, 0x16, 0x78, 0x67, 0xbd, 0x34, 0x97, 0xf2, 0x24, - 0x41, 0x66, 0x05, 0xae, 0xb8, 0xec, 0x07, 0x0f, 0x96, 0x2d, 0x4d, 0x80, 0x66, 0xab, 0xc8, 0x00, 0x19, 0xca, 0x25, - 0x08, 0x69, 0x83, 0x8c, 0xfe, 0x2d, 0x88, 0xa2, 0x24, 0xc7, 0xa7, 0xb3, 0x27, 0xd1, 0x0d, 0x95, 0x3e, 0x39, 0x32, - 0xb0, 0xb2, 0x0e, 0x50, 0x4b, 0x32, 0x54, 0x88, 0x84, 0x90, 0x64, 0x02, 0x60, 0x9f, 0x14, 0x1a, 0x0a, 0x9f, 0x6b, - 0x39, 0xed, 0xfc, 0xc2, 0xb7, 0x4c, 0x90, 0x78, 0x4c, 0x8e, 0x5a, 0xc9, 0x84, 0xb6, 0x7b, 0xad, 0xf9, 0xe8, 0xee, - 0xe2, 0xa9, 0x5d, 0x1c, 0x63, 0x3e, 0xf2, 0x3f, 0x4a, 0x73, 0x22, 0xf2, 0xb5, 0x0e, 0xc0, 0x4a, 0xde, 0x0a, 0x94, - 0x2d, 0x6f, 0x98, 0x17, 0x58, 0xfc, 0x56, 0x4b, 0x76, 0xf5, 0x0c, 0x02, 0xb4, 0x21, 0x9c, 0xb4, 0xe3, 0x5c, 0xe1, - 0xba, 0x99, 0x62, 0x0d, 0xe5, 0xf5, 0x0a, 0x47, 0x15, 0x9a, 0x26, 0xc6, 0x74, 0x73, 0x2d, 0x7a, 0x31, 0xf5, 0x9a, - 0x7a, 0x42, 0x92, 0xbc, 0x08, 0x67, 0xd5, 0x9e, 0xae, 0xfd, 0x67, 0x06, 0x48, 0x3d, 0x27, 0x17, 0xca, 0x36, 0x17, - 0xe3, 0xbb, 0x79, 0xe3, 0x59, 0xc6, 0xcd, 0xbd, 0x57, 0x6b, 0xaf, 0x9e, 0x67, 0xb7, 0x98, 0x8f, 0x25, 0xe4, 0xd3, - 0x0e, 0x31, 0x37, 0xb2, 0x50, 0x72, 0x84, 0x71, 0xd7, 0x86, 0x21, 0x13, 0x37, 0x2e, 0x2c, 0x98, 0x90, 0x1e, 0x1b, - 0x89, 0x71, 0x90, 0x35, 0xdf, 0xd5, 0x7e, 0x71, 0x7c, 0xfb, 0xa3, 0xb8, 0x48, 0x0b, 0xf6, 0xf0, 0xa4, 0xb3, 0x5f, - 0xf9, 0x4c, 0xa3, 0x6e, 0x23, 0x47, 0x02, 0x51, 0x0c, 0x44, 0x02, 0xba, 0x56, 0xa9, 0xd0, 0xcb, 0x8a, 0x79, 0xa8, - 0xc0, 0x67, 0x2d, 0xb4, 0x51, 0xc1, 0xaa, 0x57, 0xf8, 0x89, 0x08, 0x65, 0xa6, 0xd6, 0x6b, 0x80, 0x5c, 0x64, 0x6b, - 0xad, 0x9f, 0xf5, 0x76, 0x6d, 0xb8, 0x9c, 0x27, 0xf0, 0x57, 0xef, 0xe2, 0xd6, 0xc7, 0x04, 0x5d, 0x6e, 0xed, 0x9f, - 0x68, 0xcf, 0x32, 0x46, 0x12, 0x55, 0x9e, 0xc3, 0xd3, 0x0a, 0xe4, 0xeb, 0x8e, 0x34, 0x5e, 0x62, 0x43, 0xf6, 0x16, - 0xa5, 0x9f, 0x52, 0xc6, 0xbd, 0xfc, 0xa4, 0xd8, 0x03, 0xf6, 0xdc, 0x03, 0x82, 0x35, 0xfb, 0x5a, 0x5f, 0x6e, 0x4d, - 0xd3, 0x60, 0xff, 0xa3, 0x03, 0x4f, 0x40, 0xd9, 0xbe, 0x1c, 0x47, 0x18, 0x8f, 0xdc, 0x92, 0x31, 0x65, 0x08, 0x39, - 0xc1, 0x62, 0xbb, 0xd7, 0x1d, 0x6b, 0x68, 0x39, 0x95, 0x28, 0x16, 0x49, 0xee, 0x7e, 0x94, 0xce, 0x68, 0xff, 0xd4, - 0x4e, 0x25, 0xb4, 0xf5, 0xb7, 0x9a, 0x9d, 0x14, 0x4d, 0xd7, 0xf5, 0x0e, 0xe8, 0x3c, 0x4a, 0x94, 0x4f, 0x2c, 0x8f, - 0x5d, 0x7e, 0x79, 0xb7, 0x6b, 0x64, 0xbb, 0x29, 0xb7, 0xb5, 0x37, 0x1a, 0xa9, 0x18, 0x69, 0xe8, 0x81, 0xed, 0x70, - 0xd9, 0x29, 0x6d, 0x02, 0x22, 0x64, 0x54, 0x2f, 0x8e, 0xa4, 0x96, 0xfa, 0x42, 0x9c, 0x75, 0xe7, 0x23, 0xad, 0x68, - 0x2f, 0x82, 0x02, 0x10, 0x5a, 0x1d, 0xa9, 0x92, 0x35, 0x5c, 0x97, 0x11, 0x6c, 0x0f, 0xe3, 0xf5, 0x0d, 0x14, 0x55, - 0x79, 0x7a, 0x2d, 0xd8, 0x8a, 0x1f, 0xc7, 0xa6, 0xf0, 0xb0, 0x8b, 0x0c, 0xf6, 0xe0, 0x66, 0x8e, 0x13, 0x3c, 0x5d, - 0x9b, 0xd3, 0x92, 0x3d, 0xd9, 0x20, 0xb3, 0xe6, 0xeb, 0xdb, 0x21, 0x5e, 0xb8, 0x71, 0x3d, 0x2c, 0xd9, 0x25, 0x1c, - 0x5c, 0xfa, 0x04, 0x3e, 0xf8, 0xb5, 0x1d, 0xb3, 0x41, 0xa1, 0xab, 0xcb, 0x09, 0xf6, 0x92, 0xc6, 0xe7, 0xf9, 0x6f, - 0x66, 0x73, 0x51, 0xb2, 0x02, 0xac, 0xbd, 0xba, 0x6f, 0x11, 0x2e, 0x73, 0x07, 0x5f, 0xfe, 0x3f, 0x75, 0xd4, 0x76, - 0xf3, 0xf9, 0x2d, 0x9c, 0x9b, 0x1c, 0x3c, 0xc3, 0x21, 0xea, 0xf2, 0x40, 0xae, 0x5c, 0xbf, 0xfa, 0x4f, 0xa0, 0xe4, - 0x9d, 0x4e, 0x9a, 0x7f, 0xdd, 0x77, 0x61, 0x31, 0x36, 0x76, 0xd9, 0x3e, 0xe2, 0x84, 0xd1, 0x75, 0xa2, 0xd0, 0x7e, - 0xcf, 0xc4, 0xb6, 0x1a, 0x64, 0x04, 0x8b, 0x90, 0xd7, 0x77, 0xb6, 0x00, 0xf4, 0xfd, 0x65, 0x0f, 0x8b, 0xb7, 0x7e, - 0x45, 0xe2, 0x4d, 0xb1, 0xf0, 0xef, 0x47, 0x64, 0xe1, 0xda, 0xfe, 0x8a, 0x03, 0x04, 0xdb, 0x5a, 0xfb, 0x5a, 0xdc, - 0x72, 0xfb, 0xe8, 0x04, 0x20, 0x28, 0x61, 0x2d, 0x9e, 0x44, 0xed, 0x5f, 0x22, 0x23, 0x52, 0xf7, 0x19, 0x33, 0x35, - 0x4c, 0xc4, 0x9d, 0x15, 0xc7, 0xb0, 0xd2, 0x7d, 0x74, 0xe5, 0x36, 0xb9, 0x1a, 0x44, 0xd1, 0x1e, 0x9a, 0x88, 0x3b, - 0x36, 0x04, 0x79, 0x4f, 0xc8, 0x63, 0x81, 0xaa, 0xac, 0xc2, 0x96, 0x47, 0x29, 0x82, 0xe1, 0x39, 0x74, 0x01, 0xb6, - 0x52, 0xd4, 0x3a, 0x73, 0xc9, 0xe2, 0xc4, 0x81, 0xd7, 0x8f, 0x07, 0xbc, 0x1a, 0xdd, 0xcd, 0x91, 0x40, 0xdc, 0x49, - 0xb2, 0x5b, 0x50, 0xf5, 0xe6, 0x65, 0xe8, 0x56, 0x3c, 0xaf, 0x14, 0x77, 0xa3, 0xad, 0xbe, 0x0d, 0x21, 0x22, 0x0e, - 0xd1, 0xae, 0x1d, 0xa5, 0x07, 0x74, 0x20, 0x8b, 0x2e, 0xaa, 0x9a, 0x94, 0xe0, 0xbf, 0x29, 0xb3, 0x36, 0xa1, 0x86, - 0x09, 0x05, 0x05, 0xe7, 0x6c, 0xdc, 0x4a, 0xf8, 0xf4, 0x46, 0xc6, 0xdc, 0x52, 0xe0, 0x55, 0x68, 0x6e, 0xaa, 0x03, - 0xb9, 0x22, 0x1a, 0x74, 0xc9, 0xb5, 0xed, 0x32, 0xc7, 0x87, 0x59, 0xbb, 0xf0, 0xfc, 0xb9, 0xd8, 0x2f, 0x95, 0x52, - 0xe4, 0xa5, 0xa0, 0x10, 0x71, 0x6a, 0xa3, 0x12, 0x9a, 0xfa, 0x00, 0xd6, 0x74, 0x44, 0x70, 0x67, 0x3c, 0x7a, 0x87, - 0x48, 0xf2, 0x3f, 0x95, 0x52, 0x67, 0x23, 0x79, 0x04, 0x4c, 0xeb, 0x81, 0x49, 0xfd, 0x92, 0x6d, 0x81, 0xe0, 0xf0, - 0x40, 0x7f, 0x0c, 0x14, 0xc9, 0x13, 0x89, 0x58, 0x50, 0xc7, 0xd3, 0xa8, 0x6f, 0xec, 0x4b, 0xb5, 0x27, 0xf7, 0x76, - 0x7c, 0xd6, 0x1e, 0x7b, 0x88, 0x24, 0x63, 0x7e, 0x10, 0x41, 0x12, 0x24, 0xc2, 0x18, 0x8b, 0x3c, 0xc4, 0x40, 0x34, - 0x3f, 0xb9, 0xc1, 0xe0, 0x54, 0x53, 0x6d, 0xfd, 0x58, 0xa2, 0x23, 0x10, 0xa8, 0xcb, 0x34, 0xaa, 0xd8, 0xaa, 0x4c, - 0x10, 0xcc, 0x67, 0xfd, 0xbb, 0x76, 0x44, 0x60, 0xa6, 0x1d, 0x03, 0xb4, 0xc9, 0x1b, 0xcc, 0x47, 0x44, 0x66, 0xcb, - 0xce, 0x27, 0x89, 0x09, 0xa6, 0xae, 0xda, 0x23, 0xb3, 0x71, 0xc6, 0xc9, 0xa6, 0xe9, 0xd4, 0xee, 0xaa, 0x32, 0xb8, - 0x08, 0x2f, 0x5e, 0xa2, 0x11, 0x80, 0xe8, 0x5a, 0xbe, 0x16, 0x9e, 0x1c, 0x08, 0x20, 0xcc, 0x2d, 0x0d, 0xfc, 0x96, - 0x69, 0xfb, 0x27, 0x5d, 0xab, 0x1b, 0x22, 0x36, 0x0a, 0x11, 0xfe, 0xd3, 0x04, 0xd9, 0xf5, 0x5b, 0xab, 0x1d, 0xfb, - 0xfb, 0x4e, 0x5b, 0xf6, 0x5b, 0x8b, 0x59, 0x4a, 0x43, 0x17, 0xc2, 0x86, 0x61, 0x6b, 0x35, 0x14, 0x56, 0xcb, 0x59, - 0x73, 0xe8, 0x80, 0xcc, 0xbc, 0x10, 0x90, 0x65, 0xee, 0x57, 0x3e, 0x42, 0x67, 0x07, 0xf6, 0x3f, 0xf2, 0x9d, 0xe4, - 0x1c, 0x1b, 0x76, 0x88, 0xaf, 0x77, 0xc1, 0xa2, 0x89, 0xac, 0x24, 0x94, 0x8f, 0xa0, 0x7c, 0xae, 0x5d, 0x5a, 0x47, - 0x6a, 0xe7, 0x46, 0x87, 0x46, 0x06, 0xfc, 0xc1, 0x98, 0x09, 0x1c, 0x88, 0x40, 0x2f, 0x05, 0xdd, 0x87, 0xef, 0x3b, - 0x26, 0xd3, 0x6f, 0x0d, 0x04, 0xff, 0xfe, 0x8d, 0xfb, 0x2d, 0x25, 0x60, 0x09, 0x88, 0x3b, 0x2d, 0xa0, 0x1b, 0xc4, - 0xfc, 0x7a, 0x69, 0x88, 0xc9, 0x8b, 0x43, 0x1b, 0xbd, 0x2c, 0x64, 0x78, 0xed, 0xc1, 0xc3, 0xe7, 0x99, 0xf7, 0xb2, - 0x53, 0x71, 0x86, 0x6b, 0xb3, 0x9b, 0x5e, 0xe2, 0xb6, 0xe3, 0xd7, 0x23, 0xbf, 0x45, 0xdc, 0xc0, 0xcd, 0x6e, 0x50, - 0xe6, 0x21, 0xcc, 0x3c, 0x0b, 0xdc, 0xa3, 0x61, 0x4a, 0x7f, 0xc3, 0x42, 0xac, 0x1b, 0xb2, 0xf5, 0x99, 0xc1, 0xea, - 0xb6, 0x8a, 0x41, 0x2c, 0x4f, 0x72, 0x3c, 0xc1, 0xc8, 0x42, 0xba, 0x61, 0x91, 0x93, 0x84, 0x37, 0x49, 0x1c, 0x71, - 0xaf, 0x8d, 0xd9, 0x56, 0x60, 0xea, 0x10, 0x1a, 0x72, 0x7b, 0xfc, 0xbe, 0x0b, 0x09, 0x66, 0x1e, 0x67, 0xa9, 0x8a, - 0x46, 0xea, 0xed, 0x5f, 0x3c, 0xb1, 0x47, 0xf5, 0x11, 0x6a, 0x7b, 0xcb, 0x92, 0xdb, 0xd5, 0xbf, 0xf7, 0xad, 0xd3, - 0x80, 0x5e, 0x30, 0x73, 0xc3, 0x70, 0xdc, 0x37, 0x36, 0x00, 0xd9, 0x48, 0x0a, 0x0c, 0x84, 0xd5, 0x08, 0x56, 0xd2, - 0x62, 0xec, 0xe9, 0x5d, 0xfd, 0xec, 0x18, 0x01, 0x2e, 0x81, 0xf5, 0x63, 0xa5, 0xb0, 0xe1, 0x14, 0xec, 0x7a, 0x03, - 0xf4, 0xdd, 0x76, 0x7b, 0x14, 0x4a, 0x93, 0x1b, 0x1a, 0x78, 0x9f, 0x0d, 0x04, 0x36, 0xfd, 0x14, 0xcf, 0xa1, 0x77, - 0x5d, 0xbf, 0xee, 0xfd, 0xbd, 0x31, 0x10, 0x21, 0xad, 0x91, 0xa0, 0xb5, 0xef, 0x7b, 0x2f, 0x69, 0x66, 0x65, 0x94, - 0xa1, 0x29, 0xdb, 0x54, 0xfb, 0x69, 0x38, 0xb6, 0x2d, 0x8b, 0x40, 0xed, 0x00, 0xaf, 0x5c, 0xe7, 0xe0, 0x3a, 0x53, - 0x14, 0xba, 0x12, 0x1f, 0x4a, 0x27, 0x78, 0xb7, 0x8d, 0x62, 0x12, 0x10, 0xe7, 0x07, 0x2b, 0xb8, 0x41, 0xc8, 0x59, - 0x23, 0x04, 0xd6, 0x66, 0xbb, 0xeb, 0x23, 0xd3, 0x95, 0xf8, 0xb5, 0x07, 0x59, 0x43, 0xa5, 0xa8, 0x14, 0xb8, 0xb4, - 0x38, 0x25, 0x79, 0xd2, 0x60, 0x78, 0x5d, 0x3f, 0xad, 0x69, 0x55, 0x25, 0xbe, 0xd6, 0xc5, 0x4e, 0xa9, 0x80, 0xb9, - 0xcf, 0xe9, 0xa9, 0x75, 0xa4, 0x78, 0x6b, 0xad, 0xad, 0x4f, 0x18, 0xe6, 0xf6, 0xde, 0x69, 0x0f, 0x20, 0x7f, 0xcc, - 0x67, 0x26, 0xc1, 0xc0, 0x88, 0x30, 0xc0, 0x5b, 0xa2, 0x97, 0x33, 0x26, 0x4f, 0xd0, 0x4c, 0x5f, 0xdc, 0xa3, 0xdc, - 0xbb, 0xdc, 0x7d, 0xca, 0x37, 0x2a, 0xb3, 0x47, 0x37, 0x5d, 0x04, 0xb4, 0xd6, 0x4d, 0x94, 0x1a, 0x1e, 0xc7, 0xb5, - 0xcb, 0x0b, 0xb1, 0x94, 0xc4, 0xeb, 0x10, 0xcd, 0xbf, 0xcb, 0x4f, 0x0e, 0x9b, 0x94, 0x95, 0x25, 0xdf, 0x19, 0x4b, - 0xc3, 0x8f, 0x15, 0xf2, 0xc2, 0x46, 0xaa, 0x01, 0x14, 0x57, 0x7a, 0x1d, 0xed, 0x64, 0xed, 0x5d, 0x56, 0x41, 0xa3, - 0x54, 0xc8, 0xd1, 0xa3, 0x35, 0x70, 0x94, 0x3a, 0x21, 0xd9, 0xc0, 0x5b, 0x60, 0x26, 0xaf, 0x0c, 0x4e, 0x01, 0xb5, - 0xf2, 0x48, 0x78, 0xe6, 0x42, 0x5e, 0x9a, 0xfc, 0x4c, 0xde, 0x8d, 0xc0, 0x78, 0xca, 0x07, 0x9e, 0xb8, 0xb0, 0x4c, - 0xfc, 0xb7, 0xec, 0x0f, 0x10, 0x95, 0x4c, 0x06, 0x15, 0x08, 0x4c, 0x83, 0x5d, 0x7c, 0x2d, 0x8d, 0xd4, 0x7a, 0x08, - 0xc1, 0xc9, 0xd5, 0x46, 0x7f, 0x30, 0xeb, 0x6b, 0x40, 0xa9, 0x7a, 0x83, 0x8a, 0x46, 0xec, 0xca, 0xf6, 0xd3, 0xbc, - 0x3e, 0x98, 0xa8, 0x7d, 0xa3, 0x81, 0x1b, 0xb6, 0xf9, 0xd5, 0x1e, 0xc5, 0xae, 0x8d, 0xe7, 0x4b, 0x60, 0x13, 0xb5, - 0xbc, 0x65, 0x52, 0x14, 0x1c, 0xda, 0x34, 0xa8, 0x76, 0x04, 0x23, 0x66, 0xba, 0x83, 0xce, 0x5b, 0xdb, 0x20, 0x3a, - 0x1d, 0x9c, 0x46, 0xd0, 0x19, 0x8c, 0x8b, 0x53, 0x5b, 0x35, 0x42, 0x49, 0x8c, 0x2f, 0xc7, 0xd0, 0x2f, 0xb2, 0x78, - 0xa3, 0x66, 0xda, 0x00, 0x5d, 0x49, 0x05, 0xf3, 0x6c, 0xc4, 0x4c, 0x0a, 0xb7, 0xec, 0xb9, 0x5d, 0x8a, 0xff, 0xa5, - 0x3b, 0xd7, 0xf7, 0x3c, 0x11, 0xe4, 0x03, 0x59, 0x3a, 0x0e, 0xfe, 0xb5, 0x98, 0xe1, 0xe7, 0x19, 0x8c, 0x5e, 0x64, - 0xd6, 0xc6, 0x2c, 0xc9, 0x17, 0x7c, 0x67, 0xf8, 0xa5, 0x06, 0x93, 0x9f, 0xb0, 0x9c, 0x21, 0xfa, 0x1a, 0x04, 0x38, - 0x72, 0xb5, 0xeb, 0x69, 0xc3, 0x78, 0x07, 0x8b, 0x17, 0xc5, 0x02, 0x51, 0xd4, 0xfb, 0x6a, 0x8e, 0xc3, 0xe2, 0x9c, - 0xa4, 0x04, 0x33, 0x9b, 0x1a, 0x49, 0x21, 0x64, 0xef, 0x9b, 0x93, 0x57, 0x56, 0x1a, 0x52, 0x9c, 0xc0, 0xcb, 0x81, - 0x5e, 0x23, 0xd2, 0xf1, 0xb1, 0x3a, 0x6b, 0x28, 0x4e, 0x1a, 0x99, 0x62, 0x36, 0xb1, 0x90, 0xce, 0xaa, 0x07, 0x1b, - 0xf3, 0x69, 0x91, 0x2b, 0xaf, 0xeb, 0x08, 0x7f, 0xad, 0xc2, 0x70, 0x96, 0x5e, 0x6f, 0xbe, 0x18, 0x06, 0x1d, 0xfe, - 0xaf, 0xd5, 0x84, 0x6f, 0xf0, 0x6d, 0x3f, 0x5f, 0x44, 0x44, 0xa8, 0xca, 0x0f, 0x74, 0xa2, 0x1d, 0xea, 0xe8, 0x34, - 0xf4, 0xd0, 0xcc, 0x56, 0x50, 0xb0, 0x48, 0xfb, 0x7d, 0x37, 0xbd, 0xf5, 0x35, 0x39, 0x7b, 0xe7, 0xba, 0xa6, 0x35, - 0xc1, 0xfc, 0xf8, 0x35, 0xd0, 0x9a, 0x8d, 0x84, 0x93, 0xe5, 0xf7, 0xc8, 0xde, 0x6c, 0xaf, 0x76, 0x67, 0xd4, 0xbe, - 0x3e, 0x1a, 0xde, 0x34, 0x8f, 0x19, 0x1f, 0x65, 0x93, 0x26, 0x6a, 0x3a, 0x73, 0x2d, 0xe0, 0x73, 0x6a, 0xea, 0x4e, - 0x24, 0x3a, 0x70, 0x76, 0xb5, 0x3c, 0xc5, 0x6f, 0x45, 0x64, 0xfa, 0x35, 0x89, 0xea, 0x96, 0x66, 0x50, 0xe4, 0x52, - 0x5a, 0xa8, 0xba, 0xad, 0x2a, 0x80, 0x7d, 0x8d, 0xa8, 0x19, 0xa8, 0x31, 0x0b, 0xdd, 0x29, 0x1a, 0x21, 0x8d, 0xb5, - 0x8c, 0xed, 0x87, 0x9a, 0x76, 0xa5, 0xaa, 0x1e, 0xdb, 0x25, 0x0e, 0x45, 0x03, 0x34, 0x2d, 0xcc, 0xf5, 0x6f, 0x76, - 0x75, 0xb3, 0x6d, 0x4b, 0xbd, 0x41, 0x5c, 0xf2, 0x9f, 0x87, 0x2d, 0xac, 0x9d, 0x29, 0x85, 0xc3, 0x15, 0xad, 0xe8, - 0x91, 0x6c, 0x1c, 0xb4, 0x0a, 0x83, 0xa8, 0x51, 0x65, 0xda, 0x88, 0x61, 0x44, 0xc2, 0x08, 0x85, 0x42, 0xe1, 0x3e, - 0x62, 0x5d, 0x6c, 0xca, 0xa3, 0x87, 0xd2, 0xea, 0x92, 0x1f, 0xb3, 0x58, 0xa3, 0x79, 0x5a, 0x7b, 0x2c, 0x64, 0xa9, - 0xc3, 0xc7, 0x2b, 0xc1, 0xe8, 0xf7, 0xcb, 0x84, 0x56, 0x6e, 0x31, 0x41, 0xa9, 0x0e, 0x24, 0x76, 0x3b, 0x79, 0x8b, - 0xf4, 0x63, 0x8b, 0x42, 0x12, 0xb2, 0x3f, 0xbd, 0x2c, 0x93, 0xa7, 0x8a, 0xe1, 0x55, 0xe4, 0x2c, 0x47, 0x09, 0xf1, - 0x0e, 0xfc, 0xa4, 0x5f, 0x7f, 0x92, 0x7a, 0xad, 0xba, 0xad, 0x75, 0x54, 0xd4, 0xce, 0x6d, 0xe9, 0x86, 0x71, 0x9d, - 0x0c, 0xaa, 0xe0, 0x06, 0x4c, 0xd2, 0xe8, 0x5b, 0x27, 0xa8, 0x4f, 0x31, 0x9a, 0xf2, 0x6a, 0x07, 0x65, 0x2d, 0xc3, - 0x60, 0x8d, 0xf1, 0x61, 0xf8, 0xc0, 0x64, 0xc6, 0x18, 0x61, 0x6c, 0xc3, 0x1c, 0xf9, 0x6c, 0xfa, 0xeb, 0x17, 0x42, - 0xea, 0x4d, 0x12, 0x11, 0x81, 0x7c, 0x90, 0x7c, 0x30, 0x22, 0xfd, 0xa7, 0x25, 0x56, 0x3b, 0xbc, 0x70, 0x48, 0x9f, - 0xc4, 0x16, 0x0e, 0x84, 0xcd, 0xfa, 0xd1, 0x6f, 0x98, 0x64, 0xde, 0xbe, 0x38, 0x41, 0x7e, 0x09, 0x6e, 0xd8, 0xde, - 0x6a, 0x08, 0xaa, 0x18, 0xad, 0x10, 0xc4, 0x0a, 0x1a, 0x21, 0x9e, 0xc0, 0xf9, 0x26, 0x63, 0xd5, 0xab, 0x25, 0x2e, - 0x73, 0x45, 0x83, 0x7f, 0xf6, 0x6d, 0x5a, 0x24, 0x3d, 0x88, 0xf7, 0x03, 0x59, 0xcf, 0xb0, 0x87, 0xa0, 0xc7, 0xc2, - 0x8a, 0xe4, 0xbb, 0x42, 0x96, 0xae, 0xe3, 0xd3, 0x49, 0xaa, 0xf7, 0xa4, 0x5f, 0x3f, 0xc0, 0x1e, 0xb4, 0xa9, 0x2d, - 0x34, 0x7f, 0x85, 0xaa, 0x0a, 0xf3, 0x7a, 0x33, 0xca, 0xa3, 0x25, 0x9b, 0xee, 0x08, 0x74, 0x10, 0x08, 0xb5, 0xd6, - 0x4b, 0x03, 0x8c, 0xe3, 0xfb, 0xb0, 0x19, 0x3d, 0x7e, 0x5d, 0xc4, 0x84, 0xab, 0x97, 0x2d, 0x45, 0x69, 0x93, 0x46, - 0x8f, 0xfb, 0xae, 0x59, 0x76, 0x19, 0x22, 0x88, 0xc4, 0x1f, 0x47, 0xd0, 0x66, 0x5c, 0x0b, 0x17, 0xd1, 0x09, 0x86, - 0x96, 0x2b, 0x9e, 0xb8, 0x47, 0xdd, 0x2f, 0xbb, 0xe7, 0xdb, 0xe6, 0x49, 0x0c, 0x58, 0x8a, 0xf8, 0xae, 0xac, 0xcd, - 0x39, 0x94, 0xa2, 0x74, 0x9b, 0xc0, 0x2c, 0x47, 0x7a, 0x8c, 0x47, 0xf2, 0x48, 0xd4, 0x01, 0x83, 0x68, 0x54, 0x7c, - 0x67, 0x65, 0xe0, 0x6e, 0xad, 0xb5, 0xf8, 0xf2, 0xf7, 0x7e, 0xfd, 0x7a, 0xb7, 0x42, 0xbd, 0x0c, 0x5e, 0x4e, 0xed, - 0x19, 0xef, 0xbc, 0x20, 0xa5, 0xbe, 0x88, 0xc1, 0xeb, 0xc7, 0xbc, 0x8a, 0x66, 0xdf, 0x35, 0x04, 0xa1, 0x85, 0xcb, - 0x7f, 0x0b, 0x8f, 0x3a, 0x2d, 0xd3, 0xa5, 0xa7, 0xaf, 0xa6, 0x9b, 0x4e, 0x97, 0xef, 0xe8, 0xc1, 0xad, 0x10, 0x21, - 0x61, 0x54, 0x63, 0xed, 0x93, 0x73, 0x8b, 0xc9, 0x97, 0xd1, 0xda, 0xa5, 0x55, 0x51, 0xc1, 0xe7, 0x1c, 0xdd, 0x0d, - 0xaa, 0x5b, 0xb8, 0xa9, 0x72, 0xfb, 0xe8, 0xad, 0xa8, 0xa2, 0xa1, 0x87, 0x0b, 0xa7, 0x44, 0x12, 0x85, 0xc8, 0x4b, - 0x98, 0xd8, 0x7d, 0x37, 0xa4, 0x81, 0x71, 0x75, 0x75, 0x4a, 0x75, 0x83, 0xc7, 0xd0, 0xc3, 0x10, 0x24, 0xae, 0xd9, - 0xf9, 0xff, 0xd2, 0xeb, 0xc1, 0x9b, 0x97, 0x3e, 0x25, 0x99, 0x17, 0xfe, 0x5d, 0x5a, 0xb8, 0xc5, 0x17, 0xfc, 0x8c, - 0x96, 0xa0, 0x65, 0xcb, 0xa3, 0xb2, 0x03, 0xeb, 0xa1, 0x3d, 0xd0, 0xbf, 0xae, 0x27, 0x9b, 0x55, 0x00, 0x5a, 0x5b, - 0x9e, 0x64, 0x34, 0x31, 0x7a, 0x72, 0xde, 0xa1, 0x50, 0x44, 0x42, 0x0e, 0xa3, 0x44, 0xad, 0x75, 0x20, 0xc3, 0x55, - 0x77, 0x5a, 0x0a, 0xb7, 0xb1, 0xbb, 0x9e, 0x59, 0x88, 0xe8, 0x48, 0x2f, 0x49, 0x66, 0x2e, 0x34, 0x21, 0xa8, 0x92, - 0xc8, 0x0f, 0x88, 0x6d, 0x81, 0xe3, 0x41, 0x73, 0x62, 0xeb, 0xa3, 0xd0, 0x52, 0x40, 0x18, 0xb7, 0x57, 0xf1, 0x35, - 0x01, 0x84, 0xd2, 0xba, 0xf3, 0x66, 0xbb, 0x70, 0xf9, 0x37, 0x6d, 0x65, 0x0c, 0x36, 0x3a, 0x77, 0x56, 0x71, 0x81, - 0x5b, 0xdd, 0x8b, 0x21, 0x88, 0x02, 0x25, 0x05, 0x31, 0x9c, 0x04, 0xd5, 0x07, 0x73, 0x20, 0x01, 0x97, 0xc8, 0x83, - 0x52, 0xe3, 0x5c, 0xb8, 0xf1, 0x46, 0x21, 0xc4, 0x62, 0x24, 0xaa, 0x62, 0xb2, 0x41, 0x70, 0x4c, 0x05, 0xda, 0xfd, - 0xf4, 0xdc, 0x7b, 0xe1, 0xfe, 0xa1, 0xa6, 0x56, 0x73, 0xa1, 0x08, 0xa3, 0xdd, 0xc9, 0xbd, 0xa0, 0x85, 0x64, 0xab, - 0x5e, 0xae, 0x91, 0xbd, 0xf0, 0xcd, 0x73, 0xef, 0x2b, 0x25, 0x20, 0xec, 0xdf, 0x19, 0x07, 0x02, 0x60, 0x2e, 0xed, - 0x6a, 0x2d, 0xd1, 0xdf, 0x9e, 0x48, 0xb3, 0xa1, 0xa5, 0x58, 0x37, 0xf3, 0x50, 0x01, 0xd6, 0xd4, 0xea, 0x09, 0x4b, - 0x59, 0xe5, 0x8d, 0x66, 0xa7, 0x86, 0xb7, 0x1d, 0x74, 0x75, 0x92, 0xc2, 0x93, 0xee, 0xff, 0xbd, 0x1f, 0x5e, 0xab, - 0x6e, 0x85, 0xa0, 0x3a, 0x93, 0x74, 0x16, 0x32, 0xd5, 0x7a, 0x1a, 0x53, 0x9c, 0xa4, 0xef, 0x04, 0x45, 0x45, 0x68, - 0xfc, 0x53, 0xd1, 0xd9, 0xf8, 0xa9, 0xeb, 0x03, 0xf7, 0x03, 0x2e, 0x28, 0xbe, 0xc9, 0x3a, 0x7e, 0x98, 0x45, 0x44, - 0x64, 0xe5, 0x67, 0xbb, 0xbc, 0x76, 0xdd, 0xa6, 0x33, 0xf3, 0xd2, 0x6d, 0x74, 0x9b, 0x7f, 0x83, 0x25, 0x1f, 0x8a, - 0x92, 0x97, 0xb4, 0x85, 0xa3, 0x5f, 0xe0, 0x7a, 0x28, 0xd3, 0x81, 0x21, 0x73, 0xeb, 0xba, 0xfe, 0x51, 0x31, 0xd2, - 0xd4, 0x11, 0x4f, 0x99, 0x43, 0x05, 0x9e, 0x9a, 0x9b, 0x4a, 0x0e, 0x94, 0xce, 0x30, 0x5f, 0x13, 0xe1, 0xcd, 0xde, - 0x31, 0xfd, 0x73, 0x41, 0x74, 0x7c, 0x04, 0xd3, 0x86, 0x7c, 0x58, 0x85, 0xe7, 0xe2, 0x58, 0xfd, 0x60, 0x35, 0x89, - 0x3c, 0x89, 0x03, 0xbc, 0x0f, 0x2c, 0x52, 0x61, 0x62, 0xe0, 0x6b, 0x76, 0x3b, 0xce, 0x17, 0x80, 0x19, 0x0f, 0xb9, - 0x4f, 0x77, 0xfc, 0x10, 0x04, 0x8e, 0x17, 0xaa, 0xc5, 0xcd, 0xe1, 0x2d, 0x08, 0x80, 0x8c, 0x59, 0x71, 0x5a, 0x8c, - 0xf2, 0x24, 0x25, 0xe0, 0x99, 0x5c, 0xba, 0x55, 0x43, 0x2a, 0xb3, 0x3f, 0x04, 0x94, 0x4b, 0xd7, 0x42, 0x0a, 0x6e, - 0xd5, 0x17, 0xa6, 0x84, 0x74, 0xd7, 0x68, 0xb0, 0x85, 0x7f, 0x2c, 0x88, 0x25, 0x0d, 0xea, 0x1a, 0xbf, 0xed, 0x57, - 0xee, 0xa4, 0x73, 0xe2, 0x3a, 0x4a, 0x5d, 0x22, 0x89, 0xfb, 0x3c, 0x8c, 0x04, 0x82, 0x99, 0x3d, 0x21, 0xb2, 0x18, - 0x62, 0x1f, 0x47, 0x3b, 0x02, 0xf0, 0x04, 0xa2, 0x23, 0xcf, 0xec, 0x5e, 0x40, 0x7c, 0x7a, 0x82, 0xb0, 0x2c, 0x7f, - 0x23, 0xe3, 0xd7, 0xd7, 0xc3, 0xec, 0x89, 0x62, 0x78, 0x25, 0xf1, 0x54, 0xc5, 0x12, 0x49, 0x43, 0x6b, 0xc0, 0xd2, - 0x15, 0xc9, 0x65, 0xe4, 0x19, 0x71, 0x7e, 0x66, 0x7d, 0x02, 0x7e, 0xec, 0x63, 0x38, 0xf0, 0xeb, 0x40, 0x5f, 0xa4, - 0x54, 0xf9, 0x36, 0x12, 0xe7, 0xd5, 0x7b, 0x73, 0xfd, 0x70, 0x27, 0xe9, 0xea, 0xd5, 0xa3, 0x6d, 0x68, 0x6c, 0x92, - 0xf4, 0x6f, 0xcd, 0x43, 0x80, 0x63, 0x9d, 0xa4, 0x33, 0x14, 0xc6, 0x64, 0x79, 0xd8, 0xb0, 0xea, 0x62, 0xe8, 0xce, - 0x03, 0xee, 0xa6, 0xba, 0x23, 0x75, 0xf8, 0xd2, 0xa4, 0x27, 0x85, 0x59, 0xd2, 0xd8, 0x25, 0xe9, 0xdf, 0xaa, 0x74, - 0x38, 0x54, 0x29, 0x46, 0xe4, 0xb2, 0x22, 0x46, 0xa6, 0x1d, 0xdc, 0x49, 0xfc, 0xb6, 0xe4, 0x9d, 0x80, 0x97, 0x36, - 0xf5, 0x79, 0x57, 0x2b, 0x26, 0xb4, 0x8c, 0xc9, 0x93, 0xb7, 0x76, 0x58, 0x69, 0xa1, 0x54, 0xb2, 0x40, 0x36, 0x65, - 0xa1, 0x51, 0xf0, 0x53, 0xdf, 0xfc, 0xf0, 0x83, 0xa2, 0x32, 0x10, 0x70, 0x3b, 0x58, 0xfd, 0xe3, 0x83, 0x78, 0x19, - 0x43, 0x3c, 0x32, 0x32, 0xa6, 0x7f, 0xc9, 0x50, 0xf7, 0x4f, 0x20, 0x93, 0xf0, 0x75, 0x76, 0xbf, 0x34, 0xf7, 0x17, - 0x6a, 0x1d, 0x8c, 0xeb, 0x68, 0x43, 0x13, 0xf8, 0x26, 0x14, 0xee, 0x87, 0xca, 0xc0, 0x46, 0x29, 0x43, 0xbe, 0x2f, - 0x6d, 0x9e, 0x7b, 0xe2, 0x27, 0x41, 0xf1, 0x69, 0x86, 0x59, 0xc8, 0x08, 0xa0, 0xfa, 0x70, 0x32, 0xe9, 0xba, 0x43, - 0x6d, 0x7b, 0xab, 0xa9, 0x74, 0x76, 0xd4, 0x31, 0x41, 0xce, 0xf3, 0xa3, 0x19, 0x56, 0x9e, 0xbf, 0x36, 0xf9, 0x06, - 0x81, 0xcf, 0x9d, 0xf7, 0xa6, 0x5a, 0x07, 0x6a, 0xbf, 0x9c, 0x11, 0xb4, 0x2d, 0x03, 0x1c, 0xa9, 0x72, 0xa8, 0x8e, - 0x54, 0x0c, 0x2b, 0x33, 0xde, 0x98, 0xe2, 0xc5, 0x16, 0x7b, 0x9c, 0x2f, 0x21, 0x15, 0xb0, 0x1f, 0x90, 0xb2, 0xe4, - 0x98, 0xd5, 0x22, 0x65, 0x6f, 0x22, 0x05, 0x11, 0xca, 0x6f, 0x5e, 0x1e, 0xfc, 0xdd, 0x64, 0x84, 0x15, 0x58, 0xab, - 0x8e, 0x24, 0x5b, 0x9b, 0xc8, 0x69, 0x2d, 0xa9, 0x8a, 0xf3, 0x46, 0x19, 0x66, 0xbf, 0xeb, 0xcf, 0xcb, 0x26, 0x98, - 0xda, 0xc5, 0xa7, 0xe6, 0x60, 0x8d, 0x16, 0xa6, 0xa7, 0xc2, 0xfe, 0x82, 0x0f, 0x3d, 0x21, 0xbf, 0x19, 0xfc, 0xb2, - 0xaf, 0x6d, 0x18, 0x3c, 0x6a, 0x5f, 0x61, 0x43, 0xa5, 0x75, 0x31, 0xf5, 0xa2, 0x61, 0xb2, 0x2d, 0x7f, 0x7e, 0x2a, - 0xf7, 0x18, 0xb9, 0xc2, 0x8c, 0xc0, 0x1a, 0x95, 0x77, 0xc1, 0x01, 0xe1, 0x63, 0x44, 0x91, 0x1b, 0xb6, 0x45, 0x06, - 0x05, 0xdb, 0x46, 0xd3, 0xae, 0xf4, 0x31, 0x07, 0x79, 0x12, 0x84, 0x4e, 0x76, 0xc2, 0x3b, 0x5f, 0xbd, 0x32, 0x2c, - 0x8b, 0x24, 0xcd, 0x8b, 0xa8, 0xd2, 0x87, 0x55, 0xd5, 0x48, 0xe3, 0xbf, 0x00, 0x60, 0x14, 0xce, 0x97, 0xc2, 0xbf, - 0x29, 0x5c, 0xe3, 0x9d, 0xde, 0xaa, 0x91, 0x72, 0x8e, 0xc7, 0xbc, 0x9b, 0xb1, 0xc3, 0x0f, 0xc2, 0x97, 0xdc, 0xcf, - 0xa8, 0xc0, 0x3c, 0x2d, 0x0b, 0xb3, 0x35, 0xfc, 0x5a, 0x81, 0xdc, 0x3e, 0x12, 0xe7, 0x36, 0x6c, 0xa6, 0x53, 0x7b, - 0xd3, 0x97, 0x1b, 0x84, 0xcc, 0x19, 0xcd, 0xf2, 0xf5, 0x87, 0x87, 0x01, 0x75, 0x11, 0x7e, 0x3b, 0x16, 0xea, 0x51, - 0xb6, 0x74, 0xf0, 0x02, 0x1e, 0xae, 0x69, 0x29, 0x7a, 0xbe, 0xa9, 0x9d, 0xc8, 0x1f, 0x6f, 0x7c, 0x00, 0x6d, 0xef, - 0x75, 0x8c, 0x36, 0xd1, 0x43, 0x4a, 0x01, 0x22, 0x0b, 0x6d, 0xab, 0xaa, 0x66, 0xc8, 0xb2, 0x60, 0xb9, 0x0d, 0xbc, - 0xa7, 0x96, 0x08, 0x87, 0xef, 0xda, 0xd3, 0x85, 0x35, 0xfe, 0x58, 0x00, 0xfc, 0xfb, 0x8c, 0x88, 0x0a, 0xe5, 0x7f, - 0x87, 0xed, 0x19, 0x26, 0x22, 0xee, 0x08, 0xc9, 0x60, 0xc0, 0xc8, 0x43, 0x36, 0x23, 0x29, 0xd8, 0x6a, 0xa5, 0x00, - 0xbe, 0x87, 0x40, 0xe3, 0xba, 0x06, 0x21, 0xd8, 0xa0, 0xd7, 0x40, 0x3c, 0x1c, 0x2f, 0xa8, 0x6b, 0xbc, 0x36, 0x7e, - 0xbb, 0xd6, 0x9a, 0x30, 0xe2, 0xdb, 0xaa, 0x59, 0xbc, 0x56, 0x3d, 0x52, 0x39, 0x92, 0x10, 0x79, 0xa3, 0xa0, 0xd5, - 0x9b, 0x4e, 0xf7, 0xd3, 0x64, 0x44, 0x0a, 0x6a, 0x9d, 0x1b, 0x91, 0xf2, 0x4b, 0xb1, 0x39, 0xf5, 0x87, 0x94, 0x87, - 0x2c, 0x8f, 0xb9, 0x12, 0xd5, 0xb5, 0x08, 0x61, 0xd8, 0x75, 0xf4, 0xaf, 0xa1, 0x84, 0x21, 0x5f, 0x52, 0x19, 0x14, - 0x32, 0x53, 0x59, 0x20, 0x0d, 0xe5, 0x49, 0xe4, 0xee, 0x87, 0xe9, 0xea, 0xdd, 0xab, 0xdc, 0x27, 0xa9, 0x28, 0x89, - 0xdd, 0x85, 0x48, 0x93, 0x89, 0x37, 0xa6, 0xfd, 0xf6, 0x3f, 0x23, 0xe5, 0xdf, 0x54, 0x1d, 0x53, 0x8d, 0x25, 0xb1, - 0xd0, 0x00, 0xa5, 0x7c, 0xc4, 0xd3, 0x36, 0x5d, 0x8e, 0xa2, 0xad, 0xd2, 0x1e, 0x6a, 0x1e, 0x78, 0x58, 0x58, 0x13, - 0xc7, 0x11, 0xf4, 0x74, 0x84, 0xd8, 0x92, 0xda, 0x42, 0xd7, 0xa7, 0x99, 0x67, 0x45, 0x6d, 0x76, 0x8f, 0x54, 0xcb, - 0xe8, 0xa0, 0x35, 0x91, 0x34, 0xfb, 0xa9, 0xcb, 0x34, 0x90, 0x0a, 0x81, 0xef, 0x82, 0xdc, 0x2a, 0x4a, 0x5c, 0xaf, - 0xee, 0xdd, 0x43, 0xb5, 0xf2, 0x3b, 0xff, 0x74, 0x0b, 0x22, 0x23, 0x81, 0x81, 0x14, 0xd1, 0xbc, 0xa0, 0x9f, 0x18, - 0x96, 0xc1, 0x90, 0x33, 0x25, 0xb3, 0x9c, 0xb7, 0x00, 0xed, 0x91, 0xb6, 0x82, 0x0b, 0x12, 0x46, 0xe7, 0x58, 0x2b, - 0x83, 0xcf, 0x91, 0x22, 0x5f, 0xb5, 0xc5, 0xcc, 0x5e, 0xdd, 0xd3, 0x02, 0x53, 0x01, 0xac, 0x2b, 0x05, 0xcb, 0x59, - 0xdf, 0x28, 0xaa, 0xd8, 0xc8, 0xe4, 0xc7, 0x5f, 0xd0, 0x3d, 0xa5, 0x0c, 0x9d, 0x15, 0xae, 0x34, 0x6d, 0x3b, 0x97, - 0xc7, 0xee, 0x83, 0x02, 0x7c, 0xa2, 0x81, 0xcc, 0x4b, 0xf2, 0x9f, 0x9d, 0x6d, 0xd8, 0x7d, 0x52, 0xce, 0x77, 0x13, - 0xfd, 0xde, 0x48, 0xb3, 0xbf, 0x2e, 0x75, 0x28, 0xdb, 0xaf, 0x3c, 0xae, 0x5a, 0xe6, 0x3d, 0xd2, 0xe7, 0xc6, 0x64, - 0x44, 0x26, 0xb4, 0xcb, 0xfa, 0x36, 0xc2, 0xcd, 0xed, 0x33, 0x85, 0xc7, 0x37, 0xbc, 0x9c, 0x3f, 0x69, 0xc5, 0x36, - 0xa5, 0x8e, 0x94, 0xd8, 0x48, 0x14, 0x18, 0x64, 0x91, 0x9f, 0x78, 0x8a, 0x6e, 0xef, 0xa8, 0xad, 0x77, 0xea, 0xae, - 0x93, 0x13, 0x71, 0xe6, 0xea, 0x46, 0x52, 0xa5, 0x11, 0x76, 0xf3, 0x3c, 0xf2, 0xb2, 0x56, 0xd0, 0x8c, 0xd2, 0x43, - 0xed, 0xf6, 0x8e, 0x11, 0x1a, 0x56, 0x4a, 0x8a, 0x6c, 0x51, 0x1b, 0x2d, 0x07, 0x7a, 0xc8, 0x5f, 0x6b, 0x7e, 0xfe, - 0x67, 0x0b, 0x71, 0xe3, 0x70, 0xfa, 0x8a, 0x44, 0x44, 0x61, 0x10, 0xa7, 0x55, 0x24, 0xdb, 0x99, 0x40, 0x61, 0x1c, - 0x4c, 0xb0, 0x75, 0xa3, 0xa9, 0x87, 0x45, 0xa2, 0x96, 0x48, 0xc3, 0xf6, 0x28, 0x0f, 0x81, 0x2a, 0x09, 0x3d, 0x6d, - 0xad, 0x4e, 0xa2, 0xf5, 0x87, 0x6b, 0x9f, 0x11, 0xa1, 0x55, 0x8a, 0xac, 0xca, 0x2b, 0x57, 0x68, 0x04, 0x26, 0x12, - 0x89, 0x8e, 0x20, 0x0e, 0x4d, 0x08, 0x1f, 0x1e, 0xd2, 0x4b, 0xab, 0x22, 0x86, 0x56, 0x5c, 0x43, 0x66, 0x01, 0xc4, - 0x26, 0x63, 0xfa, 0x7a, 0xbf, 0x63, 0x19, 0xd7, 0xcb, 0x83, 0x56, 0xff, 0x91, 0x88, 0x13, 0xa4, 0xfb, 0xa2, 0x03, - 0x8c, 0x06, 0x1c, 0x55, 0xc1, 0xb7, 0x8f, 0xa1, 0xa8, 0x74, 0xa0, 0x5e, 0x8e, 0xdc, 0x22, 0xf2, 0x2b, 0xe0, 0xda, - 0xdd, 0x8a, 0x08, 0xdf, 0x2e, 0x9f, 0xd3, 0xac, 0x96, 0x11, 0xd4, 0xbe, 0x8f, 0x68, 0x95, 0x89, 0xb0, 0xb9, 0xb8, - 0xeb, 0xb1, 0x5c, 0x43, 0xd5, 0x87, 0x56, 0x5c, 0x44, 0xe1, 0xcd, 0x1c, 0xa4, 0x8d, 0xc9, 0x78, 0x49, 0x3f, 0xb5, - 0x1d, 0x95, 0x80, 0x5e, 0x28, 0x0b, 0x4d, 0x06, 0x49, 0xc1, 0xa3, 0xf7, 0x40, 0x98, 0xa4, 0x57, 0xce, 0x98, 0xfd, - 0x94, 0xa7, 0x24, 0x4e, 0xdf, 0x82, 0x76, 0x36, 0x45, 0xf0, 0x70, 0xd5, 0x5e, 0xda, 0x53, 0xd0, 0x7e, 0xcf, 0x74, - 0xb6, 0xbd, 0xac, 0x91, 0x78, 0x2f, 0x3a, 0xf0, 0x2a, 0xfa, 0x3c, 0x32, 0xc3, 0x98, 0xc1, 0xfd, 0x90, 0x90, 0xe4, - 0xb3, 0x94, 0x0a, 0x66, 0x41, 0x0f, 0xe4, 0x51, 0x91, 0x8c, 0xd2, 0x4c, 0xb7, 0xfd, 0x91, 0xe8, 0xfa, 0x69, 0xaa, - 0x76, 0xb5, 0xf6, 0x14, 0x55, 0x5e, 0xbe, 0x5e, 0xf8, 0xed, 0xf4, 0x8c, 0xae, 0xdc, 0x01, 0xc9, 0x7a, 0x46, 0x6f, - 0x5a, 0xb0, 0x5a, 0x28, 0x1d, 0xa9, 0x20, 0xf6, 0x3e, 0x27, 0xb0, 0x18, 0xd7, 0x43, 0x1a, 0xd6, 0xe0, 0x03, 0xa6, - 0x27, 0xab, 0xd3, 0x77, 0xce, 0x7d, 0xd9, 0xff, 0xf6, 0xdd, 0x76, 0x83, 0x35, 0x73, 0xf1, 0x07, 0xb9, 0x4b, 0x40, - 0xcd, 0x21, 0xd3, 0x64, 0xd2, 0x57, 0x69, 0x37, 0x27, 0xaf, 0x12, 0xb3, 0x70, 0x80, 0xfe, 0xdd, 0x1c, 0x72, 0xc2, - 0x3a, 0xde, 0xb8, 0x44, 0xf4, 0xd4, 0xbb, 0x70, 0xda, 0x13, 0x07, 0xf0, 0x2f, 0x18, 0x26, 0x4a, 0x88, 0x64, 0x2e, - 0xe1, 0x61, 0x5e, 0xc2, 0x51, 0xf2, 0x43, 0xb6, 0xcd, 0x54, 0xef, 0xa8, 0x6e, 0xde, 0x14, 0xd8, 0xf3, 0x14, 0x17, - 0xbb, 0x55, 0xc2, 0x8c, 0x55, 0xec, 0xb5, 0xd8, 0x43, 0x8f, 0x37, 0x28, 0x92, 0xcc, 0xf6, 0x19, 0x0d, 0x9d, 0xf9, - 0x4d, 0x7e, 0x7d, 0x71, 0x73, 0x37, 0xfd, 0x7f, 0x5c, 0x9d, 0x18, 0x87, 0x15, 0x7c, 0x36, 0xf4, 0x65, 0xbf, 0x2a, - 0xcb, 0xe7, 0x12, 0x5f, 0xad, 0x96, 0x77, 0x94, 0x8c, 0x7b, 0x9b, 0xf8, 0xc1, 0x00, 0x06, 0x6f, 0x5a, 0x48, 0x1e, - 0x4a, 0x14, 0x61, 0x64, 0xde, 0x1f, 0x2e, 0x29, 0xf0, 0x72, 0xf9, 0x55, 0x70, 0x91, 0x98, 0xfe, 0xb6, 0xf5, 0x4a, - 0x2a, 0xe2, 0x90, 0xaa, 0x23, 0xde, 0x4d, 0x10, 0x69, 0x51, 0x0e, 0x1d, 0xfd, 0xc4, 0x01, 0x93, 0x35, 0x38, 0x10, - 0x81, 0x82, 0xce, 0x67, 0x2f, 0x89, 0xc4, 0x8f, 0xd0, 0x5f, 0x79, 0x16, 0x75, 0xda, 0x8b, 0xf6, 0xb3, 0xed, 0x85, - 0xff, 0x7f, 0xba, 0xb4, 0xaa, 0x73, 0x6c, 0x40, 0xb0, 0xfe, 0x2f, 0x90, 0xb8, 0xd0, 0x0e, 0x4a, 0x90, 0xee, 0x53, - 0xf8, 0xc4, 0x9f, 0xdd, 0x69, 0x96, 0x13, 0x96, 0xaf, 0xd5, 0x6a, 0x19, 0x4f, 0xe8, 0x9c, 0x5c, 0x68, 0x1c, 0x27, - 0x6a, 0xca, 0x10, 0x3c, 0xe3, 0xe8, 0xed, 0xb5, 0x0c, 0xbd, 0xdb, 0x98, 0x4a, 0x82, 0x4e, 0xe2, 0x71, 0xc5, 0x52, - 0xac, 0xa2, 0x75, 0xbf, 0x6a, 0x63, 0xb6, 0x99, 0xc1, 0x19, 0x30, 0xce, 0xb2, 0x80, 0xd1, 0x83, 0xa5, 0x7a, 0x38, - 0x37, 0x0e, 0x98, 0x5e, 0xcb, 0x6b, 0x4c, 0x47, 0x04, 0x8d, 0xdd, 0xf2, 0xf5, 0x7a, 0x14, 0x51, 0x49, 0x26, 0x22, - 0xde, 0x2d, 0xf0, 0x40, 0x2f, 0x7e, 0x3e, 0x56, 0xbd, 0xf6, 0xa4, 0x6e, 0xe5, 0x2b, 0x15, 0x61, 0x59, 0x47, 0x53, - 0x19, 0x30, 0x5e, 0xdd, 0xac, 0x8a, 0xd0, 0xae, 0x84, 0xb8, 0x78, 0x21, 0x7b, 0x02, 0x31, 0x31, 0x92, 0x8f, 0x38, - 0x93, 0x60, 0x93, 0x83, 0x4c, 0x05, 0xe5, 0xe3, 0x43, 0xcd, 0x16, 0x04, 0xa1, 0xab, 0xdb, 0x50, 0x0b, 0x72, 0x2c, - 0x9c, 0x27, 0x92, 0x11, 0x4d, 0x12, 0x03, 0x57, 0x59, 0x49, 0xb0, 0x6a, 0x26, 0xe3, 0x65, 0xd6, 0x9d, 0x15, 0x7b, - 0x60, 0x53, 0xad, 0x39, 0x18, 0xdd, 0xff, 0x98, 0x17, 0x18, 0xa2, 0x28, 0x5a, 0xcf, 0x12, 0xc5, 0xf3, 0x88, 0x79, - 0xc0, 0x2c, 0x4b, 0xa0, 0x51, 0x84, 0x22, 0xf4, 0x6f, 0x89, 0x3d, 0xd4, 0x67, 0x95, 0x91, 0x58, 0x58, 0x0c, 0x27, - 0x54, 0xc5, 0xe9, 0x28, 0xed, 0x95, 0x85, 0xf7, 0xc1, 0x6e, 0x10, 0xc6, 0x57, 0xff, 0xd7, 0xf8, 0xee, 0x6d, 0x0f, - 0xa9, 0xed, 0xf9, 0x38, 0x9b, 0x99, 0x8f, 0xe9, 0x46, 0xef, 0xf6, 0x4f, 0x37, 0x22, 0x36, 0xbb, 0xad, 0x6c, 0x03, - 0x91, 0x4c, 0x14, 0x94, 0x9a, 0x3f, 0xb3, 0xdb, 0x03, 0xb6, 0xe2, 0xa0, 0x78, 0xdd, 0xa9, 0xa8, 0xcf, 0xd6, 0x6a, - 0x2c, 0x01, 0x08, 0xc7, 0x92, 0x46, 0xda, 0x25, 0xb6, 0x20, 0xd2, 0xfa, 0x1c, 0x19, 0x12, 0xbc, 0xb6, 0xce, 0xf3, - 0x00, 0x0e, 0x59, 0x32, 0x4d, 0x04, 0x2b, 0xd2, 0x4b, 0x00, 0x69, 0x1b, 0x21, 0xbd, 0xc8, 0x9e, 0x41, 0x09, 0xc0, - 0xab, 0x88, 0xf6, 0x46, 0x65, 0x22, 0x92, 0x37, 0x59, 0xa3, 0x14, 0xb6, 0xe3, 0x03, 0xe1, 0x65, 0x7e, 0x44, 0xb0, - 0xc4, 0xd4, 0x68, 0xcf, 0x96, 0x4e, 0x13, 0x50, 0x8b, 0xed, 0x34, 0x42, 0x98, 0xef, 0xd7, 0x8d, 0xc1, 0xc3, 0xe1, - 0x62, 0x49, 0x8a, 0x71, 0xb5, 0xde, 0x7c, 0x2c, 0xe5, 0x76, 0xe4, 0x5a, 0xec, 0x98, 0x3b, 0x70, 0x34, 0xe9, 0x9e, - 0xa6, 0x5d, 0xbd, 0xd1, 0xcd, 0xaf, 0xf8, 0xcd, 0xeb, 0x69, 0x89, 0x97, 0xc7, 0x47, 0x42, 0xf7, 0xae, 0x46, 0xa0, - 0xb9, 0x71, 0xce, 0x0f, 0xfd, 0xee, 0x5d, 0x9d, 0xe0, 0x8d, 0xff, 0x95, 0x1a, 0x4f, 0xca, 0xe4, 0xe4, 0x6f, 0xe6, - 0xd0, 0xf8, 0x8c, 0xf1, 0xb4, 0xb8, 0xa9, 0x40, 0xa0, 0x40, 0xd5, 0x01, 0xec, 0xce, 0xa2, 0xd2, 0x7f, 0x33, 0x62, - 0x7c, 0x04, 0x5c, 0x28, 0x3a, 0x35, 0xf8, 0x69, 0x49, 0x78, 0xc6, 0xf3, 0xd9, 0x2d, 0xd4, 0x22, 0x69, 0xa5, 0xce, - 0xc4, 0x4e, 0x1d, 0x7e, 0x2e, 0xf6, 0x0f, 0x2b, 0x22, 0x3a, 0x14, 0xf1, 0x61, 0x37, 0x05, 0x05, 0x41, 0xc3, 0x0b, - 0x62, 0x99, 0xa0, 0x0b, 0xa1, 0x7c, 0xd4, 0xec, 0xbe, 0x4c, 0x52, 0xfd, 0xc3, 0xfe, 0x82, 0x9f, 0x7b, 0xdb, 0xd7, - 0x22, 0x17, 0x4b, 0xa1, 0x59, 0xd9, 0x48, 0x27, 0x0f, 0x2e, 0x15, 0x6d, 0xaf, 0x82, 0xec, 0x6c, 0xde, 0x9a, 0x35, - 0x99, 0x03, 0xc9, 0x22, 0x2d, 0xc2, 0xfa, 0xc8, 0x73, 0xfe, 0xd9, 0xe2, 0x73, 0xba, 0x74, 0xdf, 0x37, 0x63, 0xdb, - 0x64, 0x98, 0xf4, 0x24, 0x81, 0x30, 0x51, 0x82, 0x6f, 0xcb, 0xe8, 0x01, 0x60, 0x7d, 0x6d, 0x39, 0xa2, 0xcc, 0x56, - 0x01, 0x99, 0xc9, 0x76, 0x7e, 0xed, 0x29, 0x20, 0xea, 0x6c, 0x05, 0x12, 0x31, 0x94, 0x3e, 0x03, 0x17, 0x34, 0x3e, - 0xb5, 0x20, 0x49, 0x89, 0x09, 0xc9, 0x0c, 0x1f, 0x01, 0x99, 0x02, 0x6e, 0xbe, 0xf3, 0x64, 0x0b, 0xfd, 0x54, 0xc6, - 0x29, 0xb0, 0x61, 0x5a, 0x0f, 0xe6, 0x2c, 0x8c, 0x67, 0x1a, 0x32, 0xe2, 0xf8, 0xe7, 0x1b, 0x81, 0xd5, 0x88, 0x3c, - 0x8f, 0xcc, 0x6c, 0x24, 0x2e, 0x60, 0x70, 0xd9, 0xf7, 0x92, 0x26, 0x10, 0xd7, 0xaf, 0x71, 0x8d, 0xf9, 0x24, 0x8f, - 0xd7, 0xea, 0xe6, 0xb7, 0xe6, 0x6f, 0x3d, 0x94, 0x67, 0xf9, 0xe2, 0xa4, 0x36, 0xad, 0x15, 0xf4, 0xcb, 0xb5, 0x56, - 0x1b, 0xa0, 0x56, 0xc1, 0xcc, 0x96, 0x8c, 0x5b, 0x69, 0x51, 0x04, 0x8e, 0x9a, 0xe5, 0xad, 0xd9, 0x1d, 0xdf, 0x6e, - 0x90, 0x3f, 0xd1, 0x10, 0x31, 0x3e, 0x55, 0x81, 0x4b, 0xd4, 0xf1, 0x73, 0xf4, 0x36, 0x87, 0x76, 0x18, 0xaf, 0xe2, - 0x87, 0x6d, 0xbc, 0xc8, 0xd8, 0xca, 0xf2, 0xf2, 0x75, 0x74, 0x88, 0x2b, 0x47, 0xeb, 0x30, 0x34, 0xfd, 0x34, 0x8d, - 0x39, 0x52, 0xa8, 0x27, 0xec, 0xf8, 0x7b, 0x0c, 0xf8, 0x4f, 0xed, 0xf1, 0xab, 0xfe, 0x93, 0xe3, 0x5f, 0x86, 0x5d, - 0xe5, 0x65, 0xfe, 0x63, 0x90, 0xbf, 0x87, 0x4f, 0xa9, 0xf2, 0xb3, 0x89, 0x2c, 0x54, 0x9b, 0x43, 0xf2, 0x68, 0x2d, - 0x87, 0x05, 0xfa, 0xea, 0xc3, 0xc9, 0xe9, 0x04, 0xc3, 0x9a, 0x53, 0xe7, 0x07, 0xa4, 0xaf, 0x6b, 0xe2, 0xc2, 0x2f, - 0xc6, 0xa7, 0xab, 0xb9, 0x0c, 0xdd, 0x74, 0xb9, 0x87, 0x54, 0xee, 0x8b, 0x4c, 0xb5, 0xad, 0xac, 0x9d, 0xfd, 0xfd, - 0x60, 0x70, 0x47, 0x04, 0x36, 0x9c, 0x7b, 0xf3, 0x85, 0xe7, 0x9f, 0xf4, 0x9f, 0xee, 0x29, 0x5a, 0x33, 0x16, 0x5f, - 0xe8, 0x33, 0xad, 0x5c, 0xbe, 0x71, 0x6d, 0xca, 0x36, 0xf4, 0x99, 0xda, 0x74, 0x03, 0x20, 0x26, 0x15, 0x34, 0x28, - 0xeb, 0xdc, 0xc7, 0x1f, 0x14, 0x7d, 0x48, 0x28, 0xfa, 0xd2, 0xe7, 0xa3, 0xda, 0x17, 0x06, 0x11, 0x00, 0x49, 0x0c, - 0x33, 0xf3, 0x97, 0x82, 0xee, 0x84, 0x86, 0x07, 0xfa, 0xe6, 0xe9, 0x27, 0x4e, 0x7d, 0x06, 0xde, 0x1a, 0xf5, 0x0f, - 0x52, 0x5c, 0x5f, 0x81, 0x04, 0xb0, 0x70, 0x9e, 0x2e, 0xff, 0x4f, 0x5e, 0x10, 0x42, 0xd2, 0x9c, 0x2c, 0x6a, 0x3b, - 0x57, 0xdd, 0xff, 0x98, 0x6c, 0xf0, 0x11, 0x1b, 0x24, 0xf8, 0x38, 0x29, 0x26, 0x9e, 0x05, 0x41, 0x75, 0x45, 0xcf, - 0x4d, 0xae, 0xfa, 0xb3, 0x8c, 0x43, 0xfe, 0xf7, 0xb1, 0x3c, 0x5f, 0x40, 0xce, 0x63, 0xc6, 0x77, 0x7e, 0x88, 0xb0, - 0x24, 0xa7, 0x61, 0x67, 0x76, 0x5f, 0xed, 0xcf, 0xa3, 0x81, 0xfa, 0x6a, 0x77, 0x7e, 0x03, 0x09, 0xf8, 0xc2, 0x0f, - 0x6b, 0xa8, 0x51, 0xf9, 0xa3, 0x39, 0xad, 0xbd, 0x4a, 0x1b, 0x7d, 0x42, 0x25, 0x22, 0xb8, 0x94, 0x1c, 0xa0, 0xbe, - 0x48, 0x3a, 0x3f, 0x99, 0x38, 0x66, 0xda, 0x94, 0x1c, 0x74, 0xe3, 0x5c, 0x72, 0xa1, 0x1c, 0x5a, 0x49, 0x1d, 0xa5, - 0x80, 0x9c, 0x94, 0xd1, 0x81, 0x15, 0xdd, 0x5e, 0x4a, 0xc6, 0x8f, 0x8a, 0xd0, 0x2e, 0x9b, 0xcd, 0x35, 0x2b, 0x8b, - 0x99, 0x44, 0x8c, 0x54, 0x70, 0x59, 0xb2, 0x32, 0x41, 0xbf, 0xa8, 0x8d, 0xa9, 0x8d, 0x64, 0xba, 0xb7, 0x59, 0x9d, - 0xba, 0x08, 0x34, 0xb8, 0x72, 0x40, 0x5e, 0xf1, 0x2a, 0x60, 0x4b, 0x48, 0x26, 0xcc, 0x22, 0x56, 0x70, 0xa1, 0xdc, - 0xf7, 0x99, 0xfb, 0x60, 0x7f, 0x9c, 0x04, 0xe7, 0x5b, 0x6f, 0xd1, 0xef, 0x12, 0x9c, 0x82, 0xea, 0xf5, 0xe7, 0xd4, - 0xb5, 0xb8, 0xbc, 0x62, 0x0a, 0x0a, 0x1c, 0x83, 0x7c, 0xba, 0xcb, 0x03, 0x22, 0xf0, 0x43, 0xfb, 0xa4, 0x8e, 0xd3, - 0xd6, 0x7f, 0x46, 0xc7, 0x0c, 0xb2, 0x81, 0x54, 0xe7, 0xbe, 0x2c, 0xb0, 0x9d, 0x2e, 0x5b, 0x22, 0xb7, 0x02, 0x77, - 0x4a, 0x75, 0xdb, 0x0b, 0x94, 0x29, 0xd1, 0x51, 0x11, 0x81, 0x6d, 0x06, 0xd0, 0xb3, 0x15, 0xc1, 0x4b, 0x1f, 0x79, - 0xd6, 0x2c, 0x68, 0xe8, 0x8f, 0x88, 0x34, 0x99, 0xd0, 0x80, 0x41, 0x2e, 0x26, 0x5e, 0x4c, 0xbd, 0xf4, 0x98, 0x45, - 0xc7, 0x19, 0x70, 0x67, 0xe7, 0x2c, 0x2c, 0xe6, 0xf5, 0x34, 0xe5, 0x4b, 0x8f, 0x7e, 0x33, 0xbc, 0xec, 0x5e, 0xba, - 0xf9, 0xe9, 0x73, 0xba, 0xc1, 0xc4, 0x06, 0xba, 0x6b, 0x75, 0x1c, 0xcd, 0x83, 0x87, 0x16, 0x30, 0x93, 0xd2, 0x1c, - 0xef, 0x5b, 0xd0, 0xd2, 0xb9, 0x08, 0x99, 0xc8, 0xcb, 0x83, 0x9e, 0xed, 0x1b, 0xd2, 0x50, 0xb3, 0x13, 0xe0, 0xb3, - 0xb3, 0x34, 0x9a, 0x2b, 0x5e, 0xd0, 0x42, 0x09, 0x23, 0xc4, 0x65, 0xfd, 0x13, 0xe0, 0x81, 0xaf, 0x01, 0xca, 0x9f, - 0x94, 0xeb, 0x81, 0x67, 0x8e, 0xb9, 0x43, 0x38, 0xe1, 0xa7, 0x8a, 0xc0, 0xdd, 0x45, 0x85, 0xfe, 0xc9, 0x8c, 0x21, - 0x85, 0x01, 0x5f, 0x15, 0xe3, 0x29, 0xe1, 0x15, 0x68, 0x1a, 0x94, 0x07, 0x87, 0x01, 0xaa, 0x51, 0xf7, 0x7d, 0xed, - 0x53, 0x38, 0xa2, 0x09, 0x0f, 0x79, 0x87, 0xbc, 0x1a, 0xaa, 0x85, 0x26, 0x3e, 0xe4, 0x1d, 0x93, 0xe0, 0x9b, 0x0f, - 0xf7, 0x32, 0x7d, 0x44, 0x95, 0xa3, 0x17, 0xfa, 0x84, 0x05, 0x92, 0x53, 0x3d, 0x5e, 0x4b, 0xd1, 0x5e, 0xd1, 0x1c, - 0x5a, 0x09, 0x54, 0x6c, 0x4c, 0x87, 0x70, 0xe9, 0xd9, 0x5e, 0x7f, 0x7a, 0x93, 0xf8, 0x8b, 0x38, 0x63, 0x0d, 0x7b, - 0x02, 0xce, 0x99, 0x0d, 0xaa, 0xef, 0x64, 0x97, 0xb0, 0x48, 0x92, 0x9e, 0x67, 0x4e, 0xc7, 0x04, 0xb9, 0x79, 0xcd, - 0x1d, 0xdc, 0xcc, 0x43, 0xb9, 0xaa, 0x4f, 0x44, 0xa1, 0x49, 0xdd, 0x49, 0x41, 0xc4, 0x5b, 0xf7, 0xd7, 0x09, 0x6a, - 0x19, 0x75, 0x75, 0xd3, 0xdf, 0x8a, 0x78, 0x64, 0xcc, 0xbf, 0x56, 0x91, 0xd1, 0x2a, 0xf7, 0xdb, 0xd6, 0x5f, 0x57, - 0x66, 0x0f, 0xd9, 0xcf, 0xab, 0x5a, 0xf7, 0x3f, 0x6b, 0xd8, 0x6b, 0x5e, 0x2b, 0x45, 0x11, 0xce, 0x5c, 0x4d, 0x7a, - 0x64, 0xbe, 0x41, 0x2f, 0xee, 0xf6, 0xf0, 0x9c, 0x04, 0x28, 0x13, 0x27, 0x21, 0x0e, 0x24, 0xe4, 0x18, 0x6b, 0xbe, - 0xa2, 0x3d, 0xe6, 0xba, 0x61, 0x51, 0x99, 0x6b, 0x7e, 0x20, 0x68, 0x40, 0x05, 0xf4, 0x87, 0x1d, 0x4e, 0x73, 0x6f, - 0x42, 0xc3, 0x6f, 0x42, 0x3e, 0x43, 0xf0, 0xbb, 0xfc, 0x63, 0x81, 0xa1, 0xd0, 0x52, 0x43, 0x86, 0xeb, 0x54, 0x37, - 0x63, 0xb2, 0xda, 0x04, 0xfe, 0xb7, 0xb4, 0x9b, 0x51, 0x43, 0x64, 0x41, 0x84, 0x0c, 0x30, 0x90, 0xd1, 0x16, 0x03, - 0xaf, 0x69, 0xa6, 0xd9, 0x6a, 0x30, 0x56, 0x1f, 0x36, 0x59, 0x0a, 0x76, 0xaf, 0x9c, 0xa9, 0x1c, 0x06, 0xb5, 0x81, - 0xe1, 0x5d, 0xfa, 0xde, 0x3e, 0x2c, 0xe3, 0x45, 0xad, 0x90, 0xfb, 0xe9, 0x10, 0x19, 0x6e, 0x52, 0xc7, 0x6a, 0xa6, - 0xff, 0x19, 0x33, 0x31, 0x3e, 0x35, 0x38, 0x04, 0x36, 0x8c, 0x5d, 0xd5, 0x82, 0x61, 0xda, 0x28, 0xed, 0x9d, 0xb2, - 0x2e, 0x68, 0x21, 0x2c, 0xad, 0x5a, 0xe3, 0x80, 0x71, 0x4e, 0x2f, 0x2f, 0xd4, 0xe9, 0xc4, 0x9b, 0xfa, 0xdb, 0xc8, - 0x84, 0x0f, 0xfe, 0x35, 0x87, 0xba, 0x5a, 0x18, 0x85, 0xe3, 0xe1, 0xa3, 0x94, 0x07, 0x49, 0x68, 0x89, 0x85, 0xa6, - 0x6c, 0x6e, 0x48, 0x54, 0x87, 0x62, 0x23, 0x28, 0xf8, 0x7c, 0x85, 0x08, 0xb8, 0xc3, 0x61, 0x4b, 0x49, 0xf7, 0x16, - 0xcf, 0x90, 0x24, 0x2a, 0x23, 0x17, 0x9b, 0x60, 0x0f, 0x04, 0xc7, 0xa5, 0xa6, 0xac, 0xe5, 0xf9, 0xaf, 0x92, 0xe8, - 0x9f, 0xa0, 0xe0, 0x25, 0x29, 0xe5, 0xfc, 0x3b, 0x16, 0x7f, 0xd6, 0xd7, 0xc7, 0x88, 0xde, 0xfb, 0x0b, 0xc9, 0x67, - 0x7d, 0xb6, 0x6d, 0x1b, 0x23, 0xf3, 0xf2, 0x66, 0xae, 0x1f, 0xeb, 0xb8, 0x8c, 0x5d, 0xa2, 0x80, 0xe8, 0x6f, 0x58, - 0x2e, 0xd3, 0x3c, 0xc0, 0xf2, 0xb0, 0x3c, 0x8a, 0xfc, 0xb6, 0x22, 0x59, 0xa2, 0x81, 0xb7, 0x38, 0x2d, 0xd2, 0xbb, - 0xed, 0x78, 0x97, 0x2b, 0x75, 0x3a, 0x74, 0x2b, 0x63, 0x49, 0x34, 0xaa, 0x94, 0x43, 0x10, 0x03, 0x77, 0xba, 0x38, - 0x06, 0xbb, 0x33, 0x99, 0x3d, 0x4e, 0xdb, 0x98, 0xea, 0x4e, 0xdc, 0xd1, 0x13, 0xbc, 0xc4, 0x27, 0x2d, 0x35, 0xbb, - 0xa3, 0x17, 0x5a, 0x86, 0x4a, 0xc8, 0xea, 0xf4, 0x85, 0x56, 0x3f, 0x8a, 0x90, 0x24, 0x07, 0xfc, 0xaa, 0xf2, 0x97, - 0xaf, 0xd7, 0x63, 0xa1, 0x52, 0x5d, 0x54, 0x14, 0x68, 0x7e, 0x9e, 0x26, 0x1e, 0xa3, 0xdd, 0x6f, 0xa5, 0xaf, 0xce, - 0x1b, 0xc2, 0x07, 0xa0, 0x02, 0x92, 0x5b, 0x39, 0x34, 0xa4, 0xa1, 0x65, 0x3e, 0x3c, 0xce, 0x50, 0x28, 0x02, 0x74, - 0x26, 0x75, 0xe1, 0xa9, 0x8b, 0xf3, 0xea, 0x1f, 0x13, 0x54, 0x6c, 0x3f, 0xa7, 0x0c, 0x80, 0x93, 0x34, 0x40, 0x34, - 0x1a, 0xcf, 0x59, 0xa7, 0xd1, 0x85, 0x52, 0x3c, 0x03, 0x47, 0xc0, 0x7a, 0x89, 0x5c, 0x7b, 0x45, 0xeb, 0x5a, 0x86, - 0x34, 0xe9, 0xfe, 0xed, 0x51, 0xbf, 0x0d, 0xc1, 0x1d, 0x98, 0x34, 0x12, 0x3a, 0xaa, 0xae, 0x98, 0x1b, 0xc9, 0x38, - 0x50, 0x77, 0xda, 0x4d, 0x29, 0x37, 0x56, 0xd8, 0xcb, 0x5c, 0xb6, 0xaa, 0x63, 0xb9, 0x44, 0x58, 0xc4, 0xc5, 0x51, - 0x62, 0x45, 0x80, 0xef, 0x91, 0x41, 0x54, 0xaa, 0xf2, 0x24, 0x8a, 0x90, 0xe4, 0x0d, 0x16, 0x18, 0x73, 0x0b, 0xfd, - 0x26, 0x12, 0x3c, 0xf8, 0xfa, 0xa7, 0x15, 0x49, 0xa1, 0x12, 0x10, 0x40, 0x83, 0x81, 0x16, 0x50, 0xcd, 0xfc, 0xa3, - 0x51, 0xb7, 0x18, 0x3a, 0xcf, 0xe2, 0x39, 0x25, 0xc9, 0xa0, 0x3f, 0xfe, 0xfb, 0x09, 0x61, 0xd0, 0x06, 0xb0, 0x90, - 0x11, 0x07, 0xdc, 0x30, 0xd7, 0x9b, 0x44, 0xfa, 0x6c, 0x51, 0x2c, 0xb6, 0xd8, 0xcb, 0xb9, 0x4d, 0xe9, 0x05, 0x8d, - 0x97, 0x50, 0xf2, 0x8e, 0xe1, 0x2b, 0xaf, 0x83, 0x99, 0xb7, 0xbc, 0xc6, 0x0d, 0x16, 0xfa, 0x05, 0x8b, 0xae, 0x4a, - 0x72, 0xef, 0x97, 0x3d, 0x00, 0x99, 0x4f, 0x27, 0xaa, 0x37, 0x04, 0x0f, 0x21, 0x65, 0x63, 0xcc, 0x98, 0x73, 0x83, - 0x7b, 0xe7, 0x53, 0x45, 0x0a, 0x23, 0x17, 0x86, 0x00, 0x4e, 0x62, 0x94, 0xd1, 0xa6, 0x9e, 0xce, 0x42, 0x9d, 0x7e, - 0xe1, 0xad, 0x03, 0xf5, 0x6b, 0x53, 0x09, 0xc5, 0x5e, 0xd6, 0x03, 0x22, 0x6a, 0xaa, 0xae, 0x04, 0xe5, 0xa6, 0x44, - 0xf5, 0xed, 0x27, 0x78, 0xa6, 0xe3, 0x50, 0xfc, 0xcc, 0xc0, 0x48, 0x4c, 0x84, 0xe4, 0x6c, 0x90, 0x24, 0x4f, 0xd2, - 0x65, 0x2d, 0x09, 0xea, 0xda, 0xaf, 0x12, 0xc2, 0x23, 0x92, 0x71, 0x7e, 0xd6, 0x87, 0x7a, 0xe0, 0xca, 0x06, 0xbb, - 0x1c, 0x4f, 0xbf, 0x08, 0xd7, 0xdd, 0xa6, 0x3d, 0x35, 0xbc, 0xfb, 0x54, 0x64, 0x72, 0xcb, 0x56, 0xcd, 0x62, 0x63, - 0x82, 0x9f, 0xf8, 0x3c, 0xe8, 0x3e, 0xee, 0x37, 0x24, 0xa1, 0x9f, 0x6f, 0x73, 0x3f, 0xb1, 0xd2, 0xe8, 0x03, 0x78, - 0xd0, 0x58, 0x8a, 0x4b, 0x2b, 0x50, 0x63, 0xc1, 0x97, 0xdb, 0x92, 0xbc, 0xcd, 0x3c, 0xf0, 0x9d, 0xb8, 0x10, 0xba, - 0xc8, 0x7d, 0xe4, 0x6b, 0xe4, 0xad, 0xf4, 0x6e, 0xd5, 0x46, 0xfe, 0xe0, 0x57, 0x7f, 0xc8, 0x4a, 0x4f, 0xe9, 0x8f, - 0xca, 0x9c, 0xc5, 0x37, 0x5c, 0xa2, 0x9b, 0x81, 0x1d, 0xc1, 0x49, 0x5d, 0xf5, 0xe1, 0x0d, 0xa0, 0xb5, 0x85, 0xb7, - 0x1c, 0x4d, 0x13, 0x81, 0xe7, 0x66, 0xb8, 0xc4, 0x15, 0xd9, 0xe0, 0x06, 0x8a, 0x3d, 0x28, 0x60, 0x20, 0x36, 0xca, - 0x74, 0x3c, 0x00, 0xfc, 0x1c, 0xe2, 0x6e, 0xcc, 0xcf, 0xba, 0x66, 0x1b, 0x2d, 0x70, 0x4e, 0x91, 0x05, 0x64, 0x2f, - 0x43, 0x03, 0x0a, 0xd0, 0x09, 0x45, 0x50, 0xda, 0xac, 0xb1, 0x03, 0x26, 0x84, 0x15, 0x46, 0xd3, 0x00, 0xc7, 0x86, - 0x98, 0xb3, 0x97, 0xe6, 0x16, 0x81, 0x2f, 0x97, 0x88, 0x5c, 0xd3, 0x23, 0xa1, 0x7c, 0x86, 0x14, 0x38, 0xd1, 0xcf, - 0xd3, 0x7f, 0x03, 0x26, 0xbd, 0x9b, 0x13, 0xb4, 0xa7, 0x83, 0x69, 0xbf, 0xd4, 0x10, 0x28, 0x2d, 0x23, 0xf7, 0x87, - 0xe6, 0xf9, 0x7a, 0x31, 0xa6, 0x6b, 0xac, 0x76, 0x1b, 0xcd, 0x3f, 0xc5, 0xf5, 0x3c, 0x19, 0xb7, 0x88, 0x5c, 0x18, - 0x00, 0x7e, 0x6f, 0x06, 0x8d, 0xb7, 0xde, 0x4f, 0x2a, 0x3c, 0x67, 0x0d, 0x4b, 0x28, 0xdd, 0x23, 0xf2, 0x6d, 0xfe, - 0x87, 0x69, 0x3a, 0x28, 0x82, 0x53, 0x1b, 0xf2, 0xde, 0x83, 0x7b, 0x58, 0x87, 0x82, 0xa1, 0xaf, 0xc3, 0x94, 0xf9, - 0xc9, 0xce, 0xb2, 0x81, 0xd2, 0xb6, 0xb3, 0x5d, 0x35, 0x25, 0xed, 0x42, 0x32, 0x5c, 0x91, 0x18, 0xa4, 0xda, 0xc8, - 0x8f, 0xb6, 0xb2, 0xb2, 0x66, 0x6e, 0xa7, 0x38, 0xf2, 0x73, 0xa7, 0x33, 0xec, 0xde, 0xd8, 0xac, 0x1b, 0x1e, 0x80, - 0x34, 0xd7, 0x29, 0x00, 0x08, 0xc2, 0xa5, 0x6c, 0xe2, 0x1d, 0x4b, 0x95, 0xed, 0x40, 0x7d, 0xb0, 0xd0, 0x1c, 0x5c, - 0xe7, 0xa3, 0x09, 0xf9, 0xb8, 0xd9, 0xac, 0xf4, 0x3c, 0x07, 0x88, 0xc7, 0x71, 0x52, 0x19, 0x0c, 0x91, 0x82, 0x9f, - 0x4a, 0xb0, 0xc3, 0xd1, 0xe2, 0x3c, 0xfa, 0x6b, 0xe4, 0xbc, 0x4f, 0x50, 0x0d, 0x15, 0x58, 0x15, 0x6c, 0xc3, 0xc6, - 0xd3, 0x8b, 0xc4, 0x65, 0xbb, 0x53, 0xa1, 0x45, 0x87, 0x83, 0x5a, 0xf8, 0xd0, 0x01, 0xfd, 0x90, 0x14, 0x0a, 0x71, - 0x2e, 0xc2, 0x79, 0x16, 0x92, 0xa7, 0x0d, 0x14, 0x46, 0x5e, 0x8c, 0xdf, 0xc6, 0x70, 0xb6, 0xeb, 0x16, 0x23, 0x4b, - 0xd7, 0x74, 0x6b, 0x9c, 0x67, 0x93, 0x1f, 0xd1, 0x13, 0x27, 0x55, 0x24, 0xd9, 0x44, 0x2a, 0xa8, 0x51, 0x1a, 0x6c, - 0xfc, 0x15, 0x00, 0x05, 0x73, 0xc3, 0x6b, 0x1a, 0x8f, 0xb4, 0x4c, 0xc4, 0x5d, 0x8e, 0x06, 0x9b, 0xcc, 0xc1, 0xe3, - 0x60, 0xd0, 0x2b, 0xc4, 0x19, 0xda, 0x05, 0x4e, 0x72, 0xab, 0xc4, 0x1e, 0x7f, 0x91, 0xef, 0x25, 0x5e, 0xd8, 0x94, - 0xa1, 0x47, 0x3c, 0x48, 0x2c, 0x9b, 0x8f, 0xc1, 0xea, 0xfc, 0xbb, 0xfd, 0x90, 0x68, 0x34, 0xd0, 0x01, 0xe8, 0xc8, - 0x97, 0x70, 0xde, 0x13, 0xd3, 0xa4, 0x7b, 0xac, 0xbb, 0xf8, 0xd6, 0xfd, 0xf5, 0x69, 0xf0, 0xdd, 0x21, 0xa3, 0x2f, - 0x70, 0x3b, 0x69, 0xc2, 0x76, 0x6e, 0x5a, 0xeb, 0xfa, 0x03, 0x58, 0x00, 0xa7, 0xcc, 0x78, 0x26, 0x86, 0x97, 0x0d, - 0xfa, 0x46, 0x17, 0xe4, 0xb9, 0xbb, 0x73, 0x1d, 0xf7, 0xe7, 0xb8, 0xf5, 0xe2, 0x94, 0x43, 0x6a, 0x61, 0x11, 0xa8, - 0x19, 0x40, 0x05, 0xe4, 0x65, 0x34, 0x25, 0x5d, 0x10, 0xfc, 0xda, 0xa0, 0xe8, 0x7c, 0x87, 0x31, 0x40, 0xbd, 0xea, - 0x39, 0xcd, 0xfb, 0x5b, 0x4e, 0xf2, 0x42, 0xc4, 0x7b, 0x9b, 0xa6, 0xfa, 0xa7, 0x95, 0x7d, 0xca, 0x94, 0xd7, 0x2f, - 0xcd, 0xd1, 0xd9, 0x84, 0xaa, 0xad, 0xdd, 0xa6, 0x1a, 0xe2, 0x73, 0xbc, 0x64, 0x1e, 0x51, 0xbc, 0x80, 0x81, 0xe6, - 0x81, 0x1f, 0x79, 0xaf, 0xed, 0xdd, 0x68, 0x56, 0x5e, 0xa7, 0x0b, 0xfa, 0x7e, 0x4e, 0xb3, 0x12, 0xbc, 0x67, 0x67, - 0xab, 0x4a, 0xf5, 0x92, 0x0a, 0x86, 0x79, 0xd7, 0x2b, 0x7f, 0x46, 0xbf, 0xd1, 0x13, 0x3f, 0xe5, 0x4d, 0xe3, 0x37, - 0x25, 0xf3, 0xdb, 0x24, 0x4c, 0xea, 0x1c, 0xd6, 0xf4, 0x28, 0xdd, 0x4e, 0x28, 0xe7, 0x41, 0xee, 0x5e, 0xf6, 0x35, - 0xb2, 0xdd, 0x78, 0xa5, 0xfc, 0xe2, 0x8b, 0x96, 0x7f, 0xd7, 0xab, 0xba, 0xd2, 0xa4, 0x79, 0x1a, 0x3b, 0x57, 0x0f, - 0xf5, 0xe9, 0x43, 0x47, 0xa6, 0xfb, 0x23, 0xbe, 0xcc, 0xc8, 0xd0, 0x22, 0x33, 0xdf, 0x55, 0x56, 0x43, 0x5c, 0x6b, - 0x35, 0xf1, 0xee, 0x54, 0xe6, 0xcc, 0x8e, 0xdd, 0x78, 0x91, 0x64, 0x30, 0xaa, 0x8e, 0x4c, 0x0d, 0x89, 0xe4, 0x84, - 0xa8, 0x5f, 0x31, 0x08, 0x98, 0x75, 0x8d, 0x7f, 0xb5, 0xaf, 0x29, 0xb7, 0xe5, 0x5b, 0x8f, 0xb9, 0xfe, 0x36, 0x4e, - 0xb7, 0x5f, 0x13, 0x60, 0xdb, 0x56, 0x5c, 0xea, 0xc5, 0x28, 0xb6, 0x36, 0x32, 0xb1, 0x82, 0x96, 0x27, 0xe0, 0xf0, - 0x80, 0x44, 0xa2, 0xc2, 0xa4, 0xd9, 0x37, 0x92, 0x4a, 0x76, 0xd8, 0xf0, 0xcd, 0x7d, 0xab, 0xb8, 0xa0, 0x8f, 0xf6, - 0x8f, 0x3a, 0x55, 0xd3, 0xcb, 0x42, 0x51, 0x55, 0x1d, 0xf4, 0xa0, 0x29, 0x95, 0xd2, 0xe7, 0x5f, 0xee, 0x4c, 0x12, - 0x10, 0x59, 0x25, 0x81, 0x31, 0x7d, 0x80, 0x62, 0x2a, 0xed, 0x5a, 0x03, 0xa2, 0xc6, 0xbf, 0x21, 0xe1, 0xb7, 0xfa, - 0x31, 0x3d, 0xba, 0x47, 0xdf, 0xfd, 0x5c, 0xda, 0xb5, 0x08, 0x56, 0xe9, 0xad, 0xfe, 0x25, 0x3f, 0xf5, 0x44, 0xd5, - 0xbc, 0x52, 0xbd, 0x33, 0xb4, 0xa7, 0xf9, 0xd3, 0x39, 0x3f, 0x7f, 0x3a, 0xe7, 0x75, 0x30, 0xcc, 0x8c, 0xad, 0x5b, - 0x15, 0xbc, 0x5c, 0xe0, 0xba, 0x84, 0x32, 0x3c, 0xf5, 0xe5, 0x3f, 0x3c, 0x0b, 0x53, 0x19, 0x90, 0x9a, 0x6c, 0x60, - 0x4c, 0x65, 0xe0, 0x74, 0x73, 0x43, 0x47, 0xd3, 0x8d, 0x56, 0x26, 0x9a, 0x19, 0x4f, 0x86, 0x1a, 0x72, 0x1a, 0x73, - 0xb0, 0xa7, 0x87, 0x8c, 0x43, 0xad, 0x66, 0xfc, 0x68, 0xb4, 0x05, 0x6d, 0x40, 0xe1, 0x67, 0x30, 0x53, 0x01, 0x06, - 0xd1, 0xf9, 0x36, 0xce, 0x3e, 0x49, 0x7f, 0x67, 0x99, 0xf3, 0x7c, 0xd9, 0x10, 0xc1, 0xaf, 0x34, 0xe9, 0x76, 0x59, - 0x04, 0xfc, 0x98, 0xd1, 0x58, 0xc6, 0xef, 0xc5, 0xa0, 0x7f, 0x91, 0x3e, 0xaa, 0x11, 0x38, 0x17, 0x20, 0xaf, 0x46, - 0x98, 0x06, 0x8a, 0x86, 0xf6, 0x1a, 0xfa, 0x42, 0xa9, 0x32, 0x7d, 0xed, 0x69, 0x4d, 0x2b, 0xb2, 0xb4, 0xd2, 0x4f, - 0xc6, 0x14, 0xb3, 0x2a, 0x71, 0xec, 0x69, 0xde, 0x37, 0xc8, 0x24, 0x5f, 0x38, 0xcb, 0x68, 0x29, 0xa7, 0xc6, 0x02, - 0xdd, 0x2a, 0xd4, 0xda, 0x85, 0xa7, 0x37, 0x68, 0x1c, 0x18, 0x2a, 0xca, 0xbe, 0x5f, 0x62, 0x8f, 0x78, 0xdf, 0x7e, - 0xc0, 0x47, 0x28, 0xb8, 0xed, 0x39, 0x49, 0x18, 0xa9, 0xfa, 0x5e, 0x51, 0xbc, 0xef, 0x9b, 0x64, 0x24, 0x37, 0x34, - 0x31, 0xd8, 0xa8, 0x85, 0xd3, 0xd3, 0x38, 0x5d, 0x38, 0x3d, 0xc5, 0xa9, 0x45, 0x5b, 0x32, 0xd3, 0xc8, 0x48, 0xac, - 0x5d, 0xe1, 0x44, 0x2d, 0xbf, 0xc9, 0xaf, 0xb2, 0x0d, 0x09, 0x14, 0x95, 0xd6, 0x5e, 0x95, 0x1e, 0xf4, 0x02, 0x36, - 0xe0, 0x4e, 0x75, 0x20, 0xf2, 0x12, 0x5f, 0x2d, 0x40, 0x00, 0x46, 0xf7, 0x3f, 0x58, 0x4d, 0xe9, 0xb4, 0xd1, 0x6e, - 0x4e, 0x39, 0x26, 0x50, 0x72, 0xcd, 0x07, 0x93, 0x90, 0x37, 0x38, 0x71, 0x5e, 0x43, 0xa0, 0x42, 0x1c, 0x43, 0x90, - 0x67, 0xc6, 0x16, 0xf3, 0xfb, 0xb7, 0xaa, 0xfa, 0xa1, 0xa2, 0x53, 0xa8, 0x21, 0x66, 0x5f, 0x68, 0x9e, 0xf0, 0x2b, - 0x72, 0x5e, 0xe6, 0xe4, 0xd2, 0x73, 0x8f, 0xe4, 0xe3, 0xd1, 0xd9, 0x63, 0x24, 0xe8, 0xdb, 0xd5, 0x8e, 0xe6, 0x26, - 0x2c, 0xc5, 0x97, 0xec, 0x67, 0x2a, 0xff, 0x54, 0x2d, 0x14, 0xcf, 0xac, 0xce, 0x3b, 0x65, 0xb1, 0xab, 0x2a, 0x76, - 0x49, 0x67, 0xd0, 0x29, 0x78, 0x33, 0x20, 0x82, 0x8e, 0xe0, 0x24, 0x49, 0x20, 0x11, 0x8d, 0x02, 0xed, 0xd9, 0x4c, - 0x24, 0xc1, 0x5c, 0x80, 0x25, 0xcb, 0x1b, 0x2e, 0x76, 0xed, 0x2f, 0xdd, 0x92, 0xcc, 0x13, 0x70, 0xd1, 0x3c, 0xd3, - 0xe9, 0xe5, 0x3a, 0xf2, 0x39, 0xc2, 0xd4, 0xba, 0xa9, 0xcd, 0xab, 0x84, 0xf3, 0x55, 0xb9, 0x89, 0x95, 0x78, 0x1b, - 0xa8, 0x19, 0xaf, 0x56, 0x2a, 0x7f, 0x62, 0x72, 0x4a, 0x6b, 0x64, 0xa4, 0xb0, 0x3f, 0xa4, 0xaa, 0x6d, 0x94, 0x70, - 0x1b, 0xd2, 0x6f, 0xe7, 0xa7, 0xed, 0x42, 0xf1, 0x5b, 0x3b, 0x18, 0xf2, 0xff, 0xf5, 0x1a, 0xdb, 0x85, 0xa8, 0x51, - 0x60, 0x84, 0x70, 0xb1, 0x08, 0x10, 0x9e, 0x0b, 0xa7, 0x6c, 0x88, 0x85, 0x07, 0x8f, 0xc2, 0xd9, 0xeb, 0xac, 0xab, - 0xde, 0x6f, 0xfe, 0x3e, 0xa8, 0xbf, 0x8f, 0x42, 0xe3, 0x5c, 0xaf, 0xf1, 0x6f, 0x15, 0x3f, 0xfe, 0xd0, 0x42, 0xb1, - 0x81, 0x11, 0x6e, 0x22, 0x68, 0xa5, 0x68, 0xb6, 0x25, 0xd5, 0xfa, 0xaa, 0x80, 0x22, 0x84, 0xdd, 0x49, 0x55, 0x0b, - 0x26, 0xac, 0x3b, 0x3d, 0xc1, 0xf2, 0xf9, 0xc0, 0xe9, 0x3f, 0xa6, 0xed, 0x39, 0x49, 0x54, 0xfa, 0xac, 0x4f, 0x85, - 0x27, 0x4f, 0xa2, 0xd5, 0x3e, 0xf3, 0x2f, 0xc1, 0xad, 0xaf, 0x7e, 0xb5, 0xac, 0x46, 0xca, 0x4c, 0x31, 0x8b, 0xc2, - 0x6b, 0xb7, 0x86, 0x99, 0xf1, 0x8a, 0x62, 0xd2, 0x34, 0x7b, 0x58, 0x0a, 0x8a, 0xbb, 0xba, 0x66, 0x65, 0x4e, 0xe7, - 0x65, 0x46, 0xe6, 0x54, 0x82, 0x71, 0x54, 0x7c, 0x8f, 0x33, 0x7e, 0xa3, 0xad, 0x18, 0x18, 0x75, 0x3c, 0xec, 0xf4, - 0x1c, 0xeb, 0x84, 0x01, 0x7a, 0xe5, 0xb9, 0x89, 0xf0, 0x1a, 0x2f, 0x81, 0x53, 0xa7, 0x36, 0x7d, 0x10, 0x69, 0xe5, - 0xa8, 0x29, 0xcf, 0xb0, 0xc5, 0x94, 0x5e, 0xe3, 0x36, 0x91, 0x76, 0xfb, 0x2f, 0x76, 0x5e, 0x05, 0x9f, 0xd8, 0xd3, - 0x11, 0x1e, 0xd1, 0xa4, 0xb4, 0x08, 0x5e, 0x24, 0xca, 0xc3, 0xf6, 0xc0, 0x73, 0x7e, 0x25, 0xd6, 0x5e, 0x4e, 0x94, - 0xf2, 0x7c, 0xe6, 0x39, 0x88, 0x21, 0x33, 0xb4, 0x84, 0x8e, 0xcf, 0x05, 0x47, 0x62, 0x5c, 0xbd, 0x4f, 0x90, 0xc4, - 0x49, 0xb0, 0xf7, 0xd7, 0x3e, 0x17, 0x69, 0x75, 0xfc, 0xf2, 0x3e, 0x61, 0x21, 0xb6, 0x6b, 0xda, 0x05, 0x2a, 0x64, - 0x3f, 0xf8, 0x53, 0x02, 0x5e, 0x13, 0xf2, 0xb2, 0xef, 0xd4, 0x6e, 0x9d, 0x75, 0xce, 0xff, 0x0b, 0x87, 0x43, 0x8f, - 0x5e, 0x39, 0x9a, 0x6b, 0x26, 0x60, 0x72, 0xc0, 0x51, 0xd5, 0xf7, 0xa2, 0xc4, 0xd1, 0xf0, 0x40, 0xa0, 0xac, 0x12, - 0xff, 0xb0, 0x7e, 0x30, 0x24, 0xa0, 0xf2, 0x88, 0xfb, 0xee, 0xd6, 0xee, 0x9a, 0x76, 0x65, 0x13, 0x2e, 0x57, 0x32, - 0xfe, 0x1b, 0x10, 0xa6, 0xef, 0x03, 0xce, 0xe1, 0xe8, 0xac, 0xfb, 0x02, 0x6e, 0x32, 0xe7, 0x14, 0xa3, 0xb8, 0x96, - 0x7c, 0xc1, 0xf6, 0x94, 0x90, 0xd7, 0xc4, 0x2e, 0xbf, 0x58, 0x47, 0xa1, 0x57, 0x9d, 0xd4, 0xcb, 0xb2, 0xe8, 0xbf, - 0x80, 0x75, 0x82, 0x78, 0x79, 0x9e, 0xe9, 0xb2, 0x6f, 0x12, 0x87, 0x2b, 0xac, 0xb0, 0x99, 0x95, 0x86, 0x66, 0x61, - 0xfa, 0x98, 0xd2, 0x75, 0x2c, 0x30, 0x0e, 0x55, 0xce, 0x76, 0x09, 0x6c, 0x49, 0xaa, 0x99, 0xc6, 0xfb, 0x0d, 0x59, - 0x85, 0x49, 0x4b, 0x2b, 0x8d, 0x17, 0xe8, 0x1e, 0x6b, 0x3c, 0xff, 0x4b, 0x30, 0x4c, 0x18, 0x67, 0x2f, 0xc3, 0xc4, - 0x4f, 0x2a, 0xb8, 0xaa, 0x79, 0x82, 0xc3, 0xe6, 0x9d, 0xf9, 0xab, 0xb6, 0x75, 0xc0, 0x4f, 0xb2, 0x2f, 0x1a, 0xc3, - 0x98, 0x2c, 0x2c, 0x06, 0xee, 0xd2, 0xd8, 0xcf, 0xc1, 0xc2, 0x0d, 0xfb, 0xe8, 0x1b, 0x05, 0x5f, 0xfa, 0xf7, 0x77, - 0xa0, 0x4e, 0x62, 0xd4, 0x75, 0x69, 0x25, 0xa5, 0xbf, 0x46, 0xbe, 0x68, 0x03, 0x88, 0x34, 0x4f, 0x7d, 0x8c, 0x85, - 0x53, 0x41, 0xc4, 0x92, 0x12, 0x61, 0xed, 0x8c, 0x30, 0x6b, 0x65, 0xf9, 0xfe, 0x6c, 0xea, 0x08, 0x05, 0x5d, 0x3b, - 0xcb, 0x9f, 0x73, 0xe9, 0x66, 0x11, 0x5d, 0x37, 0xc0, 0x58, 0x96, 0x1d, 0x51, 0x0e, 0x9e, 0x60, 0xdb, 0xea, 0xae, - 0x88, 0xbc, 0xa1, 0x3e, 0x49, 0xac, 0x4e, 0xe8, 0x23, 0x8e, 0xd6, 0xf9, 0x68, 0x13, 0x4d, 0xbe, 0x59, 0xe5, 0xf2, - 0xd3, 0x26, 0xe6, 0x34, 0xd4, 0xc5, 0x0c, 0x62, 0x38, 0xf8, 0x4e, 0x84, 0xce, 0xa6, 0x7d, 0xdc, 0xa0, 0xcd, 0xc2, - 0x19, 0x9a, 0x86, 0xeb, 0x10, 0x6f, 0x2a, 0x61, 0x51, 0xd9, 0x42, 0xd3, 0x29, 0x9d, 0x70, 0x75, 0x57, 0x98, 0x9b, - 0x73, 0xee, 0xa9, 0xe5, 0x1a, 0xba, 0x26, 0x02, 0x85, 0xe2, 0xb1, 0xe5, 0x1a, 0x7d, 0x75, 0x50, 0xa9, 0x42, 0xa7, - 0xdb, 0x79, 0xf6, 0x2a, 0x16, 0x1c, 0xae, 0x8c, 0xc5, 0x1c, 0x60, 0xe0, 0xa7, 0x71, 0xf2, 0xbe, 0x8a, 0xec, 0x54, - 0x5a, 0x72, 0xde, 0xa5, 0xe4, 0xc0, 0x25, 0xf5, 0xcf, 0x6d, 0x5e, 0x9d, 0x2f, 0x73, 0x9b, 0x29, 0x78, 0x0b, 0x6e, - 0xe9, 0xb4, 0x1e, 0x87, 0xa2, 0xed, 0x10, 0x53, 0xe5, 0x5e, 0x29, 0x25, 0xcf, 0x9a, 0x6a, 0x28, 0x59, 0xe7, 0x18, - 0xeb, 0x8f, 0x0e, 0x51, 0x3e, 0xc4, 0x34, 0xe2, 0x70, 0xa7, 0x62, 0x55, 0xf2, 0x64, 0x25, 0x48, 0x88, 0xd5, 0x36, - 0xcc, 0x0c, 0x5a, 0x59, 0x26, 0xaa, 0x7d, 0x77, 0x9e, 0x47, 0x89, 0x31, 0xbe, 0x32, 0xcb, 0xc2, 0xd7, 0xe5, 0x0e, - 0x74, 0x82, 0x34, 0xb7, 0x9b, 0xc0, 0xb2, 0x5e, 0xa3, 0x11, 0xe6, 0xd3, 0x38, 0x4a, 0x48, 0x40, 0x24, 0xd5, 0x2f, - 0x48, 0x97, 0x12, 0xee, 0x7a, 0xf4, 0x67, 0xf9, 0x20, 0x84, 0xf9, 0xf0, 0xe5, 0x84, 0x53, 0x97, 0x60, 0x3b, 0xde, - 0xe4, 0xb5, 0x02, 0x95, 0x44, 0xd3, 0x6b, 0x2b, 0x4e, 0x75, 0xa9, 0x7a, 0x5b, 0x8c, 0xe2, 0x34, 0xfd, 0xaa, 0x27, - 0xb9, 0xd9, 0x62, 0x61, 0x2c, 0x18, 0x04, 0x1a, 0x50, 0xd1, 0x4d, 0x00, 0xab, 0x31, 0x16, 0x11, 0xaf, 0xcb, 0x0f, - 0x99, 0x54, 0x53, 0x3a, 0x54, 0xed, 0xda, 0xef, 0x0f, 0xee, 0xdd, 0xf0, 0x70, 0xfc, 0xf8, 0x9f, 0xfb, 0xbd, 0x1e, - 0x54, 0x41, 0x97, 0xf0, 0x71, 0x67, 0x3b, 0xa6, 0x42, 0x01, 0xb2, 0xb2, 0x7d, 0x79, 0x01, 0x50, 0x63, 0x2a, 0x4e, - 0xba, 0x6b, 0xeb, 0xde, 0xb4, 0xe0, 0xd3, 0xba, 0x09, 0xdf, 0xfb, 0xe6, 0x7c, 0x6f, 0x98, 0x5a, 0x77, 0xf8, 0xdc, - 0xe5, 0x33, 0x9e, 0x02, 0x99, 0x0b, 0x83, 0xf7, 0x90, 0xe2, 0x26, 0x4c, 0x32, 0xe4, 0x6c, 0x9a, 0x77, 0xda, 0xb2, - 0xbc, 0xd6, 0x52, 0xb2, 0x23, 0x26, 0xec, 0xb9, 0xf2, 0x47, 0xde, 0x93, 0xf8, 0x48, 0x35, 0x04, 0xe0, 0x04, 0xa5, - 0x8d, 0xc0, 0x5c, 0xc5, 0xcd, 0xfc, 0xa9, 0x21, 0x88, 0x08, 0xf4, 0x4c, 0xe1, 0xde, 0xce, 0x1f, 0xce, 0xc6, 0x08, - 0x41, 0x2a, 0xf8, 0x66, 0xa5, 0x36, 0x6b, 0x78, 0xe9, 0x3f, 0x66, 0x67, 0x3b, 0x32, 0xd3, 0xdd, 0x26, 0x51, 0x5b, - 0xb6, 0xa6, 0x02, 0xcc, 0x20, 0x1a, 0x03, 0x17, 0x3c, 0x30, 0xa6, 0xf1, 0xd1, 0x9b, 0x71, 0x62, 0xad, 0xdd, 0xf2, - 0xe5, 0x8c, 0x8f, 0x1c, 0x7d, 0x4e, 0x16, 0xa8, 0x71, 0x77, 0x18, 0xcb, 0xcb, 0xf8, 0x6e, 0x3d, 0x6e, 0x56, 0xf7, - 0x20, 0x0b, 0x08, 0xd0, 0x6b, 0xb1, 0xae, 0x99, 0xe8, 0x55, 0xc2, 0x9d, 0x54, 0x9a, 0x27, 0x95, 0x98, 0x59, 0xe5, - 0xe5, 0xb5, 0xd3, 0xcb, 0x90, 0xbc, 0x0c, 0xd6, 0x6e, 0x54, 0xa1, 0xc7, 0xd6, 0x58, 0xe3, 0xb8, 0x66, 0x92, 0xa5, - 0xf1, 0xd7, 0xf0, 0x51, 0x3f, 0xbc, 0x5c, 0x45, 0xeb, 0xa6, 0x5a, 0xb4, 0x5e, 0x5b, 0x39, 0xcc, 0x97, 0xbc, 0xf8, - 0x85, 0x2d, 0xb6, 0xf0, 0x7a, 0xb1, 0xb9, 0x8f, 0xa8, 0x4c, 0x25, 0xaa, 0xd3, 0x4a, 0x54, 0xa6, 0x89, 0xc9, 0xcf, - 0x9e, 0x77, 0x01, 0x5c, 0x7f, 0xd4, 0xa9, 0x55, 0xfc, 0xa8, 0x32, 0xaa, 0xfc, 0x51, 0x23, 0x54, 0xeb, 0x13, 0x40, - 0x94, 0xc0, 0xac, 0x91, 0x87, 0x99, 0xb5, 0x61, 0x32, 0x29, 0xeb, 0x0b, 0x72, 0x85, 0xc3, 0xb4, 0xaa, 0x56, 0xbd, - 0xff, 0xe5, 0x86, 0xeb, 0x2f, 0x9b, 0xfc, 0x6e, 0xc6, 0xf5, 0xbe, 0x92, 0xfd, 0x60, 0x39, 0x18, 0x2c, 0xb4, 0xf1, - 0xe3, 0x16, 0xea, 0x7e, 0x8c, 0x1e, 0x86, 0xe0, 0xca, 0xf4, 0x35, 0x88, 0xfa, 0x95, 0x20, 0xf3, 0x23, 0xf2, 0x5a, - 0x01, 0x72, 0xb1, 0xd7, 0x37, 0xd2, 0xbb, 0xd6, 0x20, 0x1a, 0x1b, 0x12, 0xa9, 0xb3, 0x48, 0x86, 0x21, 0xb5, 0x7d, - 0xb8, 0xce, 0xe8, 0x68, 0xde, 0xe4, 0x2d, 0xbe, 0xa9, 0x86, 0x26, 0xcc, 0xb7, 0x0a, 0xab, 0x91, 0x9e, 0x93, 0xa6, - 0x29, 0x69, 0x56, 0xf6, 0x4d, 0xd0, 0xaf, 0x5e, 0x44, 0x26, 0x34, 0x06, 0x5f, 0x64, 0x70, 0xe0, 0xb7, 0x2b, 0x3a, - 0x0a, 0xd1, 0x4f, 0x79, 0x73, 0xff, 0x55, 0x30, 0x4a, 0xfd, 0x00, 0xb1, 0x6f, 0xd1, 0x05, 0x66, 0x67, 0x05, 0x7c, - 0x0b, 0xeb, 0xed, 0x05, 0x79, 0x94, 0xc6, 0xce, 0xc2, 0x11, 0x35, 0x61, 0xad, 0xf7, 0xb0, 0x31, 0xb2, 0xde, 0xf9, - 0xe7, 0xba, 0x2b, 0x51, 0x84, 0x4b, 0xcf, 0x65, 0x82, 0xea, 0xc0, 0x45, 0xe5, 0x5b, 0x6a, 0x2f, 0xa5, 0x09, 0xa7, - 0x22, 0x5f, 0x0c, 0x1b, 0xde, 0x87, 0xc3, 0xbe, 0x86, 0xc3, 0x0f, 0x7c, 0xd3, 0x5a, 0x6b, 0x26, 0x5b, 0x35, 0xb2, - 0x0b, 0x2e, 0xb8, 0xc0, 0xb0, 0x83, 0x81, 0xf5, 0xec, 0x7d, 0x1e, 0x82, 0xa6, 0x03, 0x61, 0xc1, 0x58, 0x34, 0xb7, - 0x01, 0x4f, 0x3e, 0x60, 0xa0, 0xd4, 0xcd, 0xdb, 0x6d, 0xd9, 0x22, 0xb9, 0x0d, 0x0c, 0xa9, 0xc5, 0x38, 0xcb, 0xe2, - 0xc0, 0x99, 0x3a, 0x9b, 0xe7, 0xfa, 0x46, 0x19, 0x77, 0xad, 0x44, 0x49, 0x1b, 0xbf, 0x9e, 0x0d, 0xee, 0x19, 0x29, - 0x74, 0x93, 0xff, 0x2f, 0x21, 0xe5, 0x5d, 0xae, 0x0a, 0x82, 0x6e, 0x70, 0xd2, 0xd7, 0x5a, 0xba, 0xc8, 0x4c, 0x44, - 0x77, 0x80, 0x2b, 0x68, 0x52, 0xae, 0x3d, 0x41, 0xd2, 0x67, 0x2b, 0xb7, 0x39, 0x11, 0xdb, 0x3d, 0x23, 0x9d, 0xd9, - 0x12, 0xea, 0xf3, 0x0b, 0x56, 0x97, 0x77, 0xee, 0x28, 0xe5, 0x73, 0x65, 0xcc, 0x78, 0x24, 0xcd, 0xb3, 0x3f, 0x4a, - 0x21, 0x4d, 0xc6, 0xf5, 0x22, 0x32, 0x9f, 0x97, 0xdb, 0x5b, 0x81, 0x07, 0x9e, 0xaa, 0xc0, 0x10, 0xb4, 0xde, 0x4b, - 0x7c, 0xf3, 0x55, 0x0d, 0xca, 0x3a, 0x19, 0x3f, 0x3e, 0x56, 0xbf, 0x35, 0xf6, 0xa2, 0xd2, 0xe4, 0x10, 0xcd, 0xd4, - 0x76, 0x5a, 0xe7, 0x2d, 0xa8, 0x65, 0x6a, 0xd7, 0x09, 0xa4, 0xd1, 0x39, 0xcf, 0x56, 0x91, 0x98, 0x6a, 0xfe, 0x6b, - 0xa6, 0x84, 0xbe, 0xaf, 0x50, 0x25, 0xfe, 0x19, 0x17, 0x89, 0x30, 0xe2, 0x3c, 0x50, 0x0f, 0x61, 0xd5, 0x12, 0x87, - 0xca, 0xbb, 0x31, 0x8c, 0x0b, 0x66, 0xe1, 0x10, 0x1b, 0x65, 0x7e, 0x16, 0x93, 0x4f, 0x97, 0x05, 0x4f, 0xce, 0xaf, - 0x64, 0x99, 0x75, 0xb3, 0x4f, 0x88, 0x87, 0x47, 0xed, 0x21, 0xd5, 0x2d, 0x0b, 0xad, 0x59, 0x59, 0x2f, 0xca, 0x6e, - 0xd4, 0x3e, 0x8b, 0x2f, 0xc8, 0x68, 0xd2, 0x1e, 0x78, 0xdc, 0x4b, 0x12, 0x48, 0x55, 0x2d, 0xdb, 0xcc, 0x21, 0xf2, - 0xf0, 0xf2, 0x21, 0x4f, 0xf9, 0xac, 0x4c, 0x54, 0x94, 0xb6, 0xc3, 0xe0, 0x81, 0xfb, 0x3a, 0x9a, 0xa0, 0x53, 0xac, - 0xd3, 0x15, 0x44, 0x6f, 0xc0, 0xac, 0x37, 0x8a, 0x3d, 0xab, 0xac, 0x4a, 0x36, 0xed, 0xe5, 0x1a, 0xbf, 0x71, 0x90, - 0x16, 0xdc, 0xd8, 0xe3, 0x48, 0x5d, 0x2f, 0x4a, 0xd7, 0xf5, 0x66, 0x6f, 0xb9, 0xd3, 0xea, 0x03, 0x3e, 0xfd, 0x94, - 0x6c, 0xc9, 0x5f, 0x2f, 0x5d, 0x93, 0x56, 0x6e, 0x0b, 0x1a, 0x35, 0xd6, 0x64, 0xbc, 0xe9, 0x4b, 0x10, 0x15, 0x55, - 0x09, 0x9e, 0x53, 0x7e, 0x36, 0x8c, 0x46, 0x32, 0xd1, 0x80, 0x7c, 0x69, 0xed, 0x7e, 0xae, 0x55, 0xbc, 0xb5, 0x3a, - 0x74, 0xca, 0xea, 0xe0, 0xf8, 0xd2, 0xb9, 0xd9, 0xba, 0x28, 0x14, 0x20, 0x01, 0xf6, 0x50, 0x43, 0x8e, 0x9d, 0xd5, - 0x8c, 0xdb, 0xdb, 0x6f, 0x1b, 0x31, 0x90, 0x62, 0x2e, 0xfb, 0xae, 0x1f, 0x22, 0x64, 0x32, 0x23, 0x4c, 0xac, 0x91, - 0xdd, 0x18, 0xa3, 0x09, 0x09, 0xc9, 0x78, 0x2d, 0x84, 0x84, 0xde, 0xea, 0x3c, 0x01, 0x5c, 0x12, 0x4f, 0x06, 0x6b, - 0x0a, 0x66, 0x45, 0x5e, 0x57, 0x5b, 0x71, 0x25, 0x16, 0x89, 0xf0, 0x75, 0x51, 0x23, 0xdb, 0x26, 0x58, 0x41, 0x8b, - 0x62, 0x0e, 0x14, 0x3a, 0xf3, 0x0d, 0x5f, 0x30, 0xe1, 0x9c, 0xdf, 0x75, 0x6b, 0x94, 0x96, 0x99, 0xa0, 0xaf, 0x1a, - 0x16, 0xb6, 0xdb, 0x2f, 0x10, 0xd7, 0x34, 0x03, 0x03, 0x8a, 0xb2, 0x43, 0x35, 0xbf, 0x03, 0x4b, 0x94, 0x9c, 0xb4, - 0x91, 0x9b, 0x5f, 0xa1, 0x63, 0x82, 0x18, 0x58, 0x68, 0x6c, 0x2f, 0x64, 0x2b, 0xd1, 0x9a, 0x1a, 0xb2, 0x11, 0x36, - 0xc1, 0x07, 0xa7, 0xa7, 0x70, 0x6d, 0x02, 0x55, 0xd3, 0x94, 0x46, 0x60, 0x9e, 0xf0, 0x40, 0x69, 0xbe, 0x9f, 0x5d, - 0x10, 0xd8, 0x98, 0xb7, 0x22, 0x8b, 0x03, 0x92, 0x12, 0x1b, 0x58, 0x78, 0xd4, 0x58, 0x2f, 0xef, 0x34, 0x8e, 0x2e, - 0xab, 0x51, 0x57, 0xa4, 0x58, 0x2a, 0x69, 0xc6, 0xa2, 0xc1, 0x98, 0x92, 0x90, 0x64, 0x2d, 0x04, 0x6b, 0x36, 0xfc, - 0xcd, 0x7b, 0x54, 0x7f, 0x6b, 0x2e, 0x60, 0x4f, 0x30, 0xdb, 0x79, 0x31, 0xbd, 0xbe, 0x1a, 0x65, 0xdb, 0xba, 0x85, - 0x79, 0x4f, 0x61, 0xd0, 0x9c, 0x8f, 0x29, 0x65, 0xce, 0x33, 0x40, 0x99, 0x49, 0x16, 0x01, 0x39, 0x0c, 0xb8, 0x07, - 0xa4, 0x2b, 0x2f, 0x0e, 0x7b, 0x19, 0xad, 0x9c, 0xb9, 0xf0, 0xf2, 0x72, 0x75, 0x34, 0x41, 0x39, 0x50, 0xe4, 0xc0, - 0x8b, 0xeb, 0x17, 0x4f, 0x73, 0x2d, 0x56, 0x59, 0x6f, 0x58, 0xa8, 0xae, 0xb7, 0xf4, 0xb9, 0xf5, 0x31, 0xf0, 0xf9, - 0xb5, 0x96, 0x5a, 0x34, 0x7f, 0xe8, 0xb5, 0xd5, 0xa4, 0xa0, 0xdd, 0xbf, 0xf5, 0x64, 0x14, 0x39, 0xa5, 0xd5, 0xb2, - 0xf8, 0x54, 0xbb, 0xe8, 0x29, 0x5a, 0xca, 0x26, 0xef, 0xb2, 0x87, 0xaa, 0x0b, 0xb3, 0xd6, 0x51, 0x98, 0xd5, 0xf6, - 0x28, 0xef, 0xec, 0xbd, 0x36, 0x0b, 0xca, 0x94, 0x56, 0x9f, 0x70, 0xbd, 0xf1, 0x02, 0xaa, 0xf9, 0x96, 0x1a, 0x8b, - 0x63, 0x7e, 0x49, 0x5d, 0xd9, 0x28, 0x7b, 0x9e, 0xd6, 0xb8, 0xbc, 0xeb, 0xa2, 0x17, 0x53, 0xc0, 0x29, 0xca, 0x4a, - 0x17, 0x37, 0x72, 0x09, 0x5d, 0x2b, 0xd2, 0x1a, 0xf8, 0x0c, 0x8c, 0x62, 0xb2, 0x9a, 0x7c, 0x90, 0xf4, 0xdf, 0x99, - 0xf5, 0x57, 0x9d, 0xb9, 0xfe, 0xa6, 0x17, 0xae, 0xbf, 0x5e, 0x54, 0x56, 0x85, 0x7d, 0x63, 0xec, 0x19, 0x70, 0x05, - 0x93, 0x4a, 0x7c, 0xa5, 0x73, 0x8e, 0x08, 0x6a, 0x0c, 0x15, 0xb2, 0x9b, 0x2f, 0x2c, 0x6c, 0x88, 0x3c, 0x3b, 0xbb, - 0xcc, 0xd2, 0x35, 0xd6, 0x98, 0xdc, 0xdf, 0xbb, 0x82, 0x59, 0x17, 0x56, 0x2d, 0xe3, 0x55, 0xce, 0xa5, 0x28, 0x92, - 0x62, 0x72, 0x91, 0x83, 0x14, 0x21, 0x80, 0x90, 0x8b, 0x24, 0xd0, 0x51, 0xda, 0xa2, 0x68, 0x24, 0x14, 0x80, 0xfa, - 0x72, 0xbe, 0x05, 0x08, 0x1c, 0x82, 0x39, 0x21, 0x08, 0x46, 0xf2, 0x2c, 0x20, 0x72, 0x42, 0xf6, 0x4e, 0x54, 0x88, - 0x30, 0xab, 0x83, 0x13, 0x68, 0x50, 0x16, 0xd8, 0xa2, 0x79, 0x99, 0x09, 0x8a, 0x2a, 0x44, 0x84, 0x65, 0xc5, 0xe5, - 0xea, 0x8f, 0x2e, 0x6d, 0xbd, 0x5c, 0x53, 0xe8, 0x92, 0xe5, 0xd3, 0xec, 0x1a, 0xca, 0xfc, 0x00, 0xfc, 0x6b, 0x51, - 0x07, 0xf6, 0xb4, 0x83, 0x34, 0xb0, 0x15, 0x17, 0xa7, 0xe2, 0xfa, 0xe7, 0x9c, 0x02, 0x42, 0x49, 0x4f, 0x2b, 0xc4, - 0x5c, 0xa0, 0x73, 0x0f, 0x71, 0x0d, 0x0a, 0x80, 0xe1, 0x92, 0xf1, 0xc2, 0x50, 0xdb, 0x7a, 0x7a, 0xea, 0xfc, 0x1e, - 0xc9, 0x35, 0x3a, 0x34, 0xf1, 0x22, 0xca, 0xdc, 0x65, 0x61, 0x3b, 0x52, 0xbc, 0xe7, 0x46, 0xb5, 0xc7, 0x94, 0x97, - 0xe7, 0x7b, 0xe8, 0x2f, 0xc4, 0xbc, 0x6d, 0x82, 0xaa, 0xa7, 0x7b, 0x6f, 0xad, 0x8b, 0xc0, 0x8f, 0x5e, 0x16, 0x05, - 0xea, 0xdb, 0x15, 0x23, 0x0d, 0x3d, 0xd9, 0xb1, 0x42, 0x97, 0x69, 0x59, 0xdb, 0xbb, 0xcd, 0xfa, 0xb6, 0x06, 0x83, - 0x8c, 0x7d, 0xa5, 0x78, 0x05, 0x84, 0x4d, 0xf1, 0x64, 0x26, 0xda, 0x6a, 0x08, 0x4e, 0x10, 0xca, 0xe9, 0xea, 0xf0, - 0xad, 0x20, 0x45, 0x45, 0x60, 0xeb, 0x7e, 0xac, 0x3d, 0xd4, 0xbe, 0x1b, 0x4a, 0xa7, 0x67, 0x8f, 0x1a, 0x1c, 0x3d, - 0xe5, 0x05, 0x3b, 0x34, 0x52, 0x97, 0x16, 0x21, 0x55, 0xf1, 0xb6, 0x01, 0xab, 0xf4, 0xe0, 0xd3, 0x06, 0x33, 0x9f, - 0xb1, 0xe2, 0x0e, 0x72, 0x15, 0x1b, 0xd1, 0x08, 0x05, 0xdd, 0x23, 0xf2, 0xf3, 0xfe, 0x82, 0x3d, 0x37, 0xe6, 0x56, - 0xf0, 0x4b, 0xc0, 0x30, 0xd5, 0x2b, 0x4c, 0x58, 0x2d, 0x8d, 0x16, 0x60, 0xe9, 0x8d, 0x67, 0xab, 0x66, 0xaf, 0x7c, - 0x2a, 0x95, 0x14, 0xab, 0x10, 0xbe, 0x53, 0x65, 0x05, 0x27, 0x1f, 0x62, 0x30, 0xc4, 0x4f, 0xdf, 0x56, 0x7e, 0xbd, - 0xea, 0xe6, 0x50, 0xf1, 0xa8, 0xb1, 0xa7, 0x3d, 0x8c, 0x92, 0xda, 0xf0, 0xaa, 0xc3, 0x10, 0x77, 0x67, 0xd9, 0x99, - 0x3d, 0x45, 0xd6, 0x54, 0x02, 0xf8, 0x15, 0x9b, 0xa2, 0x2e, 0x83, 0x8f, 0x88, 0x79, 0x23, 0x60, 0xfe, 0x66, 0x50, - 0x8c, 0xe6, 0x4d, 0x15, 0xad, 0x16, 0xf7, 0x26, 0x74, 0xc9, 0xb8, 0x44, 0xd9, 0xd3, 0x87, 0xf4, 0x3b, 0x24, 0x18, - 0x39, 0xdd, 0xac, 0xb8, 0xaf, 0x07, 0x87, 0x63, 0x1f, 0x5b, 0x97, 0x30, 0x05, 0x40, 0x8b, 0x5c, 0x4c, 0x80, 0xe9, - 0x7a, 0xcd, 0xb1, 0x90, 0xad, 0x0b, 0x49, 0x34, 0x34, 0x85, 0xa2, 0x6e, 0x41, 0x30, 0x31, 0x2a, 0xed, 0xf6, 0x83, - 0xb4, 0x30, 0x9e, 0x33, 0x95, 0x5f, 0x90, 0x1f, 0x4e, 0x7d, 0xd9, 0x1a, 0x7b, 0xa3, 0x63, 0x56, 0x34, 0xf1, 0xa4, - 0x99, 0x80, 0x48, 0x00, 0x2f, 0x17, 0xd1, 0x66, 0x9c, 0xa7, 0x92, 0x9a, 0xd7, 0x76, 0x81, 0x98, 0x01, 0x02, 0x9d, - 0x6a, 0x49, 0xa5, 0x78, 0x73, 0x3e, 0x48, 0x71, 0x10, 0x80, 0xb2, 0x63, 0x36, 0xb4, 0xa5, 0xa0, 0x1e, 0x32, 0xb4, - 0xd9, 0x5c, 0xdb, 0x5a, 0xee, 0xd4, 0xd9, 0xac, 0x45, 0x6d, 0x99, 0x3f, 0xdc, 0xe6, 0x17, 0x11, 0xe3, 0xa2, 0xee, - 0x13, 0x09, 0xd5, 0x14, 0x23, 0xd0, 0x79, 0x02, 0xf2, 0x7a, 0x38, 0xe1, 0xcd, 0x7d, 0xbf, 0x6f, 0xe9, 0x9a, 0x64, - 0xf1, 0xa2, 0xc0, 0xb9, 0x2f, 0x53, 0x78, 0x99, 0x70, 0x02, 0x97, 0x78, 0xa8, 0x33, 0x1f, 0x67, 0x5b, 0x9d, 0x29, - 0x46, 0xa0, 0xa4, 0x16, 0x91, 0x4d, 0x7a, 0x43, 0x90, 0x9a, 0xf1, 0x32, 0x10, 0x6a, 0x47, 0xa9, 0x01, 0xc9, 0xfb, - 0xba, 0x32, 0x5e, 0x4b, 0xb6, 0x2e, 0x42, 0xd9, 0x6c, 0xc7, 0xb5, 0xbb, 0x9c, 0x4e, 0x77, 0x37, 0x2b, 0xe4, 0x0e, - 0x28, 0x9d, 0x0d, 0x97, 0x11, 0xdf, 0xd0, 0xec, 0x40, 0x81, 0xd0, 0x6e, 0xdf, 0x66, 0x65, 0xcc, 0xc2, 0xe2, 0x75, - 0x43, 0x8e, 0x4a, 0xfe, 0x50, 0xde, 0x9d, 0xf5, 0x6e, 0xc3, 0x53, 0xdb, 0xc1, 0x7a, 0x50, 0x28, 0xfb, 0xd8, 0xa7, - 0x46, 0xe1, 0x0f, 0xdc, 0x2a, 0x91, 0x75, 0x08, 0xeb, 0xec, 0xc2, 0x3b, 0x2a, 0xd3, 0x31, 0x6d, 0x3b, 0x9b, 0x87, - 0xcd, 0x46, 0x41, 0xba, 0x2c, 0xe1, 0x78, 0x6d, 0xa5, 0xee, 0xd4, 0xc3, 0x73, 0x37, 0x4a, 0xdf, 0x97, 0x58, 0x5e, - 0xb6, 0x51, 0xf7, 0x76, 0x12, 0x4b, 0xf8, 0xcc, 0x3a, 0x71, 0x09, 0xee, 0x80, 0xb9, 0xca, 0x4e, 0x44, 0x2d, 0x90, - 0xd4, 0x7f, 0xe1, 0xe5, 0x8f, 0x06, 0xe3, 0x92, 0x93, 0xab, 0x5e, 0x4d, 0x20, 0x31, 0x13, 0x32, 0x47, 0xab, 0x77, - 0x03, 0x9a, 0x82, 0xae, 0x6b, 0x91, 0x03, 0x02, 0x4f, 0x6c, 0x7a, 0xf9, 0xed, 0x08, 0xe2, 0xec, 0x2e, 0x27, 0x34, - 0xac, 0xe1, 0x59, 0x76, 0xb1, 0x92, 0xb1, 0x6b, 0x8f, 0xa7, 0xc7, 0x2e, 0x95, 0x96, 0x4d, 0x18, 0xf3, 0xdb, 0xba, - 0xde, 0x28, 0x9e, 0x22, 0xa6, 0x5d, 0x9c, 0xca, 0x18, 0xae, 0x56, 0x9f, 0xe3, 0x79, 0x51, 0x05, 0x49, 0x5c, 0x12, - 0xa5, 0x37, 0xd6, 0x6f, 0xb9, 0x1c, 0x55, 0x15, 0xcb, 0xd9, 0xf9, 0x6d, 0xca, 0xab, 0xdf, 0x83, 0x7f, 0x7c, 0x95, - 0xb1, 0x08, 0xaa, 0x8c, 0xc8, 0x8c, 0x7d, 0x74, 0x11, 0x2d, 0xf4, 0xb3, 0xb6, 0x74, 0x15, 0x5d, 0xaf, 0xcc, 0x6b, - 0x88, 0x20, 0x70, 0xab, 0xea, 0x14, 0x52, 0x66, 0xd1, 0x98, 0x67, 0x15, 0xb3, 0x6b, 0x3d, 0xc6, 0x31, 0x67, 0x03, - 0xe1, 0x26, 0x93, 0x13, 0x24, 0x27, 0xe1, 0x33, 0x95, 0xd9, 0x96, 0x11, 0xf5, 0xc8, 0x6b, 0xa4, 0x8b, 0x9a, 0x35, - 0xe7, 0x6d, 0xd7, 0x59, 0xbc, 0x60, 0x71, 0xde, 0xaf, 0x6e, 0x44, 0x42, 0x80, 0x70, 0x11, 0xfe, 0x1c, 0xc0, 0xff, - 0x6d, 0x33, 0xc5, 0xfd, 0xdd, 0xfc, 0x92, 0x77, 0x4d, 0x1b, 0x07, 0xe0, 0x80, 0x82, 0xc5, 0xc9, 0xe0, 0x02, 0xc9, - 0x08, 0x43, 0xbd, 0x42, 0xb4, 0xc1, 0x52, 0x31, 0xce, 0x2d, 0x3d, 0x8f, 0xec, 0x68, 0xd0, 0xa7, 0xe5, 0xc4, 0x5c, - 0x79, 0x83, 0x31, 0x5b, 0xa3, 0x12, 0x42, 0xed, 0x08, 0x31, 0x85, 0xc9, 0x74, 0x56, 0x16, 0x25, 0x7f, 0x15, 0x26, - 0xb4, 0x82, 0x49, 0x4a, 0x9b, 0x51, 0x63, 0x88, 0x8d, 0x8a, 0x50, 0xbd, 0xe7, 0x94, 0x35, 0x04, 0x73, 0x7b, 0x42, - 0xfa, 0x35, 0x44, 0xd7, 0x3f, 0xd6, 0xcf, 0x13, 0x4e, 0x6a, 0xdb, 0xf9, 0xba, 0xd0, 0x82, 0x83, 0x6b, 0x2a, 0xaa, - 0x72, 0x35, 0x0c, 0x51, 0x40, 0xa1, 0xd4, 0x91, 0x3a, 0xd4, 0x12, 0x59, 0x9b, 0x55, 0x3a, 0xd9, 0x61, 0xb4, 0x9c, - 0x4c, 0x89, 0x2b, 0x48, 0x6b, 0x5d, 0x39, 0x57, 0xbe, 0xd1, 0x97, 0x6d, 0xd0, 0x1b, 0x8d, 0x44, 0x2e, 0x3b, 0x8f, - 0x3f, 0xdf, 0xfa, 0x1c, 0xa0, 0xd6, 0xff, 0x6a, 0xed, 0x72, 0xc9, 0x02, 0x76, 0xb1, 0xab, 0x23, 0xf1, 0x7e, 0xde, - 0x0a, 0xb8, 0xbe, 0x10, 0x08, 0x75, 0xdd, 0x85, 0x72, 0xd2, 0x15, 0xab, 0xa2, 0x5f, 0xbe, 0x47, 0xd1, 0xac, 0xb7, - 0x11, 0x94, 0x4d, 0x90, 0xd6, 0xbb, 0x3a, 0x0e, 0x29, 0x21, 0x51, 0x59, 0x4c, 0x75, 0x61, 0x8d, 0x1e, 0xe8, 0x0e, - 0x5b, 0x45, 0x34, 0xa7, 0xe9, 0x26, 0xfb, 0xfe, 0x50, 0xa1, 0x04, 0x22, 0xfc, 0xbf, 0x7b, 0xd3, 0x33, 0xd0, 0x20, - 0x49, 0x5d, 0x80, 0x4a, 0x49, 0xfb, 0x85, 0xd3, 0xfe, 0x50, 0x65, 0x0b, 0x80, 0xc2, 0x1e, 0x6f, 0x14, 0x6d, 0xcb, - 0xef, 0x66, 0x3d, 0x28, 0xd1, 0xfa, 0x3f, 0x2a, 0x43, 0x16, 0x10, 0x6d, 0x47, 0xd7, 0x6a, 0xe9, 0x95, 0x4f, 0x52, - 0x0c, 0x47, 0x13, 0x62, 0xfb, 0x9d, 0xbe, 0x7c, 0x87, 0xea, 0xc2, 0x5a, 0xe2, 0xdc, 0x4b, 0x6a, 0x4b, 0x16, 0xb0, - 0x9f, 0x31, 0x62, 0xba, 0x51, 0xc1, 0x2f, 0x1f, 0x75, 0xb9, 0x9a, 0x85, 0xab, 0x21, 0x60, 0x66, 0x5f, 0x5d, 0xf1, - 0x20, 0x58, 0xc0, 0xd4, 0xb0, 0x30, 0x63, 0xc7, 0x51, 0x9f, 0x39, 0x96, 0xb2, 0xcf, 0x7d, 0x46, 0xd7, 0x37, 0xc7, - 0xfe, 0x11, 0xeb, 0xf6, 0x5b, 0xec, 0x8a, 0x71, 0x3c, 0xb0, 0xaf, 0x2e, 0xb2, 0x81, 0x69, 0x42, 0x92, 0xf5, 0xcb, - 0x29, 0x90, 0xaa, 0xd5, 0x83, 0x98, 0xab, 0x3a, 0x01, 0x8c, 0xf6, 0x5d, 0x51, 0xf0, 0x88, 0x1c, 0x7f, 0x22, 0x8d, - 0x0e, 0x98, 0xe2, 0x0e, 0x84, 0x30, 0x74, 0x47, 0xbc, 0xd9, 0x5b, 0x81, 0x60, 0x44, 0xbb, 0x20, 0xfc, 0x8d, 0xf3, - 0x12, 0x5b, 0xd0, 0x36, 0x5a, 0x2f, 0x02, 0x68, 0x88, 0x44, 0xf2, 0x63, 0xe4, 0xf9, 0x70, 0x76, 0xee, 0x41, 0x31, - 0xdc, 0xa4, 0x2e, 0x88, 0xeb, 0xe9, 0x05, 0xdb, 0x65, 0x42, 0x32, 0x51, 0xe8, 0xd0, 0x14, 0x58, 0x59, 0x3b, 0x71, - 0x3a, 0xc0, 0x87, 0xf7, 0xf7, 0xf0, 0xc0, 0x76, 0x54, 0xfc, 0x40, 0x02, 0xb7, 0x2f, 0xac, 0xe0, 0x50, 0x67, 0xc1, - 0x0c, 0x3a, 0xe0, 0x91, 0xde, 0xa7, 0x46, 0x8c, 0x66, 0xd6, 0x3b, 0x40, 0x14, 0x11, 0x65, 0xb6, 0x4d, 0x6e, 0x87, - 0xbb, 0xe3, 0x29, 0x10, 0x20, 0x63, 0x5a, 0x15, 0x96, 0x61, 0x26, 0xb0, 0xc4, 0x7c, 0x33, 0xbe, 0x68, 0xd1, 0x8f, - 0xfd, 0x3e, 0xaa, 0xe4, 0xa2, 0x52, 0x83, 0xb1, 0x8d, 0x79, 0x63, 0x8b, 0x9e, 0xe0, 0x1b, 0x8d, 0x74, 0xf4, 0x0c, - 0x63, 0xb9, 0x84, 0x39, 0x58, 0xe9, 0x1c, 0xf5, 0x23, 0x58, 0x51, 0x05, 0x88, 0xb3, 0x1f, 0xa7, 0x48, 0x0d, 0x98, - 0x25, 0x3f, 0xa4, 0x45, 0x4d, 0x4e, 0x03, 0x7e, 0xcd, 0x40, 0xcf, 0x1e, 0x55, 0xc6, 0x3d, 0x79, 0x09, 0x5c, 0x9a, - 0xde, 0x7a, 0xda, 0x77, 0x6f, 0xc0, 0x31, 0x16, 0xe4, 0x0d, 0xe6, 0xec, 0x4e, 0x30, 0xc5, 0x8a, 0x6d, 0x5d, 0x2d, - 0xf3, 0x6a, 0xfd, 0x40, 0x67, 0x25, 0x18, 0x4e, 0x93, 0x48, 0xe2, 0x04, 0x4c, 0xa3, 0x18, 0x7f, 0x60, 0xbb, 0xbc, - 0xdb, 0xea, 0x13, 0xbf, 0x0d, 0x7f, 0x1d, 0x29, 0x55, 0x9f, 0x7f, 0x12, 0x0b, 0x33, 0x99, 0xd8, 0x6f, 0xe4, 0xe8, - 0x0c, 0x32, 0x2b, 0xf0, 0x55, 0x3d, 0xe3, 0x59, 0xf2, 0x5c, 0x79, 0xca, 0xcd, 0x8a, 0x2d, 0xb3, 0xe0, 0xe7, 0x51, - 0x49, 0x8d, 0xbd, 0x19, 0xd5, 0xa9, 0x56, 0x8c, 0x51, 0x9d, 0x9e, 0x1c, 0x08, 0x97, 0x29, 0xc0, 0x2a, 0x3b, 0x80, - 0xc6, 0xf3, 0xeb, 0xd2, 0x23, 0x11, 0xd9, 0x2a, 0xa6, 0x1e, 0x83, 0x97, 0x8a, 0xa0, 0x77, 0x10, 0x85, 0x18, 0x1c, - 0x49, 0xdf, 0x68, 0xf5, 0xc5, 0x9f, 0xf8, 0x7d, 0xaf, 0x97, 0x70, 0x17, 0xec, 0x7c, 0x53, 0x63, 0xe9, 0x2c, 0x41, - 0x63, 0xf6, 0x3f, 0x87, 0xac, 0x45, 0x58, 0xe4, 0x34, 0xd3, 0x10, 0x34, 0x41, 0xf1, 0x47, 0xd0, 0xc0, 0x66, 0x4d, - 0xd7, 0x7a, 0x13, 0x94, 0x51, 0x48, 0x82, 0xff, 0x57, 0x19, 0x2f, 0x87, 0x2a, 0x27, 0x93, 0xa8, 0x05, 0xf7, 0x89, - 0x9b, 0x6a, 0x68, 0x05, 0xea, 0xec, 0xe1, 0x29, 0xf4, 0x64, 0x2c, 0xa2, 0x67, 0x58, 0xc4, 0x46, 0x9b, 0xc0, 0x78, - 0x24, 0xf3, 0xb0, 0x2e, 0xa2, 0xdd, 0x72, 0x36, 0xc5, 0x57, 0x76, 0xcc, 0xdb, 0x6e, 0x1f, 0xbb, 0x09, 0x95, 0x78, - 0xfa, 0x7d, 0x57, 0xcc, 0xbe, 0xc7, 0xbe, 0x94, 0xd2, 0x3d, 0x70, 0x58, 0x4a, 0xeb, 0x22, 0x28, 0x9c, 0x3a, 0xd8, - 0x02, 0x9a, 0xec, 0xe4, 0x6c, 0x1a, 0x25, 0x58, 0x9c, 0xb9, 0x49, 0xc0, 0xaf, 0x74, 0x12, 0x42, 0x2a, 0x1b, 0xbe, - 0x63, 0x2d, 0xf9, 0x2b, 0x90, 0x6b, 0xfe, 0xe2, 0x69, 0x20, 0x44, 0x6d, 0x23, 0x14, 0x01, 0x6b, 0xe2, 0xca, 0xbc, - 0x33, 0x08, 0xae, 0xe8, 0x2f, 0x7b, 0x0d, 0xff, 0xdc, 0x98, 0xf6, 0xad, 0x90, 0xda, 0xd0, 0xc1, 0x5a, 0x44, 0xc6, - 0xf3, 0x50, 0xf8, 0x6f, 0xf8, 0xd8, 0x73, 0x84, 0x48, 0x22, 0x17, 0xc9, 0x8f, 0x28, 0x6e, 0x31, 0xdd, 0x42, 0xb9, - 0xb5, 0x9d, 0x8f, 0x23, 0x61, 0xd0, 0x3c, 0x6a, 0xf5, 0x92, 0x94, 0xf7, 0xd4, 0x6a, 0xe6, 0x1e, 0x05, 0xb7, 0x8b, - 0xa5, 0x86, 0x17, 0x88, 0xd2, 0xd5, 0x0f, 0x0a, 0xcd, 0xe2, 0x3f, 0x66, 0xb5, 0x79, 0xea, 0xf6, 0x51, 0xc9, 0x37, - 0xc9, 0xca, 0x91, 0x05, 0x27, 0x51, 0xf8, 0x43, 0x08, 0xbc, 0xd4, 0x19, 0x4f, 0xf5, 0x36, 0x62, 0x1e, 0x0a, 0x4d, - 0x41, 0xae, 0x07, 0xed, 0x13, 0x4d, 0x8e, 0xdc, 0x90, 0x63, 0x7a, 0xd0, 0x3e, 0xac, 0x81, 0xed, 0x08, 0x71, 0x71, - 0x9f, 0x88, 0xe1, 0xb4, 0xea, 0x72, 0x02, 0xe4, 0xce, 0x79, 0xd2, 0x32, 0x04, 0x35, 0x72, 0x13, 0xd4, 0xb8, 0x73, - 0x9c, 0xda, 0x45, 0xd1, 0xed, 0x4b, 0x2e, 0x91, 0x62, 0x94, 0xe9, 0xbe, 0xf4, 0xdf, 0xab, 0xad, 0xa2, 0x01, 0x64, - 0x03, 0xbe, 0xde, 0x7b, 0xc7, 0xe8, 0x00, 0xf5, 0x72, 0xeb, 0xa6, 0x6c, 0x5e, 0x9e, 0xd3, 0x6c, 0x6b, 0xb8, 0xc7, - 0xd0, 0xfe, 0x12, 0xea, 0x9c, 0xfb, 0xac, 0xf8, 0xad, 0xbc, 0x0b, 0xc4, 0xe4, 0xe4, 0x66, 0x23, 0x4f, 0x93, 0x75, - 0x84, 0x75, 0x8f, 0xa1, 0xb9, 0x88, 0x7f, 0x69, 0xac, 0x5c, 0x10, 0x9e, 0x58, 0xc9, 0x82, 0xbf, 0x30, 0xcc, 0x60, - 0x53, 0x79, 0x4d, 0x7f, 0x87, 0x39, 0x80, 0xf7, 0xdb, 0xcd, 0x5a, 0x41, 0x3e, 0x25, 0xb5, 0xe3, 0x6b, 0xad, 0xe3, - 0x97, 0x6f, 0xd0, 0x83, 0xd4, 0xc4, 0x63, 0x51, 0x3d, 0x10, 0xb3, 0xa4, 0x37, 0x2f, 0x71, 0xf4, 0xcd, 0x4f, 0x9b, - 0x67, 0x5c, 0xe3, 0xb9, 0x08, 0xc9, 0x80, 0xb5, 0xc1, 0xa5, 0xbd, 0x37, 0x12, 0x77, 0x9f, 0x95, 0xa9, 0x45, 0x6b, - 0x63, 0x26, 0x0a, 0xb4, 0xb0, 0xee, 0x12, 0xf1, 0x7c, 0xf9, 0xa6, 0xbf, 0x76, 0xa4, 0x58, 0x9a, 0x8f, 0x64, 0x1e, - 0x55, 0x29, 0xe1, 0x8f, 0x01, 0x8d, 0x7f, 0x43, 0x5e, 0x24, 0x31, 0xd0, 0x60, 0x91, 0x1a, 0x2b, 0xef, 0x13, 0x70, - 0x88, 0xa1, 0x89, 0xa8, 0x4d, 0xb4, 0x13, 0xb8, 0xa3, 0xf1, 0x89, 0xa4, 0x3e, 0x26, 0x95, 0x34, 0x01, 0x1e, 0xdd, - 0xc5, 0xe4, 0x64, 0xec, 0x02, 0x7c, 0x81, 0xc7, 0xc7, 0xd3, 0x6f, 0xda, 0xd5, 0xd1, 0x0d, 0x52, 0x6e, 0x2a, 0xc8, - 0x26, 0x60, 0xad, 0x05, 0xe0, 0x29, 0xd7, 0x44, 0xf3, 0x8e, 0x54, 0xbf, 0x0c, 0x02, 0xf6, 0xbb, 0x8b, 0x7a, 0xee, - 0x4d, 0x63, 0x65, 0xf9, 0x38, 0xf1, 0x52, 0xd3, 0x08, 0xb1, 0x62, 0x9f, 0x71, 0xca, 0x11, 0x11, 0xef, 0xf0, 0x6b, - 0xeb, 0xcd, 0x22, 0xbd, 0x4d, 0x8a, 0x73, 0x93, 0x01, 0x86, 0x91, 0x6b, 0x84, 0x5f, 0xcc, 0xb4, 0xb3, 0x75, 0xe5, - 0xc3, 0x02, 0xc9, 0x68, 0x29, 0xfc, 0xad, 0xc8, 0xac, 0xb6, 0x59, 0x8b, 0x10, 0xff, 0x50, 0xf4, 0xb3, 0x43, 0x69, - 0x14, 0x90, 0x57, 0x5f, 0x2e, 0x2b, 0x36, 0x39, 0x05, 0x9d, 0xf6, 0xb9, 0x79, 0x67, 0x59, 0x7e, 0xfc, 0xf9, 0x8f, - 0x73, 0x3b, 0x61, 0x8b, 0x99, 0x27, 0x6e, 0xb1, 0x8c, 0xb2, 0xf2, 0xa2, 0xd5, 0x79, 0x4b, 0xd6, 0xcd, 0xec, 0xba, - 0x40, 0x09, 0xff, 0xd4, 0x8f, 0x0e, 0x67, 0xe5, 0x0c, 0x7a, 0x85, 0x56, 0x16, 0xf6, 0x28, 0x6d, 0xdf, 0xda, 0x97, - 0x03, 0x9d, 0xc6, 0x5d, 0xd8, 0x1c, 0x27, 0x48, 0x52, 0x79, 0x28, 0x3f, 0xf3, 0x14, 0x67, 0xdf, 0x59, 0x4d, 0x47, - 0x3b, 0x7a, 0xc7, 0xd1, 0xe5, 0x60, 0xb1, 0x43, 0x94, 0xac, 0x0f, 0xce, 0xb6, 0x59, 0x7c, 0x70, 0x94, 0x69, 0x3e, - 0xe3, 0x15, 0x0b, 0xa4, 0x34, 0x4f, 0x9f, 0x22, 0xe8, 0x09, 0x64, 0x62, 0x0c, 0xbd, 0x0b, 0x36, 0x4d, 0x81, 0x63, - 0xce, 0xb7, 0x89, 0xa0, 0xcd, 0x32, 0x9a, 0x45, 0xf4, 0x62, 0x64, 0x29, 0xbc, 0xf6, 0x8e, 0x7a, 0xae, 0x64, 0x5d, - 0x42, 0xab, 0x23, 0xab, 0x1f, 0x6c, 0xf7, 0x69, 0xe1, 0x07, 0xf3, 0xbb, 0xd5, 0x42, 0x7d, 0x65, 0xac, 0x7e, 0x8c, - 0xcc, 0x52, 0xe7, 0x2c, 0x67, 0xb7, 0xd3, 0xd8, 0xc0, 0xeb, 0x64, 0xb3, 0xf5, 0xeb, 0x76, 0x7f, 0xb9, 0xe4, 0xdf, - 0x66, 0xca, 0xdb, 0x24, 0x47, 0xd8, 0xef, 0x13, 0x59, 0x03, 0xb2, 0x3e, 0x6d, 0x71, 0x96, 0x92, 0x3a, 0x56, 0x49, - 0x94, 0x18, 0xdb, 0x09, 0x5c, 0x61, 0x10, 0x12, 0xcf, 0x66, 0x75, 0x25, 0x4c, 0xce, 0xab, 0x78, 0xa7, 0x30, 0x57, - 0x22, 0x59, 0x2c, 0xf2, 0x04, 0x45, 0xda, 0x37, 0xcb, 0xe5, 0xa5, 0x3c, 0x35, 0xa5, 0x1d, 0x09, 0x8d, 0xbc, 0xa4, - 0xff, 0x0c, 0xb8, 0x24, 0x44, 0x2a, 0x50, 0x89, 0xcf, 0x7d, 0x47, 0x2a, 0xd1, 0xa4, 0x8a, 0x52, 0x14, 0xd4, 0xca, - 0xf4, 0x8f, 0x98, 0x97, 0xa6, 0xb4, 0xee, 0x81, 0xc0, 0x75, 0x9b, 0x2b, 0x89, 0xa7, 0x7f, 0x99, 0xcc, 0x2e, 0x00, - 0xe7, 0x65, 0xb9, 0xc1, 0x2f, 0x63, 0xc2, 0xe5, 0xd1, 0x65, 0x4d, 0x20, 0xd8, 0xf1, 0x06, 0x7e, 0x98, 0x48, 0x10, - 0x1c, 0x57, 0x24, 0x22, 0x16, 0x9c, 0xa1, 0x88, 0xa7, 0x60, 0x00, 0x48, 0xce, 0xbf, 0x4f, 0x9f, 0x17, 0x34, 0x7f, - 0x40, 0x54, 0xe1, 0xa8, 0x02, 0xc4, 0x01, 0x09, 0x06, 0x5d, 0x78, 0x27, 0x8b, 0x6c, 0x35, 0x3b, 0x5e, 0x9e, 0x93, - 0xce, 0x9d, 0x45, 0x44, 0x7a, 0x51, 0x12, 0x41, 0x9c, 0x61, 0xf1, 0x83, 0xa0, 0xc4, 0xe8, 0xf5, 0xba, 0x20, 0x8c, - 0x2e, 0x96, 0x64, 0xa3, 0xd1, 0x20, 0x20, 0xfd, 0x23, 0xc4, 0x4c, 0xb6, 0x4b, 0x39, 0x66, 0x5f, 0x7b, 0xc5, 0x39, - 0x6b, 0xcd, 0x10, 0x4a, 0x06, 0x76, 0x6f, 0x09, 0xa4, 0x3a, 0x87, 0x32, 0x9a, 0x4a, 0x53, 0x7e, 0x21, 0x47, 0x50, - 0xeb, 0xd0, 0x6b, 0x93, 0xa1, 0xdf, 0x06, 0x4f, 0x22, 0x20, 0x45, 0x0a, 0xcf, 0x4b, 0x60, 0xc1, 0x64, 0xe7, 0xb6, - 0x64, 0x16, 0x1f, 0x3f, 0xa4, 0x38, 0xc9, 0x9c, 0xcd, 0x40, 0xff, 0x42, 0x13, 0x5c, 0x2c, 0xd2, 0x11, 0x23, 0xab, - 0xe0, 0x72, 0x58, 0xf7, 0xbf, 0xed, 0x72, 0xe8, 0xa6, 0x20, 0xb7, 0x39, 0x1b, 0x33, 0xe5, 0x78, 0xdc, 0xcd, 0x59, - 0x5f, 0xfa, 0xcb, 0x24, 0x8d, 0x34, 0x15, 0x4a, 0x67, 0xd6, 0x77, 0xf7, 0xbb, 0x7a, 0xec, 0x96, 0x47, 0xf7, 0x16, - 0x10, 0xd0, 0xc6, 0x1d, 0x39, 0x65, 0x05, 0x96, 0x84, 0x63, 0x12, 0x0e, 0x1f, 0x00, 0x73, 0xad, 0x1f, 0x44, 0x25, - 0xfd, 0x5d, 0xb2, 0x4f, 0x07, 0x22, 0x3f, 0xd7, 0x65, 0x7d, 0x96, 0xfa, 0x93, 0x69, 0xf7, 0x71, 0xec, 0xe3, 0x19, - 0xa7, 0x39, 0x42, 0x52, 0x96, 0xe4, 0xd7, 0xcb, 0xcd, 0x71, 0xb6, 0x95, 0xfc, 0x4f, 0x28, 0xce, 0x1f, 0x94, 0xd1, - 0x3a, 0x5b, 0x36, 0x7d, 0xb6, 0x60, 0x38, 0x67, 0x92, 0x96, 0xe0, 0x94, 0x4f, 0xfc, 0x4b, 0xd5, 0xe1, 0xf1, 0x69, - 0x8f, 0x58, 0x0f, 0x22, 0x49, 0xf0, 0x5f, 0x73, 0xc2, 0xe3, 0x53, 0x33, 0xe1, 0x87, 0x67, 0x88, 0x4f, 0x6f, 0x8c, - 0x8e, 0xa9, 0xd4, 0x1c, 0xcb, 0x8a, 0x4b, 0x2f, 0x2a, 0x82, 0x53, 0x5d, 0xd8, 0xe0, 0xd9, 0x9d, 0x3e, 0xa5, 0x39, - 0xcd, 0x41, 0x78, 0x92, 0x66, 0x3b, 0xb7, 0x68, 0xb1, 0xa4, 0x05, 0x94, 0x92, 0xca, 0x49, 0xb4, 0x9a, 0xc6, 0x91, - 0xad, 0x23, 0xcc, 0x0b, 0x9c, 0xdd, 0x46, 0x62, 0x84, 0xb5, 0x33, 0x9e, 0xa8, 0x91, 0x9a, 0x92, 0x9b, 0x3a, 0x22, - 0x59, 0x8f, 0xc1, 0xfc, 0x9f, 0x1f, 0x7b, 0x5c, 0x63, 0x66, 0x67, 0xe1, 0x8a, 0x72, 0xfb, 0x6a, 0xaa, 0x76, 0xb2, - 0xa5, 0x2b, 0xaf, 0x5b, 0x3b, 0xa7, 0xd2, 0xe6, 0xc2, 0x15, 0x87, 0x6e, 0xb8, 0x7a, 0x6d, 0x17, 0x24, 0xd7, 0xcf, - 0x91, 0xdf, 0x0c, 0x83, 0x25, 0x89, 0xd4, 0xcd, 0x9d, 0x27, 0x65, 0x4b, 0xa9, 0xba, 0xaf, 0xc0, 0xe2, 0xb0, 0x34, - 0x54, 0xbb, 0x0a, 0xca, 0xf2, 0x46, 0x0d, 0x61, 0x11, 0xd6, 0xd4, 0x0b, 0x0e, 0xa7, 0x74, 0x9e, 0x05, 0x35, 0xb5, - 0x38, 0x3f, 0x69, 0xd4, 0x5e, 0x52, 0xe4, 0x54, 0x40, 0xbc, 0x89, 0x22, 0x17, 0x2f, 0x51, 0xaf, 0xf2, 0xb8, 0x82, - 0xfd, 0x91, 0x92, 0xaa, 0x9d, 0x5e, 0xa8, 0xc2, 0xe9, 0x99, 0x2a, 0x9f, 0x5e, 0x9e, 0xae, 0x70, 0x98, 0x4b, 0xb5, - 0x2b, 0x91, 0x45, 0x59, 0x52, 0x96, 0xe3, 0xca, 0x95, 0xf1, 0xdc, 0x9e, 0xbb, 0x8c, 0x4c, 0xd5, 0x29, 0x06, 0x93, - 0x32, 0xa5, 0xd5, 0x63, 0xdb, 0x11, 0x43, 0xc3, 0x04, 0x82, 0x5d, 0xd6, 0xca, 0x68, 0x7d, 0xbf, 0x78, 0x62, 0x51, - 0xa8, 0x2d, 0xad, 0x4f, 0x4f, 0x92, 0x90, 0xb5, 0xbe, 0xb4, 0x09, 0x94, 0xd8, 0x79, 0x3f, 0x56, 0xd1, 0x5e, 0x3c, - 0x77, 0xcf, 0xda, 0x83, 0x08, 0xb8, 0x5e, 0xeb, 0xcb, 0x0f, 0xc7, 0xf4, 0x90, 0xbd, 0x6c, 0x91, 0xa2, 0xfc, 0x81, - 0x04, 0xce, 0x07, 0x84, 0x30, 0x13, 0x58, 0x05, 0x0b, 0xe5, 0x95, 0x04, 0x56, 0x81, 0x8f, 0x18, 0xb5, 0x98, 0x9d, - 0x96, 0xde, 0xfb, 0xa4, 0x58, 0xe3, 0x26, 0xc4, 0x0b, 0x40, 0x5e, 0x4f, 0x21, 0xb2, 0x85, 0x28, 0xd0, 0x4c, 0x11, - 0x24, 0xfc, 0x80, 0x7d, 0x78, 0x81, 0xd6, 0x8f, 0xe9, 0xc8, 0x57, 0xb3, 0x72, 0x07, 0x6d, 0x3d, 0xb6, 0xa7, 0x2a, - 0x5d, 0x35, 0x29, 0x3e, 0x4a, 0xbc, 0x93, 0x58, 0x34, 0xf0, 0xca, 0x15, 0x3b, 0xbd, 0xf3, 0x81, 0xdf, 0xb0, 0x2d, - 0x73, 0xfc, 0xf2, 0x34, 0xc7, 0x15, 0xa8, 0x1a, 0x55, 0x68, 0xbb, 0x3d, 0x40, 0xa6, 0xa6, 0x57, 0x09, 0xe2, 0xb0, - 0x69, 0x1a, 0x2e, 0x40, 0x07, 0x0e, 0x51, 0x09, 0xa4, 0x4c, 0x35, 0x0b, 0x34, 0x72, 0x8d, 0x14, 0x36, 0x5b, 0xb3, - 0xa8, 0x4d, 0xd8, 0xe7, 0xdf, 0xd0, 0xbc, 0xb6, 0x2d, 0x9f, 0x88, 0x3b, 0x54, 0xf2, 0x19, 0xbc, 0xf4, 0xe1, 0x1e, - 0xdf, 0x03, 0x76, 0xe4, 0x4a, 0xc5, 0xc8, 0x94, 0xc4, 0xf6, 0x78, 0x41, 0xb5, 0xc9, 0x3c, 0x79, 0x54, 0xa7, 0x26, - 0x6c, 0x28, 0x57, 0x38, 0x61, 0xfb, 0x11, 0xbb, 0x80, 0x77, 0x28, 0x31, 0x37, 0xd5, 0x6f, 0x0e, 0xa1, 0xab, 0x3d, - 0xf0, 0xae, 0x8c, 0x7e, 0x79, 0xf9, 0x62, 0x8b, 0xb7, 0xb9, 0x83, 0xbf, 0xa6, 0xc1, 0xb6, 0x50, 0x1c, 0xea, 0xae, - 0x80, 0xf4, 0xb2, 0x97, 0x2b, 0x45, 0x49, 0x6f, 0xcd, 0xe0, 0xa9, 0xde, 0x20, 0x5d, 0x34, 0x05, 0xea, 0x60, 0xd2, - 0x83, 0x30, 0x21, 0xc8, 0x01, 0x95, 0xd1, 0xbb, 0x2b, 0xd9, 0xe2, 0x5e, 0xf0, 0x6c, 0x08, 0xc8, 0xd0, 0x8a, 0xe4, - 0xd3, 0x28, 0x8d, 0xba, 0x64, 0x68, 0x8f, 0x4d, 0x2c, 0x13, 0x80, 0x64, 0x57, 0xaf, 0x2c, 0x91, 0x09, 0x60, 0x0b, - 0xec, 0xd9, 0x3c, 0x86, 0xe1, 0xdb, 0xed, 0xc9, 0x80, 0xb1, 0x65, 0xf6, 0xbe, 0xa7, 0x9b, 0x8f, 0x26, 0xe4, 0x1a, - 0x6a, 0x0d, 0xc7, 0x39, 0x30, 0x64, 0xaa, 0x68, 0xf0, 0xc9, 0x86, 0x68, 0xc2, 0xda, 0x5c, 0x76, 0x5d, 0x08, 0x61, - 0xd0, 0x63, 0x53, 0x58, 0x41, 0x5c, 0x3b, 0xd6, 0xb0, 0xbe, 0x58, 0x46, 0xa0, 0x69, 0x4d, 0x1f, 0xc8, 0x98, 0xb6, - 0x97, 0x08, 0x75, 0x27, 0xca, 0x37, 0xcc, 0x69, 0x16, 0xc4, 0x7d, 0xaf, 0xcb, 0xe7, 0x1a, 0x36, 0x7e, 0xa2, 0x62, - 0xae, 0xa7, 0xba, 0x85, 0x01, 0xea, 0x40, 0x5c, 0x0c, 0xf8, 0x78, 0x1b, 0x42, 0x5f, 0xf9, 0x77, 0xd8, 0xf7, 0x4a, - 0x29, 0x8f, 0x3a, 0x3e, 0x2d, 0x35, 0x72, 0xd4, 0x5e, 0xf6, 0x7f, 0xb2, 0xfa, 0x90, 0x3f, 0x56, 0xa8, 0xd0, 0x84, - 0x34, 0x34, 0x89, 0xba, 0x79, 0x02, 0xb1, 0xed, 0x7b, 0xae, 0xd0, 0x8b, 0x45, 0xa4, 0x3c, 0x02, 0xba, 0x29, 0x8f, - 0x77, 0xab, 0x19, 0x46, 0x7c, 0xab, 0xd7, 0xda, 0x68, 0x4b, 0x34, 0x8b, 0x23, 0xde, 0x45, 0x3b, 0x3b, 0x9c, 0xca, - 0x48, 0xcf, 0x4e, 0xe1, 0x38, 0x27, 0xd1, 0xbb, 0x74, 0xd8, 0x69, 0xae, 0xbe, 0x7e, 0x67, 0x43, 0x1f, 0xe2, 0x6a, - 0x21, 0x6a, 0x7b, 0xce, 0x68, 0x6e, 0x26, 0x2e, 0x10, 0x0b, 0xa0, 0xd9, 0xbb, 0x57, 0xa9, 0xa6, 0xc9, 0x98, 0x71, - 0x59, 0xcc, 0x12, 0x29, 0xc2, 0x0e, 0xe8, 0x25, 0x9a, 0x30, 0x51, 0x75, 0x9c, 0x1b, 0xb1, 0xe7, 0xa3, 0xba, 0x29, - 0x77, 0x25, 0x19, 0x94, 0x45, 0xeb, 0xb6, 0xeb, 0xe5, 0x25, 0xf4, 0x7e, 0x1e, 0x70, 0x5d, 0x1b, 0x2b, 0x38, 0x61, - 0x0b, 0x13, 0x9f, 0x25, 0x41, 0x6e, 0x8d, 0x24, 0x5b, 0x84, 0xa5, 0x7a, 0x67, 0xfe, 0x69, 0xe9, 0xd5, 0x76, 0xa4, - 0x5e, 0x38, 0xcc, 0xdc, 0x9e, 0x85, 0xe5, 0x57, 0xc0, 0xe3, 0xbc, 0xf7, 0xbc, 0x11, 0x9a, 0xf2, 0xc7, 0xab, 0x3d, - 0xa8, 0x88, 0x66, 0x63, 0x47, 0x3d, 0x91, 0x6b, 0xba, 0xa9, 0x82, 0x6b, 0x32, 0xd1, 0xea, 0x41, 0x9c, 0x59, 0xd1, - 0x76, 0x62, 0x19, 0xfc, 0x33, 0xd8, 0xe0, 0x1b, 0xd8, 0x17, 0x4b, 0x00, 0xeb, 0x37, 0xc6, 0x57, 0x21, 0x0f, 0xcb, - 0xf7, 0x74, 0x7e, 0x86, 0xb0, 0xaf, 0x30, 0x57, 0x24, 0x2c, 0x4f, 0x95, 0x5a, 0xc9, 0x41, 0xc5, 0xb4, 0x7c, 0x6e, - 0xc1, 0x27, 0xd5, 0x56, 0x29, 0x5e, 0xff, 0x55, 0x5c, 0xab, 0xd0, 0xf9, 0x79, 0xa2, 0x10, 0xe2, 0xfe, 0x23, 0x12, - 0x55, 0x94, 0x9f, 0x86, 0xdb, 0x66, 0xdf, 0xc3, 0x8f, 0x1b, 0x7e, 0xd0, 0x65, 0x81, 0xca, 0xaa, 0x71, 0x83, 0x71, - 0xb8, 0x3c, 0xcd, 0xaa, 0x11, 0x0b, 0x45, 0xf8, 0xc6, 0xa5, 0x03, 0x47, 0x6f, 0x63, 0xab, 0xe6, 0x52, 0x85, 0x2a, - 0x20, 0xf6, 0x14, 0x7a, 0xde, 0x44, 0x35, 0x52, 0x2a, 0x12, 0x08, 0x93, 0x06, 0xed, 0x12, 0x17, 0xec, 0x16, 0xab, - 0x76, 0xb5, 0xbb, 0x15, 0xf3, 0x9a, 0x4c, 0x04, 0x8c, 0xf1, 0x0e, 0xb4, 0x6e, 0x66, 0x4b, 0x06, 0x74, 0x4e, 0xec, - 0xa8, 0xc0, 0x79, 0x8c, 0x71, 0x70, 0xb8, 0xc7, 0xcd, 0xf4, 0xa4, 0x92, 0x1d, 0x66, 0xe4, 0xa1, 0x39, 0x74, 0x86, - 0x2b, 0x0f, 0xe5, 0x21, 0x2b, 0x71, 0xb6, 0xc0, 0xcb, 0x35, 0x72, 0x95, 0xe8, 0xaa, 0x25, 0x68, 0x78, 0x20, 0xb9, - 0xdb, 0x37, 0xdf, 0xbd, 0xd3, 0xbb, 0x01, 0xa7, 0xd2, 0xdf, 0x0c, 0xd8, 0x1d, 0x2c, 0x78, 0xb7, 0x3a, 0x1d, 0x4b, - 0x0c, 0x00, 0x64, 0xd7, 0xf4, 0x83, 0xb0, 0x85, 0xee, 0x74, 0x87, 0x6b, 0xc7, 0x55, 0x04, 0x6d, 0x88, 0xaa, 0x8c, - 0xa1, 0x23, 0xbb, 0x88, 0x04, 0xb2, 0xeb, 0x88, 0x15, 0xdd, 0x32, 0x16, 0xc2, 0x09, 0x3c, 0xee, 0x01, 0xf5, 0x83, - 0x23, 0xa4, 0x54, 0x44, 0x42, 0xc9, 0x85, 0xf8, 0xdb, 0x34, 0xd4, 0xac, 0xe0, 0x6e, 0xb3, 0x21, 0x76, 0x93, 0x88, - 0xfe, 0xa0, 0x2a, 0xbc, 0x39, 0x8f, 0xf2, 0xad, 0x03, 0x0a, 0x1f, 0xcd, 0xc8, 0xc0, 0x59, 0xda, 0xb7, 0xa7, 0x5d, - 0x7b, 0x37, 0xe6, 0xa5, 0xb4, 0x94, 0x0a, 0xc1, 0xcd, 0x1d, 0x3c, 0xeb, 0x1f, 0x5c, 0x49, 0x13, 0x9b, 0x9a, 0x7d, - 0x99, 0x73, 0xb4, 0x33, 0xe5, 0x79, 0x14, 0x5f, 0x6b, 0xd9, 0xf3, 0xb6, 0x79, 0x36, 0x76, 0x67, 0xb7, 0x8b, 0xfd, - 0x0c, 0x49, 0x61, 0x8b, 0x19, 0xcc, 0x35, 0x89, 0x62, 0x12, 0x18, 0x6d, 0x80, 0xbd, 0x89, 0x66, 0xd8, 0x45, 0x0b, - 0x94, 0xbd, 0x5b, 0x77, 0x6b, 0xc3, 0xf1, 0xdb, 0xcc, 0xd7, 0xaa, 0xf6, 0xc2, 0x9d, 0x12, 0x05, 0xe7, 0xc3, 0xde, - 0x39, 0xaf, 0xff, 0xa3, 0xc4, 0x9b, 0x19, 0xc6, 0x92, 0x48, 0xb4, 0x36, 0x10, 0x3c, 0x4a, 0xeb, 0xb5, 0x59, 0x96, - 0x20, 0x3b, 0xb5, 0xbc, 0xfd, 0x07, 0x1d, 0x20, 0x15, 0xe3, 0xdd, 0xe2, 0xe6, 0x0c, 0x0b, 0x8e, 0x49, 0xa9, 0x2d, - 0x37, 0xbf, 0xfe, 0x49, 0x32, 0xa5, 0xa2, 0x4d, 0xae, 0x27, 0x9a, 0xe7, 0xe2, 0xca, 0x01, 0x80, 0x40, 0x69, 0x36, - 0xac, 0x8b, 0xeb, 0xcd, 0x64, 0x73, 0xab, 0xd0, 0x11, 0x66, 0xaa, 0xc0, 0xf8, 0x9b, 0x55, 0x4a, 0x4f, 0xa9, 0x56, - 0x49, 0xc2, 0xdc, 0x4e, 0x5f, 0xab, 0x44, 0x68, 0x3f, 0x06, 0xe2, 0xdb, 0xc9, 0x77, 0xf5, 0xa7, 0x6c, 0x8b, 0x3c, - 0x8e, 0x03, 0x93, 0xb3, 0xb7, 0x76, 0x50, 0xd0, 0xa8, 0xed, 0x5c, 0x8e, 0xd7, 0x3c, 0x2b, 0xa8, 0x7d, 0xe5, 0x57, - 0xb3, 0xb5, 0xc7, 0x17, 0xee, 0x08, 0xb2, 0x02, 0xa9, 0xc7, 0xe4, 0xc1, 0x34, 0x46, 0xa5, 0xd9, 0x39, 0x2f, 0x76, - 0x58, 0x1e, 0x93, 0x64, 0xd7, 0xf8, 0xcf, 0xc8, 0xa5, 0x14, 0x48, 0xfe, 0xc4, 0xb9, 0x13, 0x2a, 0x16, 0xb3, 0x64, - 0x61, 0x6a, 0xd7, 0x24, 0x2f, 0xdf, 0xc5, 0x75, 0x3c, 0x2d, 0xc7, 0x7f, 0x56, 0x4c, 0xf4, 0x24, 0x10, 0x52, 0xeb, - 0x1d, 0x0d, 0x1e, 0x40, 0xdd, 0x3a, 0x83, 0x6f, 0x64, 0xf3, 0x50, 0x24, 0x83, 0x8c, 0xd9, 0x56, 0xdd, 0xa5, 0x1a, - 0x89, 0x7a, 0xb0, 0x0c, 0xb4, 0xdb, 0x49, 0xe0, 0x12, 0xb5, 0xf6, 0x10, 0x1c, 0x54, 0xf4, 0x3e, 0x54, 0xc1, 0x52, - 0x33, 0x58, 0xaa, 0xac, 0xd4, 0x06, 0x6b, 0x2f, 0xd5, 0xda, 0x32, 0xa3, 0x2b, 0x2f, 0x0f, 0x8e, 0x39, 0x0e, 0x00, - 0x5b, 0xcf, 0xa5, 0x0e, 0x03, 0xe8, 0x44, 0x36, 0x70, 0x03, 0x32, 0x00, 0x65, 0x2d, 0xa1, 0x72, 0xd3, 0x82, 0x73, - 0xad, 0x4d, 0x29, 0x96, 0x80, 0x44, 0x70, 0xc6, 0xfe, 0xe8, 0x51, 0xe9, 0xed, 0xc8, 0x11, 0xae, 0x5a, 0x37, 0x6d, - 0x05, 0x6b, 0xeb, 0x0c, 0x69, 0xe3, 0x31, 0xde, 0x65, 0x3f, 0x01, 0xdf, 0xc5, 0x8b, 0xd6, 0x91, 0x19, 0x6f, 0x71, - 0xa4, 0x20, 0x14, 0xba, 0xde, 0x31, 0x16, 0xa6, 0x04, 0x86, 0xd9, 0xdd, 0x15, 0x61, 0x7a, 0x7b, 0x29, 0x20, 0x58, - 0xb8, 0xb1, 0x16, 0x37, 0x0e, 0xcf, 0x6f, 0x1c, 0x26, 0x8a, 0x70, 0x68, 0xa6, 0x4a, 0xf8, 0x5c, 0xaa, 0x0c, 0x05, - 0x39, 0x35, 0x38, 0x0a, 0xdc, 0xdf, 0xbe, 0x77, 0xb4, 0x28, 0x12, 0x82, 0x2c, 0x2e, 0x43, 0x13, 0xe5, 0x75, 0xc6, - 0x05, 0xe9, 0xcb, 0xe1, 0xfe, 0x62, 0x6e, 0x87, 0xa9, 0x59, 0x99, 0xb7, 0x48, 0x7c, 0x6f, 0x5a, 0x8c, 0x11, 0xe1, - 0x7c, 0xaf, 0x5d, 0x60, 0x8b, 0xb5, 0xec, 0x6f, 0x3f, 0xee, 0x09, 0x57, 0x16, 0x0e, 0x0c, 0x5d, 0x64, 0xda, 0xab, - 0x75, 0xb7, 0x52, 0xc4, 0xf9, 0x47, 0xf4, 0xc8, 0xfc, 0xc1, 0x38, 0x8e, 0x1d, 0xdc, 0xee, 0x84, 0xda, 0xe7, 0xfc, - 0x86, 0x85, 0x3a, 0xa2, 0xd5, 0x0d, 0xd4, 0xb0, 0x06, 0x97, 0xca, 0x2c, 0x2d, 0xe6, 0x9f, 0xdd, 0xdc, 0x3c, 0x25, - 0xe0, 0x24, 0xf1, 0x05, 0x24, 0xd9, 0xe1, 0x7a, 0xf7, 0xe9, 0x2d, 0x93, 0xbe, 0x0d, 0x92, 0x12, 0xbb, 0x95, 0xca, - 0x76, 0x49, 0xd3, 0x94, 0x1d, 0xee, 0x8a, 0xaa, 0x35, 0xd8, 0x13, 0x13, 0xa5, 0xa3, 0xbe, 0x10, 0x26, 0x4d, 0xec, - 0x4b, 0x18, 0xef, 0x8b, 0x09, 0x9c, 0x37, 0x0c, 0xf1, 0xaa, 0x03, 0xa5, 0x50, 0x22, 0x65, 0x2f, 0xbb, 0xe3, 0x4d, - 0x69, 0x26, 0x1f, 0x51, 0xc5, 0x81, 0x96, 0xde, 0x5a, 0xee, 0x4a, 0x00, 0xd0, 0xbd, 0xba, 0xbc, 0xfc, 0xfd, 0xc1, - 0x7d, 0x8c, 0x95, 0xc8, 0x37, 0xef, 0xf7, 0xc3, 0xd3, 0xfd, 0x17, 0x12, 0xc1, 0x81, 0xe6, 0x71, 0x7a, 0xf9, 0x5d, - 0xa5, 0x8b, 0x5b, 0xd5, 0xf7, 0xab, 0xa0, 0x8c, 0xd4, 0xe3, 0xee, 0x2c, 0x6c, 0x09, 0x26, 0xac, 0x0d, 0x38, 0x67, - 0x3e, 0x08, 0x65, 0x2e, 0xff, 0xfa, 0x2c, 0xce, 0xdd, 0x78, 0x58, 0x78, 0x26, 0xb0, 0xb1, 0x31, 0xd4, 0x61, 0xae, - 0x3b, 0xf3, 0xe9, 0xe0, 0x19, 0xb9, 0xee, 0x1a, 0x32, 0x2c, 0x8d, 0x03, 0xbe, 0xde, 0xfa, 0xf1, 0xfe, 0x3f, 0x8f, - 0x5f, 0x06, 0xe6, 0x81, 0x99, 0xf1, 0x1c, 0x95, 0xf6, 0xb0, 0xa4, 0xc1, 0x61, 0x64, 0x3b, 0xea, 0xda, 0xbf, 0x47, - 0x23, 0x82, 0x8c, 0x10, 0x21, 0xc7, 0xa1, 0x1d, 0x43, 0x39, 0x3d, 0x8e, 0x55, 0x95, 0xf6, 0xa2, 0x37, 0x18, 0x37, - 0xb2, 0x85, 0x22, 0x60, 0x4a, 0xf4, 0xfd, 0xea, 0xac, 0x2a, 0xee, 0x4d, 0xff, 0xf2, 0xe8, 0x8b, 0xec, 0xaa, 0x51, - 0x03, 0xe1, 0x77, 0x24, 0xaa, 0xa2, 0x37, 0x96, 0xef, 0xb4, 0x05, 0x5b, 0x43, 0x0e, 0x8c, 0x1a, 0x49, 0x9b, 0x11, - 0x3b, 0x6f, 0x32, 0xe7, 0x92, 0x2f, 0xd4, 0x58, 0x7a, 0x94, 0x93, 0x65, 0x0a, 0x00, 0xd3, 0x95, 0x16, 0x11, 0x17, - 0x18, 0x82, 0x2b, 0x0e, 0xab, 0x5b, 0xc8, 0x8c, 0xf5, 0x6c, 0x77, 0x16, 0x8d, 0x26, 0x08, 0xd3, 0xfa, 0x90, 0xa8, - 0x30, 0x73, 0xca, 0xa4, 0x0c, 0x97, 0xda, 0x09, 0xc8, 0x93, 0xdf, 0xd2, 0x8a, 0x01, 0x98, 0x31, 0x91, 0x5c, 0x6e, - 0x6c, 0x22, 0xeb, 0x90, 0xcf, 0x49, 0xbf, 0x99, 0xf3, 0xe1, 0x9b, 0x18, 0x1f, 0x5c, 0x9c, 0x06, 0xeb, 0x0f, 0x50, - 0xf2, 0xdc, 0x0d, 0x97, 0xab, 0x4d, 0xda, 0x72, 0x5b, 0xd1, 0x16, 0x8c, 0x89, 0x76, 0x79, 0x61, 0x9b, 0xa8, 0x40, - 0x9f, 0x49, 0x6f, 0xb8, 0x06, 0xa2, 0x1c, 0x06, 0xf1, 0x52, 0x0e, 0xc5, 0xcd, 0xda, 0x23, 0x54, 0x69, 0x2c, 0x50, - 0x03, 0x2b, 0x7c, 0xc2, 0x30, 0xaa, 0x26, 0xd8, 0x7d, 0xff, 0xd8, 0xe0, 0xcb, 0xd5, 0xb7, 0x83, 0x35, 0x6f, 0x5a, - 0x26, 0xda, 0x21, 0x3a, 0x9c, 0x83, 0x8a, 0x87, 0xd8, 0x69, 0x92, 0xd3, 0x60, 0xea, 0x7a, 0x72, 0xb9, 0x21, 0x63, - 0x33, 0x19, 0x69, 0x7a, 0xc0, 0x1d, 0xe6, 0xb6, 0x1f, 0x1a, 0xcc, 0x21, 0x8e, 0x8d, 0xa3, 0xba, 0x71, 0x9d, 0x31, - 0x84, 0x40, 0x27, 0x48, 0xa7, 0x3b, 0xa3, 0xcb, 0x8b, 0xf2, 0xd6, 0xda, 0x34, 0x74, 0x64, 0xdf, 0x9a, 0xee, 0x38, - 0xc2, 0x88, 0x88, 0xc7, 0x4c, 0x17, 0x2c, 0x2c, 0xb5, 0xb3, 0xb8, 0x29, 0x62, 0x39, 0xb6, 0x23, 0xac, 0x06, 0x60, - 0x16, 0xd8, 0xef, 0xcc, 0x4b, 0xef, 0x35, 0x7a, 0x21, 0x7c, 0xb0, 0x91, 0xf3, 0xb2, 0x98, 0x91, 0xb9, 0xef, 0xd0, - 0x14, 0x1e, 0xb8, 0x3f, 0x55, 0xa7, 0x15, 0x1c, 0xc4, 0xda, 0x71, 0xf4, 0xf7, 0x03, 0x6a, 0x89, 0x17, 0x04, 0x21, - 0x9c, 0x8a, 0xcd, 0x96, 0x0e, 0x88, 0x7d, 0x88, 0x65, 0x6a, 0x00, 0x42, 0x50, 0x0e, 0x56, 0xbb, 0x4f, 0x3b, 0x7d, - 0x8f, 0xd0, 0xf7, 0x11, 0xf3, 0x4d, 0x80, 0xcc, 0x14, 0x94, 0x27, 0x6a, 0x9f, 0x92, 0x88, 0x9e, 0xfc, 0xa4, 0x9b, - 0x6c, 0xd6, 0xa6, 0x4e, 0x02, 0xa5, 0x23, 0x4e, 0xde, 0x62, 0x14, 0xce, 0x8b, 0x13, 0x06, 0x74, 0xbd, 0x14, 0x83, - 0x69, 0xe3, 0x8b, 0xe2, 0x95, 0x2d, 0xa7, 0x86, 0xfd, 0x38, 0xb7, 0x35, 0x27, 0x1c, 0x8e, 0x32, 0x51, 0xf6, 0x4e, - 0x95, 0x1e, 0x0a, 0xac, 0x9b, 0x06, 0xea, 0xfd, 0x84, 0x5d, 0x70, 0xb7, 0x3d, 0x3e, 0xa6, 0x72, 0x04, 0x15, 0x42, - 0x21, 0x41, 0x2d, 0x53, 0xfa, 0x23, 0xe6, 0x39, 0x35, 0x62, 0xaf, 0x3c, 0x2a, 0x65, 0x22, 0x88, 0xc7, 0x3e, 0x7b, - 0xb0, 0xc7, 0x16, 0x08, 0x87, 0x1d, 0x4e, 0x74, 0xa5, 0x80, 0x7e, 0x90, 0x36, 0x82, 0x9d, 0x8f, 0x85, 0x22, 0x59, - 0x80, 0x62, 0x68, 0x37, 0xe2, 0xa4, 0xca, 0xee, 0x92, 0xd0, 0xef, 0xc5, 0x02, 0x67, 0x76, 0x2e, 0x81, 0xe4, 0x3a, - 0x5b, 0x18, 0x64, 0x54, 0x08, 0xed, 0x16, 0x12, 0x10, 0xa6, 0x74, 0x91, 0x0f, 0xf8, 0x91, 0x5e, 0x2a, 0x97, 0x0a, - 0xc9, 0xd3, 0xa5, 0xcf, 0xe1, 0x97, 0x1d, 0xb5, 0xe2, 0xc6, 0x5b, 0x1b, 0xe5, 0x1a, 0xe5, 0x62, 0xd6, 0xfc, 0x47, - 0xec, 0x71, 0x89, 0x74, 0x6c, 0x81, 0xb5, 0xa1, 0x1b, 0x54, 0x96, 0xd2, 0xc0, 0x89, 0x07, 0x12, 0xa9, 0xdb, 0x0e, - 0x47, 0xda, 0xa2, 0xf6, 0x93, 0xbd, 0x57, 0xd7, 0xa0, 0xf4, 0xcc, 0x7a, 0x2b, 0x71, 0x68, 0x2a, 0x64, 0x91, 0x55, - 0xd5, 0x80, 0x95, 0x7c, 0x1c, 0xd2, 0x64, 0x88, 0xee, 0x92, 0xc4, 0x93, 0xcc, 0xe9, 0x37, 0x99, 0xe9, 0x45, 0xff, - 0xa3, 0x12, 0x95, 0x0f, 0x65, 0xff, 0x93, 0x1c, 0xcf, 0x3a, 0xa9, 0x1f, 0x85, 0xd3, 0x90, 0xc6, 0x26, 0x13, 0x30, - 0x80, 0xd5, 0x86, 0x39, 0x94, 0x19, 0x2d, 0x5b, 0xc5, 0xb9, 0xdb, 0x46, 0x4a, 0x6c, 0xe8, 0x27, 0x3b, 0x06, 0xec, - 0x8f, 0xbf, 0x02, 0x71, 0xc0, 0x23, 0x66, 0x1c, 0xec, 0xad, 0x98, 0xb4, 0xa9, 0x28, 0xf8, 0x5d, 0x69, 0x34, 0x81, - 0x6b, 0x3a, 0xa4, 0x69, 0x73, 0xe5, 0x18, 0x32, 0xbd, 0x6c, 0xcc, 0x84, 0x98, 0x39, 0x78, 0x46, 0x28, 0xf6, 0xdf, - 0xfd, 0x77, 0x09, 0x8e, 0x16, 0x8d, 0xf2, 0xe4, 0xb4, 0x0e, 0xe6, 0x56, 0x5d, 0x7a, 0xe7, 0x7e, 0x08, 0x69, 0x03, - 0x80, 0xca, 0x9d, 0xed, 0x59, 0x88, 0xbb, 0xdb, 0x2a, 0x44, 0x1f, 0xcc, 0x52, 0x93, 0xf2, 0xae, 0x97, 0x6c, 0x2c, - 0x61, 0x9e, 0x32, 0x2b, 0x87, 0xd6, 0x81, 0x9d, 0xfd, 0x63, 0xfa, 0x1f, 0xc9, 0xf7, 0x9b, 0xfc, 0x7c, 0xb7, 0x46, - 0x14, 0x98, 0x91, 0x57, 0xf4, 0x3e, 0x07, 0xa0, 0xde, 0x40, 0x24, 0x97, 0xe5, 0x3d, 0x5c, 0xd4, 0x3d, 0xfc, 0x65, - 0x2e, 0x1a, 0x1f, 0x78, 0xcc, 0x57, 0x94, 0xdb, 0x0f, 0x1b, 0x1e, 0x08, 0x44, 0xee, 0x02, 0x23, 0x4c, 0xff, 0x3e, - 0x39, 0xe6, 0xe3, 0xa9, 0xf0, 0xca, 0xab, 0x17, 0xb0, 0xea, 0x89, 0x0f, 0xaf, 0xcf, 0xb0, 0xb5, 0xff, 0x44, 0x66, - 0x15, 0x97, 0x60, 0x66, 0xb0, 0xa8, 0xb8, 0x5f, 0x73, 0x65, 0x07, 0x17, 0xad, 0xee, 0x3b, 0x19, 0xff, 0x7c, 0x19, - 0xee, 0xbe, 0x7e, 0xee, 0x14, 0x8d, 0x73, 0x78, 0x8f, 0x71, 0xc4, 0x35, 0x2e, 0xe1, 0xed, 0xc7, 0x67, 0x55, 0x37, - 0xf7, 0x8c, 0x7d, 0xd6, 0x74, 0x63, 0x55, 0x33, 0xb4, 0x21, 0x71, 0xfe, 0xc3, 0xd6, 0x5f, 0x2c, 0xbc, 0xd8, 0xfd, - 0xc4, 0x4e, 0x8a, 0xac, 0x0b, 0x5a, 0xb7, 0x5d, 0xab, 0xf2, 0x83, 0x01, 0x97, 0x3a, 0x1e, 0x4b, 0xb6, 0x3a, 0xbb, - 0x5f, 0x8c, 0x3f, 0x9a, 0x09, 0xb4, 0x3f, 0xfa, 0xe0, 0x66, 0x09, 0x55, 0x7b, 0x9c, 0xd1, 0xdd, 0xb7, 0x3f, 0x7b, - 0x39, 0x76, 0x59, 0x9a, 0xf8, 0xdc, 0x27, 0xc7, 0xc8, 0x13, 0xe9, 0x2d, 0xb4, 0x0a, 0xc3, 0xf4, 0xdc, 0x3d, 0x44, - 0x6a, 0x91, 0x2c, 0x3d, 0x7b, 0x0b, 0x97, 0x9c, 0xd0, 0x99, 0x7e, 0x29, 0x09, 0x75, 0xdb, 0x6b, 0xc5, 0x25, 0x62, - 0x7e, 0x8d, 0xd4, 0xc0, 0x55, 0x12, 0x3c, 0x44, 0x44, 0xa0, 0xb3, 0x17, 0xe5, 0x33, 0x45, 0x75, 0x85, 0x57, 0x7f, - 0x8d, 0xb2, 0x80, 0x57, 0x66, 0xe3, 0x61, 0xe5, 0x4c, 0x1f, 0x9d, 0xd6, 0x59, 0xae, 0xcb, 0x00, 0x72, 0x71, 0x01, - 0x4e, 0xec, 0xdf, 0x72, 0x06, 0xc3, 0xda, 0x86, 0xfb, 0x23, 0x35, 0x1a, 0xa3, 0xe4, 0x1b, 0x02, 0x30, 0x0a, 0x8a, - 0x36, 0xb3, 0xef, 0x36, 0xa4, 0x0b, 0x19, 0xd5, 0xfb, 0xfd, 0xf7, 0xfc, 0xe5, 0xd1, 0x77, 0xbe, 0x5d, 0x7a, 0xad, - 0x85, 0x49, 0x65, 0x91, 0xad, 0xa3, 0x83, 0xec, 0xae, 0x87, 0x6d, 0x90, 0xdf, 0x74, 0x9f, 0x49, 0x37, 0x2f, 0x06, - 0xd8, 0xd2, 0xf6, 0x23, 0x32, 0x8d, 0x24, 0x51, 0xc8, 0xb1, 0x96, 0x22, 0xa8, 0x65, 0x20, 0x15, 0x47, 0x0e, 0x0f, - 0x4f, 0x46, 0xbe, 0x99, 0x33, 0x0e, 0x2d, 0x69, 0x0b, 0xd8, 0x18, 0xd6, 0xdd, 0xd7, 0x52, 0x9b, 0x65, 0xd6, 0xab, - 0x47, 0x76, 0x22, 0xbc, 0xe0, 0x08, 0x4a, 0xec, 0x53, 0x48, 0x0b, 0xab, 0xb1, 0x0c, 0x6e, 0x5e, 0x4f, 0x28, 0xa0, - 0x6d, 0x2e, 0x9d, 0x53, 0xab, 0xc8, 0x57, 0xfc, 0x7c, 0x58, 0x83, 0x21, 0xf9, 0xd6, 0x4a, 0xc1, 0xc6, 0xae, 0x55, - 0xa5, 0xf1, 0x1c, 0x6f, 0x68, 0x52, 0x1c, 0x1d, 0xed, 0x51, 0x76, 0x08, 0x47, 0x63, 0x70, 0x73, 0x6f, 0xa8, 0xa4, - 0x4c, 0x63, 0xdf, 0x4b, 0xd2, 0xbf, 0xea, 0xcb, 0x50, 0x25, 0x24, 0x8a, 0xf9, 0x1f, 0x54, 0x63, 0x0e, 0x3c, 0x52, - 0x1f, 0xbd, 0xc8, 0x04, 0xa3, 0x85, 0x42, 0x74, 0x83, 0x87, 0x9d, 0x3a, 0x11, 0xcf, 0x5e, 0xa2, 0x70, 0xd2, 0xbd, - 0x24, 0x9a, 0x17, 0xfe, 0xd9, 0x6f, 0x9e, 0x7b, 0x01, 0xd0, 0x29, 0x2c, 0x9d, 0x31, 0x70, 0xca, 0x9a, 0x74, 0xa4, - 0xe0, 0xd6, 0x68, 0xa0, 0x09, 0x6c, 0xc1, 0xd3, 0xa9, 0x0c, 0xb9, 0x28, 0x67, 0x96, 0xf4, 0x64, 0x17, 0x53, 0x6a, - 0xcd, 0xf7, 0x85, 0xb2, 0xb0, 0x7e, 0xb7, 0x79, 0x94, 0x3b, 0x47, 0x66, 0x25, 0x82, 0x45, 0x9e, 0x02, 0xaf, 0x5c, - 0xde, 0x78, 0xd1, 0xe8, 0x39, 0x78, 0x99, 0x9a, 0x79, 0x0e, 0x07, 0x79, 0xe9, 0x2f, 0xbc, 0x78, 0xfb, 0x7e, 0x0f, - 0xfa, 0x1a, 0xb9, 0x0a, 0x8b, 0xa8, 0x07, 0xe4, 0xbc, 0xe3, 0xa8, 0xbb, 0xfb, 0xe0, 0x93, 0x8e, 0x97, 0x5c, 0x35, - 0x3e, 0x84, 0xbf, 0xa4, 0xd1, 0x17, 0x92, 0xa0, 0x39, 0x15, 0x52, 0x60, 0xe0, 0xaf, 0x5b, 0xd8, 0xf8, 0x3e, 0x4b, - 0xb7, 0x23, 0x26, 0x7f, 0xf5, 0xbe, 0xd2, 0x93, 0x5d, 0x8f, 0x49, 0x3d, 0x05, 0x8a, 0x3a, 0x3b, 0x5a, 0x36, 0x23, - 0xad, 0xd4, 0xbc, 0x5b, 0xb8, 0xf5, 0x81, 0x4f, 0xe9, 0xc0, 0x8e, 0x02, 0x77, 0x41, 0x2c, 0x9e, 0x71, 0x7e, 0x6d, - 0x66, 0xb7, 0x3e, 0xfb, 0x2e, 0x03, 0x8c, 0x5a, 0x4f, 0xf4, 0x41, 0x10, 0xdf, 0x67, 0x47, 0xac, 0xbb, 0x04, 0x96, - 0x60, 0x4c, 0x4f, 0xdb, 0x24, 0x9c, 0x96, 0xfb, 0x64, 0x7e, 0xc8, 0xc6, 0x04, 0x8a, 0x4a, 0x31, 0x57, 0x81, 0x4f, - 0x26, 0x40, 0xcc, 0x21, 0x25, 0xdb, 0xab, 0x33, 0xf9, 0x44, 0xcc, 0x85, 0x2a, 0x45, 0x73, 0x31, 0x02, 0x42, 0x90, - 0xc3, 0x8c, 0xed, 0x3f, 0xc2, 0x85, 0x08, 0x70, 0x87, 0x83, 0x2c, 0x73, 0xde, 0xe0, 0xaa, 0xcc, 0x2f, 0x00, 0x73, - 0x19, 0xea, 0xad, 0xc6, 0x4e, 0x8f, 0x61, 0xf9, 0x7d, 0x1a, 0x64, 0xbd, 0x22, 0x77, 0x61, 0x19, 0xc2, 0xeb, 0xa2, - 0x54, 0x8d, 0x40, 0xba, 0x3b, 0x8c, 0xd3, 0xaf, 0x20, 0x61, 0xfa, 0x59, 0x02, 0x9e, 0xa3, 0x38, 0x11, 0x0b, 0xfe, - 0xdc, 0xd0, 0xa5, 0x13, 0xe4, 0x80, 0xa1, 0x1e, 0x9e, 0x5e, 0x51, 0xf7, 0x92, 0x1d, 0xdd, 0x6d, 0x59, 0xa5, 0xec, - 0x6f, 0x27, 0xf2, 0x63, 0xd9, 0x39, 0x5e, 0xf2, 0xa6, 0xbb, 0x89, 0xdf, 0x22, 0x8e, 0x02, 0x88, 0x63, 0x55, 0x76, - 0xa1, 0x4a, 0x44, 0xbe, 0x2e, 0x9c, 0x39, 0xe5, 0x79, 0x64, 0xc9, 0xce, 0xdb, 0xdd, 0x77, 0xa6, 0xd8, 0x91, 0x66, - 0x76, 0xce, 0x7b, 0xc5, 0x4f, 0x95, 0x12, 0xd3, 0x37, 0x0e, 0xce, 0xfd, 0x9d, 0xf4, 0xfd, 0xf1, 0x70, 0x2c, 0xb1, - 0x9e, 0x5f, 0x73, 0xd5, 0xf6, 0x94, 0xaa, 0x65, 0xad, 0xbf, 0x53, 0xbe, 0xa6, 0x6c, 0xdd, 0xec, 0x67, 0xb0, 0x23, - 0xd7, 0xcc, 0x97, 0x2e, 0xa4, 0x77, 0x7d, 0x39, 0xc9, 0xae, 0x0a, 0xec, 0xd1, 0x07, 0x06, 0xd0, 0xb4, 0xae, 0x0c, - 0xc5, 0x57, 0x6a, 0x19, 0xb9, 0x4c, 0x80, 0xd7, 0xc1, 0x4f, 0x5f, 0xcc, 0x7c, 0x39, 0x66, 0xab, 0x77, 0xde, 0x1f, - 0x31, 0x2f, 0xba, 0xb3, 0xe7, 0x7a, 0x87, 0xb8, 0x18, 0xe7, 0x7d, 0x07, 0x66, 0xe9, 0xb7, 0x1e, 0xf3, 0x79, 0x7f, - 0x9d, 0x60, 0x7f, 0x64, 0x45, 0x30, 0xc8, 0xe0, 0xae, 0x7a, 0xc1, 0x71, 0x16, 0x86, 0x68, 0xda, 0x76, 0x5f, 0xd4, - 0xcc, 0x6d, 0x49, 0xd3, 0xe7, 0xbc, 0xa5, 0x12, 0xf6, 0x8b, 0x3b, 0xce, 0xac, 0xef, 0xbc, 0x83, 0xac, 0xb5, 0xea, - 0xd0, 0xaf, 0x48, 0xbd, 0x0c, 0xeb, 0x3f, 0x81, 0x62, 0xbc, 0xec, 0xb0, 0xda, 0x5a, 0x69, 0x7a, 0xae, 0xca, 0xde, - 0xe1, 0x49, 0x05, 0xa0, 0x14, 0x01, 0x9d, 0x75, 0xe3, 0xb8, 0x9b, 0x02, 0xf5, 0xc5, 0x29, 0xda, 0xf5, 0xf7, 0xd7, - 0xc0, 0x28, 0x88, 0xd4, 0xf7, 0xab, 0xbc, 0x27, 0xfd, 0x95, 0xf8, 0x58, 0x78, 0x45, 0xa1, 0xdb, 0xf2, 0xf8, 0x2f, - 0x8a, 0x94, 0xe9, 0x27, 0x21, 0xdc, 0xf9, 0xb9, 0xba, 0x85, 0x89, 0xf9, 0x74, 0xe9, 0xf9, 0x3d, 0x5a, 0x87, 0x2b, - 0x68, 0x7d, 0xe6, 0x07, 0x69, 0xcc, 0xff, 0x39, 0x56, 0x59, 0xe2, 0x1d, 0x9a, 0xe5, 0xdb, 0x04, 0xc7, 0x74, 0x78, - 0x4a, 0x3a, 0xcf, 0x71, 0x42, 0xa1, 0x1b, 0x94, 0x7a, 0xa7, 0x0e, 0x35, 0x93, 0xc0, 0x42, 0x81, 0x93, 0x7e, 0x44, - 0xf3, 0xa8, 0x38, 0x12, 0xc0, 0xc8, 0xf4, 0xfa, 0xdb, 0x5c, 0x5b, 0xe4, 0xc3, 0x5e, 0xfb, 0x65, 0xe3, 0x5e, 0x1f, - 0x05, 0xc9, 0x7f, 0xc7, 0x01, 0x12, 0x6b, 0x43, 0xf6, 0x26, 0x60, 0x19, 0x51, 0xcc, 0x51, 0xf0, 0x6d, 0x41, 0x52, - 0xa8, 0x54, 0x82, 0x0b, 0x7b, 0x84, 0x85, 0x4b, 0x2d, 0x2d, 0x63, 0x2d, 0x3c, 0x6f, 0x01, 0x3a, 0x3a, 0x7c, 0x5d, - 0x7c, 0x97, 0x9d, 0x5e, 0x0c, 0x92, 0x73, 0x8f, 0x10, 0x24, 0xa8, 0xc7, 0x45, 0x09, 0xb8, 0x6f, 0x56, 0xe3, 0x6b, - 0x41, 0x4d, 0x9a, 0xd4, 0x5d, 0x05, 0xa7, 0xbb, 0x50, 0xc0, 0x65, 0x74, 0xd6, 0x40, 0xd0, 0xf0, 0xdd, 0x91, 0x0c, - 0xb0, 0x2a, 0x48, 0x90, 0xb8, 0xe4, 0x87, 0xc4, 0x4a, 0x45, 0x77, 0x78, 0x47, 0x63, 0xbc, 0xa3, 0xb6, 0x2e, 0x3b, - 0xed, 0x6b, 0xef, 0x36, 0x0c, 0xc2, 0x88, 0xf1, 0x99, 0x81, 0x8e, 0xec, 0xed, 0x80, 0x4d, 0x9e, 0x9d, 0xb0, 0x01, - 0x8f, 0xe5, 0x8e, 0x8c, 0xd6, 0xf9, 0x35, 0xcb, 0x17, 0x7b, 0xda, 0xe7, 0x9e, 0x84, 0x8c, 0x8d, 0x23, 0x70, 0xa3, - 0x06, 0x64, 0x4a, 0x98, 0x25, 0xfc, 0xc8, 0xa1, 0xfa, 0x2c, 0x09, 0xfe, 0x2b, 0x6d, 0x40, 0x01, 0x39, 0xda, 0x93, - 0x4a, 0x92, 0x79, 0x0c, 0xb3, 0x26, 0x85, 0x0f, 0xc8, 0x50, 0xe6, 0xf8, 0x69, 0xa8, 0x29, 0xd6, 0x89, 0xa1, 0x1a, - 0x99, 0x26, 0x86, 0xef, 0x1a, 0xf3, 0x57, 0xdc, 0xfc, 0xd9, 0xab, 0xaa, 0xa7, 0x43, 0xf0, 0x10, 0x4a, 0x09, 0xca, - 0xcd, 0x4c, 0x28, 0x03, 0xe8, 0x17, 0x69, 0xb2, 0x01, 0xad, 0x1f, 0xa1, 0xc3, 0xf7, 0x9b, 0x23, 0x38, 0xb9, 0x2c, - 0x55, 0x58, 0x17, 0x3f, 0xfe, 0x4a, 0x60, 0xef, 0xdd, 0x61, 0xba, 0x51, 0xce, 0xe6, 0xd4, 0x96, 0x4c, 0x5d, 0xf0, - 0x75, 0xb9, 0x3e, 0x09, 0x5e, 0x59, 0x20, 0x35, 0x0b, 0xab, 0x75, 0xe2, 0x12, 0x59, 0xb4, 0x38, 0x4d, 0xde, 0xcd, - 0x5f, 0x9e, 0x66, 0x13, 0xaf, 0x5c, 0x0a, 0x4c, 0x7e, 0x16, 0x55, 0xe2, 0x22, 0xb3, 0x5c, 0x36, 0xfc, 0xcd, 0x01, - 0x9f, 0x67, 0x7d, 0x3d, 0xf0, 0xbb, 0xfe, 0x5c, 0xdf, 0x1e, 0xf2, 0x90, 0x50, 0x8b, 0xdb, 0x1a, 0x67, 0x4e, 0x8d, - 0x6d, 0xe6, 0xbd, 0x5d, 0xda, 0xc7, 0x71, 0xcc, 0x7c, 0x44, 0x45, 0xba, 0xa2, 0x24, 0xec, 0x4e, 0x87, 0xa4, 0x53, - 0x4c, 0x56, 0x9c, 0x39, 0xf5, 0x54, 0xb8, 0x2d, 0xce, 0x6b, 0x7c, 0xb8, 0x44, 0x74, 0x82, 0xa9, 0x03, 0x24, 0xd7, - 0xb1, 0x25, 0xb8, 0xab, 0x08, 0x5c, 0x9a, 0x5a, 0xa8, 0xa2, 0x78, 0xc6, 0x59, 0xec, 0x16, 0x52, 0xf3, 0x53, 0xf5, - 0xb8, 0xd4, 0xad, 0x2a, 0xe1, 0x95, 0x6c, 0x85, 0x29, 0x90, 0xc9, 0x8a, 0xa4, 0x39, 0x89, 0x15, 0x0e, 0xfa, 0x9e, - 0x43, 0x92, 0xbd, 0x58, 0xf6, 0xb6, 0x7f, 0xeb, 0x6a, 0xcd, 0x0a, 0xa3, 0x5d, 0xac, 0x16, 0xc5, 0x8b, 0x54, 0x6d, - 0x1f, 0xa8, 0xbb, 0xca, 0x7d, 0xc7, 0x40, 0xa3, 0x46, 0x2a, 0x5b, 0x51, 0x47, 0x6a, 0x78, 0xcc, 0x5f, 0x9b, 0xe9, - 0x88, 0x31, 0x6c, 0xd8, 0xd1, 0x41, 0xb3, 0xb9, 0x0c, 0x8a, 0xa9, 0xc5, 0x61, 0x54, 0x1a, 0xba, 0x8d, 0xc8, 0x57, - 0x28, 0xcf, 0xec, 0x1b, 0x63, 0x43, 0x2c, 0xd9, 0x53, 0xbc, 0x06, 0xc2, 0x24, 0xa5, 0xcf, 0x62, 0x8b, 0xc2, 0xa6, - 0xcd, 0xed, 0x99, 0x63, 0x03, 0x0e, 0xae, 0x92, 0x52, 0xa6, 0xab, 0xc2, 0xab, 0x40, 0x29, 0xac, 0x44, 0x67, 0x09, - 0x21, 0x63, 0x9e, 0xbd, 0xf3, 0x53, 0xd3, 0x73, 0x8f, 0x80, 0x68, 0xf6, 0x05, 0x1c, 0x05, 0x1f, 0xc4, 0x88, 0x8f, - 0x34, 0xe4, 0x1c, 0xbe, 0x72, 0x98, 0xbe, 0xb7, 0x85, 0xe4, 0x47, 0x3f, 0x1f, 0x2f, 0x94, 0x29, 0x49, 0xb5, 0x83, - 0xd0, 0x06, 0x12, 0x67, 0x80, 0x78, 0x96, 0x81, 0x25, 0x28, 0x8d, 0x01, 0x83, 0x83, 0xcf, 0x47, 0xbb, 0x22, 0xd4, - 0x12, 0xa1, 0xbb, 0x2c, 0x5d, 0x80, 0xb3, 0x6e, 0x90, 0xd1, 0x26, 0xf6, 0x70, 0x7f, 0xe1, 0x80, 0xee, 0xc4, 0xe0, - 0xc8, 0xc9, 0xec, 0xb2, 0x25, 0xc1, 0xc4, 0xbf, 0x8b, 0xa6, 0x8d, 0x25, 0x52, 0x21, 0xde, 0x58, 0x3a, 0xc0, 0x4c, - 0xa1, 0x3d, 0x55, 0xeb, 0x8e, 0x48, 0xf1, 0x1b, 0xe0, 0x41, 0x34, 0x42, 0x03, 0x47, 0xa2, 0x7e, 0x1e, 0xa3, 0x25, - 0xc6, 0x23, 0xce, 0x7f, 0x4c, 0x2d, 0x07, 0x93, 0x04, 0x72, 0x18, 0xed, 0x1e, 0x3b, 0x13, 0x8a, 0xb3, 0x9d, 0xb4, - 0x6c, 0x3d, 0xfd, 0xdc, 0xa6, 0x0f, 0x66, 0xef, 0x15, 0xde, 0x10, 0x5c, 0x28, 0xfa, 0xcb, 0x2d, 0xcf, 0x30, 0x02, - 0x0c, 0x86, 0xdd, 0x60, 0xfe, 0xfd, 0xe9, 0x24, 0x3a, 0x3c, 0xaa, 0x1f, 0xae, 0x7a, 0x3b, 0x98, 0x3a, 0x93, 0xc1, - 0xf9, 0xe4, 0x97, 0x89, 0xbb, 0xef, 0x44, 0xf2, 0xc5, 0x94, 0x79, 0x8e, 0x7c, 0xd2, 0x09, 0xcc, 0x76, 0x0d, 0xa3, - 0x9a, 0x5a, 0x02, 0x91, 0x88, 0xa9, 0xd0, 0x8d, 0x94, 0xf3, 0x72, 0x7b, 0x4b, 0xe1, 0xfb, 0x6d, 0xaa, 0x52, 0xa5, - 0x46, 0x11, 0x96, 0x9b, 0xf4, 0x83, 0x83, 0xee, 0xf7, 0xa5, 0xbc, 0x5c, 0x4e, 0x6b, 0x91, 0xc7, 0x43, 0x21, 0xea, - 0x7c, 0xa4, 0xbd, 0x7f, 0xa2, 0xf3, 0x33, 0x49, 0xc8, 0xae, 0xff, 0x54, 0x11, 0x60, 0xfc, 0x15, 0xa2, 0xae, 0x4d, - 0x32, 0xa8, 0xd4, 0x4b, 0x2b, 0xbc, 0x83, 0xaf, 0x88, 0xdc, 0x0a, 0xfa, 0x95, 0x51, 0xe5, 0xad, 0x57, 0x6d, 0x97, - 0xb3, 0x2f, 0xb0, 0x60, 0xd3, 0x9a, 0x0e, 0x5e, 0xf9, 0xeb, 0xe0, 0xa8, 0xa0, 0x37, 0x9c, 0x3a, 0x23, 0xf5, 0x10, - 0xef, 0xe7, 0x02, 0x05, 0x27, 0xc4, 0x3f, 0x0a, 0x86, 0x46, 0xe9, 0x5a, 0x6a, 0x63, 0x6c, 0x0f, 0x98, 0xaf, 0x57, - 0x95, 0x71, 0x95, 0xdd, 0x09, 0x1e, 0x3b, 0x37, 0x3e, 0x85, 0x91, 0xb4, 0x3c, 0xc0, 0x39, 0xab, 0x43, 0x07, 0xce, - 0x6b, 0xf6, 0x85, 0x6a, 0x3d, 0x14, 0x33, 0x12, 0x6d, 0x4d, 0xb0, 0x8c, 0x3c, 0x9b, 0xb5, 0xe7, 0xa9, 0x49, 0x66, - 0x35, 0xd2, 0x66, 0x7c, 0x6a, 0xfa, 0xaf, 0x01, 0xb1, 0x1e, 0x74, 0xf9, 0x6d, 0xa5, 0xfa, 0x5a, 0x21, 0xeb, 0x11, - 0xc7, 0x4a, 0x95, 0x6d, 0x83, 0x63, 0x07, 0x6e, 0x35, 0x1e, 0x0f, 0xbe, 0x17, 0xd2, 0x58, 0x9d, 0x04, 0x2e, 0x9d, - 0x50, 0xf9, 0x86, 0x2b, 0x06, 0x76, 0x12, 0xdd, 0x2c, 0x17, 0x51, 0x22, 0x45, 0xfe, 0x36, 0x70, 0x8a, 0xe1, 0x50, - 0x08, 0x0f, 0xe2, 0xdf, 0x24, 0x09, 0xf3, 0x3a, 0x52, 0x9d, 0x58, 0xed, 0xe0, 0x7a, 0x95, 0x1e, 0x05, 0x07, 0x6b, - 0xaa, 0xa4, 0x0d, 0x25, 0xea, 0x52, 0x8f, 0x61, 0x4d, 0x0f, 0x87, 0x7a, 0x71, 0xe3, 0x70, 0xe5, 0x63, 0xcd, 0xa2, - 0xf5, 0x17, 0x35, 0x1c, 0xab, 0x11, 0x36, 0x53, 0x11, 0xcd, 0xec, 0xff, 0x88, 0x2b, 0x1d, 0xb2, 0x0b, 0x80, 0xda, - 0x8f, 0xf8, 0x06, 0x55, 0x31, 0x02, 0xb4, 0x9f, 0x96, 0x6f, 0xa4, 0x3e, 0xe5, 0x19, 0x8b, 0xeb, 0x16, 0x51, 0xe4, - 0x22, 0x18, 0x6b, 0x8a, 0x0d, 0x00, 0x61, 0xd9, 0x02, 0x1b, 0x88, 0xa2, 0x59, 0x94, 0x4d, 0xdd, 0x60, 0xb7, 0x78, - 0x01, 0xd1, 0x9a, 0xc7, 0x67, 0x62, 0xcd, 0x9c, 0x1b, 0xa9, 0x2c, 0x2b, 0x7c, 0xff, 0xea, 0x8a, 0xb9, 0x42, 0x83, - 0xf7, 0xf6, 0xdc, 0xca, 0x1e, 0x9d, 0x0f, 0x76, 0x33, 0xfd, 0x0b, 0xbb, 0x0e, 0x6f, 0xd9, 0x26, 0xcc, 0x08, 0x9f, - 0xdc, 0x3e, 0xfe, 0x8a, 0x35, 0xe1, 0xfc, 0x47, 0x51, 0x31, 0x28, 0x5c, 0x41, 0xb0, 0xa8, 0x35, 0xe3, 0x94, 0xc2, - 0x63, 0x1f, 0xa8, 0xd0, 0x1e, 0x24, 0x26, 0x08, 0xa3, 0x2a, 0x53, 0x25, 0xb2, 0xe7, 0xe2, 0x57, 0x6d, 0x22, 0x83, - 0xc9, 0x38, 0x94, 0x0d, 0xdc, 0xd4, 0xae, 0x39, 0x33, 0x3b, 0x4b, 0xeb, 0xdf, 0x6b, 0x8e, 0x75, 0x58, 0xb0, 0x44, - 0x6b, 0xa8, 0x99, 0x5e, 0x56, 0x2d, 0xc2, 0x5b, 0xc3, 0x74, 0x78, 0x08, 0x52, 0xcb, 0x22, 0xe1, 0x0f, 0xdd, 0x77, - 0xd0, 0x22, 0x18, 0xa3, 0x11, 0x58, 0x19, 0xa7, 0x90, 0xeb, 0xfc, 0x38, 0x25, 0x0a, 0xd4, 0xb2, 0xde, 0x67, 0x2c, - 0x73, 0xe4, 0x35, 0x2b, 0xf3, 0x34, 0x2b, 0x7a, 0x8f, 0xb2, 0xa1, 0xe3, 0xfa, 0x73, 0x26, 0x1a, 0x49, 0x87, 0x86, - 0x3a, 0x1d, 0xe7, 0xc4, 0x95, 0x35, 0x47, 0x53, 0x24, 0xb7, 0xf5, 0x40, 0xda, 0xcd, 0x6c, 0x25, 0x4c, 0xb6, 0xd8, - 0x6c, 0x46, 0xd8, 0xee, 0x68, 0xec, 0x33, 0x4f, 0x1c, 0xd7, 0x10, 0x3d, 0xd0, 0xe6, 0xce, 0x4b, 0x6e, 0x5c, 0xfc, - 0xef, 0xa0, 0x88, 0x6e, 0x1e, 0x8e, 0x08, 0xe6, 0x72, 0x4e, 0x51, 0x3c, 0xdd, 0x1c, 0x87, 0xc0, 0x86, 0xf5, 0x9f, - 0x9b, 0xe8, 0x4a, 0x8e, 0xe7, 0xa8, 0xd2, 0x23, 0x05, 0x71, 0x62, 0x7b, 0x76, 0x0d, 0x49, 0xfb, 0x11, 0x09, 0xcf, - 0x29, 0xeb, 0x6c, 0x74, 0xae, 0x73, 0x5d, 0x7a, 0x1f, 0x7f, 0x25, 0x3d, 0x21, 0x08, 0x0c, 0xf3, 0xa7, 0xb8, 0x9f, - 0xc0, 0x8a, 0x0b, 0xab, 0x52, 0xae, 0x78, 0xe1, 0x5f, 0x73, 0xc6, 0xf7, 0xb4, 0x2a, 0x2b, 0xd9, 0x71, 0x79, 0xa5, - 0x73, 0xd6, 0x50, 0xa5, 0x63, 0xa6, 0xcb, 0x8a, 0xc5, 0xf4, 0x8e, 0xfd, 0xba, 0x36, 0x04, 0x34, 0x74, 0xe7, 0xdc, - 0x51, 0x31, 0x93, 0xe0, 0x69, 0x88, 0xa5, 0x52, 0x80, 0xae, 0xd0, 0x67, 0xe6, 0xe4, 0x9b, 0x61, 0x1e, 0x0c, 0xf9, - 0x59, 0x00, 0x08, 0x57, 0x26, 0xa8, 0xac, 0xc0, 0xb3, 0xe2, 0x5a, 0xd1, 0x79, 0x0d, 0xe6, 0x22, 0xa2, 0xde, 0x6b, - 0xa4, 0xff, 0x00, 0x09, 0x97, 0x60, 0x2f, 0x05, 0x2e, 0x06, 0x74, 0xf9, 0xcc, 0x1d, 0x5a, 0x97, 0x08, 0x31, 0xd6, - 0x80, 0xa4, 0xb6, 0xf1, 0xcb, 0xc5, 0x84, 0x7b, 0xde, 0xcf, 0x03, 0xce, 0xba, 0x7e, 0x06, 0x90, 0x07, 0xf9, 0xf3, - 0x57, 0xb7, 0x72, 0x39, 0xc8, 0x09, 0x48, 0x5c, 0x5c, 0xb8, 0xf2, 0x88, 0x76, 0x4e, 0x8b, 0xb6, 0xcc, 0xd5, 0x28, - 0xe3, 0xb6, 0x06, 0x29, 0x52, 0xb8, 0xd8, 0x48, 0xfb, 0x18, 0xb8, 0x20, 0xe9, 0x89, 0x0d, 0x85, 0x84, 0x1d, 0xbb, - 0xf6, 0x62, 0x2a, 0xb7, 0x33, 0xea, 0x06, 0xfa, 0x62, 0xeb, 0x6f, 0x34, 0xfe, 0xb4, 0xb1, 0x76, 0xa6, 0xef, 0x19, - 0x5c, 0x11, 0xa9, 0x46, 0x9f, 0x57, 0x58, 0x7d, 0xda, 0xef, 0xca, 0x1d, 0xac, 0xd6, 0x97, 0xd1, 0x57, 0x15, 0x1b, - 0xf5, 0x89, 0x0d, 0x82, 0x49, 0x92, 0x54, 0x72, 0x6b, 0x50, 0x52, 0xd0, 0x98, 0xb7, 0x51, 0x43, 0x56, 0x4a, 0x6b, - 0x26, 0x7b, 0xf1, 0xbf, 0x73, 0xc5, 0xcc, 0xc4, 0xc0, 0x8f, 0xb1, 0xa5, 0x3e, 0x79, 0xf4, 0xc4, 0x5b, 0xeb, 0xf7, - 0x9c, 0xa1, 0x63, 0xf6, 0x00, 0x81, 0x42, 0x60, 0x5e, 0xba, 0xc4, 0x9c, 0x5b, 0x33, 0x6b, 0xd6, 0xd4, 0xcb, 0x7f, - 0x66, 0x57, 0xba, 0xc0, 0xd8, 0x27, 0x82, 0xfe, 0x5c, 0xda, 0xed, 0xd4, 0x37, 0x66, 0xef, 0x06, 0x9c, 0x06, 0x98, - 0xb9, 0x78, 0x53, 0xe9, 0xdd, 0xd5, 0xe6, 0x11, 0x0b, 0x60, 0x72, 0x36, 0xfa, 0x97, 0xa6, 0x22, 0xf8, 0xcb, 0xa3, - 0xb3, 0x17, 0xeb, 0x23, 0x0a, 0x05, 0x5f, 0x46, 0x23, 0xde, 0x65, 0xf4, 0x2f, 0x1a, 0x5a, 0xff, 0x03, 0xfb, 0x60, - 0x1b, 0x97, 0x61, 0x0f, 0xed, 0xc3, 0x24, 0x76, 0x45, 0xd0, 0xd6, 0xc6, 0x82, 0x20, 0x6b, 0xea, 0x72, 0x60, 0x44, - 0x8a, 0xdf, 0x5a, 0x27, 0x9d, 0xd7, 0xb1, 0xef, 0xda, 0x89, 0xf3, 0x21, 0x11, 0x23, 0xf0, 0x5b, 0xf4, 0x7c, 0x24, - 0xa1, 0x82, 0x4b, 0x47, 0x2f, 0x13, 0x3c, 0xea, 0x12, 0x27, 0xd5, 0xae, 0x97, 0xa3, 0xf6, 0xcf, 0xfa, 0x66, 0x3f, - 0x18, 0x94, 0xae, 0x1b, 0x86, 0x6f, 0xe9, 0xb5, 0xcc, 0x91, 0x87, 0x77, 0x7d, 0xa3, 0xb5, 0x05, 0xd6, 0xba, 0x6c, - 0x0b, 0x45, 0x9d, 0xf0, 0xfa, 0x5d, 0xe3, 0xf8, 0xbf, 0x94, 0x59, 0xc1, 0x50, 0x98, 0xcc, 0x44, 0xbd, 0xd9, 0x82, - 0x74, 0x16, 0x7a, 0x7b, 0xd7, 0xbf, 0x54, 0x9a, 0x03, 0xb6, 0x98, 0x31, 0x38, 0xd5, 0x83, 0x66, 0xf0, 0x12, 0x0a, - 0x84, 0xb9, 0x77, 0x86, 0xce, 0xa0, 0xfb, 0xd5, 0x09, 0xca, 0x44, 0x31, 0xe8, 0x59, 0x0a, 0x25, 0x6d, 0x42, 0x6a, - 0xdd, 0xef, 0x0d, 0x6e, 0x7d, 0xe8, 0xdf, 0xcc, 0x28, 0xa2, 0x51, 0xef, 0x9c, 0x24, 0xa0, 0xe8, 0x15, 0x07, 0x3a, - 0x51, 0xde, 0x6c, 0x89, 0x11, 0xeb, 0x78, 0x9c, 0xe4, 0xea, 0xe0, 0xf1, 0x4a, 0xc9, 0xf1, 0xaa, 0x10, 0x7a, 0x0e, - 0x60, 0x88, 0x23, 0x70, 0x2f, 0x87, 0x05, 0x74, 0x01, 0xcf, 0xf4, 0x8e, 0x7a, 0x36, 0x73, 0xb4, 0xfb, 0x7f, 0x97, - 0x7b, 0xd4, 0x5b, 0x3c, 0xdb, 0x24, 0x0e, 0x58, 0xd6, 0x34, 0x02, 0xdf, 0xfc, 0xf4, 0xae, 0xd6, 0x63, 0xc9, 0x9b, - 0x2d, 0x95, 0x39, 0xd8, 0x10, 0x5d, 0xa7, 0x45, 0xd2, 0xa7, 0x5c, 0x1d, 0xdb, 0x14, 0x50, 0xc3, 0xfd, 0xb4, 0x73, - 0x45, 0x78, 0x9c, 0xb0, 0x86, 0x73, 0x2a, 0x1c, 0x76, 0x70, 0xb4, 0x11, 0x46, 0x37, 0xe4, 0x18, 0x4b, 0xea, 0x20, - 0xbe, 0x1d, 0xe0, 0x13, 0x7c, 0xbf, 0x30, 0xca, 0x97, 0x0e, 0xf1, 0x47, 0x06, 0x8d, 0x0e, 0x72, 0x89, 0x95, 0x3c, - 0x61, 0xea, 0xaf, 0x95, 0xda, 0xca, 0x75, 0xb9, 0xb9, 0xb7, 0x57, 0xb7, 0x92, 0x59, 0x38, 0xc9, 0x28, 0x3e, 0x92, - 0x1e, 0xd5, 0x2b, 0xf9, 0xcf, 0xed, 0xc6, 0x20, 0x99, 0xb9, 0xbd, 0x7b, 0x27, 0x30, 0x76, 0xa8, 0x74, 0xa2, 0xe0, - 0x5f, 0x22, 0xe1, 0x67, 0xa3, 0x11, 0x29, 0x28, 0x2c, 0xb9, 0x0a, 0x55, 0x68, 0x9f, 0xb9, 0xe9, 0xa5, 0xa2, 0x72, - 0x8c, 0x51, 0x31, 0x9b, 0xf1, 0x8b, 0xa1, 0x1a, 0x23, 0xf5, 0xd3, 0x9c, 0x6d, 0xbf, 0xf5, 0x44, 0xaf, 0x45, 0x73, - 0x20, 0x09, 0x1a, 0x57, 0x02, 0x14, 0xe0, 0x10, 0x13, 0x8c, 0xc9, 0x5d, 0x62, 0xd0, 0x34, 0xc3, 0xf3, 0x14, 0xea, - 0x5a, 0x8d, 0x27, 0x95, 0x6f, 0x6d, 0x97, 0x95, 0x54, 0xb6, 0x13, 0xa3, 0x79, 0x27, 0x41, 0xe2, 0xa8, 0x71, 0x8a, - 0x82, 0x55, 0xf5, 0x0c, 0x29, 0xc3, 0x12, 0x20, 0xad, 0x38, 0x87, 0x6f, 0xcf, 0x43, 0x66, 0x17, 0x96, 0xd8, 0x2b, - 0xdd, 0x2c, 0x85, 0x08, 0x6e, 0x17, 0x15, 0x09, 0xb9, 0xbe, 0x65, 0x93, 0x2c, 0x74, 0xe9, 0x5b, 0x67, 0xe8, 0x12, - 0xd2, 0x87, 0x1c, 0xf5, 0xdb, 0xbd, 0x04, 0x9c, 0x20, 0x8c, 0x8d, 0x09, 0xd9, 0x7c, 0xd4, 0x0b, 0xf2, 0x28, 0x6f, - 0x05, 0x8d, 0xab, 0xcd, 0xd2, 0xfb, 0x9f, 0x30, 0x1a, 0xca, 0x65, 0x43, 0x26, 0xb3, 0xa2, 0x83, 0xe6, 0xab, 0x65, - 0x6c, 0x0e, 0x2a, 0xc8, 0x31, 0x27, 0x01, 0x7a, 0x5c, 0x81, 0xe7, 0x96, 0x45, 0xbd, 0x49, 0xf5, 0x67, 0xc5, 0x0b, - 0x5d, 0x83, 0xdd, 0xd7, 0x0e, 0x62, 0xc7, 0x26, 0x53, 0xbb, 0x58, 0x05, 0x4a, 0xe2, 0x88, 0x6e, 0x85, 0x3e, 0x85, - 0x2a, 0x77, 0xa4, 0x10, 0xc3, 0x3a, 0xc0, 0xc2, 0x59, 0xc9, 0x4c, 0xd8, 0x3e, 0xcc, 0xe7, 0x8f, 0x51, 0x6b, 0x01, - 0xd3, 0x43, 0x08, 0xf5, 0xdd, 0x1d, 0xee, 0x28, 0x3a, 0x3a, 0x93, 0xc9, 0x5d, 0x56, 0xc8, 0xa0, 0x5f, 0xf8, 0x58, - 0xc0, 0x05, 0x57, 0xe4, 0x92, 0xb1, 0xa0, 0xe9, 0x14, 0x4c, 0xcb, 0xd4, 0xb9, 0xfc, 0xdd, 0xfb, 0x98, 0x40, 0x2d, - 0x88, 0x45, 0xd3, 0x84, 0x13, 0xd4, 0xd0, 0x1d, 0x44, 0x6b, 0xda, 0x93, 0xc7, 0x8b, 0xec, 0x19, 0xc6, 0xca, 0x09, - 0xfe, 0xd4, 0xe5, 0xba, 0xfa, 0xf2, 0x5d, 0x90, 0x4a, 0xef, 0x8d, 0x4e, 0x4b, 0xd2, 0x3b, 0xca, 0x11, 0xd1, 0xa4, - 0xe3, 0x6f, 0x1f, 0x91, 0xb7, 0x20, 0x13, 0x1b, 0x3e, 0x5c, 0xd6, 0x9a, 0xf7, 0x5f, 0x51, 0xb0, 0x4a, 0x11, 0xce, - 0x7e, 0xa2, 0x49, 0x1c, 0xb2, 0x15, 0x21, 0xed, 0x8b, 0x60, 0xa4, 0xa3, 0x82, 0xd8, 0x8a, 0xed, 0x6a, 0x6d, 0xb9, - 0x87, 0x40, 0xc4, 0x39, 0xb8, 0x42, 0x66, 0x19, 0x9c, 0x63, 0xaf, 0x7e, 0x79, 0x80, 0xe0, 0xf2, 0x14, 0xf5, 0xbf, - 0x5e, 0x16, 0x7e, 0xf4, 0x70, 0xa0, 0x75, 0x64, 0x65, 0x65, 0x4e, 0xbd, 0x54, 0x1f, 0xcb, 0x3a, 0x1e, 0xad, 0xfa, - 0x9a, 0x7e, 0xa3, 0x94, 0x46, 0x9b, 0x41, 0x8b, 0xdb, 0x94, 0x95, 0x1a, 0xc3, 0x9f, 0x59, 0x2d, 0xea, 0xa1, 0xc2, - 0x1d, 0xae, 0x0d, 0xde, 0xb3, 0x77, 0x30, 0x91, 0x22, 0xef, 0xdb, 0x3f, 0x35, 0xb8, 0x21, 0x61, 0x3a, 0xe1, 0x90, - 0x3b, 0x70, 0x05, 0xd3, 0x93, 0x4e, 0xdd, 0x35, 0xc4, 0xd7, 0x22, 0xc9, 0x8e, 0xfe, 0x1b, 0x05, 0xcf, 0x17, 0x32, - 0xd6, 0x84, 0x8c, 0x6e, 0x0b, 0x6b, 0x11, 0x69, 0xa5, 0xc1, 0xc4, 0x18, 0xc5, 0x7c, 0x4a, 0x94, 0x88, 0x65, 0xb7, - 0x25, 0x23, 0xb1, 0xcf, 0xd6, 0x96, 0xbd, 0xd5, 0x4d, 0x4b, 0x82, 0x96, 0xa5, 0x20, 0x5e, 0x2e, 0xcf, 0x44, 0x15, - 0xd0, 0xb5, 0x71, 0x03, 0x22, 0x4e, 0xef, 0xac, 0xf6, 0x16, 0x04, 0xd0, 0x3e, 0xff, 0xfb, 0x4a, 0xe9, 0xe2, 0x56, - 0x85, 0x12, 0x82, 0x1f, 0xb2, 0x4c, 0x96, 0x40, 0x19, 0xe4, 0x63, 0xcb, 0x07, 0xf7, 0x15, 0x56, 0xeb, 0xbb, 0xf5, - 0x10, 0xb1, 0x79, 0x3e, 0x84, 0xb4, 0x83, 0xe1, 0x99, 0x02, 0x4f, 0xf6, 0x2f, 0xdb, 0x87, 0x0d, 0xd0, 0xba, 0xc9, - 0x50, 0x7e, 0x57, 0xaa, 0x89, 0x32, 0x82, 0x8f, 0x5f, 0xed, 0x70, 0x61, 0x43, 0xed, 0xc0, 0x68, 0x1a, 0x76, 0xcb, - 0x3f, 0x20, 0x56, 0x48, 0xe8, 0xea, 0x08, 0x60, 0xeb, 0x32, 0x26, 0x7c, 0xc9, 0xbe, 0x41, 0x18, 0x00, 0x89, 0xdf, - 0xfe, 0xaa, 0x1d, 0x9f, 0x98, 0xeb, 0xf2, 0xfb, 0xb6, 0xbd, 0x4a, 0x44, 0xef, 0x63, 0x27, 0x66, 0x3b, 0xf6, 0x01, - 0x2b, 0x1e, 0x56, 0x8d, 0xe8, 0xd8, 0xf3, 0xa1, 0x70, 0x9f, 0xe2, 0xd1, 0xd6, 0x21, 0xfa, 0x9d, 0x28, 0xb2, 0xc5, - 0x76, 0xc9, 0xfe, 0x42, 0x4b, 0xe7, 0xd3, 0x07, 0x9a, 0x41, 0xdd, 0x1e, 0x23, 0xaf, 0x22, 0x80, 0x78, 0x0c, 0x76, - 0xe1, 0xeb, 0x32, 0xef, 0x99, 0xbc, 0x02, 0xfc, 0x9c, 0x72, 0xf2, 0x97, 0xe7, 0x8b, 0x26, 0x22, 0xe8, 0xb3, 0x2e, - 0x49, 0x02, 0x22, 0xe2, 0x71, 0x3a, 0x3b, 0x36, 0x4d, 0x7a, 0x19, 0x39, 0x3c, 0x62, 0x33, 0x2b, 0xdf, 0xb1, 0xaa, - 0x8b, 0xb3, 0x5b, 0x3e, 0xda, 0x5f, 0xe8, 0x41, 0x67, 0x90, 0xa8, 0x5d, 0x9c, 0xc9, 0x68, 0x76, 0x64, 0x1a, 0x63, - 0x43, 0xb4, 0x97, 0x8a, 0x29, 0x19, 0x66, 0x39, 0x46, 0x1d, 0xd7, 0x46, 0x4e, 0x97, 0x93, 0x25, 0x0e, 0xc3, 0x12, - 0xe3, 0x7d, 0x1a, 0x10, 0xf4, 0x72, 0x05, 0x1d, 0xec, 0xe2, 0x5c, 0x6f, 0x87, 0x1c, 0x1a, 0x10, 0x97, 0x1a, 0xef, - 0xe2, 0x5c, 0xf7, 0xa0, 0xca, 0x53, 0x64, 0xc5, 0xc3, 0x9f, 0x52, 0xbf, 0x54, 0x8e, 0xf1, 0x9e, 0x81, 0xc4, 0xd8, - 0x6f, 0x6c, 0xcf, 0xfd, 0x26, 0x28, 0x66, 0x99, 0xa2, 0x91, 0x9e, 0x17, 0xee, 0xc1, 0x6c, 0x4f, 0xdb, 0xab, 0xd1, - 0x54, 0xc1, 0xcc, 0xa2, 0x13, 0xc0, 0xe6, 0x0f, 0xc4, 0x54, 0x45, 0x57, 0x3c, 0x52, 0x08, 0xc2, 0x70, 0xb5, 0xde, - 0x91, 0xed, 0xb3, 0x42, 0x68, 0xb9, 0x63, 0x26, 0x19, 0xf8, 0xb9, 0xf1, 0x61, 0xd6, 0x35, 0xbe, 0xa8, 0x27, 0x40, - 0x33, 0x71, 0xe5, 0xc3, 0xc7, 0xc9, 0x42, 0x61, 0x82, 0x92, 0xd1, 0x4f, 0xae, 0xa6, 0x5a, 0xd2, 0x9d, 0x74, 0xd8, - 0x9b, 0x2d, 0x5f, 0x27, 0x65, 0x1d, 0x76, 0x29, 0xfb, 0x58, 0xca, 0x03, 0xed, 0x76, 0x33, 0xdb, 0xc3, 0xdf, 0x70, - 0xf3, 0x01, 0xa0, 0x8b, 0x84, 0x95, 0x49, 0x6e, 0xd1, 0x80, 0x5f, 0x7c, 0x30, 0x38, 0x19, 0xc3, 0xf6, 0xe0, 0xc5, - 0xdc, 0x61, 0x9d, 0x63, 0xff, 0xd6, 0x91, 0x9b, 0x38, 0x0a, 0xa4, 0xe4, 0xab, 0x85, 0x45, 0x15, 0xa2, 0xc3, 0x40, - 0xe3, 0xaa, 0xcf, 0x13, 0xb0, 0x90, 0x33, 0xb5, 0x26, 0xd9, 0xfc, 0x53, 0x05, 0xc4, 0xf3, 0xd9, 0x72, 0x08, 0x24, - 0xc8, 0xb7, 0xb2, 0x5a, 0x16, 0xaf, 0x09, 0x27, 0xb0, 0x3d, 0x82, 0x45, 0x63, 0x77, 0x04, 0x00, 0x5a, 0xe8, 0x20, - 0xa4, 0xd4, 0x85, 0x0b, 0x65, 0x2f, 0xd7, 0xc8, 0x86, 0xa9, 0x6b, 0x81, 0x17, 0xdf, 0x4e, 0x38, 0xfa, 0xf7, 0x47, - 0x43, 0xb2, 0x8e, 0x00, 0x2e, 0x27, 0x78, 0x1f, 0x36, 0x8d, 0x3d, 0x03, 0xce, 0x48, 0xfb, 0xa2, 0x70, 0x45, 0x3f, - 0x0c, 0xac, 0x0b, 0xf1, 0x2c, 0x38, 0x47, 0x26, 0xbb, 0x12, 0xfa, 0x45, 0xd1, 0x0c, 0x09, 0x5e, 0x30, 0x8e, 0x6d, - 0xe0, 0x73, 0x07, 0xf4, 0xd3, 0x98, 0x8b, 0xb6, 0x05, 0x1e, 0x2b, 0xaa, 0xcc, 0x29, 0x87, 0x6e, 0x10, 0xad, 0xbd, - 0xfa, 0x5c, 0xea, 0x3b, 0x9c, 0x95, 0xce, 0x8a, 0x7b, 0x97, 0x55, 0x0f, 0x05, 0x9f, 0x20, 0xc7, 0xfb, 0x57, 0x14, - 0xfb, 0x9f, 0x36, 0xe2, 0x68, 0xc1, 0xa6, 0x00, 0x0c, 0x20, 0x21, 0xd3, 0x08, 0xdb, 0x3a, 0x09, 0x3a, 0x7e, 0x28, - 0x3d, 0x46, 0x1c, 0x4a, 0x5a, 0x61, 0x70, 0x98, 0xaa, 0x6f, 0x83, 0x0c, 0x29, 0x79, 0xb9, 0x94, 0x1e, 0x86, 0x18, - 0x39, 0x20, 0x95, 0xb9, 0xf2, 0x3d, 0x7b, 0x55, 0x3c, 0x51, 0xea, 0xc4, 0x07, 0x10, 0x8b, 0xa1, 0x47, 0x46, 0x7d, - 0x20, 0x53, 0x5d, 0x80, 0x26, 0x86, 0x90, 0x51, 0x02, 0x88, 0x8d, 0xa1, 0x11, 0x02, 0x25, 0xe4, 0xd8, 0xfa, 0xc5, - 0xac, 0x0a, 0x12, 0xa1, 0x88, 0x45, 0x4b, 0xb4, 0x38, 0x62, 0x14, 0x60, 0x86, 0x34, 0xd0, 0x63, 0xee, 0x9a, 0x0e, - 0x8c, 0x0b, 0x30, 0xa6, 0xe2, 0x1e, 0x40, 0x7e, 0x33, 0x86, 0xb1, 0x88, 0xe0, 0xe5, 0xae, 0x3c, 0x4f, 0x1a, 0x35, - 0x58, 0xc3, 0x5a, 0x34, 0x17, 0xab, 0xb7, 0x81, 0x99, 0x72, 0x0c, 0xc9, 0x55, 0xab, 0x52, 0xd8, 0xe9, 0xcd, 0x7e, - 0x1f, 0xf2, 0xb9, 0x83, 0xd0, 0xd6, 0xc1, 0x99, 0x25, 0x28, 0x33, 0x12, 0xdb, 0x98, 0x50, 0x40, 0x32, 0xd0, 0x81, - 0xd4, 0x15, 0x88, 0x90, 0x90, 0x64, 0x92, 0x84, 0xe6, 0x64, 0x8a, 0x44, 0x7c, 0x71, 0xc2, 0x5c, 0x1f, 0xc4, 0xc9, - 0x12, 0xd9, 0xbc, 0x6f, 0x97, 0xc0, 0xfc, 0x81, 0x91, 0x59, 0x91, 0xab, 0xaa, 0xa0, 0x01, 0x12, 0x09, 0xa3, 0xd5, - 0x09, 0x43, 0xe7, 0xf5, 0xd9, 0xdf, 0x07, 0x8c, 0x2d, 0x4c, 0xe8, 0x40, 0x30, 0x0c, 0x65, 0x51, 0xa8, 0xe4, 0x4f, - 0x0a, 0x1c, 0x56, 0x68, 0x78, 0x7f, 0x16, 0x7c, 0xf1, 0xd4, 0x62, 0x61, 0x15, 0x1e, 0x09, 0xb9, 0x1f, 0x6a, 0x89, - 0xb3, 0x02, 0x92, 0x13, 0x84, 0x56, 0xf7, 0xef, 0x7f, 0x77, 0x54, 0x12, 0xe6, 0x45, 0x8b, 0xd2, 0xab, 0x23, 0x6e, - 0x73, 0xb5, 0xc0, 0xd0, 0xa4, 0xd9, 0x21, 0xdf, 0x3e, 0x55, 0x22, 0x6e, 0x14, 0x5c, 0xee, 0x42, 0x2c, 0x01, 0x69, - 0x33, 0x18, 0x7c, 0x69, 0x3d, 0xa5, 0x1f, 0x20, 0xf4, 0x8d, 0x7b, 0x76, 0xfa, 0x38, 0x46, 0x32, 0x26, 0x17, 0xd6, - 0xcf, 0xac, 0x6a, 0x35, 0x71, 0x44, 0x42, 0xce, 0x59, 0xe8, 0x50, 0xec, 0xab, 0x61, 0x39, 0x73, 0xc5, 0xd9, 0xc3, - 0xc3, 0x68, 0x05, 0x24, 0x1d, 0x69, 0xb8, 0x21, 0xc7, 0xb3, 0x0f, 0x50, 0xe7, 0x51, 0x30, 0x92, 0x4a, 0xe6, 0xbd, - 0x62, 0x38, 0x6f, 0x88, 0xb6, 0xd4, 0xb3, 0xd6, 0x20, 0x70, 0x4e, 0x16, 0x49, 0xc9, 0x9b, 0x20, 0xb5, 0xf2, 0xf2, - 0x64, 0x1e, 0x31, 0xc5, 0xe9, 0x54, 0x59, 0x61, 0x74, 0x72, 0xd1, 0x73, 0x64, 0x94, 0x5d, 0xb0, 0xa1, 0x9a, 0x4f, - 0x4b, 0x53, 0xee, 0x2b, 0xac, 0x94, 0xae, 0xb4, 0xc0, 0x74, 0x24, 0xc6, 0xea, 0x66, 0x8e, 0xea, 0x81, 0x41, 0xc4, - 0x7a, 0xf9, 0x06, 0x91, 0x87, 0x34, 0xbf, 0x70, 0xa4, 0x22, 0x6d, 0x09, 0xcf, 0x4a, 0x3e, 0x60, 0x36, 0x03, 0xd2, - 0xca, 0xfb, 0x04, 0x5c, 0xf9, 0x4d, 0x81, 0x82, 0xe4, 0x8b, 0xf3, 0x04, 0xcd, 0x20, 0x7e, 0x1d, 0x64, 0xb3, 0xb1, - 0x11, 0xe3, 0xf9, 0xd6, 0xe0, 0xd5, 0x10, 0x39, 0x58, 0x1d, 0xfd, 0xba, 0x1b, 0xb0, 0x75, 0xb8, 0x4d, 0xa7, 0x67, - 0x5f, 0x6a, 0x81, 0x16, 0x83, 0xe3, 0xa9, 0x98, 0xe2, 0xa4, 0x7a, 0x44, 0x2c, 0x53, 0x61, 0x1a, 0x13, 0x5d, 0x21, - 0x6b, 0x6c, 0x29, 0xd8, 0x7c, 0xcb, 0x7b, 0x5e, 0x64, 0x48, 0xb8, 0x6b, 0x44, 0x17, 0xc3, 0x18, 0x04, 0x2f, 0x2f, - 0xa5, 0x73, 0x5f, 0x1b, 0x25, 0x56, 0xcc, 0x13, 0x1f, 0x5e, 0x37, 0x49, 0xf2, 0x82, 0xb4, 0x66, 0xcf, 0x6a, 0x2c, - 0x7f, 0x78, 0xf3, 0x83, 0xa9, 0x4a, 0xac, 0xd9, 0xc9, 0x4f, 0x52, 0xb6, 0xef, 0x87, 0xa6, 0x41, 0xde, 0x56, 0x2c, - 0x7e, 0x69, 0xf2, 0x0d, 0xa2, 0x0b, 0x46, 0xc9, 0x4e, 0x17, 0x8b, 0x75, 0x03, 0xf7, 0xeb, 0x25, 0xe8, 0xca, 0x0c, - 0x83, 0x76, 0xef, 0x6b, 0xdd, 0x2f, 0x8a, 0x48, 0x8f, 0xb1, 0x0f, 0x19, 0x29, 0x5a, 0x89, 0x5a, 0xcb, 0xfc, 0x6c, - 0x5b, 0xeb, 0x08, 0x09, 0x33, 0xd1, 0x4b, 0x73, 0xb4, 0x43, 0x22, 0x56, 0x33, 0x13, 0xa1, 0xc1, 0xba, 0x19, 0x79, - 0x57, 0x53, 0xfe, 0xb4, 0x84, 0x0e, 0x8f, 0xb5, 0xae, 0xda, 0xdc, 0xcb, 0x68, 0x3a, 0x23, 0xae, 0xe7, 0x69, 0xea, - 0x9a, 0xd2, 0xd3, 0xa0, 0xc3, 0x9d, 0x14, 0xb1, 0xc5, 0xad, 0xff, 0xc0, 0x4c, 0x8b, 0x42, 0x42, 0x35, 0x94, 0xb9, - 0xbd, 0xae, 0x1e, 0x4b, 0xd5, 0x53, 0xb2, 0xfb, 0x9e, 0xe8, 0x6b, 0xac, 0xd2, 0xbe, 0x46, 0xb2, 0x6a, 0x85, 0xc7, - 0xc6, 0xb8, 0x0e, 0x9e, 0xf5, 0x1b, 0xdc, 0x24, 0x8a, 0x10, 0xc3, 0xb8, 0xf4, 0x0b, 0x1f, 0xe1, 0x5c, 0xe0, 0xf5, - 0x30, 0x6d, 0xdd, 0x0e, 0xa9, 0xa6, 0x20, 0x8e, 0xdd, 0x16, 0xce, 0xd9, 0xad, 0x39, 0x78, 0xe8, 0x8e, 0xa3, 0xbc, - 0x50, 0x8f, 0xf3, 0x0e, 0x85, 0x76, 0x28, 0x69, 0x78, 0x5c, 0xb7, 0xa3, 0xc9, 0x83, 0x23, 0x9a, 0xb8, 0x5d, 0x6e, - 0x7f, 0x26, 0x94, 0x79, 0x1a, 0x20, 0xa2, 0x31, 0xfc, 0xfb, 0x92, 0x3d, 0x19, 0xd3, 0x09, 0x49, 0x64, 0x43, 0x66, - 0x1b, 0x30, 0xf6, 0x90, 0x48, 0x8f, 0xbf, 0x22, 0xf7, 0x6f, 0x8d, 0x82, 0xe3, 0xa5, 0xb8, 0xa1, 0xa4, 0x3f, 0x2c, - 0xc2, 0x4c, 0x27, 0x31, 0x4d, 0x3c, 0x90, 0xc5, 0x55, 0x00, 0x2e, 0xd3, 0xae, 0xb0, 0x40, 0x96, 0x0b, 0x2c, 0x90, - 0xb2, 0xfa, 0x1c, 0x25, 0x91, 0xb8, 0x47, 0x42, 0x76, 0x3a, 0x79, 0x2f, 0x8e, 0x71, 0xc1, 0x73, 0x35, 0x39, 0xba, - 0xe0, 0xc5, 0x4c, 0x10, 0xb5, 0x3b, 0x8d, 0xf4, 0x22, 0x34, 0xef, 0xe5, 0xea, 0x3a, 0xd2, 0xa7, 0xd0, 0x82, 0x0a, - 0xf5, 0x0b, 0x69, 0xbf, 0x7f, 0x9d, 0xc8, 0x80, 0xa3, 0x41, 0x93, 0x0d, 0x3b, 0x24, 0xac, 0x90, 0xd7, 0x2e, 0xbe, - 0x10, 0x3a, 0x22, 0x33, 0x7a, 0x94, 0x61, 0x7a, 0x99, 0x8f, 0xd1, 0xce, 0x5b, 0x39, 0x9a, 0x2e, 0x1c, 0xfc, 0xe7, - 0xb0, 0xb7, 0x40, 0x87, 0xab, 0xe3, 0x22, 0xdd, 0x4f, 0xce, 0x5c, 0xfc, 0x0f, 0xa6, 0xab, 0xae, 0x7d, 0x36, 0x13, - 0x5f, 0xc9, 0x63, 0x44, 0x7d, 0xd5, 0x0b, 0xa7, 0x34, 0x1b, 0xd5, 0x4c, 0x1f, 0x45, 0xe4, 0x79, 0xa8, 0x72, 0x5b, - 0x30, 0x9e, 0xd6, 0x60, 0xf8, 0xe8, 0x28, 0xe1, 0x10, 0x34, 0xc1, 0x99, 0xb9, 0x1f, 0x51, 0x65, 0x64, 0x09, 0xe3, - 0xc6, 0x02, 0xcb, 0x9b, 0xe9, 0x3c, 0x8e, 0x4b, 0xa1, 0xe5, 0x33, 0xc6, 0xdf, 0xdf, 0xa2, 0xcf, 0x4f, 0x85, 0xcd, - 0x12, 0x17, 0x3f, 0xe8, 0xc4, 0x51, 0x2f, 0x5c, 0x69, 0xeb, 0x14, 0xab, 0x52, 0xd9, 0x4d, 0xed, 0x7c, 0x6c, 0x5b, - 0x5e, 0x4a, 0xc6, 0xa7, 0x14, 0xe5, 0x24, 0xd7, 0x14, 0x8a, 0xc1, 0xc0, 0x1b, 0x59, 0xf5, 0xe7, 0x0b, 0x98, 0xc9, - 0x0d, 0x78, 0xa6, 0xaf, 0x63, 0xbd, 0x03, 0x3c, 0xd8, 0x73, 0x0b, 0x33, 0x57, 0x90, 0xc8, 0xe3, 0xf1, 0x1c, 0x8f, - 0x75, 0xc0, 0xf9, 0x83, 0xdc, 0x3b, 0x0a, 0xf8, 0x6e, 0x00, 0x62, 0x76, 0xde, 0x08, 0xf0, 0x0b, 0xec, 0x70, 0xb6, - 0xc4, 0x12, 0x54, 0x29, 0xd4, 0x82, 0x1d, 0x19, 0x7c, 0x96, 0x60, 0x35, 0x13, 0x70, 0x96, 0x20, 0x28, 0xca, 0x62, - 0xbe, 0x20, 0x28, 0x71, 0x14, 0x4a, 0x66, 0x2e, 0x3f, 0x35, 0x9b, 0xa2, 0x28, 0x12, 0xe1, 0xa7, 0x76, 0x70, 0x9e, - 0x11, 0x2e, 0xf1, 0xb5, 0xa2, 0xca, 0x07, 0x06, 0x5f, 0x10, 0x68, 0x80, 0xfe, 0xcd, 0x54, 0x44, 0xfb, 0x73, 0xd2, - 0x28, 0x29, 0xdc, 0xb3, 0xb0, 0x18, 0x67, 0x9d, 0x59, 0xd2, 0x6f, 0xb2, 0xcc, 0x6b, 0xd1, 0xcc, 0xaf, 0x6c, 0xc8, - 0x5a, 0xe7, 0xbb, 0x9f, 0xf7, 0x03, 0xa5, 0x9d, 0xf5, 0xcc, 0x92, 0x7d, 0xb4, 0x67, 0x9a, 0x36, 0x0b, 0x87, 0x9e, - 0xc5, 0xd5, 0x0d, 0x53, 0x10, 0x07, 0x5e, 0x9e, 0x46, 0x2a, 0x03, 0x7f, 0x2a, 0x0a, 0x38, 0x52, 0x4e, 0xf1, 0x5b, - 0x4a, 0x78, 0x37, 0xbf, 0x20, 0x8e, 0xdd, 0x5d, 0xfd, 0x0a, 0x90, 0xb5, 0x85, 0xd5, 0xc1, 0x4d, 0x8e, 0x9b, 0xa8, - 0x21, 0xca, 0xc1, 0xdb, 0x40, 0xbe, 0x34, 0x4f, 0x5a, 0xe2, 0xa8, 0x97, 0x45, 0xab, 0xcf, 0xd3, 0xdc, 0x10, 0xf8, - 0xa9, 0x0b, 0xc7, 0xe3, 0x3c, 0xfa, 0xe6, 0xd0, 0x34, 0xf2, 0x63, 0xd2, 0xf6, 0x80, 0x81, 0xa4, 0x99, 0x68, 0xe3, - 0x23, 0x5b, 0x4e, 0x77, 0x3b, 0x0b, 0xe9, 0x7a, 0x3d, 0x0d, 0xa5, 0xb0, 0x58, 0xb8, 0x70, 0x34, 0x66, 0x9f, 0xd0, - 0xe9, 0xd6, 0x6c, 0x48, 0x74, 0x07, 0xc3, 0x95, 0x18, 0xb9, 0x0e, 0xe3, 0x9c, 0xd9, 0x70, 0x84, 0x95, 0xea, 0xb1, - 0x37, 0x6e, 0x1b, 0x12, 0x3c, 0xa1, 0xe2, 0xc8, 0x03, 0x8f, 0xf0, 0x59, 0x1d, 0x74, 0x78, 0x98, 0x07, 0x2e, 0xf9, - 0x06, 0x73, 0x75, 0x04, 0x03, 0xe5, 0x08, 0x42, 0x11, 0xf9, 0xfe, 0x0e, 0x73, 0xe1, 0xb1, 0x7c, 0x83, 0x99, 0x5a, - 0x79, 0xe1, 0x73, 0xbd, 0xe4, 0x76, 0xc0, 0xf3, 0xf6, 0x13, 0x2f, 0xe9, 0x1a, 0xc1, 0xe1, 0x47, 0x7e, 0xd5, 0x62, - 0xfd, 0x75, 0x1f, 0xf3, 0xe7, 0x41, 0xaa, 0x4b, 0xb8, 0x2a, 0x0c, 0x80, 0x3f, 0xba, 0x32, 0xee, 0x06, 0x0c, 0xeb, - 0x23, 0x44, 0x8d, 0xf0, 0x88, 0xfd, 0xe1, 0xa9, 0x17, 0x00, 0xca, 0x9d, 0x9b, 0x81, 0xc8, 0x42, 0x34, 0x3f, 0x2f, - 0x57, 0xdb, 0xe6, 0x65, 0x68, 0x4b, 0x4b, 0x37, 0x8f, 0x13, 0x49, 0xd8, 0x4c, 0x9c, 0x5a, 0xa8, 0x5e, 0x11, 0x31, - 0x45, 0xcc, 0x02, 0xad, 0x97, 0xf1, 0x7b, 0x7c, 0x67, 0x08, 0xa3, 0x36, 0x6c, 0x84, 0xd7, 0xed, 0x68, 0x6d, 0xf0, - 0x7e, 0xbf, 0xd6, 0x46, 0x21, 0xd8, 0xb7, 0xf4, 0x0b, 0x14, 0x69, 0xd8, 0xd2, 0x8e, 0xff, 0x79, 0xc0, 0x17, 0xfd, - 0x43, 0x08, 0x9b, 0xc4, 0x06, 0x05, 0x85, 0x97, 0xda, 0x64, 0x6f, 0x03, 0x25, 0x4c, 0x62, 0xad, 0xd6, 0x13, 0xf0, - 0xa2, 0x0d, 0x20, 0x15, 0xba, 0x67, 0xcc, 0xaf, 0xc8, 0xe4, 0xf9, 0x13, 0xd2, 0xb2, 0x85, 0x71, 0xca, 0x27, 0xd1, - 0x8e, 0x04, 0x3b, 0x3f, 0x45, 0x91, 0xbc, 0xe2, 0xbb, 0x44, 0x92, 0xaf, 0x4f, 0xbb, 0xf9, 0xcb, 0xdd, 0x83, 0x26, - 0x85, 0x40, 0x07, 0x8f, 0xee, 0x08, 0x19, 0x6a, 0xb5, 0x8c, 0xea, 0xf0, 0x18, 0x8b, 0x4c, 0xcf, 0x1f, 0xce, 0xea, - 0x8b, 0x0c, 0x03, 0x27, 0x96, 0xc0, 0x28, 0x95, 0x5d, 0x6e, 0xd9, 0xd8, 0x9f, 0xf4, 0xde, 0x78, 0x89, 0x52, 0x75, - 0x3c, 0xc7, 0xad, 0x1a, 0xba, 0x43, 0x57, 0xc4, 0x1b, 0x3e, 0xf0, 0xd8, 0xbf, 0xba, 0x31, 0xa8, 0x63, 0x4d, 0x9b, - 0x08, 0x5e, 0x07, 0xfd, 0xcc, 0x14, 0x9c, 0x6c, 0x7c, 0x4a, 0x74, 0x0a, 0x83, 0x04, 0x0a, 0x66, 0x28, 0xf6, 0x99, - 0x96, 0x8f, 0x4b, 0xe9, 0xce, 0x5a, 0x2a, 0xea, 0xd8, 0x38, 0x33, 0xca, 0xfa, 0xe5, 0x72, 0x69, 0xe3, 0x6d, 0x04, - 0xf4, 0x92, 0x7b, 0x79, 0x7f, 0xc5, 0x49, 0xe3, 0x18, 0x91, 0x2c, 0x38, 0x1e, 0x1e, 0xc7, 0x1c, 0xf2, 0xc6, 0xad, - 0x05, 0x1d, 0x26, 0xb4, 0x06, 0x36, 0x3b, 0x67, 0x39, 0xe5, 0x6b, 0x11, 0xce, 0xb2, 0xcb, 0x6f, 0x36, 0x40, 0x04, - 0x84, 0x9e, 0x16, 0x91, 0x04, 0x3e, 0x2b, 0x90, 0x31, 0x47, 0x4e, 0x72, 0x64, 0x79, 0xad, 0xe4, 0x11, 0x48, 0x26, - 0x46, 0x8a, 0xb7, 0xe1, 0xa6, 0x9f, 0xa2, 0x4b, 0x76, 0xb0, 0x51, 0x37, 0x08, 0xa2, 0x04, 0x3b, 0xc0, 0x5f, 0xf8, - 0xf3, 0xa1, 0xef, 0xfc, 0xe9, 0xb7, 0x5b, 0x87, 0xff, 0x27, 0xb8, 0xb4, 0x8f, 0x18, 0x3b, 0xfd, 0x25, 0x56, 0x7d, - 0xf5, 0x7f, 0x73, 0xd7, 0xd0, 0x3a, 0xf0, 0xe1, 0x03, 0x17, 0x1e, 0x7f, 0x1b, 0x96, 0xd0, 0x6a, 0x6b, 0x77, 0x58, - 0x52, 0x88, 0x13, 0xe5, 0xc4, 0x8e, 0xea, 0x3d, 0x8a, 0xf6, 0xc5, 0xd3, 0xfb, 0x23, 0x01, 0xac, 0xbf, 0x7f, 0xe3, - 0x51, 0x69, 0xa4, 0xbb, 0x5f, 0x82, 0x4c, 0x6c, 0xad, 0x4d, 0x90, 0xab, 0xd4, 0x7e, 0x7e, 0xee, 0x5b, 0xeb, 0xa8, - 0xa5, 0xab, 0x6c, 0x70, 0x7f, 0xd1, 0x55, 0x7b, 0xb0, 0xc9, 0xf2, 0x61, 0xbb, 0xb9, 0xb5, 0x4f, 0x2b, 0x57, 0x19, - 0xe1, 0x43, 0x01, 0x02, 0xec, 0x54, 0x99, 0x9c, 0x3c, 0xe3, 0xb7, 0x52, 0xf0, 0x8e, 0xa5, 0x9e, 0xf6, 0x37, 0x9b, - 0xe0, 0xef, 0x0d, 0x6b, 0xbb, 0xab, 0x47, 0xeb, 0x03, 0x08, 0xca, 0xa5, 0xd7, 0x50, 0xc1, 0x21, 0xc4, 0x4b, 0x0a, - 0x12, 0x72, 0x18, 0xce, 0x5c, 0x74, 0x92, 0x43, 0x4c, 0x1b, 0x31, 0xac, 0xab, 0xb4, 0x55, 0x71, 0xe2, 0xb5, 0x3c, - 0xb0, 0x5b, 0x18, 0xb7, 0x60, 0x61, 0x58, 0x64, 0x30, 0xf2, 0x0c, 0xec, 0x70, 0x2e, 0x1e, 0x7a, 0x35, 0x0b, 0x5e, - 0x90, 0x26, 0x5c, 0x96, 0xfa, 0x7d, 0xb0, 0x38, 0x66, 0xf5, 0x55, 0x0b, 0x7e, 0xcd, 0xc1, 0xa9, 0x29, 0x6a, 0x43, - 0x7e, 0xb5, 0x6f, 0x66, 0x84, 0xcb, 0x0b, 0xb9, 0xc7, 0x42, 0x10, 0x2a, 0xdb, 0xb8, 0x65, 0xd2, 0xc1, 0xc9, 0x50, - 0xdf, 0xa7, 0x0d, 0x61, 0x84, 0x17, 0x04, 0x32, 0x4d, 0x51, 0xca, 0xf0, 0x5b, 0xb8, 0xaf, 0x1d, 0xca, 0x06, 0xb9, - 0x99, 0x0e, 0x23, 0xe1, 0x8a, 0xec, 0x38, 0xf0, 0x2c, 0xcd, 0xa7, 0x6a, 0x7f, 0x6c, 0x5d, 0x07, 0xfd, 0xce, 0x25, - 0x44, 0xed, 0x91, 0x9a, 0xf1, 0x31, 0x9b, 0x76, 0x0a, 0xfe, 0xe6, 0x73, 0x29, 0x36, 0x10, 0x1f, 0x69, 0xb9, 0x4b, - 0xa9, 0x89, 0x63, 0xb9, 0xb4, 0xca, 0x38, 0xd4, 0xd0, 0x29, 0x0b, 0x6d, 0x23, 0x97, 0x19, 0x44, 0xda, 0x2e, 0x4e, - 0x49, 0x95, 0x49, 0x1e, 0x8b, 0x13, 0x62, 0xc8, 0x42, 0xbf, 0xc0, 0xda, 0xfe, 0x72, 0xf3, 0x4b, 0x32, 0x54, 0x21, - 0x76, 0xee, 0x10, 0xfa, 0xb0, 0xc0, 0xe6, 0xa5, 0xb4, 0x14, 0x46, 0x15, 0xa6, 0xae, 0xda, 0xea, 0xb9, 0xa5, 0x6d, - 0x48, 0x32, 0x90, 0xcc, 0xb2, 0x84, 0x8f, 0xb2, 0x81, 0x41, 0x8e, 0xff, 0x6d, 0x00, 0xd9, 0xf6, 0x20, 0xd8, 0xde, - 0x32, 0x65, 0xa9, 0xef, 0x2d, 0x7e, 0x9a, 0x84, 0x4f, 0x4c, 0x08, 0x5c, 0x06, 0x5c, 0x75, 0xfe, 0x6c, 0x76, 0x8d, - 0xff, 0x10, 0x06, 0xfe, 0x1b, 0x6e, 0xf4, 0x0d, 0xbe, 0x4a, 0x3f, 0x77, 0xc9, 0xfd, 0xc8, 0xfb, 0x91, 0x3c, 0xdb, - 0x96, 0xc6, 0x4f, 0x5c, 0xac, 0x78, 0x53, 0x7e, 0x0a, 0x7f, 0x33, 0x9a, 0xef, 0xcb, 0xfa, 0xce, 0xb6, 0xd3, 0x47, - 0x60, 0x33, 0xd8, 0x23, 0x3b, 0x74, 0xd7, 0x47, 0xa3, 0x54, 0xcc, 0x1c, 0xf1, 0xed, 0xc3, 0x9f, 0xdb, 0xda, 0x2f, - 0xce, 0x86, 0xe8, 0x3a, 0x30, 0x85, 0xd3, 0xd7, 0x01, 0xca, 0x0e, 0x59, 0x62, 0xda, 0x81, 0x4a, 0x14, 0x1d, 0x74, - 0x66, 0x5d, 0x0a, 0xb0, 0x7c, 0xe3, 0xe8, 0x67, 0x0d, 0xae, 0x95, 0xa4, 0xc3, 0x50, 0x6b, 0x11, 0x9f, 0x4d, 0xa7, - 0xf7, 0xa3, 0x58, 0x51, 0xc0, 0x02, 0xe6, 0xeb, 0x04, 0x76, 0x91, 0xde, 0xbc, 0x3c, 0x92, 0xe0, 0x9c, 0x70, 0x38, - 0x72, 0x81, 0x00, 0x2a, 0xb4, 0x5d, 0x48, 0x13, 0x7e, 0x9d, 0x3b, 0xba, 0xb6, 0x9f, 0x90, 0x5a, 0xb2, 0x1c, 0xe8, - 0xa5, 0xfa, 0xbf, 0xee, 0xee, 0x7e, 0x51, 0x1e, 0x2f, 0xec, 0xed, 0x89, 0x70, 0xcb, 0xb3, 0xaf, 0xac, 0xb0, 0xea, - 0x15, 0xf7, 0xfb, 0x24, 0x13, 0xad, 0xdd, 0x5c, 0x1f, 0xac, 0x4e, 0xd4, 0x2a, 0x78, 0xe8, 0xab, 0xf4, 0x3f, 0x33, - 0xbd, 0xdc, 0x73, 0x53, 0x1e, 0x4a, 0x84, 0x03, 0x5f, 0x34, 0x34, 0x3e, 0x43, 0x35, 0x44, 0xf1, 0x58, 0x0d, 0x38, - 0x8c, 0x49, 0x73, 0xdc, 0x27, 0x58, 0xc9, 0xd4, 0x89, 0x51, 0xb5, 0x11, 0x05, 0x24, 0x98, 0x82, 0xce, 0xa5, 0x2d, - 0xa1, 0x40, 0x05, 0xcd, 0xa2, 0x84, 0x46, 0xdf, 0xf3, 0x61, 0x45, 0x1a, 0x1d, 0xdc, 0x13, 0xc8, 0x08, 0x82, 0xca, - 0xb2, 0xf9, 0xcd, 0x76, 0x35, 0x8a, 0xc2, 0xa9, 0xef, 0x13, 0x0a, 0xca, 0x7f, 0x9c, 0xf9, 0xd2, 0x66, 0xc7, 0xdd, - 0xa3, 0x81, 0x50, 0x54, 0xeb, 0x12, 0x2f, 0x5b, 0x6d, 0xe4, 0x26, 0x37, 0x45, 0xa4, 0x09, 0xc4, 0x1e, 0xfe, 0x04, - 0x4d, 0x52, 0xc4, 0x74, 0x11, 0x37, 0x97, 0xe6, 0xe2, 0xe0, 0x4a, 0xe9, 0xea, 0x81, 0xdb, 0xd0, 0xc8, 0xab, 0x89, - 0x5e, 0xed, 0xe2, 0x0f, 0x02, 0xd1, 0x09, 0x4b, 0x26, 0xf2, 0x8a, 0x81, 0x48, 0x82, 0x81, 0x02, 0x45, 0xdb, 0x82, - 0x29, 0x0a, 0xbd, 0x6e, 0xeb, 0xc5, 0x71, 0x7e, 0x21, 0x53, 0x11, 0x64, 0x2a, 0x6d, 0x6e, 0x80, 0xab, 0x9f, 0xb6, - 0xec, 0x07, 0x1a, 0xff, 0x93, 0x9c, 0x70, 0xd3, 0x43, 0xcf, 0x42, 0x7c, 0xea, 0x3e, 0xb6, 0xde, 0x55, 0xa0, 0x30, - 0xbd, 0x78, 0x11, 0x2d, 0x90, 0xa2, 0x6e, 0xcc, 0x89, 0x25, 0x9f, 0xab, 0x16, 0xdf, 0x57, 0xe5, 0x97, 0x54, 0x50, - 0x43, 0x40, 0x98, 0x09, 0x20, 0x2b, 0xb1, 0x92, 0xcd, 0x2b, 0x72, 0xee, 0x4b, 0xb6, 0x61, 0x27, 0x78, 0x53, 0x6b, - 0x6e, 0x77, 0x46, 0x8c, 0xe0, 0xfd, 0x10, 0x01, 0x21, 0xaa, 0x15, 0x99, 0x25, 0xbf, 0x2a, 0x45, 0x9b, 0x01, 0x0f, - 0xa1, 0x20, 0x2c, 0xce, 0x5e, 0x21, 0xf3, 0x58, 0x2c, 0xf4, 0x03, 0x72, 0x8d, 0xb8, 0x87, 0x43, 0x04, 0x60, 0xd8, - 0xef, 0xee, 0x11, 0x31, 0xd2, 0xe1, 0xc2, 0x44, 0x0c, 0x03, 0x48, 0xd8, 0x06, 0x2e, 0xb3, 0xf3, 0xf1, 0xbe, 0x7b, - 0xff, 0xc7, 0x18, 0xce, 0x0d, 0xd6, 0x4a, 0xb8, 0x75, 0x74, 0xd5, 0x09, 0xf2, 0xf2, 0x3e, 0xe2, 0xd3, 0xdc, 0x8e, - 0xa8, 0x97, 0x03, 0x51, 0x69, 0x35, 0x9e, 0x6d, 0x84, 0x87, 0x65, 0x0a, 0x8f, 0x7d, 0x2e, 0x28, 0x9d, 0x79, 0x09, - 0x2e, 0x01, 0xd5, 0x07, 0x19, 0x5f, 0x79, 0x23, 0xd1, 0xab, 0xcc, 0xc6, 0x9f, 0xc7, 0xf3, 0x3d, 0x6c, 0xd3, 0x45, - 0x1b, 0xd7, 0xd3, 0xe9, 0x1d, 0x4a, 0x32, 0xc1, 0xb4, 0xbb, 0x49, 0x36, 0xec, 0xfa, 0x89, 0xc9, 0x37, 0x2a, 0xe2, - 0x06, 0xa4, 0xf6, 0xdd, 0x38, 0xd0, 0x54, 0xb0, 0xde, 0x7c, 0x4a, 0xa2, 0x81, 0xe9, 0x11, 0xc9, 0xdc, 0xac, 0xd7, - 0xf6, 0x66, 0x0d, 0x01, 0x20, 0x05, 0x8b, 0x96, 0xe0, 0xbd, 0x2b, 0x67, 0x4d, 0x93, 0x12, 0x5b, 0x00, 0x31, 0xdd, - 0x40, 0xe2, 0x38, 0xa2, 0x5a, 0xe3, 0xee, 0x9b, 0xa5, 0x87, 0xf7, 0x3b, 0x62, 0xf7, 0xee, 0x48, 0x6a, 0x7a, 0xe5, - 0x84, 0xed, 0xde, 0x91, 0x53, 0xa3, 0x1c, 0x1f, 0xd5, 0xb3, 0x1b, 0xb6, 0xb4, 0x8e, 0xe5, 0xc9, 0x8c, 0x1e, 0x05, - 0xbe, 0x64, 0xde, 0xbb, 0x7a, 0x50, 0x92, 0x70, 0xf6, 0x0b, 0x01, 0xe2, 0x68, 0xfd, 0x4b, 0xad, 0xd2, 0xa5, 0xe6, - 0x94, 0xfb, 0xbd, 0x0d, 0xfb, 0xaa, 0xb0, 0x72, 0x49, 0x2d, 0x7a, 0x39, 0x99, 0xaa, 0x9e, 0xca, 0xd7, 0x5e, 0xcb, - 0x35, 0xce, 0x86, 0x1a, 0xda, 0x43, 0xef, 0x35, 0x4d, 0xd5, 0xb2, 0x15, 0xce, 0xa2, 0x98, 0xb6, 0x77, 0xd1, 0x9d, - 0x42, 0x63, 0x1f, 0x39, 0x91, 0x38, 0x61, 0x6e, 0xfd, 0x55, 0x1e, 0x89, 0x1d, 0x1e, 0xc1, 0x16, 0xbe, 0x91, 0x74, - 0x48, 0xca, 0x41, 0xc7, 0x09, 0xb8, 0xad, 0x0c, 0x4f, 0x33, 0x10, 0xb1, 0x5a, 0x44, 0x9a, 0xcc, 0x00, 0xc6, 0x31, - 0x45, 0x5c, 0xab, 0x60, 0xa8, 0x41, 0x72, 0xae, 0x06, 0xc1, 0x4c, 0xc7, 0x82, 0x9d, 0xf9, 0x28, 0x3f, 0x41, 0x5b, - 0x1b, 0xb3, 0xb0, 0xd0, 0xb3, 0x31, 0x35, 0xbb, 0x29, 0x01, 0xac, 0x11, 0x74, 0x7b, 0x49, 0x77, 0xcf, 0x0d, 0xc2, - 0xfb, 0xe5, 0xc8, 0xe5, 0x8c, 0xc1, 0x7a, 0xec, 0xa3, 0x6c, 0x71, 0xea, 0xc1, 0x83, 0x00, 0x33, 0x82, 0xc3, 0x56, - 0xb9, 0x81, 0xf6, 0x6c, 0xe8, 0x3f, 0xf0, 0x4d, 0x34, 0xfb, 0xa2, 0xc6, 0x82, 0x83, 0x33, 0xeb, 0xb3, 0x78, 0x57, - 0xc5, 0x04, 0x59, 0xc4, 0x90, 0x24, 0x67, 0x4d, 0x31, 0x37, 0xeb, 0x62, 0x3d, 0x83, 0x40, 0xb0, 0x7c, 0x85, 0xc9, - 0x00, 0xe1, 0x60, 0x76, 0xa3, 0x21, 0x26, 0xd6, 0x93, 0x77, 0xfd, 0x08, 0x80, 0xc0, 0x00, 0xdc, 0xc5, 0xb9, 0xd0, - 0x26, 0x3a, 0x80, 0x22, 0xbf, 0x07, 0x07, 0x40, 0x12, 0x98, 0xa1, 0x48, 0x50, 0xd0, 0xab, 0xd6, 0xbe, 0xe6, 0xc5, - 0x18, 0x0a, 0x2d, 0x24, 0x04, 0xc1, 0x56, 0xee, 0x92, 0x35, 0x2a, 0xb3, 0x75, 0xd0, 0x90, 0xf0, 0xed, 0x59, 0x51, - 0x49, 0x8a, 0x90, 0x5f, 0xe7, 0x81, 0xf4, 0x4f, 0x07, 0x34, 0xf6, 0x1c, 0x25, 0xa7, 0x9b, 0x4c, 0xcc, 0x1a, 0xe2, - 0xe5, 0x69, 0x3d, 0x5b, 0x84, 0x62, 0x0f, 0xdd, 0xa0, 0xcc, 0xc9, 0xd8, 0x89, 0x2f, 0xa8, 0x11, 0x49, 0xfd, 0xe3, - 0x14, 0xd5, 0x83, 0x7a, 0x14, 0x23, 0x93, 0x71, 0x3d, 0xa1, 0x96, 0xaf, 0xb5, 0x1b, 0x81, 0x36, 0x29, 0xcf, 0xb8, - 0xc9, 0xd8, 0x52, 0xbf, 0x54, 0xa8, 0x65, 0xa7, 0xa6, 0x14, 0xec, 0xe4, 0x3c, 0x2f, 0x38, 0x7a, 0x2a, 0x76, 0xc2, - 0x38, 0x08, 0xf6, 0xa7, 0xd3, 0x6e, 0x8d, 0xf7, 0x7c, 0x82, 0x78, 0xbc, 0xea, 0xdc, 0x3e, 0x64, 0x6a, 0xd5, 0xd4, - 0x14, 0x68, 0xc6, 0xd3, 0xf4, 0xfe, 0x3f, 0x80, 0x3e, 0x0f, 0xc1, 0x9e, 0xe9, 0xa3, 0x10, 0xb7, 0x83, 0x18, 0x7f, - 0xd0, 0xc2, 0x4f, 0xf8, 0x1a, 0x25, 0x5c, 0xff, 0x2d, 0x09, 0xd0, 0xf1, 0x83, 0x56, 0x82, 0x2d, 0x49, 0x9c, 0xce, - 0x45, 0xaa, 0x3b, 0xc7, 0x0c, 0xab, 0x20, 0x17, 0x44, 0x8e, 0xe7, 0x3a, 0x8d, 0xca, 0x42, 0x96, 0x22, 0xe1, 0xc6, - 0x2f, 0x7e, 0xcd, 0x96, 0x0a, 0x3f, 0x06, 0x0e, 0x02, 0x51, 0x01, 0x24, 0xec, 0xa7, 0x97, 0xda, 0x73, 0x66, 0xe7, - 0x01, 0x43, 0x16, 0x48, 0x4b, 0x1d, 0xfb, 0x0a, 0x9d, 0x04, 0x00, 0x44, 0xc7, 0xc4, 0x18, 0xc8, 0xab, 0x1d, 0x55, - 0x7f, 0x80, 0x43, 0xef, 0xa4, 0x63, 0x6d, 0xee, 0x26, 0x10, 0x45, 0x08, 0x08, 0x90, 0x58, 0x1b, 0x0a, 0x22, 0x6b, - 0x39, 0x88, 0xa0, 0x4a, 0xec, 0x04, 0x8e, 0xd2, 0x66, 0xc1, 0x8d, 0x78, 0x44, 0x1a, 0x01, 0xf4, 0x0a, 0x2e, 0xc4, - 0x8c, 0xc0, 0x28, 0xcb, 0x48, 0xe3, 0x17, 0x58, 0x68, 0x5c, 0x04, 0xc1, 0xe7, 0x94, 0xb5, 0xde, 0x83, 0x78, 0x3e, - 0xb7, 0x8a, 0xe6, 0x63, 0x42, 0x88, 0x35, 0x00, 0x6b, 0x28, 0xf3, 0xdf, 0xb2, 0x18, 0x30, 0x1a, 0x28, 0xd9, 0xde, - 0xe3, 0xcc, 0x54, 0x2f, 0x2d, 0x57, 0x55, 0x98, 0x32, 0x8f, 0xc8, 0xa5, 0xf3, 0xae, 0x3f, 0x85, 0xf5, 0xa2, 0x76, - 0x41, 0xd3, 0x84, 0xc7, 0xea, 0xa5, 0x7a, 0xd6, 0xc8, 0x0d, 0xc5, 0x7f, 0x52, 0x9a, 0x1b, 0xe3, 0xa8, 0xfc, 0x62, - 0x5a, 0xf5, 0xc9, 0xe8, 0xb0, 0xde, 0x45, 0x76, 0xa7, 0xa2, 0x02, 0xe0, 0xb4, 0x5b, 0x61, 0x9c, 0xd3, 0x2b, 0x7f, - 0xb5, 0xc3, 0x47, 0xab, 0xcc, 0xdc, 0xa2, 0x2e, 0xb3, 0x86, 0x82, 0xf2, 0xd1, 0x54, 0x7e, 0x87, 0xab, 0xbb, 0x3c, - 0x61, 0xf4, 0xa9, 0x2c, 0x8a, 0x53, 0x77, 0x0f, 0x47, 0xfe, 0x75, 0xd8, 0x12, 0x62, 0xa7, 0xba, 0xf5, 0x17, 0x17, - 0x1e, 0x4c, 0x7d, 0xe2, 0x15, 0x6e, 0xdc, 0x42, 0x9f, 0xb1, 0xd7, 0x8c, 0xa1, 0x13, 0x02, 0xc0, 0x3b, 0x4b, 0x14, - 0x65, 0x41, 0xf8, 0xf7, 0x47, 0x9b, 0xa7, 0x45, 0x34, 0x4f, 0xfa, 0x36, 0xde, 0x4e, 0x40, 0x53, 0x60, 0x83, 0x75, - 0x20, 0x30, 0x1f, 0xd0, 0xbf, 0x19, 0x6c, 0xa3, 0xc6, 0xf7, 0xad, 0x2e, 0x8a, 0x10, 0x5b, 0x18, 0x7c, 0x69, 0xfd, - 0xa5, 0x20, 0xb2, 0x3e, 0xa9, 0x01, 0x6d, 0x3f, 0x4d, 0xd6, 0x5d, 0x61, 0x28, 0x79, 0xda, 0xad, 0x87, 0x11, 0x3b, - 0x68, 0x96, 0xf4, 0x86, 0xc9, 0x1f, 0xd2, 0x41, 0xe1, 0x26, 0x26, 0x8b, 0x44, 0xf9, 0xbb, 0x1f, 0x53, 0x92, 0xdc, - 0xf5, 0x0e, 0x67, 0x29, 0xea, 0x2a, 0x4c, 0xfd, 0x59, 0x79, 0xbf, 0x52, 0xff, 0x96, 0xde, 0xd8, 0x42, 0xc3, 0x91, - 0xb5, 0x20, 0x91, 0xd3, 0x30, 0xe4, 0x5a, 0x1d, 0xce, 0x9c, 0xb8, 0xb5, 0xce, 0x76, 0x84, 0x04, 0x1e, 0x96, 0x9c, - 0x25, 0x4c, 0xd5, 0x9b, 0x5a, 0x10, 0x1c, 0x26, 0x82, 0xc2, 0x74, 0x51, 0x9c, 0x22, 0x61, 0xf1, 0x66, 0x87, 0x16, - 0xa7, 0xcb, 0x60, 0xe7, 0xab, 0xfd, 0x44, 0x85, 0x67, 0x6c, 0x16, 0x0b, 0x50, 0x2d, 0xa2, 0xfc, 0x78, 0x31, 0xc0, - 0xee, 0x9f, 0xf0, 0xb1, 0x74, 0x12, 0xb6, 0x1e, 0x74, 0x4d, 0x6a, 0xb9, 0x54, 0x6a, 0x54, 0x5b, 0xc6, 0x35, 0xd7, - 0x50, 0x71, 0xed, 0xf0, 0xd0, 0x76, 0xf8, 0xee, 0x83, 0xf7, 0x45, 0xe2, 0x19, 0x4c, 0xe5, 0x91, 0x43, 0x10, 0x2d, - 0x6e, 0x59, 0xb7, 0x3e, 0x0c, 0x35, 0x97, 0xa7, 0xb0, 0x8f, 0x86, 0x72, 0xba, 0x88, 0x97, 0x24, 0xdf, 0x41, 0x1d, - 0x48, 0x0f, 0x1d, 0x26, 0x7a, 0x7b, 0x5f, 0x35, 0xeb, 0x0e, 0x34, 0xdf, 0xf4, 0x88, 0x40, 0x9b, 0xbb, 0x6a, 0x31, - 0xaf, 0x98, 0xba, 0x44, 0xb7, 0xa4, 0x96, 0x20, 0xee, 0xba, 0x3c, 0x6e, 0x2d, 0x5f, 0x02, 0x29, 0xa5, 0x84, 0x43, - 0xcb, 0xa5, 0xe6, 0xae, 0xf7, 0x1d, 0x87, 0x84, 0xad, 0xd0, 0x92, 0x75, 0xeb, 0x70, 0x1b, 0x6b, 0xfd, 0x29, 0x30, - 0xa9, 0x7f, 0x69, 0x45, 0x38, 0x78, 0x75, 0xc1, 0xba, 0x2d, 0x3e, 0x78, 0x61, 0x5d, 0x83, 0xae, 0x3d, 0xac, 0x44, - 0x87, 0x1d, 0x56, 0xa1, 0xd5, 0x66, 0x2d, 0x71, 0xb5, 0x12, 0xe3, 0x1b, 0xfa, 0xc3, 0x05, 0x27, 0x96, 0x9d, 0x65, - 0x48, 0xe3, 0x91, 0x93, 0xde, 0x8a, 0x3c, 0x55, 0x64, 0xbf, 0x62, 0x46, 0xc5, 0x4f, 0xd7, 0x91, 0xd6, 0x0b, 0x38, - 0x23, 0x94, 0xbd, 0xfc, 0x80, 0x8d, 0x63, 0x0e, 0xb6, 0x65, 0xd6, 0xde, 0xbb, 0x90, 0x56, 0x62, 0x87, 0x08, 0x5e, - 0x71, 0x17, 0xc3, 0x03, 0xcd, 0x0a, 0xc8, 0x98, 0x82, 0x98, 0x50, 0xf0, 0xf7, 0xba, 0x22, 0x64, 0xec, 0xf0, 0xa4, - 0x73, 0x6c, 0xd9, 0xf1, 0x09, 0x0a, 0x70, 0x64, 0x19, 0x18, 0x8f, 0x51, 0xa5, 0xa2, 0x3d, 0x9d, 0xe1, 0x18, 0xd5, - 0x2c, 0xad, 0x98, 0x5f, 0xc5, 0x02, 0x59, 0x01, 0xbb, 0x71, 0xd6, 0xb2, 0xd7, 0x16, 0xb9, 0x44, 0xf1, 0x86, 0xec, - 0x4e, 0x15, 0x99, 0x85, 0xb1, 0x4e, 0x95, 0x2c, 0xb0, 0xf4, 0xb8, 0x26, 0x94, 0xf1, 0x3f, 0x4d, 0x09, 0xca, 0xb7, - 0xfb, 0x9a, 0x4e, 0x2a, 0x34, 0x0a, 0xd7, 0x64, 0x7d, 0x9a, 0x5f, 0xd1, 0x13, 0xb9, 0xc0, 0xba, 0x24, 0x09, 0xe3, - 0x06, 0x31, 0xaa, 0xda, 0x84, 0x80, 0x6e, 0x08, 0xc5, 0x9b, 0x82, 0xd0, 0x94, 0x21, 0xb4, 0x9c, 0xe4, 0xa8, 0x1e, - 0x70, 0x96, 0xc8, 0xcd, 0xc1, 0x6b, 0x04, 0x57, 0xd1, 0x0e, 0x52, 0x54, 0x61, 0xb8, 0x8b, 0x6a, 0x90, 0xe6, 0xda, - 0x23, 0xa5, 0xe0, 0xaf, 0x09, 0xd0, 0x01, 0x08, 0xc3, 0xca, 0xdf, 0xdc, 0xa8, 0xe0, 0x15, 0xca, 0x4a, 0xe9, 0x54, - 0x73, 0x98, 0x26, 0xa6, 0xa5, 0x53, 0x46, 0x3a, 0xe1, 0x07, 0xaf, 0x11, 0xe7, 0x82, 0xa0, 0xb6, 0xab, 0xc5, 0x6a, - 0x30, 0x4c, 0xea, 0xa4, 0x2b, 0x40, 0x3e, 0x6a, 0x1a, 0x4c, 0x68, 0xb7, 0x94, 0xe8, 0x45, 0xd8, 0x2b, 0xb0, 0x9c, - 0x76, 0xb3, 0x5d, 0x03, 0x88, 0xd5, 0x5a, 0xd8, 0x41, 0x06, 0xc6, 0x32, 0xfe, 0x08, 0xc8, 0x03, 0x9f, 0x3e, 0x2f, - 0xad, 0x78, 0x64, 0xbd, 0x72, 0xf8, 0xe1, 0xe3, 0xaf, 0x29, 0x18, 0x2c, 0x15, 0x0d, 0x39, 0xbd, 0xd7, 0xe7, 0xf4, - 0x9d, 0x6c, 0x30, 0xd6, 0xa2, 0x73, 0x10, 0xf9, 0x2e, 0xb4, 0x23, 0xdd, 0x95, 0x75, 0x99, 0x91, 0xed, 0xeb, 0x81, - 0x2c, 0xf4, 0x5c, 0x5f, 0x8a, 0x20, 0xd5, 0x82, 0xc2, 0xdf, 0x01, 0x8a, 0x4b, 0x43, 0x28, 0x0d, 0xe5, 0xa0, 0x8c, - 0x14, 0x8e, 0x32, 0x19, 0xee, 0x34, 0x90, 0x02, 0x32, 0x22, 0x10, 0xcc, 0x99, 0x65, 0xed, 0x2d, 0x16, 0xd8, 0x92, - 0x9d, 0xa9, 0x5b, 0xb5, 0x6b, 0x4c, 0x98, 0x97, 0x39, 0x34, 0x7a, 0xe0, 0xd4, 0x96, 0xd3, 0xa3, 0x68, 0xa9, 0x9e, - 0x4e, 0x86, 0xa2, 0x99, 0x95, 0xa4, 0xb3, 0x97, 0xcf, 0xab, 0x86, 0x56, 0x92, 0x7e, 0x67, 0xa1, 0x06, 0xa4, 0x38, - 0x81, 0x3f, 0xbe, 0x08, 0x21, 0x5f, 0x72, 0x1f, 0xee, 0xe9, 0x2f, 0x3b, 0x0b, 0x4e, 0x2f, 0x51, 0x83, 0x9a, 0xbf, - 0x2c, 0x9c, 0xe9, 0x8d, 0x29, 0x1d, 0x94, 0x38, 0x16, 0x84, 0x3d, 0xbc, 0x97, 0xbe, 0xa8, 0x46, 0xdb, 0x45, 0x45, - 0xc1, 0x74, 0x00, 0xa8, 0x68, 0x1a, 0x0e, 0x1d, 0xd7, 0x9a, 0xa4, 0xac, 0xa4, 0xe2, 0xda, 0xcd, 0x15, 0x9f, 0x3e, - 0x76, 0x8c, 0xd4, 0xba, 0x03, 0x93, 0x78, 0x00, 0xcb, 0x3f, 0x07, 0xde, 0x8f, 0x09, 0x20, 0x5c, 0x4a, 0x79, 0x7e, - 0x71, 0x36, 0xe8, 0xf1, 0xdb, 0xad, 0xb8, 0x17, 0xde, 0xab, 0x8e, 0x31, 0x22, 0x66, 0x0b, 0x21, 0x79, 0xc8, 0x96, - 0x48, 0x6c, 0x36, 0x37, 0x4e, 0xba, 0xdb, 0x1c, 0x75, 0x78, 0x7f, 0xf0, 0x7a, 0xc9, 0x3b, 0x76, 0xa7, 0x69, 0xf0, - 0x41, 0xab, 0x53, 0x23, 0xad, 0xe9, 0x3f, 0xf8, 0xb7, 0x72, 0x91, 0x4e, 0xea, 0x1a, 0x90, 0xe8, 0x7c, 0x09, 0x09, - 0xf6, 0x07, 0x49, 0x91, 0x15, 0x5d, 0x2a, 0x65, 0x1b, 0x15, 0xeb, 0x97, 0x66, 0x39, 0x0b, 0xd7, 0x9b, 0x92, 0x7e, - 0xd9, 0xa5, 0x9b, 0x9c, 0x81, 0x75, 0xc1, 0xaa, 0xec, 0x39, 0xc7, 0xe2, 0x19, 0x32, 0xb1, 0xb0, 0xd7, 0x25, 0xca, - 0x52, 0x17, 0x36, 0x90, 0x64, 0xc7, 0xf0, 0x96, 0xf1, 0xe8, 0x4f, 0x9b, 0xc3, 0xbb, 0x9f, 0xf6, 0xed, 0x83, 0xfc, - 0x79, 0x1d, 0xed, 0x0c, 0x0a, 0x71, 0x29, 0xe9, 0xc2, 0xc3, 0x45, 0x0d, 0x2e, 0x09, 0x2d, 0xbc, 0x2d, 0x21, 0x2e, - 0x1e, 0xc3, 0x79, 0xfb, 0x0e, 0x41, 0xad, 0xac, 0xd8, 0xde, 0x71, 0xc4, 0x42, 0x3a, 0xeb, 0x95, 0x00, 0xfa, 0x2d, - 0x95, 0xb5, 0xb8, 0x23, 0xa7, 0x05, 0x94, 0x44, 0xca, 0x2e, 0xd1, 0xd3, 0xd1, 0xa9, 0xad, 0x3d, 0x9b, 0x0f, 0x6b, - 0x4b, 0xd1, 0x36, 0x12, 0x55, 0x9c, 0x43, 0x1c, 0xa3, 0x61, 0x68, 0x73, 0x6d, 0x6d, 0x8b, 0x3a, 0xcc, 0x50, 0x1d, - 0x6b, 0x08, 0x9b, 0x6e, 0x29, 0xe6, 0x5f, 0xaa, 0x1d, 0x97, 0x6e, 0x0d, 0x86, 0x09, 0xc9, 0x83, 0xa0, 0x4c, 0xc2, - 0xa5, 0xbc, 0xbd, 0xf0, 0x21, 0xdd, 0xd7, 0xeb, 0x77, 0x28, 0xff, 0x6e, 0x41, 0x5b, 0x8b, 0x6f, 0x9a, 0xff, 0x20, - 0xff, 0x2f, 0x1b, 0x30, 0x34, 0xe6, 0xf1, 0xe1, 0x58, 0xd2, 0x46, 0x19, 0x2d, 0xe5, 0x14, 0x1e, 0x3b, 0xd3, 0xf4, - 0x12, 0x4b, 0x87, 0x70, 0x77, 0x27, 0x99, 0x05, 0x87, 0x2d, 0x9b, 0x03, 0x24, 0x28, 0xc1, 0xe4, 0xcd, 0xc5, 0x68, - 0xd3, 0x63, 0xba, 0xc2, 0xe1, 0xbb, 0x15, 0x49, 0x36, 0x7b, 0x8d, 0x8b, 0x18, 0x20, 0x3d, 0x57, 0x30, 0x81, 0x02, - 0xfe, 0x30, 0x43, 0x51, 0x77, 0xe3, 0x5a, 0x4a, 0x31, 0x65, 0x8d, 0x20, 0x98, 0xe5, 0x2d, 0x9e, 0x63, 0xc8, 0xb4, - 0xad, 0x9e, 0xbb, 0x4f, 0x7a, 0xc0, 0x80, 0x13, 0x39, 0xfb, 0xd5, 0x62, 0x43, 0xa8, 0x6a, 0xdd, 0xae, 0xbd, 0x26, - 0xba, 0x42, 0x24, 0x7a, 0x72, 0xd2, 0x69, 0x40, 0x6c, 0x8b, 0x30, 0xe4, 0x50, 0xc8, 0xf8, 0xb8, 0x15, 0x39, 0x93, - 0xf0, 0x19, 0xdf, 0xb2, 0x4b, 0x16, 0x77, 0xa2, 0x99, 0x63, 0xc8, 0x67, 0x26, 0x41, 0xc4, 0xe8, 0x5a, 0x2a, 0xe7, - 0x84, 0x14, 0x5d, 0xa9, 0x47, 0xdf, 0x0f, 0xc8, 0xd2, 0x48, 0x82, 0x38, 0x3a, 0x55, 0x63, 0x9e, 0xff, 0x9d, 0x59, - 0x44, 0x67, 0xf0, 0x0f, 0xe3, 0xcc, 0xb3, 0xaf, 0x88, 0x7d, 0x96, 0x70, 0x32, 0xe9, 0xd5, 0xd6, 0x7a, 0x18, 0x44, - 0x20, 0xe0, 0xf3, 0xdd, 0xe8, 0xcd, 0x46, 0x5b, 0x37, 0x68, 0xbc, 0xa3, 0x79, 0x3a, 0xec, 0xcf, 0xc8, 0xdd, 0xa0, - 0x99, 0xd6, 0x6a, 0x53, 0xe2, 0x33, 0x08, 0x9c, 0xcb, 0x48, 0x35, 0x67, 0x19, 0x98, 0x60, 0xbf, 0x5f, 0x6c, 0x7d, - 0x01, 0xd5, 0x99, 0x11, 0x48, 0xfd, 0xae, 0x7a, 0xa9, 0x55, 0x9a, 0x31, 0xa6, 0xd3, 0x45, 0x6d, 0xaf, 0x0d, 0x1c, - 0xf8, 0x3e, 0xd9, 0xc4, 0xa4, 0xad, 0x5e, 0xe2, 0x04, 0x45, 0x77, 0x68, 0xd1, 0xf9, 0x5e, 0x35, 0xd1, 0x54, 0x66, - 0xec, 0xc9, 0xb8, 0x90, 0xed, 0xeb, 0xed, 0x7e, 0x43, 0xe6, 0xe8, 0x5a, 0xc7, 0x48, 0xc9, 0x45, 0x7d, 0x8e, 0xb8, - 0xca, 0x90, 0x7f, 0x5e, 0xc8, 0x62, 0x47, 0x1c, 0x6e, 0x7f, 0x87, 0x87, 0xd5, 0xa2, 0x2e, 0x66, 0xc7, 0x81, 0x38, - 0x46, 0xfe, 0x21, 0x72, 0x7e, 0x14, 0xb0, 0x19, 0x7e, 0x9a, 0xe1, 0x33, 0x68, 0xb3, 0x37, 0xfb, 0xc9, 0x36, 0xbf, - 0xf5, 0xd8, 0xf5, 0xef, 0x1a, 0x5e, 0xf9, 0xc6, 0x2a, 0x1c, 0x76, 0xdf, 0x76, 0x62, 0xcc, 0xfb, 0xf3, 0xd3, 0xaf, - 0x35, 0x46, 0xde, 0x10, 0xb0, 0xd9, 0xc1, 0xfb, 0x38, 0x67, 0xbf, 0xa5, 0xc3, 0x42, 0x2f, 0x6a, 0x15, 0x90, 0x51, - 0xe7, 0x3e, 0x71, 0x7d, 0x0b, 0x90, 0x56, 0x68, 0xa1, 0xd5, 0xa3, 0x5b, 0x42, 0xf7, 0x12, 0x21, 0xeb, 0x9b, 0x4b, - 0xb1, 0xe9, 0xb4, 0x67, 0x4d, 0x25, 0x25, 0x4d, 0xf1, 0x96, 0x14, 0x8a, 0xdf, 0xcf, 0xa8, 0x93, 0x07, 0xb8, 0xcf, - 0xa7, 0x8d, 0x64, 0xa6, 0xee, 0x26, 0xeb, 0xf9, 0x93, 0xd9, 0x13, 0x4a, 0xdb, 0x30, 0x9a, 0x43, 0x7e, 0xd3, 0x68, - 0x40, 0x8f, 0x47, 0x8b, 0x89, 0xd8, 0x0f, 0x02, 0x14, 0x7c, 0x1a, 0x2a, 0xa0, 0x7a, 0xa0, 0xdf, 0xf6, 0xd7, 0x01, - 0x27, 0x15, 0x31, 0x06, 0x7b, 0x03, 0x50, 0x30, 0x44, 0xb6, 0x91, 0xc5, 0x7b, 0xa1, 0x43, 0xd1, 0x27, 0x09, 0x9d, - 0xe9, 0x85, 0x12, 0x91, 0xd0, 0x23, 0x88, 0xce, 0xe9, 0xae, 0xf8, 0xc6, 0xe6, 0xc3, 0xeb, 0x58, 0xec, 0x59, 0x26, - 0xdf, 0x61, 0xb3, 0xb2, 0x0e, 0xf5, 0x35, 0x93, 0x86, 0xee, 0x45, 0xfb, 0xa8, 0x71, 0xeb, 0x45, 0x42, 0xc7, 0x5f, - 0xce, 0xeb, 0x91, 0x55, 0x6f, 0x89, 0x18, 0xa6, 0x98, 0x79, 0xcf, 0xa2, 0xde, 0xba, 0x68, 0x09, 0xd7, 0xac, 0xab, - 0x0e, 0x82, 0xa6, 0xc4, 0xd3, 0x7a, 0x70, 0x9d, 0x0b, 0xb1, 0xf8, 0xc9, 0x24, 0x5a, 0x3f, 0xf9, 0x6d, 0xdc, 0xa0, - 0xe4, 0x5c, 0x68, 0xd0, 0x85, 0x02, 0xa1, 0xf7, 0xde, 0x7b, 0x9b, 0x8f, 0xf6, 0x36, 0x35, 0xfd, 0x85, 0x79, 0xf1, - 0x47, 0x72, 0xd6, 0x6f, 0x76, 0x39, 0x70, 0x10, 0x4a, 0x9c, 0x30, 0x22, 0x5c, 0xd8, 0x34, 0x97, 0xbc, 0x94, 0x59, - 0xb9, 0x70, 0x86, 0x03, 0xd1, 0x19, 0xf1, 0x0d, 0x3f, 0xd8, 0xb6, 0x40, 0x20, 0x6e, 0xb5, 0x4c, 0x14, 0xcf, 0x88, - 0x38, 0x91, 0x65, 0x0e, 0x93, 0x9a, 0xe6, 0x72, 0xa6, 0x15, 0xbb, 0x6d, 0x05, 0x8d, 0x6f, 0x8c, 0x73, 0x2c, 0x81, - 0xde, 0xac, 0xd0, 0xce, 0xa5, 0x92, 0x8f, 0xfd, 0x8e, 0xaa, 0x9d, 0xeb, 0x2f, 0xaf, 0x65, 0x5e, 0xee, 0x3c, 0xbb, - 0x36, 0xcd, 0xcb, 0x35, 0x86, 0xce, 0x40, 0x66, 0x47, 0x75, 0x95, 0xa9, 0xbb, 0xd8, 0xe0, 0x8e, 0x42, 0x75, 0xb5, - 0x20, 0x1c, 0x80, 0x22, 0x9a, 0xe6, 0x98, 0x1b, 0xcc, 0xa2, 0xaf, 0xae, 0xf0, 0x4e, 0x07, 0x6d, 0xb5, 0xb4, 0x01, - 0x25, 0x20, 0x9c, 0x74, 0xd1, 0x61, 0x89, 0x07, 0x77, 0xa7, 0xee, 0x54, 0xd2, 0x60, 0x5c, 0x2c, 0xce, 0xc3, 0xb3, - 0x28, 0xee, 0x0a, 0xd3, 0xcc, 0x68, 0xf4, 0x03, 0x4d, 0xb4, 0xe7, 0x9b, 0xa5, 0xc4, 0x92, 0x0b, 0x76, 0xb9, 0xc7, - 0xf6, 0x03, 0x45, 0xe2, 0xa5, 0x3c, 0x56, 0x3a, 0xa5, 0xc4, 0x4e, 0x4d, 0x3b, 0x2b, 0xd3, 0x1c, 0x7a, 0x96, 0x65, - 0xe2, 0xb9, 0xf4, 0x3b, 0xaa, 0x67, 0x5b, 0x66, 0x7d, 0x53, 0xb8, 0xdb, 0x3b, 0x91, 0x12, 0x3f, 0x38, 0xd6, 0xf0, - 0xb6, 0xe8, 0x76, 0x9a, 0xbe, 0x2d, 0xdc, 0xfa, 0x05, 0x63, 0x0f, 0x8b, 0x55, 0xac, 0xbe, 0x28, 0x8e, 0x26, 0x14, - 0xd8, 0xea, 0xdf, 0xe4, 0x24, 0x4d, 0xdc, 0x4a, 0xe3, 0xaf, 0x69, 0x09, 0x53, 0x75, 0xaa, 0x7b, 0x2f, 0xb1, 0x8a, - 0xb0, 0x70, 0xff, 0x7d, 0xf5, 0x70, 0x28, 0x64, 0xb6, 0x79, 0xd6, 0x3c, 0x42, 0xba, 0x92, 0x7b, 0xc8, 0xa7, 0x4a, - 0xa6, 0xe6, 0x93, 0x93, 0xec, 0x86, 0xbb, 0x56, 0xab, 0x56, 0xc2, 0x9b, 0x66, 0xab, 0xc3, 0x75, 0xae, 0xd8, 0x68, - 0x99, 0x4d, 0x6a, 0xbb, 0x82, 0xe9, 0xdc, 0x3a, 0xf1, 0x38, 0x44, 0x22, 0x94, 0xb1, 0xbb, 0xbd, 0x51, 0x07, 0x17, - 0xb0, 0x29, 0xc1, 0x5d, 0x29, 0x38, 0x37, 0xd9, 0xe0, 0x2e, 0x88, 0xd4, 0x28, 0xae, 0x74, 0xdc, 0xdb, 0x86, 0x48, - 0xc1, 0x4e, 0x7a, 0xa4, 0x88, 0xc5, 0x69, 0xba, 0xf0, 0x34, 0xbe, 0xf2, 0x66, 0xd7, 0x34, 0x53, 0xdf, 0xa1, 0x46, - 0x8e, 0x68, 0x54, 0xee, 0x65, 0x48, 0x4c, 0x81, 0x87, 0x56, 0xe3, 0x59, 0xaa, 0x42, 0x6e, 0x30, 0xa3, 0x5b, 0xae, - 0xdb, 0xfd, 0xe2, 0xe3, 0x71, 0x39, 0x13, 0xd1, 0x85, 0xf1, 0x95, 0x1a, 0x92, 0x95, 0xec, 0x27, 0x22, 0x2f, 0x38, - 0xa6, 0xb3, 0x37, 0x45, 0x02, 0x6e, 0xe9, 0x8d, 0x8b, 0xb4, 0xa1, 0x5c, 0xcb, 0x06, 0x9d, 0x26, 0x39, 0x15, 0x54, - 0x88, 0x99, 0xb1, 0x66, 0xf1, 0xbe, 0x04, 0x09, 0x87, 0x3d, 0x85, 0x03, 0xd9, 0xd4, 0xcc, 0x6d, 0x87, 0x32, 0xd7, - 0xa1, 0x1a, 0x47, 0x62, 0xa3, 0x72, 0x08, 0x8e, 0xce, 0xdc, 0xee, 0xb1, 0xb0, 0xae, 0x60, 0x4e, 0x15, 0x59, 0x1e, - 0x9c, 0xae, 0xf6, 0x5f, 0xb8, 0x23, 0xfa, 0x62, 0x20, 0xfa, 0x9d, 0x56, 0x4d, 0xb4, 0xc0, 0x43, 0x8b, 0xeb, 0xda, - 0x42, 0x63, 0x0a, 0xe2, 0x80, 0xf4, 0x66, 0x82, 0xa2, 0xe1, 0x93, 0x66, 0x98, 0x83, 0x9e, 0xea, 0x9b, 0x9f, 0x3b, - 0x75, 0xf6, 0x65, 0x9a, 0x5e, 0x18, 0x66, 0x97, 0x06, 0xee, 0x8c, 0xa3, 0xa6, 0x18, 0x36, 0x5f, 0x8c, 0xbe, 0x89, - 0x5c, 0x9e, 0x7b, 0x56, 0x33, 0xc1, 0x34, 0x1d, 0x73, 0xe4, 0xbf, 0xc6, 0xf3, 0x7e, 0xc1, 0x71, 0x8c, 0x4a, 0x2f, - 0xbf, 0x28, 0x73, 0xa6, 0x25, 0x1b, 0xef, 0xab, 0x0b, 0xb8, 0x9e, 0x8c, 0x72, 0x24, 0x1e, 0x96, 0x59, 0x2c, 0x3f, - 0x80, 0x6f, 0x46, 0x2e, 0x41, 0x1b, 0xbb, 0x97, 0x89, 0x01, 0xc0, 0xb2, 0x5d, 0x73, 0x52, 0xbb, 0x46, 0xbe, 0x0a, - 0xb5, 0x55, 0xd7, 0xee, 0x24, 0xf3, 0x95, 0x08, 0xf6, 0x55, 0xfa, 0xe3, 0xa7, 0xa8, 0x07, 0xb5, 0xb7, 0x43, 0x92, - 0xab, 0x4d, 0xc2, 0xbe, 0x5f, 0x56, 0xa7, 0x27, 0xde, 0xbf, 0xc2, 0xe3, 0xe0, 0x02, 0x36, 0x3d, 0xf4, 0xf5, 0xb6, - 0x19, 0x89, 0x51, 0x77, 0x0d, 0xfe, 0xa0, 0xea, 0x21, 0x99, 0x1e, 0x74, 0x92, 0x47, 0x22, 0x30, 0xeb, 0xa9, 0x8e, - 0x89, 0xfc, 0x93, 0xf0, 0x73, 0xb5, 0xe7, 0xff, 0xf2, 0xf5, 0xd2, 0xcc, 0x9e, 0x21, 0xbc, 0x3b, 0xbc, 0xf9, 0xaa, - 0xd0, 0x75, 0xc6, 0xe5, 0xb1, 0x08, 0xe7, 0xce, 0xdf, 0x03, 0x70, 0xe5, 0x75, 0x79, 0xbb, 0x98, 0xef, 0x38, 0xed, - 0x2e, 0x6d, 0xde, 0xad, 0xa3, 0x86, 0x9f, 0x7f, 0xb0, 0x8d, 0x8a, 0x1f, 0xa9, 0x22, 0xfa, 0x75, 0x93, 0x05, 0x45, - 0x20, 0xe4, 0xe9, 0xeb, 0x84, 0x18, 0xff, 0x0c, 0x68, 0xfa, 0xa6, 0x50, 0xd9, 0x7f, 0xc3, 0x15, 0xa6, 0x0e, 0xe1, - 0x8f, 0xcc, 0xea, 0x60, 0x40, 0x73, 0x5b, 0xb8, 0x27, 0xfd, 0x17, 0x88, 0x35, 0x77, 0x10, 0xe0, 0x44, 0x91, 0xa4, - 0xe2, 0x87, 0x3e, 0xbc, 0x82, 0x26, 0xf7, 0x89, 0x14, 0xd4, 0x0c, 0xc5, 0x6d, 0x1b, 0xb8, 0x59, 0x0b, 0xca, 0x47, - 0x87, 0xa8, 0x73, 0xf4, 0x88, 0xdd, 0x5f, 0xda, 0x9d, 0xc9, 0xc3, 0x37, 0x94, 0xac, 0x89, 0x50, 0x31, 0x98, 0x50, - 0xfe, 0x5c, 0xf7, 0x4b, 0xde, 0xb3, 0xf2, 0x95, 0xb1, 0x28, 0xb8, 0xd8, 0x1b, 0x54, 0xfd, 0x00, 0x16, 0xd0, 0x59, - 0x24, 0xa0, 0x62, 0xb7, 0x13, 0xd6, 0xa9, 0xc6, 0xf1, 0x93, 0x58, 0x36, 0xf1, 0xc3, 0xf2, 0x0d, 0xff, 0xa5, 0x21, - 0x24, 0xa1, 0x88, 0x39, 0xa9, 0xc3, 0x60, 0x47, 0x2c, 0x6e, 0x63, 0x36, 0x0f, 0xa5, 0xe6, 0x61, 0x39, 0x71, 0xde, - 0x41, 0x0b, 0x10, 0x97, 0xa3, 0xee, 0xaa, 0xb5, 0x4b, 0xa7, 0x6b, 0x1d, 0x86, 0x93, 0xd8, 0x29, 0x56, 0x78, 0x18, - 0x5b, 0x8f, 0x1c, 0x23, 0xfc, 0x77, 0x20, 0x8f, 0x2f, 0x69, 0x7e, 0x78, 0x7b, 0x47, 0x83, 0x24, 0x1a, 0x2b, 0x15, - 0xa9, 0x78, 0x4a, 0x0f, 0x2b, 0x92, 0x21, 0x4d, 0x24, 0x7a, 0x78, 0x2f, 0xdf, 0xd2, 0x78, 0x58, 0xa5, 0x62, 0x43, - 0xc7, 0xcd, 0x56, 0x07, 0x92, 0x8f, 0xb2, 0xdd, 0x5f, 0x2f, 0xbd, 0x15, 0x9a, 0x75, 0x0a, 0x9b, 0x97, 0x1e, 0xb7, - 0xd8, 0xbb, 0x67, 0x31, 0xf5, 0x53, 0xa0, 0xc6, 0x91, 0x1c, 0x88, 0x89, 0xb1, 0xa9, 0x80, 0x3c, 0xf3, 0xe4, 0xe4, - 0xfd, 0xe0, 0xf5, 0x87, 0x63, 0x1f, 0x4f, 0xa4, 0x7c, 0xcc, 0xce, 0x70, 0xcf, 0xa7, 0x5e, 0x7e, 0xa6, 0x59, 0x1e, - 0x88, 0x9d, 0x8e, 0xe2, 0x21, 0x1f, 0xdd, 0x89, 0x50, 0x23, 0x2c, 0x27, 0x6b, 0xd5, 0x4a, 0x6b, 0x0c, 0x6a, 0x85, - 0x32, 0x97, 0xfb, 0x58, 0xdc, 0xda, 0xfd, 0x68, 0x93, 0xef, 0x7e, 0xa6, 0x88, 0xe7, 0x24, 0x02, 0xb9, 0xfe, 0x61, - 0x90, 0x96, 0x82, 0x79, 0x69, 0xa4, 0x95, 0xfa, 0x13, 0x4a, 0x39, 0xf0, 0x10, 0xf0, 0x25, 0x11, 0x97, 0x86, 0xb6, - 0xfe, 0x07, 0x4c, 0x5e, 0xd7, 0xbd, 0x6f, 0x25, 0xce, 0x9a, 0x70, 0x6e, 0x89, 0x7b, 0xac, 0xe5, 0x27, 0xb5, 0x24, - 0x0f, 0x0a, 0xa3, 0xbd, 0x9d, 0x1e, 0x1a, 0xa6, 0xc5, 0x2b, 0x16, 0xc5, 0x27, 0x7d, 0x2a, 0xbf, 0x07, 0xb5, 0xeb, - 0x2c, 0x75, 0xd9, 0x0b, 0xe5, 0x4c, 0xa9, 0xce, 0x0a, 0xbf, 0x76, 0x18, 0x5a, 0xe9, 0x48, 0x9a, 0x25, 0xce, 0xd5, - 0x7b, 0xec, 0x26, 0x4e, 0xb8, 0x21, 0x0d, 0x14, 0xa8, 0x64, 0x36, 0x1c, 0xd1, 0x53, 0x18, 0xdb, 0xfa, 0x32, 0xc3, - 0xed, 0x87, 0x32, 0xee, 0xe0, 0x68, 0xb2, 0x9a, 0x22, 0x5f, 0x27, 0x45, 0x2c, 0x14, 0x49, 0xd8, 0x85, 0x4b, 0x3b, - 0xbf, 0xc1, 0x5a, 0x69, 0x7e, 0x31, 0x5e, 0x30, 0xde, 0x65, 0x5d, 0xc9, 0x87, 0xcf, 0xba, 0x3b, 0x47, 0x04, 0xc8, - 0xa3, 0x9c, 0xd4, 0x3c, 0x82, 0xdb, 0x84, 0xa8, 0xb7, 0xb7, 0x3d, 0xb9, 0xe1, 0xcc, 0xb6, 0x45, 0x8b, 0x55, 0x2f, - 0x57, 0xb2, 0xdf, 0x9e, 0x95, 0x85, 0x82, 0xec, 0x6e, 0xe0, 0xc8, 0x9d, 0xe9, 0xc4, 0x6f, 0x18, 0x48, 0xef, 0x41, - 0x2d, 0x38, 0xba, 0x6e, 0x01, 0xa8, 0x35, 0xb4, 0x91, 0x4e, 0x5f, 0x23, 0xdb, 0xc8, 0xb8, 0xbc, 0x77, 0x1c, 0x41, - 0x71, 0xc0, 0xf8, 0xfa, 0xde, 0x31, 0x9d, 0x96, 0x80, 0xa4, 0x8f, 0x98, 0x0f, 0x03, 0x8c, 0x82, 0x18, 0x03, 0xd5, - 0xea, 0xf1, 0x01, 0x4f, 0x40, 0xc4, 0x91, 0xad, 0x0e, 0x6e, 0xdc, 0x20, 0x6f, 0x1d, 0x19, 0x07, 0x9f, 0x90, 0x6e, - 0x28, 0x61, 0x30, 0x5e, 0xfe, 0xc8, 0x40, 0x75, 0xa1, 0x8e, 0x0d, 0xae, 0x6d, 0x14, 0x34, 0xce, 0x0c, 0x10, 0x08, - 0x3e, 0xbd, 0x5d, 0xe9, 0xaf, 0xe3, 0x0f, 0x3a, 0xab, 0x37, 0x05, 0xa9, 0x95, 0xd3, 0xa3, 0x36, 0x5b, 0xe8, 0x2a, - 0xa0, 0x70, 0xa6, 0x7a, 0xc2, 0x80, 0xeb, 0x0f, 0x1b, 0x06, 0xe6, 0x3d, 0x27, 0x94, 0xd9, 0x1c, 0x09, 0x7f, 0x49, - 0xb3, 0x6f, 0xd6, 0x30, 0xcf, 0xe5, 0xd8, 0x83, 0x1d, 0x02, 0xb9, 0x7a, 0x18, 0xfb, 0x2d, 0xb6, 0x4d, 0x10, 0xe6, - 0xb0, 0xfc, 0xf8, 0x9f, 0x0a, 0xb5, 0x15, 0x4a, 0xed, 0xcd, 0x8f, 0x1c, 0xd6, 0xce, 0x73, 0x79, 0xfc, 0x4f, 0x28, - 0xf2, 0xd9, 0x3c, 0xe4, 0x79, 0xb2, 0xd8, 0x36, 0x88, 0x3f, 0x3d, 0xb2, 0x77, 0x36, 0xbb, 0xd6, 0x3e, 0xc8, 0xcf, - 0x60, 0x97, 0x7f, 0x0f, 0x09, 0xd5, 0xb0, 0x65, 0x05, 0x3f, 0x8c, 0x47, 0x04, 0x80, 0x85, 0x5e, 0xbf, 0xd9, 0x37, - 0xe4, 0x66, 0x1f, 0x90, 0x19, 0xf4, 0x39, 0xa2, 0x91, 0x67, 0xc6, 0x35, 0xec, 0xcc, 0x73, 0x3e, 0xf7, 0x0c, 0xe7, - 0x07, 0xca, 0x7a, 0xca, 0x9c, 0xe7, 0x25, 0x1b, 0xf7, 0xb6, 0x70, 0x06, 0xba, 0xd5, 0x8c, 0x5d, 0xd8, 0x82, 0xe5, - 0x3b, 0x6b, 0xc1, 0xa9, 0x1b, 0x30, 0x7b, 0x7b, 0xee, 0x4f, 0x74, 0xe0, 0xcf, 0x50, 0xde, 0xc9, 0xa8, 0xd5, 0x6f, - 0xbe, 0x75, 0x3b, 0x8d, 0x01, 0x6f, 0x84, 0xa7, 0x8a, 0xea, 0xcc, 0x39, 0x7b, 0x0a, 0x72, 0x21, 0xfe, 0xa2, 0x1b, - 0x7c, 0x42, 0xb7, 0x2a, 0x0a, 0x01, 0x5f, 0xda, 0x62, 0x44, 0xc8, 0x3a, 0xb4, 0xa4, 0x94, 0x27, 0x6d, 0x3e, 0x51, - 0x73, 0xa7, 0xe8, 0x34, 0xb7, 0x32, 0x3f, 0x9c, 0x39, 0x81, 0x0d, 0x02, 0x49, 0x48, 0x11, 0xc2, 0x3f, 0xc5, 0x8e, - 0x7b, 0x67, 0x6c, 0xb9, 0x91, 0xd0, 0xa0, 0x5d, 0x94, 0x8a, 0x18, 0x1f, 0x95, 0x4e, 0x23, 0xae, 0x7b, 0x8f, 0xf0, - 0x0f, 0xf6, 0x3f, 0xd3, 0xa8, 0x4c, 0xff, 0x9d, 0x46, 0x61, 0xfa, 0xcf, 0x69, 0x08, 0xa6, 0xff, 0x9e, 0x06, 0xbb, - 0x4b, 0xad, 0x0e, 0xec, 0xab, 0x23, 0xfb, 0xea, 0xce, 0x1e, 0xa7, 0xd9, 0x1e, 0x5a, 0x7b, 0x5f, 0x83, 0x76, 0x6c, - 0x3f, 0xf1, 0x2d, 0x39, 0xe0, 0xad, 0x63, 0x59, 0xb2, 0xf1, 0x76, 0x8a, 0xbd, 0xcf, 0xe9, 0xd2, 0xe5, 0x71, 0x1f, - 0xc5, 0x53, 0x1e, 0x87, 0xd5, 0x74, 0x56, 0x51, 0x67, 0x5a, 0xa6, 0x91, 0x3a, 0xbb, 0x7b, 0x28, 0x9e, 0x6a, 0x3e, - 0x42, 0xde, 0xad, 0x25, 0x9c, 0x81, 0xd2, 0x04, 0xf9, 0xad, 0xe7, 0x8f, 0x8d, 0x62, 0x2f, 0x1a, 0x6f, 0xbb, 0xfb, - 0x99, 0x21, 0xce, 0x5f, 0x0c, 0x91, 0x54, 0xa6, 0x15, 0x26, 0xda, 0xc1, 0xd4, 0x6d, 0xcd, 0x5a, 0xac, 0x29, 0x20, - 0xb3, 0x3d, 0x8f, 0xb2, 0x25, 0x08, 0xe1, 0xb9, 0x6d, 0xe1, 0x3f, 0x0b, 0x58, 0x75, 0xb1, 0x85, 0x5e, 0x73, 0x39, - 0xe8, 0xb4, 0x52, 0xe9, 0x3e, 0x6b, 0x10, 0xbb, 0xa1, 0x4c, 0x77, 0x84, 0x8c, 0xe1, 0x05, 0x8b, 0x2b, 0x28, 0xea, - 0x17, 0x62, 0x71, 0x17, 0xb3, 0x87, 0xe7, 0x27, 0x65, 0x1a, 0xfc, 0xbf, 0x16, 0xdb, 0x41, 0x77, 0x42, 0x53, 0xe3, - 0x92, 0x4b, 0x2a, 0xec, 0x17, 0x62, 0xdc, 0x9e, 0xdb, 0x45, 0xd7, 0xb7, 0x4e, 0x19, 0x89, 0xcf, 0xf9, 0x0c, 0xe4, - 0x7a, 0xe9, 0xa7, 0xfa, 0xf4, 0x88, 0x0b, 0xb2, 0xa8, 0xa7, 0x39, 0xc1, 0xaa, 0x10, 0x33, 0x52, 0x87, 0x9a, 0x12, - 0x9f, 0xbf, 0xfa, 0x9f, 0xf6, 0x6b, 0x49, 0x3c, 0x68, 0xa7, 0x5f, 0xf9, 0xf5, 0xb1, 0x10, 0x97, 0xf6, 0x33, 0xf1, - 0xe3, 0xad, 0x62, 0xed, 0x0f, 0xa8, 0x7a, 0x9c, 0xaa, 0xff, 0x3d, 0x6a, 0xd1, 0xaf, 0xc3, 0x65, 0xd3, 0x7f, 0x2d, - 0x89, 0x07, 0xec, 0xf5, 0xeb, 0xf3, 0x3b, 0x18, 0xfc, 0x13, 0x43, 0xf2, 0xc8, 0x76, 0x02, 0x94, 0xe3, 0x47, 0xd1, - 0xe4, 0x38, 0xe4, 0x4c, 0x53, 0xae, 0x2b, 0x3c, 0xbd, 0xea, 0x68, 0x4c, 0x95, 0x8b, 0x23, 0xe9, 0xf4, 0x7c, 0x02, - 0x13, 0xd9, 0xf0, 0x96, 0xd9, 0xa5, 0xc8, 0xde, 0xc3, 0x11, 0x64, 0xb7, 0xcd, 0x27, 0x31, 0xcb, 0x67, 0x11, 0x2d, - 0xdb, 0x35, 0xd8, 0xe8, 0x94, 0xc3, 0x54, 0x5c, 0x38, 0xc0, 0xbe, 0xb7, 0x5c, 0x18, 0xec, 0x46, 0x6a, 0x1f, 0xa2, - 0x72, 0x7a, 0x1b, 0xd1, 0x6f, 0xca, 0x71, 0xf4, 0x7e, 0x1b, 0xac, 0x96, 0xc2, 0xc3, 0x43, 0x83, 0x58, 0xb5, 0xc3, - 0x2b, 0x46, 0xfd, 0xe2, 0x3a, 0xd4, 0x6e, 0x00, 0x4e, 0x9c, 0x69, 0xd3, 0xf5, 0xe3, 0xfc, 0xc2, 0x9f, 0xea, 0xd3, - 0x95, 0xd5, 0x53, 0x0f, 0x5d, 0xc4, 0xd1, 0x19, 0x97, 0x9d, 0x83, 0x12, 0x23, 0x8c, 0x19, 0x9e, 0xbf, 0x37, 0x2b, - 0x4b, 0x28, 0x48, 0x0b, 0xbd, 0x16, 0x54, 0x19, 0xfd, 0xfb, 0x03, 0xc5, 0xb9, 0xbc, 0x7f, 0xae, 0x7b, 0xff, 0x1e, - 0x33, 0xb4, 0xcd, 0x8c, 0x7a, 0xab, 0xe0, 0x3e, 0x9f, 0x24, 0xb0, 0x48, 0x96, 0x58, 0xdb, 0xe2, 0xff, 0xea, 0x12, - 0xeb, 0x34, 0xaa, 0xbd, 0xc2, 0xd5, 0x99, 0xb6, 0xe6, 0xab, 0xfa, 0x52, 0x73, 0xaf, 0xee, 0x47, 0x3f, 0xd8, 0x30, - 0x8d, 0x4b, 0x7b, 0x5a, 0x90, 0x9b, 0x64, 0xcf, 0xa2, 0xc7, 0xe6, 0x64, 0x1c, 0x5a, 0xf5, 0x43, 0x93, 0x00, 0x51, - 0xc6, 0xa9, 0x47, 0x9a, 0xf2, 0x59, 0xee, 0xc3, 0x12, 0x2f, 0xb8, 0x10, 0xd7, 0xc3, 0xed, 0xee, 0x9e, 0x91, 0x1d, - 0xa8, 0xf2, 0x9b, 0x77, 0x87, 0xf7, 0x7d, 0xa4, 0xfc, 0x0e, 0x54, 0xb3, 0xbe, 0x59, 0xa9, 0x08, 0xd4, 0x15, 0x28, - 0x02, 0x5c, 0xbe, 0x67, 0x95, 0xe0, 0xae, 0xe6, 0x79, 0x18, 0xb1, 0x92, 0x84, 0x9a, 0x2b, 0x05, 0x87, 0xc5, 0xa6, - 0x29, 0x45, 0x61, 0xb1, 0x26, 0xfa, 0x75, 0xcd, 0xa6, 0xd3, 0x45, 0x0d, 0x9c, 0x1b, 0x98, 0xa5, 0x9b, 0x35, 0xa2, - 0x1f, 0x12, 0xf2, 0x0e, 0x9e, 0x66, 0x8b, 0x6d, 0x20, 0x86, 0x5a, 0x5c, 0x63, 0x60, 0x7b, 0xf8, 0x90, 0x07, 0xf4, - 0xa4, 0xff, 0x74, 0x0d, 0xd1, 0x23, 0xdb, 0x80, 0xc5, 0x6f, 0x26, 0x75, 0x78, 0xf7, 0x30, 0x3d, 0xe3, 0xa5, 0x4f, - 0xc6, 0x2f, 0x0e, 0x6d, 0x86, 0x9f, 0x1e, 0x51, 0x54, 0x24, 0x2a, 0x77, 0x76, 0xd9, 0xcf, 0x86, 0x4c, 0xed, 0xe9, - 0x78, 0xb2, 0xbf, 0x60, 0x6e, 0xfd, 0x60, 0x7f, 0x18, 0xc7, 0x83, 0x04, 0x35, 0x14, 0x1b, 0xfe, 0x71, 0xa3, 0x58, - 0x24, 0x3d, 0x5b, 0x6f, 0xfb, 0xe0, 0x95, 0x50, 0xce, 0x2b, 0xd7, 0x32, 0x3d, 0xd3, 0xb1, 0x83, 0x67, 0xfa, 0xc1, - 0xea, 0x32, 0x01, 0x95, 0xdf, 0x85, 0x89, 0x81, 0x73, 0x24, 0xca, 0x11, 0x19, 0xf1, 0xa2, 0xe8, 0x4b, 0x36, 0x87, - 0x56, 0x58, 0x0d, 0xba, 0xa5, 0xe8, 0xaf, 0x57, 0x76, 0x97, 0xfa, 0xae, 0xcf, 0x5e, 0xe4, 0xf6, 0xe6, 0x63, 0x19, - 0x3a, 0x14, 0x29, 0x25, 0xa7, 0xfe, 0x64, 0x0c, 0x79, 0x7d, 0x3d, 0x75, 0xa6, 0xf8, 0xcf, 0x6c, 0x90, 0xc4, 0x6c, - 0x00, 0x0a, 0x59, 0x34, 0x8f, 0x00, 0x60, 0x49, 0x5f, 0x24, 0x81, 0x37, 0xfc, 0x43, 0xac, 0x59, 0x37, 0x44, 0xcc, - 0x57, 0xfb, 0xe6, 0xe2, 0x32, 0x0b, 0x77, 0x76, 0xec, 0xd1, 0x3d, 0x79, 0x10, 0x95, 0x25, 0x99, 0x4d, 0x67, 0xed, - 0x3d, 0xa4, 0xaf, 0x0c, 0x79, 0x06, 0x99, 0x32, 0x40, 0x02, 0xa6, 0x23, 0xac, 0x33, 0x3c, 0xb9, 0xe6, 0xdc, 0x6a, - 0xb2, 0x50, 0x82, 0x43, 0x74, 0x1c, 0xdd, 0x0a, 0x49, 0xd6, 0xdc, 0x6b, 0xbe, 0xd4, 0x0f, 0x52, 0x4e, 0x3e, 0xad, - 0x98, 0x27, 0x8e, 0xe3, 0x37, 0x24, 0xa2, 0x27, 0x11, 0xe5, 0x69, 0xd7, 0x39, 0xe4, 0xb7, 0xac, 0x54, 0xcc, 0x0c, - 0x54, 0x3d, 0xf2, 0x54, 0x13, 0xac, 0xbb, 0xc5, 0x1d, 0x88, 0xa7, 0x0f, 0x44, 0x13, 0x8a, 0x93, 0xac, 0xf2, 0x5a, - 0x0f, 0xb3, 0xe1, 0x2b, 0x62, 0x73, 0x35, 0xda, 0xec, 0x58, 0xcc, 0xd8, 0x0a, 0x9a, 0x60, 0x40, 0x9d, 0x11, 0x4e, - 0xbb, 0x76, 0xf7, 0x28, 0x30, 0xb2, 0xe9, 0x94, 0x7e, 0x8c, 0xa8, 0xd0, 0x6d, 0xbe, 0x8c, 0x0a, 0x71, 0x54, 0x84, - 0x9e, 0x87, 0xa1, 0x50, 0xfa, 0x69, 0x59, 0x14, 0xf1, 0x59, 0x3f, 0xe7, 0xae, 0xc6, 0x18, 0x53, 0x34, 0x95, 0x65, - 0xd7, 0x15, 0xc8, 0x1b, 0xb1, 0x35, 0x44, 0x80, 0x3c, 0x5f, 0x35, 0xed, 0xea, 0xd7, 0x9b, 0xa8, 0xfc, 0x3b, 0x36, - 0xba, 0x8d, 0x76, 0x13, 0x78, 0x56, 0x9c, 0xb9, 0x2d, 0x80, 0x35, 0x3a, 0xd7, 0x25, 0x71, 0xe4, 0xe8, 0x71, 0xbd, - 0x1f, 0xcc, 0xfe, 0xa4, 0xb5, 0x78, 0x90, 0x6f, 0x91, 0xa9, 0x95, 0x52, 0x17, 0xea, 0xb5, 0x65, 0x1a, 0xcf, 0x07, - 0x99, 0x49, 0xf9, 0x84, 0xd1, 0xf9, 0xd2, 0x4d, 0xf3, 0xc2, 0x66, 0x81, 0xc9, 0x44, 0x25, 0x4e, 0x61, 0x3a, 0xb7, - 0x4b, 0x84, 0xa4, 0x3b, 0x82, 0x53, 0x59, 0x56, 0x14, 0x77, 0xb7, 0xad, 0xd9, 0x37, 0x93, 0xbf, 0x26, 0x3d, 0x1c, - 0xe3, 0x2e, 0xe8, 0xd8, 0x28, 0x6f, 0x26, 0xdb, 0x83, 0xc9, 0xc3, 0xea, 0x42, 0xe9, 0xb4, 0x9a, 0x6e, 0xea, 0x19, - 0xb9, 0xb9, 0x71, 0xea, 0x6a, 0xa2, 0xeb, 0x12, 0x30, 0x9e, 0x8d, 0xe2, 0x1e, 0x5b, 0xe4, 0x1a, 0x79, 0x6d, 0x2d, - 0x41, 0xb7, 0x2c, 0x14, 0x8b, 0xd1, 0xd4, 0xc0, 0x18, 0x61, 0x52, 0x11, 0x83, 0xd7, 0xe7, 0xb0, 0xc9, 0x13, 0x13, - 0xa8, 0xea, 0xdf, 0x94, 0x93, 0x25, 0xbb, 0x98, 0xa5, 0x91, 0x0c, 0xcb, 0x41, 0xd9, 0x7b, 0xa2, 0xa5, 0x8f, 0x78, - 0x2e, 0x70, 0x6d, 0xdb, 0xf6, 0x61, 0x6d, 0x6b, 0xc0, 0xc0, 0xfb, 0xa6, 0x7d, 0x07, 0xc1, 0x15, 0xbb, 0xd5, 0x9c, - 0x67, 0xf0, 0x78, 0xc0, 0xec, 0x5b, 0xf2, 0x7c, 0x5e, 0xa8, 0xb2, 0x7d, 0xa2, 0xb3, 0xfb, 0x02, 0x42, 0x31, 0xbb, - 0xd1, 0xe5, 0xd9, 0x6e, 0xbb, 0x87, 0x0c, 0x59, 0x90, 0xb1, 0x24, 0x29, 0x3c, 0xf2, 0x9d, 0x5e, 0x6d, 0x19, 0x6b, - 0x3e, 0x73, 0x19, 0xb7, 0xa4, 0x16, 0x94, 0x6a, 0x0f, 0x6d, 0x8a, 0xf2, 0x5a, 0x84, 0x49, 0x15, 0xd6, 0x6e, 0xf3, - 0x99, 0xca, 0xd1, 0x16, 0x91, 0x09, 0x1e, 0x17, 0x12, 0x3b, 0x83, 0x25, 0x82, 0x0c, 0x9d, 0x26, 0x68, 0x6a, 0x9f, - 0x44, 0xb3, 0xb8, 0xe6, 0xc1, 0xa8, 0xa6, 0xca, 0xe1, 0x75, 0x13, 0x26, 0xcc, 0xe3, 0x42, 0xda, 0x76, 0xc4, 0x24, - 0x5d, 0xc7, 0xf9, 0x6a, 0x25, 0xeb, 0x51, 0x8e, 0xcc, 0x73, 0xa5, 0xb3, 0x95, 0x2e, 0xe6, 0x41, 0x29, 0xca, 0xcb, - 0x50, 0x20, 0x91, 0x93, 0xad, 0x66, 0x6f, 0x2f, 0x8d, 0xd5, 0x40, 0xa4, 0x57, 0xd6, 0xc7, 0x23, 0x58, 0x4c, 0x17, - 0x29, 0xa5, 0x60, 0x03, 0x85, 0xb0, 0xd1, 0xd8, 0xb3, 0x56, 0xfe, 0xf9, 0xb9, 0xa6, 0x5a, 0xf5, 0x67, 0x84, 0x6d, - 0xf6, 0x8b, 0xf7, 0xe5, 0x58, 0x05, 0x18, 0x75, 0x2f, 0xb2, 0x22, 0x79, 0xab, 0xcb, 0x5b, 0x24, 0x6f, 0xbe, 0xbc, - 0x32, 0x71, 0xc2, 0xf3, 0xb5, 0xd6, 0x86, 0x09, 0x77, 0x6e, 0x55, 0xeb, 0x88, 0x2b, 0x31, 0xd7, 0x7e, 0xdf, 0xa0, - 0x9f, 0x26, 0xa2, 0xec, 0x5f, 0xcd, 0xa8, 0x90, 0x4d, 0x9f, 0x12, 0xaa, 0xcd, 0xbc, 0x8c, 0x90, 0xbb, 0x17, 0x83, - 0x49, 0xa9, 0x4e, 0x5d, 0xdd, 0xe6, 0xe3, 0x8b, 0x31, 0xb1, 0x7e, 0xf9, 0xd7, 0xb8, 0x38, 0x5f, 0x30, 0x1c, 0xba, - 0xe2, 0xce, 0x7b, 0xd6, 0x0a, 0xe5, 0x0a, 0x73, 0xc0, 0x39, 0x5a, 0xaa, 0x2a, 0x63, 0xd4, 0xb6, 0xea, 0xdc, 0x01, - 0x8f, 0x23, 0x08, 0xfc, 0x8e, 0xae, 0x1a, 0x49, 0x46, 0xa9, 0xef, 0xea, 0x18, 0xfc, 0x65, 0xa4, 0xf1, 0xe1, 0xf7, - 0x05, 0x11, 0xf4, 0xd2, 0x55, 0xe5, 0xda, 0xa3, 0x2a, 0xa2, 0xac, 0x82, 0x24, 0xe6, 0x64, 0xb9, 0x0b, 0x47, 0xb9, - 0xf1, 0xe7, 0x93, 0x5d, 0xad, 0x0d, 0x42, 0xcc, 0x62, 0xcc, 0xd9, 0x52, 0xcc, 0x59, 0x11, 0xbe, 0x7d, 0x1e, 0xfd, - 0xce, 0x55, 0x25, 0x10, 0xf9, 0x68, 0xe3, 0x51, 0x7c, 0xf9, 0x22, 0xe0, 0x69, 0x55, 0x7d, 0x20, 0x24, 0xbe, 0xb3, - 0xd3, 0x2e, 0xf9, 0x6b, 0x7f, 0xa6, 0x44, 0x32, 0x69, 0x89, 0x21, 0x70, 0x47, 0xfc, 0xce, 0x75, 0x03, 0x19, 0x80, - 0x9c, 0xd1, 0xae, 0x01, 0x73, 0x12, 0x4d, 0x43, 0x42, 0xd5, 0xb4, 0x96, 0x77, 0xf3, 0x0a, 0x7d, 0x22, 0x89, 0x7e, - 0x97, 0x37, 0xc3, 0xaf, 0xb4, 0x48, 0xe5, 0x9c, 0xbf, 0xef, 0xe2, 0x57, 0x75, 0x64, 0xe7, 0x4c, 0x63, 0xa5, 0xc0, - 0x97, 0x01, 0xb8, 0x81, 0x76, 0xc5, 0x8d, 0x38, 0xce, 0x92, 0x1e, 0xd9, 0x19, 0x3d, 0x50, 0xdb, 0x57, 0xb2, 0x68, - 0x29, 0x5e, 0x98, 0xa6, 0x10, 0xca, 0xe0, 0x22, 0x3e, 0x91, 0xb9, 0xa8, 0xb2, 0xe6, 0x55, 0x5f, 0xe0, 0x36, 0x22, - 0x66, 0x44, 0x79, 0x22, 0x92, 0x9c, 0x95, 0xba, 0xa1, 0xc1, 0xe2, 0xe8, 0xd2, 0x62, 0x4d, 0x4e, 0x90, 0xcc, 0x97, - 0x88, 0xe0, 0x5f, 0x2e, 0xd5, 0xb3, 0xfb, 0x9b, 0xb5, 0x67, 0x85, 0xb6, 0x46, 0x60, 0xb2, 0x8b, 0x5e, 0xbd, 0xe8, - 0x35, 0xb7, 0x86, 0xf2, 0x21, 0x51, 0xc8, 0xef, 0x49, 0xdd, 0x1a, 0xd6, 0x4c, 0x52, 0x30, 0xdf, 0x17, 0xb5, 0x45, - 0xeb, 0xce, 0xb1, 0x97, 0x3f, 0xea, 0x71, 0xf7, 0x54, 0x82, 0x82, 0xd0, 0x6d, 0x26, 0xb5, 0x40, 0x24, 0x59, 0x63, - 0x8b, 0x7d, 0x22, 0x7a, 0xc7, 0x74, 0xa5, 0x74, 0x71, 0x64, 0xfb, 0xe5, 0x1d, 0x32, 0x93, 0x9c, 0x5d, 0x2a, 0x31, - 0xe5, 0x3f, 0x47, 0xd9, 0xe4, 0x62, 0x67, 0x8b, 0x3e, 0x73, 0x90, 0x36, 0x53, 0x13, 0x61, 0x0a, 0xf6, 0xce, 0xcc, - 0x46, 0x08, 0x8f, 0x24, 0x93, 0xc2, 0x28, 0xb1, 0x97, 0x7c, 0x8a, 0xa7, 0x90, 0xdf, 0x2a, 0x34, 0xb4, 0xe4, 0x99, - 0xfe, 0x07, 0xeb, 0x08, 0xdf, 0x4a, 0xe0, 0x20, 0xc9, 0x3f, 0x60, 0xc1, 0x6d, 0xe9, 0x7a, 0xc7, 0x6c, 0x90, 0x84, - 0xfb, 0x67, 0x96, 0xcf, 0x76, 0x7b, 0x10, 0xbf, 0x29, 0x12, 0x82, 0x2f, 0x3a, 0xaa, 0x5d, 0xb2, 0x8c, 0x4a, 0xaa, - 0x45, 0xe5, 0x7e, 0x7c, 0xcc, 0xcb, 0x23, 0x2a, 0x2f, 0xe0, 0x97, 0xef, 0xd6, 0x1c, 0x98, 0x81, 0xaf, 0xb4, 0xd5, - 0x44, 0xc2, 0x5e, 0x18, 0xec, 0x29, 0x94, 0x2c, 0xe2, 0xc0, 0x6e, 0x36, 0xc4, 0x5c, 0xe8, 0x5a, 0x9b, 0xed, 0xc3, - 0x18, 0x6a, 0x75, 0xca, 0xf4, 0x26, 0xae, 0x6a, 0x84, 0xb9, 0x4d, 0x23, 0x8e, 0x4a, 0x66, 0xda, 0x97, 0x1b, 0x4c, - 0xd3, 0x21, 0x7b, 0x1b, 0x68, 0x2d, 0xdf, 0x1c, 0xeb, 0xca, 0x9b, 0x69, 0x8f, 0x42, 0xc0, 0x18, 0xb1, 0xe6, 0x8a, - 0x5e, 0x6b, 0x65, 0x3f, 0x48, 0xb1, 0x3f, 0xaa, 0x05, 0x22, 0xaa, 0x22, 0xa9, 0xd9, 0xb0, 0xcb, 0x5e, 0xad, 0xbd, - 0xac, 0x0b, 0xb0, 0x74, 0x6a, 0x39, 0xd6, 0xbc, 0x60, 0x30, 0x94, 0xa9, 0x5a, 0x2d, 0x73, 0x87, 0xab, 0xec, 0xa9, - 0x96, 0x97, 0xb2, 0x20, 0x61, 0x2f, 0x81, 0xe8, 0xc4, 0xf7, 0x74, 0x4f, 0x22, 0xdf, 0x99, 0xd3, 0x37, 0x66, 0x32, - 0xc4, 0xe8, 0xac, 0x58, 0x81, 0x55, 0xbd, 0x0d, 0x0c, 0x15, 0xb5, 0xc6, 0x86, 0xee, 0xf2, 0x98, 0x7d, 0x8e, 0xc3, - 0x7d, 0x61, 0xbf, 0x2d, 0xc8, 0x7d, 0xf8, 0xef, 0x59, 0x7e, 0xdb, 0xd8, 0x2c, 0xcc, 0xb2, 0xde, 0x3c, 0x46, 0x8e, - 0xf0, 0x7a, 0x4b, 0xa5, 0xca, 0x16, 0x0c, 0xd9, 0x6b, 0x58, 0xf7, 0xab, 0x99, 0xba, 0x90, 0x6e, 0x62, 0x46, 0x8c, - 0xda, 0x9d, 0x48, 0x12, 0xf4, 0x14, 0x33, 0x28, 0xa1, 0x79, 0x91, 0xd6, 0x66, 0x23, 0xf7, 0x60, 0x9d, 0x8e, 0x4c, - 0x44, 0x97, 0x60, 0x8a, 0x73, 0x36, 0xdf, 0xdf, 0x61, 0xc8, 0x61, 0x8f, 0x25, 0xaa, 0xe4, 0x41, 0xbd, 0x6f, 0x65, - 0xa5, 0xa6, 0xd8, 0x74, 0x2c, 0x23, 0x2e, 0x37, 0x80, 0x83, 0x1d, 0x6d, 0xa7, 0x86, 0x39, 0xb5, 0x73, 0x09, 0x76, - 0x72, 0x53, 0x7b, 0xb7, 0x22, 0x03, 0x4b, 0x1e, 0x08, 0x55, 0x18, 0xf0, 0x69, 0x5f, 0x11, 0x00, 0x9a, 0xe3, 0x14, - 0x89, 0x3f, 0x8c, 0xe4, 0xef, 0x77, 0x24, 0x9d, 0x84, 0xe3, 0x6e, 0x24, 0x70, 0x7a, 0x1c, 0xc0, 0x28, 0xf5, 0x64, - 0xf3, 0x03, 0x10, 0xe5, 0x22, 0x7f, 0x95, 0x18, 0x1d, 0x31, 0x44, 0x38, 0xf0, 0x53, 0x71, 0x21, 0x6d, 0xbd, 0x59, - 0x2e, 0x8e, 0x46, 0x41, 0xd7, 0xd5, 0x01, 0xf7, 0x91, 0x0a, 0x00, 0x6d, 0x36, 0xe4, 0xba, 0xbe, 0x77, 0x88, 0xd8, - 0x7d, 0x5a, 0xb8, 0x41, 0x04, 0x7e, 0x5c, 0xa6, 0xe6, 0x5b, 0x38, 0x5c, 0xd3, 0x4c, 0xbd, 0x95, 0xc7, 0xd3, 0x7d, - 0xdd, 0xed, 0x0c, 0xf0, 0x2f, 0x97, 0x38, 0x60, 0xfe, 0x3b, 0xa9, 0xe2, 0x60, 0xc4, 0x1c, 0x1d, 0xe3, 0x92, 0x66, - 0x66, 0x6a, 0xc8, 0x73, 0x73, 0xe5, 0x29, 0xca, 0x81, 0xcc, 0x27, 0xd3, 0x43, 0x76, 0x13, 0xf8, 0x35, 0x40, 0x5d, - 0x3c, 0x24, 0x54, 0x80, 0x7a, 0x82, 0xc0, 0xd5, 0x04, 0xc2, 0xb2, 0xf9, 0x73, 0x6c, 0xee, 0x99, 0x68, 0x02, 0x1a, - 0x3a, 0x50, 0xe9, 0xf4, 0xa4, 0x2c, 0x80, 0x7a, 0xa8, 0xfd, 0x10, 0x09, 0x0f, 0x7a, 0xd9, 0x74, 0xd0, 0xb8, 0x8e, - 0x46, 0x48, 0x9a, 0x50, 0x90, 0xb8, 0xc0, 0x29, 0xfa, 0x6a, 0xc9, 0xfc, 0x55, 0x22, 0xdf, 0xa8, 0x72, 0xd1, 0xe0, - 0x5f, 0xa2, 0x45, 0x56, 0xcf, 0x18, 0x99, 0x1d, 0x6d, 0xca, 0x4a, 0x65, 0xbc, 0x00, 0xf0, 0xe1, 0x10, 0x1c, 0x49, - 0x44, 0x2c, 0x93, 0x68, 0x22, 0x1b, 0x87, 0xf2, 0xa7, 0x97, 0x77, 0x0a, 0xe0, 0x7a, 0x1e, 0x09, 0x9a, 0x08, 0x7c, - 0x6c, 0x09, 0x38, 0x33, 0x83, 0x00, 0x67, 0xab, 0x4d, 0x23, 0x30, 0x11, 0x5a, 0x79, 0x6a, 0xd8, 0xc7, 0x48, 0x94, - 0xe3, 0x12, 0x61, 0xe4, 0x76, 0x4d, 0x19, 0xb9, 0x44, 0x24, 0x36, 0xe6, 0xc8, 0x73, 0xfd, 0x51, 0x0a, 0xfd, 0xcb, - 0x2c, 0x7c, 0x52, 0xa2, 0xfa, 0xd2, 0xa0, 0x78, 0xd3, 0xc6, 0x07, 0x3b, 0x18, 0xd7, 0x52, 0xcb, 0x61, 0xc2, 0x60, - 0xbe, 0xf6, 0xff, 0xc6, 0xd7, 0x4a, 0xf5, 0x95, 0xf3, 0x11, 0x5d, 0xf1, 0x18, 0x1c, 0xd6, 0x89, 0x9c, 0x5f, 0x37, - 0x4d, 0xe4, 0xf8, 0x73, 0x79, 0xb7, 0x37, 0x9e, 0xee, 0x36, 0x82, 0xca, 0xa5, 0x34, 0x67, 0x9e, 0x11, 0x7d, 0x18, - 0x58, 0xbe, 0x58, 0xa3, 0xca, 0x34, 0xbe, 0x74, 0x40, 0xb9, 0xe2, 0xf0, 0xf4, 0x24, 0x10, 0x2f, 0x07, 0x7b, 0x8a, - 0x03, 0x62, 0x45, 0x89, 0xb2, 0x67, 0x2a, 0x22, 0x8d, 0x43, 0x60, 0xbd, 0x11, 0x19, 0x19, 0x02, 0x52, 0x86, 0xed, - 0x9a, 0xf5, 0xaf, 0x58, 0x51, 0x7c, 0x0e, 0xc9, 0x26, 0xcb, 0x66, 0x7c, 0x8a, 0x1d, 0x8d, 0x57, 0x22, 0xa9, 0x68, - 0xd9, 0xf4, 0xa7, 0x6d, 0x47, 0xf6, 0x5e, 0x82, 0x43, 0xe2, 0x00, 0xa7, 0xf4, 0xce, 0x9f, 0xcb, 0x3b, 0xab, 0x23, - 0xdf, 0x83, 0xfe, 0x95, 0x97, 0xfc, 0x95, 0xef, 0xc1, 0x9e, 0xf2, 0x92, 0xa7, 0x7c, 0x0f, 0xf8, 0x94, 0x17, 0x89, - 0xe2, 0x34, 0x7d, 0xe5, 0x70, 0x1e, 0x8c, 0x11, 0x43, 0xb9, 0x3c, 0x91, 0xad, 0xe4, 0xe0, 0x17, 0xef, 0x13, 0xee, - 0xb3, 0x81, 0x94, 0x3c, 0x66, 0xa6, 0x58, 0x89, 0x4a, 0x56, 0x96, 0x49, 0x01, 0x7c, 0xea, 0xdb, 0xc5, 0x99, 0xed, - 0x17, 0xfc, 0xe1, 0x97, 0x48, 0x1a, 0x03, 0x71, 0xa7, 0x04, 0x5d, 0xbb, 0xd3, 0xd7, 0x9e, 0xdb, 0x8a, 0x08, 0xca, - 0x72, 0xc4, 0xe8, 0x44, 0xa5, 0x1d, 0x67, 0x89, 0xde, 0xbd, 0x55, 0x18, 0x08, 0xfa, 0x76, 0xa1, 0x8f, 0xc8, 0x61, - 0xfd, 0xfd, 0x17, 0x20, 0x64, 0x5c, 0x12, 0x88, 0xc0, 0x3e, 0xf6, 0xea, 0x39, 0xd4, 0xda, 0xf3, 0xa4, 0x8a, 0x06, - 0x5c, 0x93, 0x83, 0x71, 0x8b, 0x90, 0xa4, 0xa7, 0xff, 0x88, 0x1f, 0x92, 0xb3, 0x74, 0xd1, 0x3c, 0x0b, 0xf7, 0x18, - 0x2d, 0x07, 0x34, 0x27, 0x41, 0xd5, 0xcc, 0x72, 0x45, 0x34, 0x9a, 0xd3, 0x9e, 0x79, 0xe0, 0x64, 0xa9, 0xb6, 0xae, - 0xc2, 0x9d, 0xf7, 0xf8, 0x19, 0x9f, 0xd1, 0x71, 0x9a, 0x1e, 0x19, 0xb0, 0x2f, 0x72, 0x7a, 0x1f, 0x5c, 0xe1, 0x58, - 0x6b, 0x30, 0x3d, 0x91, 0x6c, 0x2d, 0xae, 0xaf, 0xc6, 0xf8, 0x82, 0xb4, 0xca, 0xfb, 0x52, 0x44, 0xc3, 0x4f, 0xc9, - 0xc4, 0x66, 0xa4, 0xa5, 0x21, 0x5f, 0x5b, 0x6a, 0xb4, 0x59, 0xb1, 0x04, 0x4b, 0x6e, 0xbf, 0x75, 0x85, 0x4d, 0xdf, - 0x64, 0x2e, 0x92, 0x6c, 0x54, 0xdd, 0x14, 0x69, 0x53, 0xe0, 0x53, 0x92, 0x15, 0xc1, 0x23, 0x50, 0x64, 0xee, 0x48, - 0x72, 0xbc, 0x74, 0xd4, 0x72, 0xaa, 0x4a, 0x88, 0xc2, 0x67, 0x15, 0x56, 0xb5, 0x14, 0x1d, 0x2f, 0x0e, 0x44, 0x08, - 0x39, 0x8c, 0x4b, 0xa7, 0xa6, 0xd1, 0x75, 0xad, 0xf6, 0x16, 0xf2, 0x1c, 0x7c, 0xf9, 0x69, 0x88, 0x25, 0x2c, 0x59, - 0x8d, 0x31, 0xe0, 0xcc, 0xb3, 0x1b, 0x7a, 0xe4, 0xb9, 0xbc, 0x1f, 0x80, 0xa3, 0x17, 0xbb, 0x16, 0x8a, 0xb9, 0xeb, - 0x33, 0x12, 0x09, 0x24, 0x32, 0x5f, 0x00, 0x70, 0x00, 0xc0, 0x55, 0x2f, 0xab, 0x3a, 0x80, 0x41, 0x2b, 0x55, 0xa0, - 0x67, 0x0a, 0x6e, 0x81, 0xcc, 0xd0, 0x72, 0x50, 0xf9, 0x23, 0x0a, 0x7c, 0xed, 0x90, 0x2c, 0x26, 0xb2, 0x34, 0x94, - 0xac, 0x63, 0x42, 0x3b, 0x1f, 0xc6, 0xd3, 0x4b, 0x14, 0xee, 0x22, 0xa5, 0xa3, 0xb6, 0xe8, 0xa7, 0x67, 0x8f, 0x7a, - 0x5a, 0x38, 0x2d, 0x22, 0x2f, 0xc7, 0xc6, 0xbf, 0xe5, 0xed, 0xba, 0x5c, 0x7f, 0x64, 0x2b, 0x9a, 0x82, 0x36, 0x04, - 0x9f, 0x3d, 0x5b, 0xf5, 0x8c, 0xbe, 0x42, 0x4e, 0x8b, 0xe5, 0xd0, 0xeb, 0xb7, 0x59, 0x6d, 0x0e, 0x1f, 0x9e, 0xd1, - 0x03, 0xd1, 0xa5, 0xbb, 0xd7, 0x48, 0xa0, 0xb9, 0x44, 0xb0, 0x18, 0x9e, 0xe3, 0xd2, 0x6e, 0x72, 0x28, 0x29, 0x0a, - 0x62, 0x15, 0xf8, 0x80, 0xde, 0xde, 0xc1, 0x90, 0xc1, 0xae, 0xdb, 0x18, 0xc0, 0xd5, 0x40, 0xf3, 0xac, 0xc5, 0x61, - 0xaf, 0x4f, 0xc0, 0xc6, 0xd2, 0xf9, 0x6a, 0xdb, 0x45, 0xb1, 0xd7, 0x5c, 0x91, 0xee, 0xda, 0x0a, 0x81, 0x3f, 0x17, - 0x9f, 0xfc, 0xed, 0x79, 0xd1, 0xfe, 0xd6, 0x7f, 0x29, 0xbd, 0x77, 0xaa, 0xf6, 0x7b, 0xa3, 0x01, 0xf5, 0xc7, 0xa9, - 0x65, 0xa9, 0x2c, 0x87, 0x59, 0xe9, 0xf9, 0x68, 0x54, 0x8b, 0xdb, 0x6b, 0x8a, 0x88, 0x8f, 0x92, 0xe0, 0x66, 0x53, - 0x2b, 0x0f, 0xee, 0x9d, 0xed, 0x85, 0xbe, 0x87, 0x81, 0xea, 0x5e, 0x0b, 0x4d, 0x77, 0x2e, 0x25, 0xa8, 0xc9, 0xc8, - 0x68, 0xa6, 0xd9, 0x98, 0xb7, 0xbe, 0xf9, 0x61, 0xaa, 0x5f, 0xd0, 0xa7, 0x52, 0x72, 0x20, 0x3b, 0xab, 0x8b, 0x52, - 0xb1, 0x2a, 0x09, 0xc1, 0xe2, 0xfa, 0xbb, 0x38, 0x24, 0x30, 0x70, 0xad, 0xba, 0x0c, 0x18, 0x89, 0x7d, 0xb1, 0xf8, - 0xa8, 0xe8, 0xf8, 0xad, 0x1d, 0x64, 0x70, 0xb3, 0xcb, 0xbc, 0x0c, 0x33, 0xfc, 0x20, 0xac, 0xd3, 0x9a, 0x82, 0x40, - 0x4d, 0x9d, 0xbc, 0x4a, 0x82, 0x62, 0xf9, 0x66, 0x4f, 0x1b, 0x7b, 0x61, 0x6c, 0x03, 0xe8, 0xd9, 0xa6, 0x91, 0x18, - 0x4c, 0xfc, 0x93, 0x63, 0xf7, 0x57, 0xa1, 0xd2, 0xaa, 0x6b, 0x51, 0x7b, 0x51, 0x1e, 0x84, 0x0e, 0xa1, 0x43, 0x51, - 0x2b, 0xb7, 0x75, 0x58, 0x9f, 0xd7, 0xb2, 0x3c, 0xe9, 0x9f, 0x78, 0xbb, 0x48, 0xd7, 0xc5, 0x1d, 0x0c, 0x35, 0x12, - 0xc8, 0x8e, 0x2a, 0x36, 0x69, 0xdc, 0x51, 0xa9, 0xb2, 0xdc, 0x7d, 0xd8, 0xa0, 0x5e, 0x5b, 0x44, 0x90, 0xa9, 0x3e, - 0xae, 0x08, 0x35, 0x86, 0x3d, 0xa1, 0x34, 0x05, 0x4c, 0x65, 0x95, 0xc5, 0x53, 0x73, 0x77, 0x7e, 0xc8, 0x99, 0xd2, - 0x70, 0xc0, 0xc7, 0xb0, 0x98, 0x0d, 0xfc, 0xfb, 0x21, 0xd2, 0xc0, 0x4d, 0xad, 0x67, 0xa1, 0x4c, 0x20, 0xad, 0x50, - 0xcc, 0x47, 0x32, 0x0a, 0x13, 0x7c, 0xcf, 0x19, 0x14, 0x39, 0x29, 0xd9, 0x68, 0xfc, 0x66, 0xdd, 0x63, 0xe8, 0x38, - 0x33, 0x3e, 0xcc, 0xd3, 0x15, 0x7b, 0xa5, 0xc0, 0xd5, 0x21, 0x14, 0x5c, 0x8e, 0xa3, 0x1a, 0xd7, 0x05, 0xd9, 0x40, - 0x49, 0x5e, 0x78, 0x8f, 0x24, 0xa4, 0x2e, 0xf4, 0x94, 0x6a, 0x47, 0x21, 0x19, 0x96, 0x60, 0x7a, 0xdc, 0x9d, 0xb1, - 0x8d, 0x2b, 0x69, 0xdb, 0xd9, 0x69, 0xa1, 0x6e, 0xcf, 0xc0, 0x83, 0x5d, 0xd5, 0x3b, 0x28, 0x47, 0x56, 0x06, 0x1a, - 0x8c, 0x80, 0xb6, 0x2c, 0x49, 0xaa, 0x89, 0xd8, 0x68, 0x14, 0x46, 0x0d, 0xa5, 0xb5, 0x92, 0x9d, 0x9c, 0xdf, 0xeb, - 0xaa, 0x98, 0x26, 0xeb, 0x50, 0x1c, 0xf4, 0xc4, 0x24, 0xa5, 0x79, 0x5d, 0xb6, 0x78, 0x7f, 0xa0, 0xf6, 0x8b, 0x54, - 0x7b, 0x62, 0x6f, 0x6e, 0x24, 0x4a, 0xb3, 0xef, 0x29, 0x94, 0x87, 0x46, 0xe7, 0xb5, 0xd1, 0x79, 0x79, 0x1d, 0xa5, - 0xff, 0x9b, 0xa8, 0x65, 0xca, 0xc6, 0x98, 0xa2, 0x0e, 0x68, 0x3e, 0x31, 0x82, 0xf6, 0xfd, 0x4b, 0xa1, 0x8e, 0x50, - 0x34, 0x4c, 0x3d, 0xce, 0xb0, 0x18, 0xe9, 0x06, 0xcf, 0x97, 0x84, 0x04, 0xf7, 0xee, 0xc0, 0xc0, 0x23, 0xc2, 0x3c, - 0x91, 0x31, 0x9d, 0x20, 0x0c, 0x51, 0x59, 0x27, 0x67, 0xde, 0xe7, 0xe6, 0xdf, 0x6e, 0x4a, 0xdb, 0x6e, 0xfa, 0x0d, - 0x26, 0xe7, 0xfd, 0x94, 0xde, 0x7b, 0x79, 0xb4, 0x69, 0x7f, 0x31, 0xb2, 0x5b, 0xf0, 0xc2, 0xe2, 0x3d, 0x16, 0xf6, - 0x2f, 0x65, 0x3e, 0x73, 0xac, 0xf4, 0x36, 0x63, 0x20, 0x83, 0x67, 0xd6, 0x58, 0xfe, 0x4a, 0xd0, 0x8e, 0x42, 0xa0, - 0x9d, 0xd8, 0x09, 0x59, 0x05, 0x09, 0x88, 0xc4, 0x58, 0xdb, 0xce, 0xc1, 0x40, 0x3b, 0xd6, 0x99, 0x5b, 0xb4, 0xf4, - 0xdd, 0x53, 0x4e, 0x4a, 0x00, 0xca, 0x4b, 0xe5, 0x9f, 0x5d, 0x9c, 0x1a, 0xfb, 0x38, 0xc1, 0xd8, 0x0a, 0xec, 0x43, - 0x02, 0xa9, 0x2a, 0x26, 0x74, 0x9a, 0x22, 0xa0, 0x8b, 0x63, 0x13, 0x7f, 0x6e, 0xdc, 0x9d, 0xc1, 0xea, 0x69, 0xbe, - 0x64, 0x9b, 0xdd, 0x4b, 0x8c, 0xa9, 0xcd, 0x3a, 0xdb, 0x16, 0xf3, 0x8c, 0xdc, 0x95, 0xc5, 0xda, 0x04, 0x52, 0xb6, - 0xe4, 0xca, 0xb5, 0x45, 0xc8, 0x84, 0x21, 0xeb, 0x1a, 0x42, 0x52, 0x20, 0xf8, 0x2d, 0x25, 0x81, 0xd5, 0xfb, 0xa5, - 0xae, 0xd4, 0xb3, 0x88, 0x76, 0x99, 0xa0, 0x1d, 0x1c, 0x39, 0xd2, 0x79, 0xe1, 0xff, 0xb7, 0x12, 0x42, 0x38, 0xd3, - 0x86, 0x6e, 0x4b, 0x68, 0x92, 0xe2, 0xe8, 0x2a, 0x5a, 0x40, 0xbe, 0xeb, 0xf5, 0x2f, 0x8d, 0xcd, 0xfb, 0x0e, 0x9e, - 0x0d, 0x22, 0x81, 0x8d, 0xa8, 0xa9, 0x51, 0x0d, 0xab, 0xad, 0x6e, 0xda, 0x6e, 0x1e, 0xdd, 0xde, 0xc8, 0xc7, 0x50, - 0xe1, 0xe8, 0xe7, 0x40, 0x89, 0xe3, 0xde, 0x34, 0xa5, 0x4d, 0x54, 0xfe, 0x17, 0xaa, 0x05, 0x4e, 0xe1, 0x93, 0x1b, - 0x9c, 0x0a, 0x4e, 0xbb, 0xa7, 0x86, 0xe2, 0x7e, 0xbf, 0x54, 0xd1, 0xf5, 0x71, 0x73, 0x95, 0x01, 0x9a, 0xf0, 0x08, - 0x72, 0x39, 0xf2, 0xfd, 0xbc, 0xae, 0xfc, 0x22, 0xbf, 0xf4, 0xed, 0x6b, 0x47, 0xd8, 0x42, 0x8d, 0xb4, 0xd0, 0xe3, - 0x24, 0xbf, 0x2c, 0x6f, 0x92, 0xee, 0x90, 0x81, 0xeb, 0x2f, 0x6b, 0xec, 0x1d, 0xaa, 0xf2, 0xd8, 0x6d, 0x8f, 0x14, - 0x08, 0xd3, 0x49, 0x97, 0xca, 0x5d, 0x29, 0x25, 0x62, 0xd4, 0x86, 0xb3, 0x4e, 0xd5, 0xa2, 0xb6, 0xb0, 0x9e, 0x2d, - 0x74, 0x93, 0x0a, 0x08, 0xd5, 0xf7, 0x94, 0x87, 0x63, 0x60, 0xd8, 0x3b, 0x5f, 0x1e, 0x27, 0x0b, 0xe7, 0x53, 0x5d, - 0x2b, 0xe7, 0xa9, 0xb6, 0xeb, 0xbe, 0xce, 0x50, 0x60, 0x6c, 0xe9, 0x1d, 0xbb, 0x0c, 0xe7, 0xb7, 0xea, 0x0a, 0x4f, - 0x96, 0x11, 0x3c, 0x4b, 0x7d, 0x42, 0xf0, 0x55, 0xf2, 0x48, 0xe1, 0xc1, 0xd1, 0xcd, 0x59, 0x40, 0x07, 0xd3, 0x49, - 0xe0, 0xc1, 0xf1, 0xb6, 0x56, 0xc9, 0xfa, 0xe0, 0xe5, 0x98, 0x10, 0x06, 0x94, 0xac, 0x0f, 0xb6, 0xdd, 0x58, 0xb9, - 0x44, 0xe3, 0xf5, 0x23, 0x0b, 0x2d, 0xba, 0x7e, 0xd0, 0xa6, 0xe7, 0x01, 0x53, 0x39, 0xaa, 0xae, 0xe7, 0x29, 0x67, - 0x87, 0x98, 0x27, 0x8c, 0x74, 0x62, 0xd0, 0xc8, 0x0e, 0x98, 0x47, 0xa9, 0xf9, 0xa9, 0x75, 0x9b, 0x6f, 0xf6, 0xe1, - 0x33, 0x61, 0xac, 0xd5, 0x46, 0x91, 0xcf, 0x53, 0x68, 0xcf, 0x8e, 0xbc, 0xdf, 0xa8, 0x21, 0x23, 0x6b, 0xd3, 0x15, - 0x90, 0x8c, 0x05, 0x7f, 0x36, 0x43, 0x58, 0xd4, 0x4a, 0xc6, 0x69, 0x62, 0x9f, 0x4d, 0x37, 0x91, 0xae, 0xf2, 0x55, - 0x04, 0x78, 0xbf, 0xe1, 0x4c, 0x9a, 0xc7, 0x96, 0x5b, 0x8c, 0x98, 0xbc, 0xd4, 0x84, 0xda, 0xa2, 0x89, 0x28, 0x01, - 0xa0, 0xd7, 0xc3, 0x3e, 0x02, 0x95, 0xbe, 0x43, 0x38, 0x37, 0x4f, 0x6c, 0x69, 0x93, 0x9a, 0x0b, 0x0a, 0xc3, 0x1d, - 0x3b, 0xd9, 0x8b, 0x4d, 0x26, 0xf6, 0x0c, 0xe6, 0xa1, 0xc5, 0x5a, 0x76, 0xf3, 0x47, 0xb1, 0xe3, 0x07, 0x34, 0x90, - 0xf9, 0x01, 0x0b, 0x92, 0x3f, 0x96, 0x0e, 0x71, 0x2e, 0x04, 0xc5, 0x43, 0x5a, 0xc9, 0x22, 0x16, 0xa4, 0xdb, 0xf1, - 0x22, 0xce, 0x65, 0x4e, 0x6e, 0x01, 0x41, 0x75, 0x20, 0x16, 0xb2, 0xdc, 0x40, 0x1a, 0x6f, 0x70, 0xe1, 0xbc, 0xc9, - 0x89, 0x24, 0xb0, 0xf5, 0x4c, 0x24, 0x93, 0x45, 0x39, 0x16, 0x81, 0xdf, 0x7c, 0xec, 0xfe, 0x66, 0x3e, 0x36, 0x1b, - 0x7b, 0xda, 0x2c, 0xdf, 0x2c, 0xc2, 0xcc, 0xda, 0xaa, 0xc2, 0x84, 0x25, 0x92, 0x71, 0xc2, 0x5b, 0x7b, 0xf8, 0xca, - 0xad, 0xe1, 0x33, 0xf8, 0xdd, 0xcc, 0x16, 0x73, 0x69, 0x3b, 0x5b, 0x24, 0xe9, 0x20, 0x4c, 0x37, 0xe1, 0x1f, 0x62, - 0x64, 0xba, 0xd8, 0xac, 0xe8, 0x47, 0x83, 0x44, 0xf1, 0x76, 0xe3, 0xe5, 0xe1, 0xb7, 0x08, 0x0e, 0x76, 0x0b, 0xb2, - 0xb9, 0xfb, 0x72, 0xa4, 0xe2, 0x21, 0x2b, 0xaa, 0x1a, 0x63, 0x84, 0x52, 0x88, 0x63, 0x88, 0xba, 0xdc, 0xbe, 0x6a, - 0xcb, 0x43, 0xba, 0xfa, 0x5a, 0x64, 0x14, 0xde, 0xca, 0x7f, 0x63, 0xbe, 0xe3, 0x9f, 0x31, 0x95, 0xd4, 0x79, 0x0e, - 0xf0, 0xb5, 0xdf, 0xc1, 0x20, 0x21, 0x2a, 0x22, 0x7f, 0x2d, 0x11, 0x20, 0x66, 0xbd, 0xc4, 0x00, 0xee, 0xbc, 0xba, - 0x95, 0x93, 0x90, 0xbe, 0x60, 0xe8, 0x6d, 0x8b, 0x85, 0x79, 0x3c, 0x92, 0x7c, 0x87, 0xb1, 0x88, 0x9d, 0xf5, 0xc1, - 0x8c, 0x9d, 0xba, 0xe6, 0xc3, 0x74, 0xf6, 0x1f, 0x23, 0x2c, 0x70, 0x04, 0x43, 0xad, 0x85, 0x9f, 0xae, 0x02, 0xb8, - 0xd3, 0x7f, 0x08, 0x52, 0xe0, 0x36, 0x7a, 0xe2, 0x67, 0xba, 0x17, 0xd8, 0x04, 0xa7, 0x62, 0xaf, 0x88, 0xed, 0x39, - 0xd0, 0xab, 0x55, 0x0d, 0xa5, 0xbb, 0x73, 0x3a, 0x08, 0x53, 0x2c, 0x0a, 0x63, 0xbd, 0x8e, 0x12, 0x9b, 0x55, 0xcb, - 0x69, 0xc2, 0x90, 0xed, 0x2a, 0xd4, 0x9e, 0xe4, 0xc2, 0xa2, 0x84, 0x23, 0x37, 0x89, 0x37, 0xd5, 0x3a, 0xa0, 0x7e, - 0xeb, 0xd7, 0x26, 0xb8, 0xf5, 0x82, 0xa5, 0x63, 0x41, 0xa1, 0xa5, 0x88, 0xc1, 0x23, 0x44, 0x06, 0xaf, 0xcb, 0x15, - 0xe2, 0xf4, 0x22, 0xfd, 0xbe, 0x75, 0x57, 0x4a, 0x4b, 0x77, 0xb3, 0x88, 0xd9, 0xcf, 0xe9, 0xaf, 0x86, 0x97, 0xd7, - 0x9c, 0x91, 0x71, 0xd1, 0xb4, 0xe8, 0xa9, 0xd7, 0xb8, 0xdc, 0x80, 0xd9, 0x43, 0xab, 0x63, 0x86, 0xfd, 0x33, 0x2d, - 0x2d, 0xc6, 0xf8, 0x9d, 0x28, 0xa6, 0xdd, 0xef, 0x3e, 0x75, 0xe2, 0x9e, 0x5e, 0xe8, 0xa0, 0xf7, 0x96, 0x78, 0xdb, - 0xe9, 0x9d, 0xae, 0x3e, 0x9b, 0x8e, 0x41, 0xf4, 0x8d, 0x32, 0x7d, 0x37, 0x0b, 0x5d, 0x2e, 0x63, 0xd6, 0x68, 0x93, - 0xf6, 0xe5, 0x72, 0xfa, 0xa5, 0x97, 0xb9, 0xf1, 0x6f, 0xe1, 0x7a, 0x32, 0x24, 0x92, 0x96, 0x2a, 0x95, 0x2a, 0x9c, - 0x74, 0x81, 0xc4, 0x9a, 0x89, 0x5a, 0xae, 0x51, 0x67, 0x1c, 0x0f, 0x97, 0x0e, 0xcb, 0xef, 0x9b, 0x0f, 0x2a, 0xbd, - 0x7c, 0x1f, 0x88, 0x7d, 0x76, 0x25, 0x19, 0x50, 0x4e, 0xe1, 0x8c, 0xec, 0xfe, 0x73, 0x58, 0xed, 0x0a, 0xa0, 0x66, - 0x18, 0xbd, 0x5c, 0x1a, 0x76, 0x50, 0x94, 0x7e, 0x3a, 0xe9, 0xa6, 0x20, 0xac, 0xaf, 0xce, 0xc2, 0xeb, 0x5a, 0x92, - 0xe8, 0x12, 0x7f, 0x25, 0xdd, 0x4f, 0x38, 0xc9, 0xbe, 0x43, 0x7d, 0x55, 0x97, 0x00, 0xd0, 0x21, 0x5e, 0xa9, 0x40, - 0x9a, 0x79, 0x4b, 0xba, 0x4a, 0x64, 0x5d, 0x7c, 0x48, 0x01, 0x5c, 0x59, 0x6f, 0x9f, 0x66, 0x21, 0x5d, 0x6b, 0x89, - 0x9d, 0x84, 0x6e, 0xb8, 0x9f, 0x30, 0x02, 0x24, 0xd8, 0xf1, 0x00, 0x9a, 0xbc, 0x13, 0x9e, 0x63, 0xbd, 0x9a, 0x58, - 0x83, 0x20, 0xa2, 0x7b, 0x2f, 0xc1, 0x6e, 0x4e, 0xf3, 0xac, 0xb0, 0x09, 0xd1, 0xec, 0xc8, 0x7d, 0x3f, 0x39, 0xf0, - 0x7a, 0x61, 0x53, 0xb1, 0x51, 0x99, 0x3a, 0xb9, 0xc5, 0x38, 0xc0, 0x3e, 0x2d, 0x05, 0xd4, 0x70, 0x15, 0xa5, 0x2c, - 0xa7, 0x29, 0xa1, 0xc5, 0x38, 0xe0, 0xf4, 0x25, 0x07, 0xff, 0x27, 0x82, 0x26, 0x64, 0x1d, 0x7e, 0xfc, 0xb1, 0x05, - 0x7f, 0x24, 0xad, 0x69, 0x56, 0x44, 0xab, 0x3d, 0xc5, 0x1a, 0x34, 0x2f, 0x93, 0x8f, 0x0d, 0x03, 0xd8, 0xbc, 0x16, - 0xb2, 0xfa, 0x89, 0x9b, 0x56, 0x3c, 0x50, 0x7e, 0xca, 0x41, 0xed, 0xa9, 0x35, 0xf6, 0x42, 0xb2, 0xd3, 0xac, 0xa8, - 0x88, 0xe2, 0x7a, 0xb2, 0x5d, 0x17, 0xef, 0xbe, 0x48, 0x54, 0xfc, 0xe9, 0x06, 0x62, 0x48, 0x40, 0x02, 0x83, 0x15, - 0xd4, 0x90, 0xd0, 0x51, 0x5f, 0x6f, 0xbe, 0xbc, 0xd5, 0x20, 0xd0, 0x7c, 0xe9, 0x14, 0x10, 0x33, 0x15, 0xb3, 0xf3, - 0x21, 0xa0, 0x8a, 0xf7, 0x6f, 0x30, 0x69, 0x36, 0x7e, 0xb7, 0x8e, 0x6b, 0x5d, 0x38, 0xff, 0x51, 0xab, 0x66, 0xc0, - 0x86, 0xb8, 0xa0, 0x4a, 0xd1, 0xb0, 0xca, 0x58, 0x20, 0x1a, 0x3d, 0x7d, 0x1c, 0x59, 0x0a, 0xfb, 0xe7, 0xe6, 0xcb, - 0x67, 0xad, 0x4e, 0x6d, 0x5e, 0xcd, 0x5c, 0x4a, 0xa2, 0xe0, 0x32, 0x7d, 0xc3, 0x57, 0x00, 0x20, 0x33, 0x6b, 0x10, - 0x14, 0x34, 0xf8, 0x1a, 0x7c, 0x6e, 0x45, 0xc8, 0x38, 0xd0, 0xfd, 0x90, 0xf1, 0x87, 0x9b, 0xf6, 0x8a, 0xde, 0xd2, - 0xca, 0x7c, 0x4b, 0xaf, 0xf7, 0xb4, 0xd5, 0xf5, 0x4b, 0x8b, 0x19, 0x75, 0xa9, 0x5a, 0x9e, 0x7e, 0xde, 0xee, 0x7b, - 0xe2, 0xd6, 0xda, 0x9f, 0x8a, 0x32, 0xb6, 0x27, 0xe5, 0xa0, 0x31, 0x37, 0xfe, 0x05, 0xea, 0x35, 0x0d, 0x8d, 0x8a, - 0x42, 0x79, 0x5b, 0xf7, 0xad, 0x50, 0xe6, 0xed, 0x89, 0x8c, 0x23, 0xa9, 0x3b, 0x86, 0xbc, 0x2f, 0x6d, 0xe3, 0xf3, - 0x9a, 0x22, 0x50, 0xf8, 0x95, 0xe9, 0x94, 0x02, 0x5d, 0x13, 0x2e, 0x11, 0x3c, 0xb4, 0xbc, 0x2e, 0xdd, 0x98, 0x41, - 0xe5, 0xe8, 0x03, 0xbd, 0xa5, 0x0d, 0xc1, 0x8f, 0x8b, 0xf0, 0x8b, 0x9d, 0xd0, 0x4c, 0xae, 0x02, 0x35, 0x13, 0x55, - 0xf6, 0x90, 0xac, 0x0c, 0x96, 0x13, 0xa9, 0x49, 0xdf, 0xd4, 0x99, 0x40, 0x83, 0xa9, 0x57, 0x3e, 0xeb, 0x82, 0xa1, - 0x8b, 0x5d, 0x69, 0x61, 0x60, 0x11, 0x26, 0x5f, 0x84, 0x5f, 0x1f, 0x7d, 0x2d, 0x8d, 0x16, 0x18, 0x02, 0x84, 0xe6, - 0x5e, 0x5e, 0x34, 0x0a, 0xb6, 0xbf, 0xbb, 0x69, 0xaf, 0x57, 0x78, 0xf0, 0xed, 0x83, 0x79, 0x89, 0xc5, 0x81, 0x9e, - 0x9b, 0xfc, 0x71, 0x27, 0xed, 0x44, 0x41, 0x48, 0x6d, 0x2e, 0x70, 0x08, 0x50, 0xd5, 0xec, 0x21, 0xce, 0x56, 0x29, - 0x3b, 0x71, 0x04, 0x64, 0xf9, 0x6d, 0x04, 0xbe, 0x7c, 0xb7, 0xc6, 0xde, 0x63, 0x91, 0xb9, 0x28, 0xed, 0xf1, 0xb7, - 0xbb, 0x0b, 0xab, 0xbb, 0x79, 0x78, 0x2c, 0x28, 0xec, 0xfc, 0xe1, 0x3c, 0xee, 0xeb, 0xba, 0x7b, 0x00, 0x98, 0x81, - 0xf0, 0x71, 0x21, 0x1c, 0x02, 0xd1, 0x4c, 0x37, 0xeb, 0xf6, 0xa3, 0x4a, 0xaa, 0x8a, 0xa7, 0x00, 0x27, 0x1c, 0x02, - 0xef, 0x4c, 0x3d, 0x36, 0x4b, 0xb0, 0xd9, 0x0e, 0xc0, 0x10, 0x4a, 0xd3, 0x1c, 0x37, 0xe5, 0x06, 0xc8, 0x77, 0x11, - 0xc3, 0x14, 0xf7, 0x62, 0x8f, 0x86, 0x0f, 0x28, 0x8a, 0x68, 0xe9, 0xbc, 0x20, 0xdf, 0x76, 0x11, 0xcb, 0x9d, 0x27, - 0xa3, 0x0c, 0x3e, 0x11, 0xf9, 0x16, 0x29, 0x64, 0xee, 0x47, 0x9a, 0xc2, 0x6a, 0x9b, 0xd6, 0x8f, 0x01, 0x91, 0x5b, - 0x5a, 0x37, 0x26, 0x5a, 0x03, 0x17, 0x7a, 0x13, 0xf9, 0x02, 0x5a, 0xdb, 0x2a, 0x76, 0x9f, 0x5f, 0x89, 0x64, 0xf0, - 0x40, 0x98, 0x7f, 0xe3, 0xe1, 0xad, 0xd1, 0x31, 0xa5, 0x66, 0x76, 0x49, 0xfb, 0xe3, 0xbd, 0xb5, 0x97, 0xad, 0xfb, - 0xd6, 0xbb, 0x6a, 0x4d, 0x9e, 0xd3, 0x21, 0x95, 0xd8, 0x49, 0x05, 0x50, 0x04, 0x1f, 0x5b, 0x3e, 0x3f, 0x07, 0xa8, - 0x13, 0x05, 0x5c, 0xa8, 0x72, 0xe2, 0xcc, 0x38, 0xcc, 0xf2, 0x2b, 0x69, 0x73, 0x70, 0xfb, 0x79, 0x93, 0x62, 0x20, - 0x84, 0x42, 0x07, 0x64, 0xea, 0x0d, 0x4e, 0x6a, 0x6b, 0xda, 0x02, 0x8b, 0x39, 0x2c, 0x10, 0x39, 0x82, 0x00, 0xe4, - 0x10, 0xe9, 0x5a, 0xa5, 0xfb, 0x78, 0xd0, 0x0d, 0x28, 0x6f, 0x04, 0x66, 0xa4, 0x3f, 0x80, 0xd8, 0xb1, 0xb6, 0x73, - 0x95, 0x88, 0x30, 0x89, 0x34, 0x16, 0x1e, 0xfe, 0x25, 0x53, 0x52, 0x3e, 0x62, 0xb1, 0x1b, 0x82, 0x69, 0x31, 0xaf, - 0x48, 0x0e, 0x29, 0x48, 0x17, 0xea, 0xea, 0x5b, 0x8e, 0xc9, 0x39, 0xcd, 0x32, 0x14, 0xa6, 0x48, 0x18, 0x39, 0x9b, - 0x36, 0x13, 0x59, 0x40, 0x33, 0x56, 0x45, 0xa0, 0x5c, 0x91, 0xf5, 0xa8, 0x92, 0x3f, 0x72, 0xfe, 0x4d, 0x58, 0x05, - 0x97, 0x63, 0xc7, 0xba, 0x61, 0x9d, 0x9d, 0x17, 0x95, 0x69, 0x99, 0x7c, 0x5b, 0x96, 0x24, 0x5e, 0xc2, 0x63, 0x21, - 0xde, 0x2f, 0xfa, 0x6d, 0xf5, 0x14, 0xfa, 0xe5, 0xae, 0x1d, 0x42, 0x85, 0x54, 0x0c, 0x6a, 0x09, 0x13, 0x45, 0x8b, - 0xd2, 0xf8, 0xbd, 0x00, 0x5b, 0x1e, 0x03, 0xda, 0x58, 0xfa, 0xc1, 0x4a, 0x5c, 0x95, 0x23, 0x6a, 0x59, 0xbd, 0x95, - 0xa2, 0x93, 0xcb, 0xca, 0x32, 0xda, 0x22, 0x92, 0x80, 0x00, 0xe7, 0x2b, 0x65, 0x35, 0xbf, 0xe4, 0x3a, 0xac, 0x5b, - 0xbf, 0xb2, 0x54, 0x2a, 0x4c, 0xd1, 0x62, 0xb0, 0x8c, 0x08, 0xe3, 0x36, 0xd7, 0xba, 0x38, 0x7e, 0xd7, 0xfc, 0x0d, - 0xe8, 0xb7, 0x70, 0x97, 0xbb, 0x6a, 0x05, 0x6e, 0x5e, 0x44, 0x74, 0x41, 0x2e, 0x03, 0xb9, 0x0b, 0xaa, 0x37, 0x68, - 0xc1, 0x96, 0xac, 0xb7, 0xe6, 0x32, 0x2f, 0x0f, 0x7d, 0x15, 0x83, 0x3c, 0x7d, 0xa8, 0x68, 0x75, 0xa8, 0xf5, 0x71, - 0x6f, 0xff, 0x69, 0xaf, 0xda, 0x69, 0x40, 0x07, 0xe4, 0xbe, 0xd6, 0xf1, 0x75, 0x97, 0xff, 0xf9, 0x87, 0xdb, 0x22, - 0x91, 0xfb, 0x25, 0x75, 0x03, 0x1d, 0x82, 0xd2, 0x81, 0x60, 0x3b, 0x5e, 0x5a, 0x7e, 0x72, 0xd0, 0x0b, 0x6b, 0x42, - 0x2d, 0xbc, 0x29, 0x4f, 0x83, 0x11, 0x21, 0x94, 0x92, 0x58, 0xe7, 0xb4, 0xd9, 0xc3, 0x82, 0x3e, 0xdc, 0xe1, 0xac, - 0x36, 0xa6, 0x3f, 0x21, 0xaa, 0x4c, 0xb4, 0x07, 0x76, 0x17, 0x4d, 0x6c, 0x78, 0xd8, 0x0f, 0x4a, 0x52, 0x42, 0xb5, - 0xaf, 0xe8, 0x03, 0x65, 0x62, 0x8e, 0x2f, 0x3b, 0x14, 0xfc, 0x55, 0x6a, 0x89, 0x4d, 0x0c, 0xf6, 0x1d, 0x87, 0x25, - 0x51, 0x31, 0x80, 0xcd, 0x65, 0x8c, 0x59, 0x5f, 0x60, 0x8b, 0xd8, 0x9f, 0x56, 0x03, 0x65, 0xbf, 0x5b, 0xf7, 0x7d, - 0x67, 0x05, 0x50, 0xe6, 0x9a, 0x9f, 0xf4, 0x7b, 0xe4, 0x7b, 0xb0, 0xa8, 0x5f, 0x87, 0xa0, 0x45, 0xd7, 0xb5, 0x35, - 0x71, 0xe6, 0xb3, 0x74, 0xcf, 0x0d, 0x17, 0xfe, 0xbe, 0x2a, 0x90, 0x09, 0xd2, 0x74, 0xa8, 0x62, 0x33, 0x2e, 0xca, - 0x28, 0xb6, 0x0c, 0xf7, 0xc2, 0xef, 0xa4, 0x20, 0x42, 0x84, 0x8c, 0x61, 0x5a, 0x22, 0xe8, 0xd4, 0x7c, 0x9e, 0x36, - 0x02, 0xd5, 0x35, 0x29, 0x73, 0x4f, 0x97, 0x3b, 0xe2, 0x41, 0x89, 0x1e, 0x59, 0x02, 0xf4, 0x7f, 0xab, 0xe7, 0x1a, - 0xb7, 0x9c, 0x11, 0xa4, 0x59, 0x10, 0x19, 0x9c, 0xc1, 0x71, 0x8e, 0x1b, 0x2d, 0x24, 0x48, 0x14, 0x77, 0x27, 0xa1, - 0x4f, 0xda, 0x38, 0x35, 0xf8, 0x05, 0xb9, 0x28, 0x37, 0x2a, 0x00, 0xb5, 0x7b, 0x88, 0x16, 0x05, 0x73, 0x66, 0xc8, - 0x8e, 0xbc, 0x87, 0x18, 0x1e, 0xda, 0x52, 0x69, 0x1e, 0x73, 0x72, 0x02, 0x51, 0x73, 0x99, 0x27, 0x35, 0xf6, 0x0a, - 0xfa, 0x06, 0x14, 0xa7, 0xd0, 0xc6, 0x98, 0x38, 0x5d, 0x9e, 0xfa, 0x54, 0x0d, 0x44, 0xe9, 0xd9, 0x7c, 0x5d, 0xac, - 0x23, 0x76, 0xc0, 0x2e, 0x34, 0x63, 0x0c, 0x7e, 0x23, 0x94, 0x02, 0x0e, 0x32, 0x9f, 0x08, 0x3a, 0xf2, 0x83, 0x81, - 0x93, 0x19, 0xe3, 0x5d, 0xd6, 0x84, 0x03, 0x3d, 0x94, 0x52, 0x7d, 0x01, 0x9b, 0x21, 0x04, 0xe8, 0xaf, 0xc4, 0x7b, - 0x67, 0xad, 0x9e, 0x51, 0x89, 0x17, 0x13, 0x39, 0x28, 0xc2, 0x84, 0x87, 0x48, 0x8d, 0x28, 0x74, 0x24, 0xda, 0x43, - 0x05, 0xb3, 0xec, 0x6c, 0x5b, 0x53, 0xde, 0x17, 0x75, 0xea, 0x34, 0x07, 0xbf, 0xbe, 0x17, 0x6f, 0xe4, 0xea, 0x31, - 0xa0, 0xc7, 0xbe, 0x6e, 0x09, 0xd9, 0xbd, 0x53, 0x40, 0x80, 0x7c, 0xb1, 0x43, 0xc6, 0x84, 0xe8, 0x58, 0xd3, 0x92, - 0xaa, 0xd9, 0x47, 0x8b, 0xd0, 0x5f, 0xae, 0x8f, 0x33, 0x2c, 0x13, 0x42, 0x6d, 0x61, 0x02, 0x88, 0xd0, 0x93, 0x4e, - 0x09, 0x96, 0xe4, 0x3e, 0x78, 0xd9, 0xb0, 0xc3, 0xc1, 0x76, 0x55, 0x0c, 0x4f, 0x0e, 0x3f, 0x0f, 0x81, 0xed, 0x98, - 0x80, 0x4e, 0xb3, 0x14, 0x0a, 0xb1, 0xe1, 0x3e, 0x56, 0x33, 0x49, 0x05, 0x31, 0x4d, 0x54, 0x3e, 0xe0, 0x0f, 0x6a, - 0x23, 0x6e, 0xd2, 0x8e, 0xe2, 0xdd, 0x08, 0x7b, 0x89, 0x43, 0x37, 0x84, 0xeb, 0x80, 0xa8, 0xd1, 0x3e, 0x97, 0x35, - 0x37, 0xa2, 0xcd, 0x33, 0x32, 0x70, 0x89, 0xd4, 0x5f, 0xa1, 0x75, 0x50, 0x69, 0x41, 0x3d, 0x8b, 0xdf, 0x02, 0x3c, - 0xb7, 0x86, 0x96, 0xfb, 0x15, 0x92, 0xb8, 0x53, 0x8c, 0x66, 0xb4, 0x47, 0x78, 0x99, 0xee, 0x10, 0xe0, 0xde, 0x6a, - 0xdd, 0x69, 0xba, 0x1e, 0xa0, 0x8c, 0x9d, 0xb8, 0xe9, 0xb6, 0xca, 0x49, 0x1a, 0x03, 0x6a, 0xcc, 0xbf, 0x47, 0xef, - 0x07, 0xdf, 0x73, 0xa4, 0xe3, 0x76, 0x59, 0xe8, 0x5a, 0xa1, 0xf9, 0xf4, 0x39, 0xd9, 0x69, 0xe9, 0x16, 0xf7, 0x26, - 0x11, 0xea, 0xf0, 0x11, 0x5e, 0x30, 0x17, 0x4a, 0x77, 0x5c, 0xd4, 0xbd, 0xf9, 0xc9, 0xc2, 0x4d, 0x51, 0x53, 0x98, - 0x42, 0xc9, 0x66, 0xc3, 0x2b, 0x89, 0x59, 0xa0, 0xb9, 0x5c, 0x29, 0x84, 0x96, 0xf7, 0x40, 0xed, 0x35, 0x24, 0x6e, - 0xad, 0xd7, 0x16, 0x6e, 0xd3, 0x45, 0x6b, 0x35, 0x59, 0xd0, 0xc6, 0x48, 0xca, 0x98, 0x39, 0x74, 0x56, 0x64, 0xba, - 0x2a, 0x0a, 0x86, 0x35, 0xb5, 0x5e, 0x5d, 0x3b, 0x7e, 0x7e, 0x69, 0x7a, 0x08, 0x13, 0x0e, 0xac, 0x56, 0xd0, 0x3b, - 0xec, 0x34, 0x7f, 0x7a, 0xe1, 0x6a, 0x0b, 0x83, 0x44, 0x01, 0x01, 0x5d, 0x72, 0xf4, 0x3e, 0x96, 0x9e, 0xa2, 0x20, - 0x52, 0xa7, 0xab, 0xae, 0x32, 0x12, 0x82, 0x95, 0x8a, 0xff, 0x76, 0x60, 0x42, 0x8e, 0x70, 0x8e, 0xc8, 0xed, 0x75, - 0x25, 0xe7, 0xc7, 0x03, 0xd2, 0xd3, 0x11, 0x91, 0xd0, 0xd3, 0x1b, 0x43, 0x70, 0x39, 0x68, 0xec, 0xef, 0x02, 0xae, - 0xf0, 0x01, 0x86, 0xaf, 0x87, 0xae, 0x94, 0x1b, 0x2c, 0xec, 0xfb, 0x02, 0x69, 0xca, 0x45, 0x04, 0x07, 0xaa, 0x1d, - 0xf9, 0x9c, 0x1d, 0xf9, 0xad, 0x79, 0x15, 0x38, 0x37, 0x27, 0xbb, 0x46, 0x11, 0xca, 0x14, 0xbb, 0xb7, 0x03, 0x23, - 0x51, 0x92, 0xf0, 0xbb, 0x43, 0x42, 0x6b, 0xad, 0xf3, 0x3b, 0xee, 0x07, 0xbc, 0x28, 0x22, 0xf9, 0x07, 0xb1, 0x79, - 0x4f, 0x93, 0xf3, 0xf2, 0x1a, 0x5b, 0xb7, 0x15, 0x23, 0x00, 0xcd, 0x4d, 0xe6, 0x6d, 0x95, 0xc1, 0x0d, 0x56, 0x06, - 0x79, 0xb2, 0x24, 0x18, 0xcf, 0x52, 0x0d, 0xe6, 0xd9, 0xb1, 0x93, 0x96, 0x05, 0x0b, 0x81, 0x22, 0xd7, 0xd4, 0x66, - 0x75, 0x12, 0x57, 0xb4, 0x63, 0xf7, 0x5b, 0xb6, 0xc0, 0x09, 0x48, 0x3d, 0x71, 0xb2, 0xb4, 0x0d, 0x3e, 0x50, 0x48, - 0x76, 0x67, 0x19, 0x06, 0xd9, 0x85, 0xff, 0x2a, 0xe8, 0x07, 0x54, 0x57, 0x21, 0x54, 0xa4, 0x4d, 0x6c, 0x95, 0x94, - 0x22, 0x6b, 0x84, 0xd6, 0xd9, 0x16, 0x64, 0xc5, 0xd9, 0x1e, 0xf1, 0xa8, 0x99, 0xc0, 0x83, 0xc9, 0x6d, 0x91, 0xcd, - 0x19, 0xee, 0x89, 0x40, 0xc7, 0xa6, 0x50, 0x66, 0xda, 0x84, 0x6d, 0xdc, 0x93, 0xcd, 0xda, 0xbb, 0xad, 0xa8, 0x19, - 0x34, 0xa2, 0x6f, 0x69, 0x9a, 0xfd, 0x7b, 0x7d, 0xf0, 0x59, 0x89, 0xbe, 0x91, 0x43, 0x4c, 0xd7, 0x6d, 0xa4, 0x49, - 0x95, 0x5a, 0xe2, 0xd7, 0x6d, 0x3e, 0xbd, 0xa7, 0xa1, 0x1c, 0x52, 0x00, 0x27, 0x94, 0x02, 0x33, 0xe4, 0x73, 0x0c, - 0xc1, 0x9d, 0x82, 0x6d, 0x24, 0xcb, 0xad, 0xc8, 0x65, 0xd6, 0x58, 0xdd, 0xf1, 0x0f, 0x16, 0x80, 0x42, 0x5f, 0xde, - 0xa1, 0xa0, 0x1f, 0x6b, 0xbd, 0x4f, 0xd4, 0x91, 0x12, 0x93, 0xe2, 0xd3, 0xa5, 0x9b, 0xac, 0x02, 0x6a, 0xae, 0x5e, - 0x17, 0x0d, 0xe8, 0x35, 0x61, 0x00, 0xa1, 0x47, 0x74, 0xd8, 0x42, 0x18, 0xfd, 0xd1, 0x14, 0xc2, 0x7a, 0x5f, 0xc5, - 0x6d, 0xb7, 0x29, 0xba, 0xa7, 0xb3, 0x3b, 0x46, 0x6a, 0x90, 0x99, 0x56, 0x34, 0xc7, 0x70, 0x7a, 0xc0, 0x9d, 0xe2, - 0xb1, 0x63, 0x81, 0xcd, 0x26, 0xd5, 0x63, 0x8c, 0x01, 0x8e, 0xcc, 0x58, 0x6c, 0x53, 0x69, 0xad, 0x8c, 0x91, 0xda, - 0x16, 0xfd, 0xb2, 0xe6, 0x4e, 0x51, 0xdc, 0xfe, 0x08, 0x80, 0x79, 0x6e, 0x32, 0xad, 0xa3, 0x98, 0xa2, 0x46, 0x49, - 0xdb, 0xc5, 0xf1, 0x52, 0x54, 0x5e, 0x7c, 0x22, 0x70, 0x6f, 0x84, 0xca, 0x95, 0xa3, 0x03, 0xab, 0x33, 0xa5, 0x1f, - 0x6e, 0xf1, 0x98, 0x39, 0x89, 0x78, 0x01, 0xa3, 0xcf, 0x98, 0x0d, 0xe7, 0x0b, 0xef, 0x88, 0xb9, 0x45, 0x4e, 0xe1, - 0x5d, 0xf1, 0x56, 0xf8, 0xa5, 0x0b, 0xa8, 0xee, 0x41, 0x9c, 0xee, 0x54, 0xd6, 0x7a, 0x9d, 0x11, 0x21, 0xe1, 0x3b, - 0x92, 0xec, 0x95, 0x8c, 0x9d, 0xf8, 0x2c, 0x32, 0x3d, 0x38, 0x86, 0x85, 0x67, 0x8c, 0xe4, 0xf6, 0x99, 0x3a, 0x9a, - 0xb3, 0xc7, 0x3a, 0xd7, 0x45, 0x77, 0x5e, 0x7b, 0x6f, 0x2b, 0xd2, 0x53, 0x33, 0x9b, 0x4e, 0xbc, 0x69, 0x80, 0x3a, - 0x1f, 0xbc, 0xf6, 0x48, 0xe7, 0x7c, 0x07, 0x47, 0x71, 0x28, 0x5c, 0xb7, 0x6a, 0xf4, 0xd9, 0xf5, 0x1e, 0x72, 0x35, - 0x6c, 0xba, 0x8b, 0xc7, 0x65, 0x8f, 0x26, 0x7f, 0xb1, 0x22, 0x10, 0xfb, 0x18, 0x1e, 0x9f, 0xd3, 0xe0, 0xd6, 0xda, - 0xce, 0xb4, 0xd5, 0x36, 0x02, 0xd5, 0xa6, 0xa9, 0x05, 0x7e, 0xd2, 0xd5, 0x71, 0x3e, 0x71, 0xbc, 0xbc, 0x6b, 0xe0, - 0x4b, 0xfc, 0x02, 0x84, 0x55, 0xe9, 0xf5, 0xee, 0xf1, 0x1d, 0x67, 0x99, 0x2d, 0x73, 0xaf, 0x01, 0x59, 0x0e, 0x73, - 0x9d, 0xc5, 0xf1, 0xae, 0x3a, 0x22, 0x95, 0xda, 0xbe, 0xf2, 0xbf, 0x33, 0xe3, 0x42, 0x97, 0x1d, 0x41, 0x1c, 0xc8, - 0x15, 0x39, 0x53, 0x2a, 0xac, 0xc2, 0x69, 0x69, 0x4d, 0x43, 0xe3, 0x3a, 0x14, 0x04, 0x64, 0xf8, 0x7f, 0x20, 0x1c, - 0x44, 0xe6, 0xad, 0x13, 0x92, 0xaa, 0x6a, 0x03, 0x6b, 0xb4, 0x17, 0x7b, 0x01, 0x52, 0x78, 0x28, 0x92, 0xad, 0x2f, - 0xda, 0xaf, 0x4b, 0x64, 0x21, 0x03, 0xc1, 0x28, 0x93, 0x24, 0xc0, 0xd6, 0xd1, 0xad, 0x9e, 0xee, 0x92, 0x5e, 0x26, - 0xa0, 0xef, 0xf4, 0x3c, 0xfe, 0x10, 0x87, 0xa2, 0xac, 0x39, 0x7f, 0x6e, 0x49, 0xb6, 0xf3, 0xe8, 0xae, 0x6a, 0xac, - 0x43, 0x2c, 0x36, 0x97, 0x1c, 0x1d, 0xe7, 0x45, 0x81, 0xb3, 0x0c, 0x7d, 0x07, 0x8c, 0x85, 0x77, 0x36, 0x0c, 0xd5, - 0x5c, 0x48, 0x35, 0x7d, 0xc5, 0xa7, 0x70, 0x1d, 0x1e, 0x52, 0x4a, 0xdb, 0x16, 0xeb, 0xc1, 0x72, 0x88, 0x97, 0xdc, - 0x50, 0xa1, 0x71, 0xb5, 0x34, 0x9b, 0xd4, 0x73, 0x98, 0xc6, 0x8a, 0x2f, 0xd0, 0xa4, 0xdc, 0x45, 0xc5, 0x53, 0x07, - 0x53, 0x87, 0x6e, 0x52, 0x88, 0x7e, 0x3a, 0x32, 0x27, 0x92, 0x34, 0xe9, 0xc7, 0xb6, 0x51, 0x05, 0x01, 0xd0, 0xd1, - 0x5a, 0x16, 0xb4, 0xfb, 0xde, 0xaf, 0x7e, 0x65, 0xa3, 0x4d, 0x8f, 0x1a, 0x92, 0xb9, 0xd6, 0xe1, 0xd6, 0xd7, 0x72, - 0x7c, 0x77, 0x45, 0x18, 0xcd, 0xdb, 0x03, 0xab, 0xc2, 0xb9, 0x88, 0x14, 0xe3, 0x16, 0xad, 0x20, 0x61, 0x1e, 0x20, - 0xc7, 0xbb, 0x61, 0xda, 0x9f, 0x2b, 0x93, 0xa3, 0xf3, 0x3c, 0x3c, 0x6f, 0xae, 0x58, 0x68, 0x45, 0x2f, 0xce, 0xb1, - 0xdf, 0xb8, 0xf7, 0xbd, 0x8c, 0xa9, 0xe7, 0x61, 0xe3, 0xdd, 0x28, 0x4f, 0x4f, 0x30, 0x3a, 0x3f, 0x44, 0x37, 0x77, - 0xa4, 0xaf, 0xec, 0x02, 0xf4, 0x7a, 0x4f, 0x8e, 0xef, 0x67, 0xdf, 0x8b, 0x8e, 0x33, 0xb8, 0x30, 0xd2, 0x28, 0xce, - 0x17, 0x84, 0x96, 0x98, 0xa3, 0x34, 0xe3, 0x45, 0x3d, 0x11, 0x8e, 0x78, 0x79, 0x0a, 0x76, 0x2c, 0xec, 0xa6, 0x3f, - 0x17, 0xbe, 0xb0, 0xf0, 0xd7, 0xe9, 0x4c, 0xe0, 0x71, 0xff, 0x6f, 0x93, 0xfd, 0x82, 0x44, 0x22, 0x7a, 0x18, 0x47, - 0x7a, 0x6c, 0xcb, 0x42, 0xef, 0x99, 0xd8, 0x22, 0xf5, 0xfa, 0xfd, 0x84, 0x50, 0xea, 0x86, 0x52, 0xea, 0x0e, 0xca, - 0x64, 0x59, 0x02, 0x1b, 0x37, 0x85, 0x10, 0x72, 0xfc, 0x33, 0x6e, 0x9e, 0x22, 0x7c, 0xd6, 0x88, 0xd2, 0xac, 0xa6, - 0xa6, 0xe0, 0xce, 0x60, 0x00, 0x56, 0xe2, 0x04, 0x07, 0x88, 0xf2, 0xa1, 0x2e, 0xbc, 0xc2, 0xc4, 0x6a, 0x55, 0x56, - 0x02, 0xb5, 0xc2, 0x50, 0x82, 0x08, 0x4e, 0x68, 0x2f, 0x22, 0xac, 0xeb, 0x98, 0x94, 0x7b, 0xb0, 0x45, 0x3b, 0xb7, - 0xf0, 0xba, 0xdb, 0xc2, 0xca, 0xc3, 0x7b, 0x35, 0xeb, 0xb1, 0x2b, 0xbb, 0xe3, 0x01, 0x72, 0x94, 0x9c, 0xfd, 0x04, - 0x88, 0xe0, 0x41, 0x12, 0xd8, 0xea, 0xad, 0x3d, 0x6c, 0xed, 0x0e, 0xa1, 0xdf, 0x16, 0xf8, 0x74, 0x07, 0xcc, 0x46, - 0x69, 0x37, 0xfb, 0xfc, 0xa7, 0x0e, 0x0e, 0x4b, 0x6f, 0x02, 0x7c, 0x9d, 0xea, 0x4e, 0xd6, 0x74, 0x1b, 0xb8, 0xc7, - 0x3f, 0x7b, 0xe8, 0x8a, 0x44, 0x3a, 0x62, 0x16, 0xb7, 0x38, 0xaa, 0xcb, 0xce, 0xea, 0xae, 0x91, 0x73, 0x5b, 0x12, - 0x57, 0xa5, 0x84, 0xec, 0x72, 0x44, 0xa5, 0x24, 0x7b, 0x44, 0x19, 0x9c, 0xf6, 0xf6, 0xf2, 0xdc, 0x98, 0x7b, 0x18, - 0x67, 0x01, 0xa8, 0x09, 0x58, 0x2e, 0xc8, 0xc6, 0x3b, 0x01, 0x80, 0x51, 0x5a, 0x35, 0x75, 0x46, 0xef, 0xe2, 0x56, - 0x5d, 0x6f, 0x1e, 0x64, 0x46, 0x33, 0x51, 0x33, 0xb9, 0x3b, 0xa0, 0x8a, 0xd1, 0xc2, 0x20, 0xfb, 0xa5, 0x54, 0x7c, - 0x5a, 0x95, 0x68, 0xa5, 0x43, 0xcd, 0x68, 0xbf, 0xfb, 0x34, 0xd0, 0x51, 0x22, 0xb6, 0x5c, 0x4c, 0xbb, 0xf2, 0x76, - 0x98, 0x78, 0x1a, 0xd8, 0x2a, 0x33, 0x22, 0x4d, 0xd9, 0xc2, 0xcb, 0x11, 0x49, 0x7e, 0xd4, 0x5d, 0xb3, 0x6a, 0x52, - 0x1b, 0x67, 0x2d, 0x3c, 0xdd, 0x52, 0xe4, 0x14, 0x0a, 0x2e, 0xda, 0xee, 0x83, 0x0c, 0x82, 0x69, 0xd3, 0xc6, 0x59, - 0x6f, 0xbb, 0x2f, 0xa2, 0x8e, 0x57, 0x74, 0x5c, 0x24, 0x6c, 0x6e, 0xf7, 0x14, 0x65, 0x07, 0x89, 0xf2, 0x24, 0x26, - 0xd3, 0x91, 0x03, 0x95, 0xb4, 0xa1, 0xd4, 0xd2, 0x7b, 0xc9, 0x8a, 0x8b, 0xb8, 0xf8, 0xbf, 0xca, 0xb2, 0xad, 0x2f, - 0xbe, 0x48, 0xb0, 0xa0, 0x83, 0x39, 0xa2, 0xc0, 0x3c, 0x97, 0xd2, 0x41, 0x89, 0x44, 0x11, 0xf9, 0x49, 0xc1, 0xec, - 0xaa, 0x64, 0x0d, 0x3e, 0x68, 0xa5, 0x3b, 0x93, 0x49, 0x43, 0x22, 0xe5, 0x9a, 0xd4, 0xda, 0x16, 0x1b, 0x19, 0xf1, - 0xcc, 0x0f, 0x56, 0x89, 0x30, 0x12, 0x0f, 0x32, 0x25, 0x96, 0xc2, 0xb3, 0x85, 0x94, 0xf8, 0x22, 0x67, 0x9f, 0xeb, - 0xc5, 0x6e, 0xb4, 0xc8, 0x62, 0x7e, 0x98, 0xf9, 0xe5, 0x70, 0xb3, 0x5b, 0x11, 0xa3, 0xde, 0x9a, 0xb3, 0x3c, 0x2b, - 0x99, 0x8d, 0x6b, 0x47, 0x8e, 0xb9, 0x04, 0x1a, 0x29, 0x04, 0x0a, 0xe9, 0x93, 0x81, 0x53, 0x8b, 0xcb, 0x81, 0x0d, - 0x1a, 0xdf, 0xf1, 0xc9, 0xb3, 0xa5, 0xbb, 0x8b, 0xa1, 0x61, 0xdb, 0x21, 0x11, 0x44, 0x68, 0xbc, 0x21, 0xd1, 0x32, - 0x34, 0x3f, 0x78, 0x3a, 0xef, 0xcc, 0xf4, 0xea, 0x8e, 0xa4, 0x67, 0x69, 0xe1, 0x11, 0xe0, 0x7c, 0x52, 0x91, 0xe6, - 0xd6, 0x3e, 0xc9, 0x21, 0xeb, 0xfe, 0x45, 0xb3, 0x7e, 0x45, 0x00, 0x77, 0x92, 0x80, 0x10, 0xa0, 0xe1, 0x75, 0x6b, - 0x21, 0x8c, 0x25, 0xcc, 0x38, 0x84, 0xee, 0x2a, 0xf8, 0x6f, 0x12, 0xa6, 0xe7, 0xa5, 0x09, 0x8d, 0x2f, 0x4a, 0xc5, - 0x60, 0x27, 0x0b, 0x84, 0x5b, 0x40, 0x14, 0x04, 0x81, 0x20, 0x63, 0x16, 0x8a, 0xa9, 0x84, 0x76, 0xa0, 0x15, 0x14, - 0x48, 0x80, 0x89, 0x37, 0x4e, 0x85, 0x41, 0x55, 0x6d, 0xc5, 0xd3, 0x1c, 0xb3, 0xe1, 0xa4, 0x61, 0x50, 0x58, 0x7f, - 0x02, 0x5d, 0x9c, 0x62, 0x92, 0x0c, 0xfb, 0xbb, 0x78, 0xe3, 0xc9, 0xba, 0x9d, 0x89, 0x52, 0x29, 0xf6, 0x59, 0x93, - 0x27, 0x34, 0xc3, 0x79, 0x21, 0xea, 0xcb, 0xba, 0xb4, 0xee, 0x83, 0xd5, 0x74, 0x07, 0x4f, 0x9e, 0x75, 0xa4, 0xb7, - 0x71, 0x60, 0xb9, 0x83, 0x44, 0x80, 0x45, 0xda, 0x03, 0xcd, 0xc8, 0x32, 0x64, 0xa8, 0x02, 0xac, 0x35, 0xe6, 0x4e, - 0x0d, 0x6d, 0x0f, 0xe5, 0x46, 0x72, 0x6d, 0x12, 0xac, 0x1e, 0x96, 0xbe, 0xbc, 0xba, 0x6e, 0x73, 0x63, 0x00, 0xbb, - 0xef, 0xd8, 0xb2, 0xa0, 0xcb, 0x11, 0x19, 0x8e, 0x27, 0xb7, 0x09, 0xd5, 0x03, 0x14, 0x8a, 0x6a, 0xaa, 0x69, 0xed, - 0x1f, 0x7e, 0xe6, 0x9d, 0x38, 0xd4, 0x39, 0x21, 0x36, 0x2a, 0x8f, 0xd0, 0x31, 0x8f, 0x7d, 0xa2, 0xcf, 0x25, 0xef, - 0x69, 0xbe, 0x41, 0xea, 0xc8, 0xc5, 0xd5, 0x79, 0x92, 0xa8, 0x1b, 0x63, 0xb5, 0x15, 0x5b, 0x84, 0x01, 0x16, 0x73, - 0x0c, 0x87, 0xe8, 0x54, 0x70, 0xb4, 0x24, 0xbd, 0x8d, 0xa5, 0xea, 0xe5, 0xf6, 0xdb, 0xaa, 0x4b, 0x6b, 0xa7, 0x09, - 0x83, 0xe8, 0xf4, 0x90, 0x01, 0x07, 0x42, 0xc6, 0xda, 0x3e, 0x58, 0xc6, 0x71, 0x46, 0xeb, 0x32, 0x68, 0x04, 0x63, - 0xe8, 0x00, 0xe5, 0xac, 0x7a, 0x1c, 0xec, 0x04, 0x62, 0x79, 0x48, 0x6f, 0x9a, 0xcc, 0x00, 0xd9, 0x22, 0x97, 0x5f, - 0x6a, 0xa2, 0x9d, 0x85, 0x8e, 0x2d, 0xfb, 0x1e, 0xd0, 0xae, 0x03, 0x47, 0x5f, 0x07, 0x1c, 0x75, 0xe2, 0x45, 0x2d, - 0x85, 0x36, 0xc7, 0xc0, 0xb9, 0xb0, 0x38, 0xd5, 0xf3, 0x6c, 0x68, 0xc7, 0xbd, 0x03, 0xbc, 0x98, 0xd2, 0xf5, 0x02, - 0xfc, 0x76, 0x70, 0x19, 0xf8, 0xc4, 0x83, 0xdb, 0xea, 0xb0, 0x63, 0x67, 0x92, 0xc6, 0x79, 0x34, 0x75, 0x73, 0xce, - 0xb9, 0xd0, 0xe5, 0xdc, 0xff, 0x9e, 0x6e, 0xfd, 0xfe, 0x8a, 0xf1, 0x69, 0xad, 0x3d, 0x61, 0xb9, 0xca, 0x69, 0x97, - 0x45, 0x2c, 0x59, 0x71, 0x8e, 0xbe, 0x10, 0xc8, 0xd7, 0xeb, 0xfc, 0x7e, 0xa1, 0x41, 0xe7, 0xd4, 0x41, 0x74, 0x8e, - 0x71, 0xb2, 0xd3, 0x83, 0xc9, 0x7b, 0x65, 0x71, 0x68, 0xac, 0x52, 0x66, 0xf1, 0x7d, 0xe3, 0x96, 0xde, 0x9e, 0x50, - 0x76, 0x29, 0x85, 0x14, 0xca, 0xb2, 0xe1, 0xb6, 0xc7, 0x81, 0xa6, 0xed, 0x36, 0x92, 0xdd, 0xd6, 0xb7, 0xef, 0x34, - 0x89, 0x48, 0xd2, 0xdd, 0x05, 0x51, 0x78, 0x86, 0xd0, 0x18, 0x50, 0xb0, 0x37, 0xa7, 0xd6, 0xe5, 0x6b, 0x2f, 0x2b, - 0xf1, 0x8a, 0x78, 0x57, 0x0c, 0xc6, 0xca, 0x09, 0x1d, 0x2c, 0xd2, 0x34, 0x50, 0xc7, 0x4e, 0x92, 0xb8, 0x55, 0x49, - 0xfc, 0xd0, 0xf2, 0x2f, 0xa4, 0xb9, 0x51, 0x79, 0x2a, 0xe2, 0xeb, 0x10, 0x7d, 0xe6, 0xb8, 0x54, 0xf7, 0x46, 0x35, - 0x83, 0x72, 0xcc, 0x93, 0x79, 0xc3, 0x5c, 0xa6, 0xdb, 0x29, 0x32, 0x4f, 0xf6, 0xbc, 0xb9, 0x99, 0x51, 0xa2, 0x44, - 0xa4, 0x2e, 0xf4, 0x32, 0xd7, 0x56, 0xa1, 0x23, 0x8d, 0xd8, 0xb4, 0x56, 0xb3, 0x89, 0x3d, 0x0e, 0x67, 0x3f, 0x59, - 0xd9, 0x13, 0xbc, 0xeb, 0x3d, 0xef, 0xec, 0xc3, 0xe6, 0x82, 0xeb, 0xd0, 0x88, 0x21, 0x33, 0x60, 0xa6, 0x59, 0x3a, - 0x53, 0x20, 0x8b, 0xb8, 0xaf, 0xee, 0x48, 0x94, 0x31, 0xfd, 0x13, 0xab, 0x75, 0x7d, 0xad, 0x54, 0x1d, 0x93, 0x1f, - 0xbe, 0xa3, 0x6b, 0x38, 0x77, 0x50, 0x94, 0x18, 0x4e, 0x34, 0xed, 0xe6, 0x52, 0x03, 0x10, 0x7e, 0x67, 0x87, 0x51, - 0x58, 0xc1, 0xb6, 0xa8, 0xb7, 0xea, 0x2a, 0x60, 0xa0, 0x86, 0x79, 0x32, 0xf6, 0x46, 0x11, 0x19, 0xf5, 0x1b, 0x76, - 0x23, 0xaf, 0x2c, 0xba, 0x65, 0x8d, 0xcf, 0x56, 0x39, 0xa5, 0xfe, 0x48, 0x6a, 0xe5, 0x0e, 0x0b, 0xe4, 0x86, 0xeb, - 0x42, 0x21, 0xa9, 0x37, 0x1c, 0x9b, 0x6d, 0x6b, 0xe6, 0x99, 0x06, 0xba, 0x6d, 0x4d, 0xef, 0x13, 0x3b, 0x70, 0xc3, - 0x6d, 0xdd, 0x30, 0x55, 0x6d, 0x3b, 0x8f, 0x5f, 0xef, 0xd3, 0x22, 0xac, 0x09, 0x6d, 0x18, 0xc6, 0x1a, 0xd8, 0xb6, - 0x45, 0x31, 0x17, 0x83, 0x98, 0xf6, 0xd8, 0x62, 0xdf, 0x81, 0x6c, 0xdf, 0xfd, 0xb5, 0x4a, 0x42, 0x4e, 0xae, 0xd2, - 0xf9, 0x35, 0xf9, 0x49, 0x27, 0x8b, 0x44, 0x66, 0x76, 0x91, 0xbf, 0xc6, 0x95, 0xfd, 0xa3, 0x95, 0xdd, 0xab, 0x95, - 0x2e, 0x52, 0x40, 0x14, 0xa6, 0xc2, 0x73, 0x08, 0x4c, 0x97, 0xac, 0xfc, 0x1f, 0xe8, 0x38, 0x27, 0x63, 0x4a, 0x68, - 0x6f, 0x34, 0x9a, 0x40, 0x37, 0x24, 0x14, 0x43, 0x0b, 0xcb, 0xe9, 0x79, 0xa9, 0x41, 0x57, 0x3b, 0x5c, 0x47, 0x96, - 0xfb, 0x22, 0xc0, 0x4f, 0x14, 0x50, 0x67, 0x6a, 0x82, 0xdd, 0x4f, 0x02, 0x63, 0x69, 0xd6, 0x59, 0xfa, 0x45, 0x87, - 0xd3, 0x15, 0x75, 0xf7, 0xf4, 0x2b, 0x86, 0x44, 0x77, 0xf8, 0x95, 0x42, 0xeb, 0x13, 0x33, 0x73, 0x81, 0x46, 0x3a, - 0x6e, 0x60, 0x10, 0x2e, 0x6a, 0x0b, 0xfe, 0x80, 0x5c, 0xc5, 0x4d, 0xe1, 0x6e, 0x72, 0x00, 0x97, 0xca, 0x6d, 0xcb, - 0xb3, 0x4a, 0x13, 0x98, 0x7d, 0x92, 0x32, 0x3a, 0x71, 0x8c, 0xba, 0x6f, 0x77, 0x3f, 0x4a, 0x52, 0xde, 0x3e, 0x7d, - 0xf3, 0x7a, 0x95, 0x35, 0xca, 0xde, 0x33, 0xb3, 0xd4, 0x55, 0xfc, 0xa9, 0x49, 0xee, 0x6a, 0xee, 0x3b, 0xe9, 0x56, - 0x20, 0x30, 0xca, 0x79, 0x85, 0xe5, 0xce, 0xb2, 0x90, 0xc3, 0xe6, 0x5e, 0xba, 0x4f, 0x4b, 0x9a, 0xec, 0x44, 0x55, - 0x62, 0x8c, 0x49, 0xa1, 0xfd, 0xf2, 0x74, 0xee, 0x8f, 0x0e, 0xdf, 0xa3, 0xa3, 0xbe, 0x4b, 0xd3, 0x72, 0xda, 0x8a, - 0xed, 0xf2, 0xc4, 0x0e, 0xa6, 0xe1, 0x9a, 0x30, 0x2d, 0x03, 0x84, 0xee, 0xea, 0x03, 0xe8, 0x5f, 0xe2, 0x1f, 0xfa, - 0xf1, 0x9c, 0xa2, 0x0f, 0xd0, 0x83, 0xd9, 0x9a, 0xca, 0x25, 0x6a, 0x50, 0x22, 0xb2, 0x4d, 0xbb, 0x34, 0x01, 0x53, - 0xe4, 0x20, 0xdd, 0x42, 0x06, 0xa2, 0x64, 0x21, 0x98, 0x41, 0xe5, 0x17, 0xf1, 0x3a, 0xf1, 0x75, 0xbe, 0x5a, 0xf0, - 0x92, 0x9e, 0x70, 0x55, 0xc8, 0xd5, 0x0d, 0xa3, 0xc5, 0xbc, 0x3a, 0xed, 0xa4, 0xda, 0x38, 0x34, 0xa8, 0x51, 0x87, - 0x48, 0xd7, 0xf1, 0xfe, 0x6f, 0x36, 0x52, 0x37, 0x18, 0xfd, 0xe4, 0x24, 0xe0, 0xfb, 0xc6, 0x48, 0xa5, 0xb3, 0x87, - 0x38, 0xb5, 0x16, 0x3c, 0x5e, 0x28, 0x7b, 0x34, 0xea, 0x11, 0xb5, 0xc6, 0x5e, 0x0e, 0x32, 0xad, 0x0d, 0x27, 0x85, - 0xd2, 0x79, 0xb8, 0x94, 0x77, 0x49, 0xe1, 0xd2, 0x1b, 0x95, 0x88, 0xf2, 0x00, 0x76, 0xc2, 0x96, 0x8a, 0x1b, 0x95, - 0xb4, 0x80, 0xea, 0x99, 0x9e, 0x0c, 0x89, 0xe9, 0x9c, 0x44, 0x8c, 0x19, 0x9e, 0xd2, 0xcd, 0x38, 0x44, 0x6b, 0x68, - 0x86, 0x3d, 0xbd, 0x8f, 0xd5, 0x13, 0xe4, 0x80, 0x9d, 0x8d, 0xeb, 0x0c, 0x21, 0x76, 0x52, 0xe1, 0x67, 0x6a, 0x55, - 0x6c, 0x99, 0x7f, 0x24, 0xa8, 0x6d, 0xf3, 0xb6, 0x8f, 0x88, 0xf2, 0xd6, 0xd2, 0x37, 0xb9, 0xbf, 0xe2, 0xca, 0x78, - 0x25, 0x81, 0x66, 0x96, 0x97, 0xfc, 0x1c, 0xe6, 0x67, 0xbf, 0xb1, 0x03, 0x13, 0x88, 0x38, 0xdb, 0x68, 0xd4, 0x53, - 0x72, 0x34, 0xd7, 0x39, 0xef, 0x5b, 0x70, 0x46, 0xc9, 0x34, 0x10, 0x62, 0x2d, 0x8b, 0x04, 0xe2, 0xc8, 0x24, 0x71, - 0x56, 0x38, 0xeb, 0x68, 0x27, 0x8f, 0x0e, 0x7a, 0x6f, 0x22, 0xf7, 0x45, 0x4d, 0x7a, 0x06, 0xfe, 0xd8, 0x51, 0x63, - 0x31, 0x8a, 0x6e, 0x5e, 0x04, 0xea, 0xe6, 0x2c, 0x8f, 0x43, 0xbd, 0xf4, 0x66, 0xae, 0xfd, 0xd2, 0xd3, 0x5a, 0xaa, - 0x0b, 0x74, 0x71, 0xe8, 0x31, 0x6a, 0x51, 0x5e, 0x41, 0x1a, 0x4c, 0x7b, 0xa0, 0xec, 0x35, 0x4c, 0xe8, 0x01, 0xbf, - 0x54, 0x82, 0x8c, 0x06, 0xef, 0x7c, 0xb4, 0xc5, 0xc5, 0x74, 0x92, 0xf3, 0x66, 0x01, 0x05, 0xb7, 0xeb, 0x2d, 0xa9, - 0x89, 0xd0, 0x1a, 0x37, 0x28, 0x6c, 0x91, 0xf0, 0x4f, 0x34, 0x87, 0xb3, 0x2b, 0x24, 0x75, 0x88, 0x4d, 0x31, 0xc2, - 0x04, 0xb4, 0x66, 0x5c, 0x6c, 0x68, 0x61, 0x37, 0xd1, 0x03, 0x6b, 0xf3, 0x20, 0x19, 0x87, 0x3b, 0x9a, 0x69, 0x33, - 0x90, 0x6b, 0x09, 0xfe, 0x2c, 0x11, 0xbd, 0xc3, 0xae, 0x0f, 0x77, 0x64, 0xd8, 0xdc, 0x12, 0xb2, 0x52, 0x66, 0x7a, - 0x78, 0x09, 0xb4, 0xeb, 0xb7, 0x8a, 0xed, 0x50, 0xc1, 0x9f, 0x22, 0x87, 0xa4, 0x98, 0x7e, 0x9f, 0xbd, 0x3a, 0x80, - 0x18, 0xc4, 0xa9, 0xd3, 0x7d, 0x53, 0x60, 0x9d, 0x43, 0x49, 0xb1, 0x21, 0x8c, 0x71, 0xc6, 0x51, 0xbb, 0xdb, 0xd1, - 0xc6, 0x7e, 0x24, 0x86, 0x40, 0xe9, 0xf0, 0xe5, 0x8e, 0x56, 0x5e, 0xb7, 0xb3, 0x75, 0xdb, 0x6b, 0xda, 0x91, 0x0f, - 0xc9, 0x11, 0x4c, 0x8a, 0x48, 0x5a, 0x36, 0x10, 0x9a, 0x31, 0x78, 0x8b, 0xb4, 0x60, 0x6d, 0xcf, 0x80, 0x96, 0xb9, - 0x5e, 0x28, 0xb4, 0xc0, 0xb3, 0x66, 0x0c, 0x4c, 0x0a, 0x8b, 0x0e, 0x2e, 0x69, 0xef, 0x74, 0x82, 0xd9, 0x40, 0xb5, - 0x5a, 0xdb, 0x6d, 0x59, 0xdf, 0x34, 0x0a, 0x84, 0xed, 0xbb, 0x72, 0x33, 0xfd, 0xc8, 0xcf, 0xac, 0x05, 0xf0, 0x55, - 0x6c, 0xbb, 0xce, 0x83, 0x76, 0x5f, 0x5b, 0xe5, 0xde, 0xc7, 0xfe, 0x5a, 0xe2, 0xc7, 0x50, 0xc3, 0xb2, 0x74, 0xc2, - 0x74, 0x65, 0x50, 0xbc, 0xe5, 0x9a, 0xfb, 0xc2, 0xc6, 0x64, 0x7a, 0x87, 0x12, 0x9b, 0xf8, 0xba, 0xb3, 0x1b, 0xcc, - 0x2d, 0x23, 0x7a, 0x59, 0xbf, 0xd3, 0xb7, 0xb2, 0xb5, 0xeb, 0x01, 0x88, 0xa9, 0x57, 0x96, 0x8c, 0x87, 0x19, 0x5d, - 0x3c, 0x04, 0xa5, 0xc9, 0x6b, 0x54, 0x92, 0xc1, 0x67, 0xf5, 0xae, 0x85, 0x44, 0xe4, 0xdb, 0x7a, 0x95, 0x27, 0xb3, - 0x91, 0x0d, 0xc7, 0xae, 0xa7, 0xe5, 0x81, 0x90, 0x32, 0x9a, 0xfd, 0x6d, 0x93, 0xd6, 0x5c, 0x4b, 0xc3, 0x2f, 0x91, - 0xe2, 0xa2, 0xd9, 0x38, 0xca, 0x36, 0x12, 0x04, 0x5d, 0xd5, 0x42, 0xb2, 0x58, 0x78, 0x58, 0xc8, 0x50, 0xbe, 0xac, - 0x84, 0x65, 0x2f, 0xee, 0xa6, 0x13, 0xf9, 0xe6, 0xc6, 0xcd, 0x8f, 0x42, 0xdb, 0xad, 0xaf, 0xdd, 0xf5, 0x43, 0x1b, - 0x51, 0x56, 0xbf, 0xe8, 0xc1, 0x57, 0xaa, 0xb1, 0x3e, 0xb2, 0xfe, 0xff, 0xa1, 0x9f, 0xd4, 0x89, 0xe4, 0xcc, 0x69, - 0x3b, 0x60, 0x7b, 0xa6, 0x80, 0xad, 0xd0, 0xf6, 0xb0, 0xa9, 0xf4, 0xfe, 0xce, 0x25, 0x81, 0x5c, 0x88, 0xf8, 0x84, - 0x85, 0x40, 0x92, 0xe2, 0x91, 0x4e, 0x03, 0x4c, 0x2d, 0x03, 0xea, 0x11, 0xec, 0xfb, 0xc1, 0x8e, 0x7c, 0xf3, 0x92, - 0xed, 0xf2, 0xb6, 0xee, 0x27, 0x28, 0xeb, 0x3e, 0x0f, 0x81, 0x8d, 0xdb, 0xd2, 0xe5, 0x80, 0x49, 0x1c, 0xc8, 0xc4, - 0x4c, 0xfb, 0x65, 0x6a, 0x2f, 0xdf, 0x2a, 0x67, 0x9c, 0xa0, 0x5b, 0x8a, 0x5a, 0x0e, 0x29, 0x71, 0x48, 0x5b, 0x79, - 0xe7, 0x86, 0xbd, 0x91, 0x7e, 0x88, 0x73, 0x8b, 0x1e, 0x07, 0x46, 0xd4, 0x69, 0xce, 0x66, 0x21, 0x42, 0x4d, 0xae, - 0x1a, 0xe5, 0xf1, 0x03, 0x57, 0xe9, 0x5a, 0xaa, 0xee, 0x2b, 0x94, 0xcc, 0x9e, 0x08, 0x93, 0x39, 0xb4, 0x3b, 0x64, - 0x49, 0x89, 0x66, 0x1f, 0xbb, 0x75, 0x40, 0x60, 0x27, 0x60, 0x9e, 0x96, 0xc8, 0xeb, 0x94, 0x4c, 0xfc, 0xf6, 0xed, - 0x3f, 0xca, 0xeb, 0x08, 0x86, 0xbd, 0xe4, 0x30, 0xa7, 0x02, 0x11, 0xc7, 0x71, 0xfe, 0xbc, 0x91, 0x0b, 0x12, 0x63, - 0xfd, 0xf9, 0x6b, 0xac, 0x5c, 0x23, 0x81, 0x56, 0x49, 0x43, 0xe1, 0x99, 0x1b, 0x67, 0xae, 0xec, 0xd4, 0xb3, 0x8c, - 0x2b, 0x76, 0x5b, 0xf4, 0x93, 0xc8, 0xa3, 0x16, 0xcd, 0x94, 0x1e, 0xa9, 0x72, 0x91, 0x94, 0xb4, 0xca, 0xa1, 0x96, - 0x6c, 0x05, 0xca, 0x61, 0x72, 0x2c, 0xe3, 0x3a, 0x73, 0x1a, 0x9a, 0xb3, 0x2c, 0x01, 0x72, 0x8b, 0x25, 0x38, 0xc7, - 0x4c, 0x2e, 0xc3, 0x4a, 0x2a, 0x24, 0x60, 0x1c, 0x84, 0xc2, 0x4f, 0xfe, 0xb1, 0xd2, 0xfe, 0x4e, 0x86, 0x5c, 0x62, - 0xf8, 0x0b, 0x1f, 0x93, 0x9e, 0x2b, 0x1f, 0x0a, 0x66, 0xb0, 0x18, 0xa2, 0xb7, 0x8c, 0x60, 0x5b, 0xee, 0xc4, 0x5b, - 0x34, 0xcb, 0xd2, 0xb9, 0x7f, 0x81, 0x66, 0xdd, 0xac, 0xd5, 0x7d, 0x8b, 0x22, 0xaf, 0x67, 0x4c, 0x9a, 0x70, 0xd2, - 0x4a, 0xa9, 0x94, 0xa6, 0xa0, 0x23, 0x4a, 0x32, 0x01, 0xcc, 0x0c, 0x50, 0xb2, 0x93, 0x8c, 0x4a, 0x0f, 0xca, 0xc9, - 0x70, 0x62, 0x31, 0xd3, 0xe8, 0x2c, 0x06, 0xac, 0x5e, 0x35, 0x3e, 0xce, 0x27, 0x1d, 0xff, 0x03, 0x40, 0xf5, 0xb5, - 0xd7, 0x82, 0xd7, 0x3c, 0x97, 0x93, 0xae, 0xe9, 0xba, 0x3a, 0xf6, 0x3f, 0xee, 0x45, 0x57, 0x50, 0x35, 0x66, 0xbf, - 0xd8, 0x5f, 0xd2, 0x38, 0x0c, 0x13, 0x62, 0x7c, 0x70, 0x1f, 0x70, 0xd4, 0x01, 0x5b, 0xac, 0x7a, 0x72, 0x91, 0x64, - 0x96, 0xa4, 0xb7, 0xb9, 0xe8, 0x3a, 0x7e, 0x70, 0x60, 0xa8, 0x2e, 0x6d, 0xba, 0xe7, 0x91, 0x15, 0x6f, 0x8d, 0x25, - 0xc0, 0x52, 0xcc, 0x81, 0x2e, 0x18, 0x1d, 0xaf, 0x08, 0xce, 0xb6, 0x13, 0x20, 0x0a, 0x63, 0x2d, 0x8a, 0x2c, 0x4e, - 0xf8, 0x5b, 0xd2, 0x26, 0x60, 0x4b, 0xc6, 0x28, 0x8d, 0x7d, 0x0e, 0xce, 0x95, 0xf9, 0xe0, 0xb1, 0xea, 0x17, 0x75, - 0xba, 0xba, 0x0c, 0xb1, 0xcf, 0x4f, 0x72, 0x79, 0xfd, 0x1d, 0xea, 0x4b, 0xbf, 0x8d, 0xd2, 0x17, 0x78, 0x67, 0x09, - 0x39, 0xef, 0x5e, 0xff, 0x54, 0xb4, 0x38, 0x30, 0x0b, 0x5d, 0xc5, 0x16, 0xb5, 0xe0, 0xfc, 0xe9, 0x85, 0x44, 0x14, - 0x65, 0x8f, 0x09, 0x90, 0xa9, 0xa6, 0xac, 0x7e, 0x53, 0xa4, 0x40, 0xc6, 0x45, 0x55, 0x9c, 0x3c, 0x06, 0xdf, 0xce, - 0x37, 0x77, 0x94, 0xc1, 0x68, 0xc8, 0xba, 0x5f, 0xef, 0xd8, 0xc4, 0x6f, 0xc4, 0xfc, 0x8f, 0x09, 0x27, 0xbd, 0x7a, - 0x4a, 0x80, 0x4a, 0xda, 0x49, 0x2a, 0x7d, 0x50, 0xe0, 0x85, 0x89, 0x26, 0x67, 0xa8, 0x41, 0x56, 0x78, 0x02, 0x9d, - 0x01, 0xcb, 0xd8, 0x62, 0x4a, 0xd9, 0x6d, 0x9b, 0xf8, 0x19, 0x85, 0x37, 0xb6, 0xb5, 0xd5, 0x18, 0xa4, 0xa7, 0x0a, - 0xd0, 0xf6, 0x38, 0x53, 0x85, 0x67, 0xd1, 0x85, 0x73, 0x6e, 0xde, 0x39, 0x70, 0x3e, 0xac, 0xcd, 0xc3, 0x97, 0xbf, - 0x20, 0x07, 0x85, 0x5d, 0x93, 0x3a, 0xa8, 0xea, 0x9a, 0x97, 0x74, 0xc2, 0x3f, 0x61, 0x7b, 0x89, 0xc5, 0x4c, 0x5e, - 0xd2, 0x7e, 0x0a, 0x1d, 0x21, 0x6d, 0x1e, 0x3a, 0xcd, 0xf6, 0x6f, 0x8a, 0x99, 0x1c, 0x2f, 0xb6, 0x9a, 0xfd, 0xb2, - 0x31, 0xfe, 0x2d, 0x92, 0x02, 0xee, 0x2b, 0xe7, 0xc3, 0x2a, 0x32, 0x89, 0x3a, 0xae, 0x8d, 0x17, 0x94, 0x3e, 0x86, - 0xe9, 0x68, 0xb1, 0xea, 0xb2, 0x8c, 0x7b, 0xa5, 0xcc, 0x91, 0x51, 0x82, 0xc3, 0x53, 0x55, 0x44, 0x15, 0x3a, 0xaf, - 0x21, 0x2f, 0xcd, 0xfc, 0xba, 0x4a, 0x45, 0xe8, 0x43, 0x99, 0x73, 0xce, 0x5b, 0xa2, 0xee, 0x7a, 0xa2, 0xf8, 0x71, - 0x81, 0x42, 0x5b, 0x22, 0x8c, 0x7c, 0x70, 0x06, 0xa7, 0x49, 0x82, 0x47, 0x26, 0x22, 0x8f, 0x3c, 0xc5, 0xf5, 0x8b, - 0x51, 0x49, 0x2f, 0x2f, 0xe1, 0x91, 0x73, 0x97, 0xc5, 0xdd, 0x47, 0xfc, 0x5a, 0x7d, 0x89, 0x3e, 0x2c, 0xe1, 0x28, - 0x88, 0x95, 0x76, 0xbf, 0x0c, 0x9f, 0xd4, 0x81, 0xca, 0xf9, 0x3f, 0xa8, 0xbf, 0x86, 0x2c, 0xb2, 0x88, 0x26, 0xeb, - 0x0a, 0x73, 0x70, 0xd4, 0x0f, 0x8b, 0x10, 0xf9, 0x22, 0x43, 0x48, 0x16, 0xdd, 0xea, 0xe5, 0x21, 0xb4, 0x9e, 0xfc, - 0xdd, 0x32, 0xf7, 0xfb, 0xbb, 0x6a, 0x7a, 0x38, 0x8d, 0x14, 0xfc, 0x48, 0x45, 0x5f, 0x76, 0x75, 0x7b, 0x12, 0xdf, - 0xf5, 0x7c, 0x0f, 0x01, 0xb3, 0x8f, 0x34, 0x44, 0xf2, 0x66, 0xd9, 0x3a, 0xf4, 0x4d, 0x6c, 0x71, 0x45, 0x6b, 0xd0, - 0xa5, 0xd4, 0xf4, 0x51, 0x81, 0x33, 0xbc, 0x12, 0x74, 0x90, 0xe1, 0x68, 0xb9, 0xf2, 0xa6, 0x42, 0xb0, 0x38, 0xa9, - 0xda, 0x6e, 0x8b, 0x32, 0xdb, 0x33, 0x38, 0x89, 0x16, 0x51, 0x93, 0x99, 0xfc, 0x3e, 0x7d, 0x14, 0xab, 0xdc, 0x28, - 0x82, 0x3b, 0xfa, 0xc2, 0x2a, 0xad, 0xbd, 0x34, 0xe4, 0x97, 0x5a, 0xb2, 0x85, 0x06, 0x54, 0x4a, 0x79, 0xa1, 0x6a, - 0x5c, 0x2e, 0x85, 0x2b, 0x63, 0x6b, 0xf4, 0x30, 0xd3, 0xaa, 0x78, 0x15, 0x57, 0x48, 0xc7, 0xd7, 0x88, 0x45, 0xe8, - 0xb2, 0x0c, 0x12, 0x1e, 0xce, 0x72, 0x2e, 0x79, 0x72, 0x0d, 0x56, 0x3c, 0xb7, 0x60, 0x0e, 0x66, 0x5d, 0xab, 0x27, - 0xd5, 0xd8, 0x80, 0x91, 0x14, 0xf9, 0x2a, 0xa6, 0x73, 0xd5, 0x01, 0x75, 0x5c, 0x43, 0x60, 0x16, 0xee, 0x23, 0x3f, - 0x4a, 0x49, 0xaf, 0x94, 0x91, 0x33, 0xdc, 0x56, 0x49, 0x36, 0xe3, 0x64, 0x24, 0xdc, 0xd1, 0xc6, 0xce, 0x45, 0x01, - 0x3f, 0x0a, 0x39, 0x53, 0x02, 0x1a, 0xd2, 0x10, 0x49, 0x2e, 0x76, 0xcf, 0x13, 0xab, 0x21, 0xd2, 0x93, 0x05, 0x05, - 0x10, 0x79, 0x58, 0xfb, 0x60, 0x49, 0x79, 0x34, 0xb5, 0x80, 0x89, 0x79, 0xae, 0xfc, 0xa8, 0xbd, 0x85, 0xaf, 0xd6, - 0xa1, 0x04, 0xcc, 0xe1, 0xcb, 0x24, 0xd6, 0x4a, 0x9b, 0xf1, 0xb8, 0x2c, 0x8f, 0xca, 0x5b, 0xcb, 0x6a, 0xba, 0xa2, - 0x7a, 0xd0, 0x98, 0x1e, 0xae, 0x53, 0x62, 0x6c, 0x2c, 0xb3, 0x4e, 0x5c, 0x2a, 0xe6, 0xbf, 0x4f, 0x5f, 0xaa, 0x8b, - 0xaa, 0x96, 0x6f, 0x23, 0xae, 0x67, 0x8c, 0x6a, 0x51, 0xc3, 0x03, 0xe6, 0x5f, 0x96, 0x31, 0x2c, 0xd7, 0x04, 0xb3, - 0x5c, 0x13, 0xbd, 0xad, 0x86, 0xa0, 0xed, 0x78, 0x14, 0x95, 0xa0, 0x1b, 0x31, 0xa0, 0x91, 0x52, 0x23, 0x60, 0x9b, - 0x14, 0x62, 0xf0, 0x1b, 0xb0, 0x3f, 0x76, 0x08, 0x48, 0x05, 0x47, 0xf0, 0x80, 0x70, 0xc9, 0x71, 0xf9, 0x61, 0xd2, - 0xdd, 0x42, 0x02, 0xa9, 0x78, 0x39, 0x2b, 0x9f, 0x96, 0x08, 0x46, 0x31, 0x28, 0x8b, 0xd0, 0x0c, 0x91, 0x52, 0x37, - 0x2b, 0x32, 0xea, 0xe0, 0x8d, 0xc1, 0x37, 0x22, 0x06, 0xbc, 0x52, 0x50, 0xc8, 0x63, 0x4e, 0x4e, 0x96, 0xcb, 0xd7, - 0xb9, 0x4b, 0x7f, 0x47, 0x4f, 0xe5, 0x38, 0x75, 0x47, 0xd0, 0xa6, 0x8f, 0x62, 0x9c, 0x8b, 0x4a, 0x82, 0xeb, 0xe5, - 0xb4, 0xf7, 0x98, 0xd1, 0x7c, 0xe1, 0xea, 0x69, 0x3c, 0x68, 0x8b, 0xdf, 0x8f, 0x39, 0xfb, 0xf0, 0x29, 0x35, 0xba, - 0x80, 0x02, 0x0f, 0x3b, 0x75, 0xdb, 0xc8, 0x29, 0x46, 0x80, 0xbf, 0xad, 0xaf, 0xc7, 0xa3, 0xcd, 0x96, 0xb9, 0x20, - 0x35, 0xec, 0x1b, 0x7c, 0x39, 0x18, 0x7f, 0x45, 0xb4, 0xc3, 0xd7, 0x64, 0xdd, 0x43, 0x83, 0xee, 0x5e, 0xd6, 0xf0, - 0x05, 0x0b, 0x74, 0x7e, 0x89, 0x21, 0x6f, 0xd9, 0x81, 0xe5, 0x3e, 0xcf, 0xf5, 0x04, 0x99, 0xc6, 0xc3, 0x78, 0x0d, - 0xc8, 0x35, 0x9e, 0xe5, 0xbd, 0x1b, 0xf5, 0x7b, 0xb6, 0x9c, 0x77, 0xc4, 0xd6, 0xd4, 0xbb, 0x6c, 0x03, 0x8c, 0xa3, - 0xea, 0x7f, 0xdf, 0x54, 0x22, 0x18, 0x99, 0xa1, 0x7d, 0x5a, 0xeb, 0xa3, 0xca, 0xd3, 0xff, 0x37, 0xb3, 0x75, 0x61, - 0x65, 0x98, 0x43, 0x30, 0xe3, 0x2b, 0x7c, 0x9a, 0x71, 0x1e, 0x44, 0x78, 0x20, 0xed, 0x5e, 0x66, 0x37, 0xd6, 0x9c, - 0xd1, 0x8f, 0xd0, 0x9d, 0x92, 0xec, 0x02, 0xc7, 0xf1, 0x6f, 0x83, 0x9e, 0x0a, 0xb5, 0x1f, 0xd5, 0x81, 0xc5, 0xdf, - 0x05, 0xa9, 0x09, 0xc9, 0x50, 0x80, 0x03, 0xb9, 0x6a, 0xd9, 0x7b, 0xba, 0x9d, 0x5d, 0x4c, 0x13, 0xc4, 0xa5, 0xb3, - 0xd5, 0x97, 0xd7, 0xad, 0x17, 0x68, 0xdf, 0xec, 0xfd, 0x68, 0x63, 0xa6, 0x98, 0xc7, 0xeb, 0xbb, 0xa6, 0xe3, 0x37, - 0x14, 0x86, 0xd6, 0xf8, 0x2a, 0x62, 0xba, 0x6f, 0x68, 0xde, 0xcb, 0xb5, 0x37, 0xed, 0x3d, 0x7e, 0xb1, 0xd7, 0xea, - 0x2c, 0xb0, 0xf1, 0x46, 0x01, 0x57, 0x26, 0x2e, 0x67, 0x94, 0xe4, 0xc3, 0x0d, 0x82, 0xeb, 0xf8, 0xd1, 0xaa, 0x19, - 0xee, 0x7a, 0x7c, 0xa3, 0x13, 0x96, 0x61, 0x60, 0xba, 0x85, 0xeb, 0x40, 0x8c, 0x61, 0x6c, 0x11, 0x42, 0x91, 0xfa, - 0x91, 0xc6, 0x30, 0x0a, 0xc6, 0xcf, 0x4f, 0xd6, 0x98, 0xd9, 0xf1, 0x1f, 0x51, 0x00, 0xae, 0x5d, 0x84, 0x23, 0x30, - 0xcc, 0x2c, 0xa8, 0x50, 0xe1, 0x42, 0x1f, 0x89, 0x2b, 0x9b, 0xb2, 0xa7, 0xf0, 0x02, 0xf2, 0x25, 0xa6, 0xfd, 0x51, - 0xd7, 0xf9, 0x83, 0x13, 0xf9, 0x49, 0xed, 0xee, 0xf7, 0x4a, 0xb7, 0x17, 0xe1, 0x32, 0xd3, 0x75, 0xc4, 0x00, 0xc9, - 0x63, 0x30, 0xc1, 0x68, 0x31, 0x64, 0xc7, 0x4d, 0xed, 0x59, 0x9d, 0xce, 0xc9, 0xf1, 0xe0, 0x7f, 0xad, 0x82, 0xd9, - 0xf8, 0x28, 0xde, 0x56, 0x8d, 0x9c, 0xed, 0x49, 0xba, 0xf9, 0x63, 0xa5, 0xc9, 0xec, 0xff, 0x61, 0x7e, 0x9d, 0x05, - 0x0b, 0xe6, 0x8b, 0x45, 0x8b, 0xac, 0x21, 0x6a, 0x80, 0x1f, 0xee, 0x34, 0xa6, 0x7c, 0x46, 0x19, 0x61, 0xab, 0x5a, - 0x2b, 0x6d, 0x68, 0x80, 0xe6, 0x94, 0x9d, 0x20, 0x45, 0x09, 0x24, 0xef, 0x58, 0x85, 0x91, 0xf9, 0xc0, 0x30, 0x78, - 0xec, 0x7d, 0x6a, 0xdd, 0x9a, 0xa2, 0xae, 0xf0, 0x3e, 0xd1, 0xd8, 0x8d, 0x59, 0x29, 0xf5, 0x73, 0x2b, 0xf4, 0x1f, - 0xd1, 0x0b, 0x11, 0x58, 0x22, 0x3f, 0x04, 0xf5, 0x93, 0xa0, 0x96, 0xf5, 0x27, 0x95, 0xb7, 0x7c, 0x6e, 0xcf, 0x2e, - 0xb6, 0xe5, 0xd3, 0x5c, 0x67, 0x20, 0xb1, 0x33, 0xbe, 0xa9, 0x2f, 0xbe, 0x48, 0xb5, 0x96, 0x5b, 0xba, 0xe5, 0xd1, - 0x34, 0xc4, 0xe8, 0x00, 0x80, 0x94, 0x01, 0xe3, 0x27, 0xf8, 0x81, 0x3a, 0xe3, 0x9f, 0xcf, 0x6f, 0xea, 0x9c, 0xea, - 0xaf, 0xde, 0x48, 0xe8, 0x2d, 0x2d, 0x01, 0xdf, 0x85, 0xfc, 0xdf, 0xfe, 0x95, 0x6e, 0x1d, 0x63, 0x45, 0x60, 0x76, - 0x70, 0x75, 0x92, 0x9d, 0xe9, 0x69, 0x6b, 0xe2, 0x2a, 0x06, 0xef, 0x87, 0x68, 0x9d, 0xfb, 0x91, 0xc0, 0x68, 0x0a, - 0x6f, 0xb3, 0xd8, 0xb4, 0x55, 0xae, 0x57, 0x33, 0x61, 0xb6, 0x8d, 0x2e, 0x91, 0x1a, 0x82, 0xeb, 0x7d, 0x2c, 0xa3, - 0x8b, 0x27, 0x83, 0xb3, 0xba, 0xbe, 0x64, 0x2a, 0xc0, 0x85, 0x14, 0xf5, 0x27, 0xca, 0xb2, 0xdd, 0xa3, 0x2e, 0x35, - 0xdd, 0x1f, 0xb4, 0x76, 0xcf, 0xa5, 0xc5, 0x36, 0x5d, 0x36, 0x7d, 0x6a, 0x3d, 0x10, 0xc1, 0xbe, 0xa5, 0x1b, 0xf6, - 0x02, 0x00, 0x1d, 0xe0, 0x85, 0x6a, 0x13, 0x5d, 0x57, 0xfd, 0x63, 0x0f, 0x48, 0x6b, 0x7c, 0x8f, 0x4d, 0xaa, 0xdc, - 0xc8, 0xa4, 0xda, 0x45, 0x82, 0xb2, 0xc3, 0xf8, 0xf8, 0x0e, 0x7b, 0xad, 0x87, 0x17, 0x6a, 0x55, 0x0a, 0x6b, 0xcb, - 0xdc, 0x9b, 0x31, 0xa9, 0x69, 0xeb, 0x0f, 0x5e, 0x0b, 0xeb, 0x86, 0x2e, 0x85, 0xcb, 0xe2, 0x51, 0xab, 0x03, 0x90, - 0x93, 0x0d, 0x84, 0x70, 0xc4, 0xd3, 0x3f, 0x92, 0x9c, 0x02, 0xbc, 0x0e, 0xdc, 0x15, 0xc7, 0xec, 0xb2, 0x1d, 0x77, - 0xc3, 0x96, 0x5b, 0xf8, 0xb3, 0x5b, 0x20, 0xc5, 0xba, 0xea, 0xdc, 0xb0, 0x83, 0xd7, 0x65, 0x8a, 0xa0, 0x54, 0x48, - 0x40, 0x38, 0x5c, 0xce, 0x2e, 0x05, 0xa1, 0x24, 0x60, 0xac, 0x0a, 0xf7, 0x87, 0xb2, 0xb7, 0xdd, 0x6e, 0xd8, 0x92, - 0x47, 0x92, 0x61, 0xa0, 0x6a, 0x3d, 0xa6, 0xf5, 0xaf, 0x76, 0x3a, 0x81, 0x4a, 0xd6, 0x6c, 0x7a, 0xa4, 0x7f, 0x58, - 0x8f, 0xf4, 0x52, 0xf0, 0x48, 0xc4, 0xe2, 0x1d, 0x19, 0xa3, 0xab, 0x1f, 0x2f, 0x90, 0xd9, 0x3b, 0x2e, 0x7e, 0x98, - 0xc3, 0xda, 0xb0, 0xcb, 0x80, 0x27, 0x98, 0x49, 0x83, 0x7a, 0xd5, 0x55, 0xf4, 0x54, 0x90, 0x0e, 0x8b, 0x86, 0x81, - 0x85, 0x53, 0xea, 0x57, 0xe9, 0x2d, 0x6f, 0x74, 0x16, 0x34, 0x86, 0x12, 0x2d, 0x65, 0xa1, 0x2c, 0x37, 0x93, 0x87, - 0x0d, 0x25, 0x2b, 0xae, 0xa9, 0x6d, 0x67, 0xab, 0x68, 0xd1, 0x0a, 0xc2, 0x1f, 0xd7, 0x30, 0x23, 0xea, 0x2f, 0x64, - 0x9a, 0x75, 0x3b, 0xfc, 0x0c, 0x69, 0xb5, 0xa4, 0x76, 0x6e, 0x81, 0xf6, 0x82, 0x06, 0xfc, 0x1a, 0x82, 0xd6, 0x92, - 0x52, 0x13, 0x9f, 0xd6, 0xb9, 0xe0, 0xf1, 0x86, 0xe1, 0xd3, 0x26, 0xa9, 0x97, 0xd9, 0xc6, 0xd5, 0x0d, 0x4f, 0x73, - 0x29, 0x3a, 0x18, 0x74, 0x90, 0x90, 0x52, 0x73, 0xa8, 0xc8, 0xdf, 0x5d, 0xac, 0x0b, 0xa7, 0x09, 0xc9, 0x66, 0x2a, - 0x59, 0x4e, 0x4a, 0xf6, 0x8c, 0x10, 0x47, 0x3f, 0x20, 0x65, 0xf2, 0x08, 0x35, 0xc9, 0xab, 0x17, 0x90, 0xc9, 0xeb, - 0x51, 0x4b, 0x8a, 0x0b, 0x6d, 0x32, 0xb1, 0x14, 0x70, 0x32, 0x8e, 0x20, 0x13, 0xac, 0xa7, 0x64, 0x75, 0x0d, 0x90, - 0xf4, 0x92, 0x3c, 0x35, 0xb0, 0x60, 0x6a, 0xef, 0x94, 0x02, 0x8b, 0x14, 0x80, 0xa1, 0x9d, 0x34, 0x2a, 0x4b, 0xf2, - 0x58, 0x76, 0xdf, 0x96, 0x65, 0x4f, 0xa1, 0xa0, 0x60, 0xc4, 0xd9, 0x63, 0x9f, 0x9d, 0x05, 0x82, 0xf2, 0x70, 0x06, - 0x65, 0xfa, 0x9c, 0x60, 0x23, 0xcf, 0x11, 0x2c, 0xf2, 0x62, 0x40, 0x8e, 0x2a, 0x5e, 0xd6, 0x08, 0xff, 0xd5, 0x02, - 0xb9, 0xc0, 0xe0, 0xe1, 0x9e, 0x74, 0x7a, 0xad, 0xdf, 0x94, 0x53, 0x50, 0x30, 0xfa, 0x94, 0xd5, 0xcd, 0x38, 0x37, - 0xd4, 0x32, 0x9a, 0x31, 0xf5, 0x67, 0xee, 0xe2, 0x49, 0xbe, 0x55, 0x32, 0xa3, 0x22, 0x93, 0x89, 0x17, 0x7e, 0x00, - 0x3b, 0x3f, 0x2f, 0x26, 0x06, 0x85, 0x27, 0x2e, 0xea, 0x98, 0x11, 0x87, 0xb8, 0x2a, 0x27, 0xbf, 0x2d, 0xc0, 0xa8, - 0xc1, 0x60, 0x72, 0x8b, 0x7a, 0x4d, 0xa9, 0xf7, 0x50, 0x9f, 0x19, 0x0c, 0xb5, 0x71, 0xac, 0x57, 0x56, 0x82, 0x0d, - 0x0d, 0x2f, 0xf9, 0x54, 0xcd, 0x3a, 0x8c, 0x15, 0x7e, 0xa5, 0x02, 0xcc, 0x05, 0xa6, 0x79, 0x1a, 0x87, 0x80, 0x95, - 0x96, 0x6a, 0x18, 0x7d, 0x75, 0xee, 0x10, 0x4a, 0xdd, 0xe8, 0x05, 0x6c, 0x00, 0xc3, 0x21, 0xa2, 0x2d, 0x7a, 0x79, - 0xe1, 0x2b, 0x0d, 0x52, 0xb5, 0x23, 0x4b, 0x5e, 0x1d, 0x72, 0x22, 0x8f, 0x27, 0xe2, 0x7f, 0x26, 0x0c, 0x49, 0x9b, - 0x1b, 0x88, 0xb7, 0x94, 0xdd, 0xd4, 0x71, 0x9a, 0x39, 0x48, 0xef, 0xe9, 0x60, 0xaf, 0x95, 0xaf, 0x6c, 0x93, 0x19, - 0x7a, 0x35, 0x1a, 0x87, 0x82, 0xb4, 0xbc, 0x98, 0x2d, 0x32, 0x69, 0x12, 0xdd, 0x96, 0x16, 0x03, 0x7a, 0x88, 0xec, - 0xcc, 0x43, 0xb1, 0xe2, 0x7d, 0x3d, 0x99, 0x16, 0x14, 0x1d, 0xc2, 0x2d, 0xe4, 0x26, 0x1a, 0xf5, 0x13, 0x5d, 0xb5, - 0x2b, 0x94, 0xd9, 0x7e, 0x26, 0x74, 0x80, 0x97, 0x16, 0x27, 0x00, 0xec, 0xd1, 0x34, 0x2e, 0xb8, 0x6d, 0x09, 0xc3, - 0xd4, 0x86, 0x6a, 0x2e, 0x3b, 0xdd, 0xd6, 0x99, 0x5c, 0x0b, 0x14, 0x83, 0x00, 0x3a, 0xcf, 0x37, 0xef, 0x4f, 0x5e, - 0xfe, 0x0c, 0xc7, 0xb1, 0x83, 0xd1, 0xc9, 0x8c, 0xaa, 0xb8, 0x4d, 0xa2, 0xde, 0x6f, 0xe9, 0xa6, 0x81, 0xbc, 0xef, - 0x41, 0x35, 0x7b, 0xd6, 0xef, 0x4e, 0xd7, 0xc6, 0x3b, 0xf5, 0x9b, 0xd9, 0x00, 0xa0, 0xbc, 0x48, 0x9a, 0x0f, 0x70, - 0xdc, 0xe0, 0xe7, 0x19, 0xab, 0x15, 0x6a, 0x24, 0x22, 0x88, 0x02, 0x12, 0xa6, 0xfe, 0x59, 0x38, 0xbc, 0xc7, 0x77, - 0x2c, 0x3b, 0x51, 0x70, 0x48, 0xea, 0xab, 0xc1, 0xd1, 0x83, 0x6e, 0x4c, 0x05, 0xc3, 0x1a, 0xe3, 0x84, 0x9b, 0x6f, - 0xb1, 0xef, 0x5a, 0x2b, 0x8a, 0xeb, 0xc2, 0x3e, 0xf7, 0x9d, 0xa2, 0x9e, 0x7d, 0x76, 0x43, 0x8f, 0xf3, 0xe0, 0x15, - 0x53, 0x56, 0x29, 0xd6, 0x5d, 0x8e, 0x3c, 0x3e, 0x01, 0x52, 0xf3, 0x5d, 0xc9, 0xdf, 0x60, 0xac, 0x20, 0x0a, 0xbc, - 0x5f, 0x6d, 0x8a, 0x74, 0x39, 0xb1, 0x22, 0x0a, 0x83, 0x20, 0xf3, 0x2a, 0x42, 0xbc, 0xa2, 0x92, 0xdf, 0xb7, 0x03, - 0x38, 0x81, 0x3c, 0x1c, 0xb6, 0x09, 0xbe, 0xdf, 0xd1, 0x40, 0xa8, 0x18, 0x37, 0xd2, 0x76, 0x4b, 0x4e, 0x37, 0x8c, - 0x7b, 0x3a, 0x69, 0xf6, 0x26, 0x91, 0x9b, 0x44, 0x0d, 0x47, 0x11, 0xcb, 0xd7, 0x64, 0x77, 0x45, 0x81, 0x42, 0x80, - 0xc8, 0x2e, 0xf9, 0x1c, 0x96, 0x9a, 0xca, 0xf5, 0xb5, 0xe4, 0x17, 0x48, 0x82, 0x8c, 0xdb, 0x40, 0xea, 0x9f, 0x14, - 0xa1, 0xec, 0x1c, 0xb5, 0x61, 0xc7, 0x8f, 0x26, 0xaa, 0x0e, 0x76, 0xa7, 0x55, 0x9b, 0x1d, 0x8d, 0x60, 0xdf, 0x6b, - 0x85, 0x96, 0x83, 0xd6, 0x59, 0x9f, 0x9a, 0xdc, 0x10, 0x3f, 0x3e, 0xe7, 0x72, 0x80, 0x00, 0x3a, 0x59, 0xa0, 0xc2, - 0xfd, 0xd0, 0x51, 0xdf, 0xae, 0x0e, 0x69, 0x02, 0x45, 0xe5, 0xa0, 0xb8, 0xe3, 0x38, 0x85, 0x5d, 0x91, 0x1d, 0xfd, - 0x42, 0x34, 0x4e, 0xd8, 0xe1, 0x23, 0x6b, 0x9a, 0x3f, 0xc4, 0x09, 0xca, 0x17, 0xf3, 0x50, 0x70, 0x89, 0x3a, 0x1b, - 0x52, 0x46, 0x00, 0x74, 0x47, 0xbb, 0xf5, 0x90, 0x7e, 0x5c, 0xdb, 0x14, 0x7b, 0xee, 0x09, 0xfa, 0xbc, 0x6f, 0x60, - 0xdc, 0x11, 0xd8, 0xd1, 0x40, 0x12, 0xda, 0x47, 0x95, 0xfa, 0x33, 0x8f, 0xc5, 0x98, 0xd9, 0xa7, 0xdb, 0x66, 0x82, - 0xca, 0x9d, 0xec, 0x52, 0x09, 0xd2, 0xe0, 0x0d, 0xf2, 0x70, 0xd8, 0xd7, 0xfd, 0x5e, 0x6a, 0xda, 0xe6, 0xc9, 0xed, - 0x2b, 0xab, 0xd5, 0x94, 0xef, 0x76, 0x99, 0x40, 0x7c, 0x71, 0x0e, 0x65, 0xbc, 0xe7, 0x81, 0xaa, 0xef, 0x1b, 0x59, - 0x43, 0xc0, 0x7d, 0xb3, 0x30, 0xcc, 0x09, 0x3a, 0xc2, 0x20, 0x69, 0xe6, 0xc1, 0x9f, 0x00, 0x6d, 0xde, 0xcb, 0xeb, - 0x55, 0x88, 0x73, 0x40, 0x77, 0xf8, 0xf2, 0x84, 0xb5, 0x8e, 0xd8, 0xe3, 0x83, 0x79, 0xc6, 0x28, 0x37, 0xbc, 0x44, - 0xc7, 0x88, 0xdb, 0xde, 0x95, 0x17, 0x32, 0x5d, 0x3e, 0xfb, 0x96, 0x04, 0xbe, 0x31, 0x52, 0x81, 0x14, 0x68, 0xc4, - 0xb1, 0x0f, 0x36, 0xdf, 0x87, 0x43, 0xb3, 0x5f, 0xe8, 0x8d, 0xc2, 0xf4, 0x72, 0xfc, 0xe5, 0xc6, 0xfc, 0x16, 0x8e, - 0xb8, 0xda, 0x2a, 0xc4, 0xc3, 0x5e, 0x8e, 0xb9, 0xd0, 0x9a, 0x07, 0xbf, 0x30, 0x27, 0xcb, 0x42, 0xe2, 0xdd, 0x45, - 0x7d, 0xc3, 0x7a, 0xcd, 0x96, 0x3d, 0x93, 0x59, 0x13, 0x0f, 0x92, 0xf5, 0xb4, 0xf2, 0x70, 0x7a, 0x2a, 0xcf, 0xb1, - 0xd9, 0x0b, 0x0b, 0xb2, 0xa1, 0xab, 0xa7, 0xb6, 0x5c, 0xf7, 0xd6, 0x34, 0x24, 0x2f, 0xf1, 0x8b, 0xab, 0x68, 0x01, - 0x4a, 0x4c, 0xd4, 0xce, 0xac, 0x5d, 0x90, 0x0a, 0xf6, 0x7a, 0x59, 0x40, 0x83, 0x63, 0xe5, 0xd8, 0x96, 0xd0, 0x53, - 0x91, 0x19, 0x9f, 0x55, 0x29, 0x20, 0x7d, 0x4d, 0xd4, 0xed, 0x45, 0x54, 0x5a, 0x42, 0x82, 0xc0, 0xc7, 0x4f, 0x92, - 0x52, 0xec, 0xcb, 0x0d, 0x20, 0x30, 0x54, 0xbc, 0xef, 0x02, 0xbe, 0xbf, 0xa9, 0x48, 0x64, 0x72, 0xb0, 0x92, 0xf7, - 0x44, 0x97, 0x14, 0xf8, 0x9f, 0x9f, 0xef, 0xac, 0x54, 0x2a, 0x72, 0x39, 0x86, 0x11, 0xc5, 0x5e, 0x33, 0x45, 0x61, - 0x6e, 0x1a, 0xa1, 0x20, 0x50, 0xcb, 0x3f, 0x5c, 0x7f, 0xa1, 0xbf, 0xa4, 0x04, 0xa7, 0x96, 0x40, 0x5c, 0x5e, 0x9d, - 0x87, 0x04, 0x67, 0xf5, 0x16, 0x79, 0xac, 0x20, 0xd8, 0x63, 0xae, 0x35, 0x3b, 0xcc, 0x81, 0x64, 0x57, 0x0b, 0x8c, - 0xb6, 0x44, 0x29, 0xf5, 0x02, 0x62, 0x97, 0x4c, 0xf7, 0x75, 0x45, 0x91, 0xee, 0x51, 0xf4, 0x98, 0xca, 0x68, 0x39, - 0x3e, 0xd1, 0xd8, 0xdf, 0x18, 0xaa, 0x96, 0xfa, 0x2a, 0x7b, 0xc2, 0xd7, 0xbb, 0xc3, 0x17, 0x1b, 0x3f, 0x12, 0xfe, - 0x3e, 0x57, 0x4c, 0x3f, 0x67, 0xf7, 0xa3, 0x5d, 0xc2, 0x68, 0xa0, 0x7a, 0xae, 0x39, 0x6e, 0x2c, 0x37, 0x5e, 0x6e, - 0x5f, 0x74, 0xc5, 0x56, 0x99, 0x9f, 0xbb, 0x05, 0xb9, 0x26, 0xdd, 0x6b, 0x32, 0xcf, 0x49, 0x6e, 0xf0, 0xce, 0xf4, - 0x20, 0x9d, 0x08, 0xae, 0xfd, 0xcb, 0xf3, 0xaf, 0x3b, 0x5c, 0x85, 0x6d, 0x5b, 0x91, 0x57, 0x66, 0x40, 0x39, 0xb4, - 0xdb, 0x04, 0xd4, 0x97, 0x6e, 0xd2, 0x1d, 0xd1, 0x36, 0xb6, 0xf0, 0x12, 0xe2, 0x35, 0x50, 0xdc, 0xd2, 0xc4, 0x57, - 0x67, 0xa3, 0x90, 0xa6, 0x64, 0xb2, 0x07, 0x84, 0x62, 0xc2, 0x02, 0xfd, 0xd3, 0x71, 0xd2, 0xac, 0x0a, 0x5a, 0xef, - 0x95, 0xaa, 0x8e, 0x95, 0xd3, 0xd5, 0x57, 0x61, 0x66, 0xa3, 0x19, 0xf1, 0x20, 0x27, 0x1b, 0x87, 0x28, 0x77, 0x9d, - 0xe9, 0xe8, 0x50, 0x3c, 0xa6, 0xdc, 0x49, 0x9d, 0x5c, 0x9c, 0xb3, 0x23, 0x09, 0xc5, 0x7f, 0xeb, 0x9c, 0x08, 0x85, - 0x6f, 0x61, 0x2b, 0x02, 0xf9, 0xda, 0xb4, 0xfc, 0xaf, 0x1d, 0xf5, 0x39, 0xe1, 0x8e, 0x76, 0xe5, 0x6a, 0xc6, 0x29, - 0xb2, 0xe1, 0x40, 0xe6, 0xe3, 0x46, 0x05, 0xaf, 0x3c, 0x55, 0x65, 0xbf, 0x8d, 0x98, 0xf4, 0x89, 0x3d, 0x9b, 0x1c, - 0x26, 0xa5, 0xa3, 0xf6, 0x13, 0x5c, 0x16, 0x1d, 0x4c, 0xc3, 0xa2, 0x0d, 0x11, 0x20, 0x6a, 0xb5, 0xd1, 0x0e, 0x8b, - 0x88, 0x04, 0x8d, 0x2f, 0x56, 0x2f, 0xe3, 0x81, 0x8f, 0xe6, 0x18, 0xc5, 0x3e, 0x6d, 0x6b, 0x49, 0xf6, 0xbd, 0x31, - 0x46, 0xca, 0x40, 0x7d, 0xa2, 0x73, 0xa0, 0x4c, 0x2c, 0xf2, 0x31, 0xc9, 0x4b, 0x2d, 0x56, 0xb8, 0x4b, 0x5e, 0x47, - 0x25, 0x60, 0x45, 0xf2, 0x57, 0x70, 0x19, 0x25, 0x08, 0x46, 0x8f, 0xa2, 0x2f, 0xfd, 0x12, 0xdc, 0x72, 0xdf, 0x1f, - 0x32, 0x05, 0x76, 0x8a, 0xb1, 0x8f, 0x18, 0xbd, 0x94, 0x22, 0xf3, 0x21, 0x12, 0x8f, 0xdf, 0xb3, 0x04, 0x29, 0x28, - 0x7d, 0x69, 0x1b, 0x60, 0x70, 0x13, 0xe8, 0xb2, 0x6e, 0x6a, 0x9c, 0x01, 0x72, 0x22, 0x57, 0xd4, 0x76, 0xdc, 0xf3, - 0xc9, 0x0e, 0x05, 0x6d, 0x0d, 0x32, 0x66, 0x7a, 0xa1, 0x59, 0xa2, 0xc0, 0xf0, 0xfd, 0x56, 0xe3, 0x28, 0x18, 0xb0, - 0x6b, 0xac, 0x47, 0xbf, 0x52, 0xd2, 0x21, 0x53, 0xfa, 0x91, 0x16, 0xce, 0xe5, 0x4c, 0xa6, 0x7a, 0xf3, 0x7b, 0x21, - 0x05, 0x10, 0x17, 0x6f, 0x25, 0xa2, 0xb7, 0x64, 0x7f, 0x1d, 0x14, 0xa0, 0x60, 0x1a, 0x19, 0x23, 0xfd, 0x5f, 0x2c, - 0x0b, 0x72, 0x3b, 0x0a, 0x6b, 0x86, 0x04, 0x06, 0x15, 0x1f, 0x77, 0x68, 0x8e, 0xbf, 0x0e, 0xff, 0x6b, 0x03, 0x50, - 0x57, 0xee, 0xbc, 0xa1, 0xac, 0xf9, 0x01, 0x29, 0x45, 0x66, 0x2f, 0xdf, 0xbd, 0x6a, 0x85, 0x3a, 0x68, 0xb1, 0xcd, - 0x75, 0x9e, 0xd7, 0x16, 0xbf, 0x9e, 0x42, 0xb7, 0x37, 0xf9, 0xcd, 0x6c, 0x57, 0x5d, 0x3d, 0x36, 0x6a, 0xd4, 0x1b, - 0x04, 0xa3, 0xb7, 0x37, 0xc3, 0x6e, 0x9d, 0x0f, 0x67, 0x25, 0xa0, 0x95, 0xcd, 0x5e, 0xfd, 0x9b, 0x08, 0x0a, 0x7d, - 0xad, 0x9f, 0x47, 0xba, 0xca, 0xb8, 0x7c, 0x96, 0x80, 0x97, 0xc6, 0x87, 0x46, 0x95, 0x6a, 0x59, 0x58, 0xb2, 0x26, - 0x11, 0x08, 0x0e, 0x7f, 0xd0, 0xac, 0x67, 0xda, 0x8f, 0xea, 0x36, 0xdf, 0xd4, 0x75, 0x15, 0x41, 0xfb, 0x11, 0xd6, - 0x74, 0xa5, 0xff, 0x37, 0x71, 0x78, 0x38, 0x45, 0xff, 0xa3, 0xf9, 0x83, 0x82, 0xed, 0xdd, 0x66, 0x53, 0x42, 0x85, - 0x6b, 0xe6, 0x5e, 0x3d, 0xc5, 0x33, 0x45, 0x62, 0x17, 0xa5, 0x67, 0x55, 0xdb, 0xc1, 0xb2, 0xa1, 0xb6, 0xd7, 0x90, - 0xb0, 0x45, 0x90, 0x6a, 0x0a, 0xc6, 0xcd, 0xd3, 0x3b, 0xdc, 0x1d, 0x71, 0xcc, 0xa0, 0x1c, 0x1a, 0x45, 0x99, 0xdf, - 0x0e, 0x93, 0xe6, 0x54, 0x6d, 0x6f, 0x51, 0xe0, 0x47, 0x00, 0x9f, 0x2b, 0x6a, 0x07, 0xf2, 0xf4, 0x61, 0x94, 0xaf, - 0x27, 0xb9, 0xef, 0xc4, 0x11, 0x09, 0xd6, 0x0e, 0x6c, 0x79, 0xc9, 0xab, 0xd3, 0x95, 0xd5, 0x3d, 0x89, 0xaf, 0x3b, - 0xc6, 0xf9, 0x21, 0x71, 0xed, 0x47, 0x4f, 0x53, 0x0e, 0xdb, 0xa2, 0x9e, 0xe0, 0xb0, 0x38, 0xb4, 0xdd, 0x10, 0xdd, - 0x76, 0x96, 0x46, 0xef, 0x00, 0x6d, 0xb1, 0xc9, 0x8b, 0x27, 0x9d, 0x63, 0x5c, 0x1f, 0x2e, 0x27, 0x69, 0xd9, 0x3f, - 0x95, 0x1a, 0xa2, 0xbe, 0xa5, 0x74, 0x8f, 0xd4, 0x1d, 0x1d, 0x6c, 0xcd, 0xde, 0x3f, 0x16, 0xcd, 0x43, 0xe4, 0xb5, - 0x1c, 0x36, 0x6d, 0x52, 0xce, 0x87, 0x2f, 0x1b, 0x7d, 0x59, 0x5e, 0x6d, 0x4a, 0xb6, 0x41, 0xea, 0x4c, 0xb4, 0x79, - 0x0c, 0xa8, 0x6f, 0x0d, 0xbd, 0x0a, 0xbe, 0x60, 0xcd, 0x16, 0xfa, 0xe6, 0xbc, 0x5b, 0x60, 0x2c, 0xc1, 0x67, 0x0c, - 0x6d, 0x73, 0xee, 0xbe, 0x93, 0xee, 0xb3, 0x1c, 0xa6, 0x5c, 0x54, 0x4e, 0x51, 0x22, 0x89, 0xba, 0xff, 0x2f, 0xaf, - 0xf7, 0x52, 0x46, 0x7a, 0x79, 0x42, 0x87, 0x9d, 0xc2, 0xc3, 0x25, 0xab, 0x80, 0x62, 0xac, 0xad, 0xf4, 0xbc, 0x72, - 0x0a, 0x52, 0xa3, 0xa3, 0xb8, 0xd0, 0x7f, 0xf8, 0xca, 0x5d, 0xef, 0x36, 0xd6, 0xf4, 0x63, 0xca, 0x92, 0xbf, 0xf6, - 0x8d, 0x04, 0x6d, 0x5d, 0x11, 0x99, 0xfc, 0x9f, 0x48, 0x4c, 0x8e, 0x2c, 0xc4, 0xa3, 0x03, 0x68, 0x60, 0xa7, 0x4e, - 0xb6, 0xa0, 0xc5, 0x24, 0x09, 0x88, 0x2e, 0xd1, 0x1c, 0x4e, 0x00, 0x6d, 0xd2, 0x12, 0x4c, 0xc8, 0x6f, 0xf4, 0xbe, - 0xcb, 0x98, 0x27, 0xfc, 0x65, 0x1e, 0x4e, 0xa0, 0xfb, 0xe0, 0xd0, 0xa2, 0x09, 0x58, 0x45, 0x92, 0x86, 0xb5, 0xb6, - 0x9d, 0x0f, 0x27, 0xdb, 0x09, 0x49, 0xaa, 0xf7, 0xfb, 0xdc, 0x90, 0x42, 0xc8, 0xed, 0x28, 0x45, 0x4d, 0xe7, 0x7c, - 0xd5, 0xea, 0xcd, 0x21, 0xd6, 0xc5, 0x0c, 0x75, 0xcf, 0x40, 0x49, 0xdb, 0xce, 0x16, 0xe8, 0xf6, 0x09, 0xff, 0xf8, - 0xcb, 0x40, 0x13, 0x14, 0xcd, 0x1a, 0xb0, 0xa4, 0x00, 0xdc, 0xc6, 0x9c, 0xef, 0x35, 0x4f, 0xa9, 0xa2, 0xba, 0x83, - 0x30, 0x77, 0xd8, 0x90, 0x62, 0x54, 0xf7, 0xe1, 0x84, 0x05, 0x41, 0xbc, 0xf6, 0x44, 0x0e, 0x22, 0x3d, 0xa8, 0x4f, - 0x3a, 0x10, 0x32, 0xeb, 0x81, 0xb3, 0x86, 0x55, 0xd2, 0xad, 0xae, 0x59, 0xd7, 0x19, 0x7f, 0xf2, 0x43, 0xd6, 0xd9, - 0x40, 0xff, 0x64, 0xa3, 0xa4, 0x73, 0x5d, 0x44, 0x04, 0x4f, 0xe2, 0x65, 0x0e, 0x94, 0xe7, 0x3d, 0x4d, 0x39, 0xb5, - 0xfc, 0xf8, 0xef, 0x5b, 0x32, 0x87, 0xfa, 0x92, 0x35, 0xf9, 0x7b, 0xa7, 0x3f, 0x59, 0x44, 0x5e, 0x31, 0x35, 0x5f, - 0x2d, 0x26, 0x2b, 0x2f, 0x32, 0xce, 0x29, 0x91, 0x0a, 0x4e, 0xad, 0xe8, 0x7c, 0x22, 0x97, 0xd8, 0xc6, 0x1f, 0x04, - 0x32, 0x67, 0x8f, 0xec, 0x3d, 0x3b, 0xa8, 0x18, 0x2d, 0xa1, 0x20, 0x66, 0x51, 0x35, 0xf0, 0xed, 0xc1, 0x9b, 0x31, - 0xb3, 0xe7, 0xa4, 0x40, 0x8b, 0x51, 0x60, 0xcb, 0x85, 0x18, 0x0d, 0xf1, 0xaa, 0x64, 0xae, 0x24, 0xe1, 0xcf, 0x96, - 0x99, 0x12, 0x3f, 0x64, 0xa5, 0x0e, 0xee, 0xbc, 0x58, 0xb9, 0x64, 0xb9, 0x7c, 0x7e, 0xfd, 0x08, 0xec, 0x7a, 0xef, - 0x11, 0x31, 0xe3, 0xf5, 0x93, 0x05, 0xbb, 0x56, 0x80, 0x12, 0x19, 0xdd, 0x30, 0xee, 0x22, 0xa1, 0x46, 0xd9, 0x61, - 0x74, 0x05, 0x2a, 0x8e, 0x75, 0x2a, 0x0a, 0x00, 0xfe, 0x78, 0x3d, 0x54, 0x2e, 0x70, 0x7f, 0x3c, 0x11, 0x80, 0x32, - 0xca, 0xf4, 0x9d, 0xc9, 0x18, 0x10, 0x1d, 0x35, 0x13, 0xf8, 0x37, 0x61, 0xac, 0x9e, 0xfb, 0xec, 0xf8, 0x28, 0xee, - 0x65, 0x23, 0x0c, 0x34, 0x96, 0x65, 0x93, 0xcd, 0xba, 0x75, 0x5b, 0xe1, 0x4f, 0xc5, 0x0a, 0xa4, 0x29, 0x40, 0xf3, - 0x31, 0x6d, 0x04, 0x9c, 0x81, 0x31, 0xfb, 0x32, 0x81, 0x9a, 0x2a, 0x18, 0x47, 0x5f, 0x5b, 0x36, 0x3c, 0x1f, 0xd5, - 0xdd, 0x0f, 0x2e, 0x73, 0x81, 0x50, 0x16, 0x0b, 0x6c, 0x7b, 0xa1, 0x4e, 0xfc, 0x56, 0x90, 0x79, 0x7c, 0x5f, 0x0d, - 0x8b, 0x36, 0x1d, 0x2d, 0x2b, 0x2b, 0xac, 0x0f, 0x7a, 0xb4, 0x47, 0xb0, 0x1a, 0x29, 0x5a, 0xcf, 0x71, 0xb7, 0x02, - 0x1b, 0xd1, 0xe3, 0xd4, 0x60, 0xf5, 0x83, 0x49, 0x81, 0xe4, 0x60, 0xc8, 0xb6, 0x23, 0x96, 0x1a, 0x18, 0x82, 0x9a, - 0x97, 0xa7, 0x00, 0x6b, 0xa4, 0x76, 0xd3, 0xd2, 0x68, 0xf2, 0xaf, 0xda, 0xa2, 0xdf, 0xfa, 0x37, 0xb3, 0xde, 0x35, - 0x42, 0x24, 0xdb, 0xc3, 0xf9, 0xec, 0x0c, 0x2d, 0x98, 0x41, 0xa3, 0x22, 0xb4, 0x87, 0x50, 0x6a, 0x4e, 0x23, 0x31, - 0xa8, 0x85, 0x10, 0xd9, 0x9f, 0xb8, 0xb7, 0x9c, 0xf0, 0x3c, 0xe0, 0x1e, 0x9e, 0x95, 0x34, 0xe9, 0x34, 0x5f, 0x2a, - 0x23, 0xb8, 0x2b, 0x70, 0x8a, 0x12, 0xcc, 0x16, 0xf4, 0x4f, 0x7e, 0x7b, 0x57, 0x8a, 0x18, 0xae, 0x0b, 0x08, 0xa5, - 0xcf, 0x9e, 0x11, 0x45, 0xbb, 0xc8, 0x88, 0x56, 0x25, 0x4b, 0x70, 0x81, 0xec, 0x23, 0xdb, 0xcf, 0x46, 0x16, 0xcc, - 0x1a, 0xf6, 0x53, 0xdd, 0x88, 0xf6, 0x21, 0x30, 0x23, 0x36, 0xc7, 0x5e, 0x4f, 0x9e, 0x40, 0x43, 0xf4, 0xb0, 0x64, - 0x5a, 0x17, 0xb4, 0x74, 0x95, 0x62, 0xa5, 0x42, 0x37, 0xf1, 0xa8, 0x1f, 0xa9, 0x51, 0xab, 0xe5, 0xed, 0x10, 0x7d, - 0x04, 0x6b, 0x5e, 0xef, 0x9f, 0xe2, 0x5d, 0x43, 0x81, 0x98, 0x85, 0x3b, 0x56, 0xd6, 0x58, 0xd9, 0x63, 0x61, 0xe2, - 0xf0, 0x8d, 0x10, 0x0b, 0x4f, 0x85, 0xde, 0x4f, 0xf3, 0xbf, 0x36, 0x78, 0xf5, 0xb5, 0x50, 0xd6, 0x04, 0xe5, 0x87, - 0xc1, 0xc2, 0x99, 0x0b, 0x7c, 0x8c, 0xb1, 0xd3, 0xe1, 0x37, 0x8a, 0x68, 0x83, 0x44, 0x4b, 0x8a, 0x61, 0x0b, 0xb7, - 0x57, 0x12, 0x57, 0x49, 0x15, 0x1c, 0x45, 0x18, 0x5f, 0x70, 0xeb, 0xf1, 0x4b, 0xd6, 0x18, 0x13, 0x8e, 0xce, 0x39, - 0x28, 0x5b, 0x11, 0x12, 0xcc, 0x02, 0x9b, 0xf4, 0x70, 0x83, 0x65, 0x5a, 0x21, 0x25, 0x08, 0x31, 0xc9, 0x74, 0x3f, - 0x86, 0xa1, 0x12, 0x5b, 0x05, 0x41, 0x46, 0x65, 0x76, 0xe8, 0xc4, 0x19, 0x6d, 0x71, 0x98, 0x62, 0x8d, 0xf0, 0xa9, - 0xa6, 0x17, 0x21, 0x4a, 0x22, 0xef, 0x99, 0x5d, 0x63, 0x98, 0x40, 0x2b, 0x32, 0x55, 0x32, 0xfa, 0x2a, 0x06, 0xdc, - 0xfa, 0x6b, 0xed, 0x43, 0xc1, 0x3a, 0xb8, 0x86, 0x5e, 0xaa, 0xe2, 0xaf, 0x4e, 0xa1, 0x55, 0xea, 0x92, 0x54, 0x49, - 0x4f, 0xa6, 0x90, 0xe6, 0xbc, 0x82, 0x1e, 0xce, 0x79, 0x88, 0xb7, 0x02, 0xde, 0x2a, 0xf8, 0x04, 0x5a, 0xd2, 0x08, - 0xf7, 0x2d, 0xbb, 0xda, 0x3e, 0x2b, 0x91, 0xed, 0xe7, 0xe6, 0x64, 0xc4, 0xb9, 0x0e, 0x34, 0x7a, 0x0e, 0x0b, 0x2f, - 0x83, 0x16, 0x7d, 0xa7, 0x06, 0xee, 0x4a, 0x44, 0xdf, 0xfa, 0x43, 0x0b, 0x8a, 0x35, 0xab, 0x54, 0xc0, 0x9e, 0xa9, - 0xf7, 0x23, 0x21, 0xf1, 0x58, 0xfe, 0xb1, 0xa7, 0xc7, 0x24, 0x51, 0xb5, 0x3c, 0x81, 0x91, 0x08, 0x51, 0x93, 0x41, - 0xd6, 0xfa, 0x04, 0x83, 0xae, 0x59, 0xae, 0x52, 0x73, 0x85, 0x30, 0x87, 0x32, 0xdd, 0xd5, 0xda, 0x2e, 0x00, 0x4e, - 0x5f, 0xad, 0xe7, 0x2b, 0xd0, 0x69, 0x61, 0x06, 0x28, 0x71, 0xa6, 0x47, 0x75, 0xc6, 0xc1, 0xa9, 0x6e, 0x11, 0xff, - 0xeb, 0x95, 0x4a, 0x58, 0x7b, 0xf0, 0x70, 0x50, 0xf1, 0xa4, 0x82, 0xfc, 0x6c, 0xa0, 0x29, 0x0d, 0x03, 0x52, 0x70, - 0x4e, 0x62, 0x57, 0x2c, 0xa7, 0x8b, 0x47, 0x5e, 0x19, 0x23, 0x9c, 0xc0, 0xba, 0xd3, 0xa7, 0xd3, 0x41, 0x31, 0x2e, - 0xd1, 0x52, 0x17, 0x35, 0xe7, 0xd6, 0x49, 0x5a, 0xee, 0x40, 0xf1, 0x57, 0x96, 0xa8, 0x6b, 0x91, 0x4e, 0x96, 0x2d, - 0xae, 0xea, 0xab, 0x31, 0xed, 0x82, 0x08, 0x2b, 0x6a, 0xe4, 0xd6, 0x42, 0x9d, 0xed, 0x77, 0x5e, 0xde, 0x50, 0x8c, - 0xe3, 0x39, 0xbf, 0xd6, 0xca, 0xc3, 0xb3, 0x96, 0x52, 0x2f, 0xde, 0x32, 0x47, 0xd3, 0x89, 0x35, 0x5f, 0x6a, 0x84, - 0x67, 0xe2, 0x2e, 0x22, 0xc3, 0x68, 0x80, 0xe9, 0xdb, 0xaa, 0x45, 0x2c, 0xa4, 0x1d, 0x40, 0x3f, 0x17, 0xd4, 0x39, - 0x00, 0x0c, 0x45, 0x28, 0x3b, 0x00, 0xae, 0x42, 0xb5, 0x5e, 0xcf, 0x2b, 0x6d, 0x6c, 0xf6, 0x27, 0x72, 0x42, 0x10, - 0x56, 0xbc, 0xa4, 0x50, 0x0a, 0x99, 0x40, 0x5e, 0xe2, 0x52, 0x95, 0xdc, 0x4e, 0xcb, 0x66, 0xd3, 0xb9, 0xc3, 0x37, - 0xd2, 0x00, 0x44, 0x4d, 0x5a, 0x66, 0xb2, 0x81, 0x0d, 0x55, 0xca, 0x94, 0xa7, 0x49, 0xad, 0x06, 0x5c, 0xf3, 0xc1, - 0x35, 0x70, 0x24, 0xe0, 0xec, 0xc0, 0xb5, 0x20, 0x0e, 0xbb, 0x66, 0xc8, 0x35, 0x75, 0x4e, 0x79, 0x8c, 0xfe, 0x6b, - 0xab, 0x35, 0xb6, 0x5f, 0x7d, 0x2d, 0x4d, 0xde, 0x4f, 0xc7, 0x48, 0x2b, 0x03, 0x52, 0x3b, 0xf9, 0xbf, 0x36, 0xa4, - 0x9c, 0xfd, 0x58, 0x88, 0xb5, 0xff, 0x9b, 0x91, 0x39, 0x9f, 0x57, 0xcf, 0x0e, 0x13, 0x37, 0x18, 0x53, 0x21, 0x8e, - 0x71, 0x12, 0x5e, 0x6c, 0x87, 0x57, 0x8d, 0x41, 0xed, 0xd7, 0x0b, 0x18, 0x72, 0xdc, 0xb1, 0xf7, 0x1e, 0x18, 0xce, - 0xbe, 0xd8, 0x5b, 0x34, 0x56, 0x87, 0xb4, 0x28, 0x96, 0x7d, 0x00, 0xe9, 0x67, 0xf9, 0xfe, 0x7f, 0xdc, 0xdc, 0xa5, - 0x41, 0x2d, 0x23, 0x2f, 0x71, 0xc9, 0x42, 0xb3, 0xfc, 0x5e, 0x52, 0xac, 0x4f, 0x1b, 0xe1, 0x12, 0xcd, 0x95, 0xd5, - 0xff, 0x82, 0x65, 0xcb, 0xea, 0x2e, 0xe5, 0xe1, 0xde, 0x81, 0x09, 0x8d, 0x6f, 0x6e, 0x7c, 0x4c, 0x9d, 0x35, 0x95, - 0x6e, 0xc6, 0xbb, 0x38, 0xc4, 0xae, 0xb7, 0x8d, 0x2a, 0xb6, 0x8b, 0x8c, 0xa9, 0x68, 0x6a, 0xf5, 0xd1, 0x0c, 0x22, - 0x37, 0xb4, 0xa0, 0xfd, 0x5b, 0x4c, 0x3a, 0x58, 0x3c, 0x28, 0xc3, 0xa5, 0xd1, 0xf2, 0xba, 0x10, 0x3b, 0x0a, 0x2e, - 0xc9, 0x48, 0x4a, 0x12, 0x64, 0x48, 0xf7, 0x1d, 0x17, 0x0f, 0x9a, 0x42, 0xd5, 0x88, 0xdb, 0x95, 0x64, 0xbf, 0xe2, - 0xfe, 0xa5, 0x7e, 0xdc, 0x30, 0xea, 0xca, 0x39, 0x50, 0x89, 0xcf, 0x9a, 0x6c, 0x4e, 0x88, 0x8e, 0xda, 0x36, 0xeb, - 0x28, 0xca, 0x91, 0x5f, 0x29, 0x25, 0xea, 0x5f, 0xd1, 0x1b, 0x48, 0xb6, 0x08, 0x60, 0x60, 0x1b, 0x80, 0xd5, 0x6f, - 0xd6, 0x2c, 0xd5, 0x32, 0x40, 0xe3, 0x57, 0xb0, 0x6b, 0x3e, 0x3e, 0x75, 0x37, 0xfa, 0x05, 0xd1, 0xd8, 0x5a, 0xd1, - 0x04, 0x97, 0xdd, 0x0b, 0xab, 0x37, 0xe2, 0xf7, 0xd4, 0xdb, 0x23, 0x88, 0x0d, 0xe4, 0xd3, 0x74, 0xbf, 0x4b, 0x4d, - 0x1f, 0x90, 0xf4, 0x3d, 0x18, 0x63, 0x1f, 0x83, 0x5d, 0x51, 0x4f, 0xad, 0xde, 0x54, 0x95, 0x43, 0x20, 0xf7, 0x74, - 0x35, 0x2a, 0xe6, 0xf1, 0x57, 0xb4, 0x5b, 0x6b, 0xd9, 0x61, 0x78, 0x95, 0x2f, 0xa0, 0x6c, 0xd1, 0xae, 0x29, 0x22, - 0xc9, 0x65, 0x8c, 0x4b, 0x15, 0x80, 0x12, 0x58, 0x90, 0x93, 0x1a, 0xbb, 0x3a, 0xdd, 0xb2, 0x79, 0xf9, 0x3a, 0x9a, - 0x90, 0x6f, 0xfd, 0xb4, 0xf2, 0xb9, 0x19, 0x1c, 0x55, 0xd4, 0x21, 0x02, 0xd3, 0x40, 0x1d, 0x16, 0x70, 0x18, 0xa9, - 0xf3, 0x52, 0x04, 0x0e, 0x78, 0x37, 0xe8, 0x73, 0xcd, 0x40, 0x51, 0x70, 0x88, 0xbc, 0x8b, 0x1a, 0x2c, 0xf0, 0x1c, - 0x3c, 0x49, 0xb4, 0x71, 0x74, 0xf8, 0xef, 0x82, 0x8e, 0xa2, 0x43, 0xb2, 0x94, 0xf5, 0xbd, 0x32, 0x15, 0xc9, 0x49, - 0xea, 0x22, 0xe9, 0xfc, 0x54, 0x9e, 0xa9, 0x4d, 0x6e, 0xcd, 0x5f, 0x24, 0x9f, 0xc6, 0xc9, 0xd8, 0x0b, 0xd8, 0xaf, - 0x61, 0xc4, 0xae, 0xf3, 0x17, 0x36, 0x9f, 0xf6, 0xcc, 0xb2, 0x46, 0xab, 0x33, 0xe0, 0x81, 0xa4, 0x13, 0x61, 0x29, - 0xbb, 0x64, 0x2e, 0x65, 0x00, 0x28, 0xd7, 0xc6, 0xcb, 0xbb, 0x21, 0xc4, 0xe7, 0xe2, 0xfa, 0x8e, 0x48, 0xa8, 0x4c, - 0x35, 0x33, 0xe3, 0xb9, 0x47, 0x11, 0x21, 0x2c, 0xd5, 0xce, 0x2c, 0x6e, 0xb3, 0xed, 0xed, 0x6c, 0x78, 0x5e, 0xb3, - 0xfd, 0xb1, 0xc0, 0x14, 0xf5, 0xa0, 0xbf, 0x8b, 0x8b, 0xa4, 0xca, 0x20, 0x44, 0xcc, 0xe0, 0x03, 0xae, 0x86, 0xb0, - 0x4b, 0xa5, 0xa2, 0x3f, 0xdb, 0x25, 0x8a, 0x9f, 0x5e, 0xa5, 0xaa, 0xc2, 0xe5, 0x48, 0xc8, 0xc4, 0x96, 0xda, 0x80, - 0x25, 0x02, 0x3c, 0xf2, 0xe4, 0x16, 0xb7, 0x65, 0xb9, 0x1b, 0x11, 0x9c, 0x16, 0x2d, 0x9d, 0x9c, 0xb0, 0x4c, 0xe8, - 0x3b, 0xd9, 0xf5, 0xae, 0x29, 0xc2, 0xec, 0x7e, 0x93, 0x6e, 0x7f, 0x94, 0xd2, 0x57, 0x95, 0xc6, 0x1d, 0xb8, 0xc6, - 0x12, 0xb8, 0xf0, 0x18, 0xd1, 0x6a, 0x68, 0x54, 0x9f, 0x7a, 0x44, 0xf1, 0xa8, 0xd6, 0x24, 0xc7, 0x41, 0xeb, 0x30, - 0x71, 0xa5, 0xa5, 0x81, 0xe2, 0x42, 0xec, 0xac, 0x43, 0x76, 0x3a, 0x0b, 0xf9, 0x92, 0x73, 0xb3, 0x75, 0x92, 0xc8, - 0x17, 0xb5, 0x0f, 0x45, 0x33, 0x12, 0x73, 0xf5, 0x5d, 0x7e, 0xce, 0xd1, 0x8f, 0x77, 0x57, 0xf9, 0x8a, 0xb7, 0x8e, - 0x73, 0x92, 0xcc, 0x27, 0xf1, 0xa2, 0xe5, 0x9f, 0xcb, 0xd2, 0x46, 0x0b, 0x4f, 0xe2, 0xd1, 0x0f, 0xa7, 0x8a, 0xfa, - 0x35, 0x12, 0x9a, 0x75, 0x52, 0xeb, 0x59, 0x79, 0x25, 0xe5, 0x7c, 0xb7, 0x47, 0x8a, 0x25, 0x62, 0x8e, 0x71, 0xb9, - 0xe4, 0x69, 0x55, 0x2d, 0x1d, 0x7d, 0x7f, 0x86, 0xe7, 0x52, 0x76, 0x02, 0x60, 0x22, 0xa9, 0x8f, 0xb0, 0xa2, 0xbd, - 0x8c, 0x1a, 0x21, 0xf6, 0x82, 0xd1, 0xb2, 0x84, 0x17, 0xfb, 0xcd, 0xad, 0x07, 0x21, 0x5b, 0x92, 0xee, 0xee, 0x2d, - 0x08, 0x5f, 0xf0, 0xd3, 0x03, 0xa7, 0x75, 0xa4, 0x26, 0x2f, 0xce, 0x42, 0x94, 0x78, 0x89, 0x74, 0x18, 0xb5, 0xb5, - 0x9c, 0x9b, 0xb0, 0x92, 0x34, 0x86, 0xdc, 0x1a, 0x65, 0xd5, 0xb0, 0xa5, 0x18, 0x73, 0x20, 0xe3, 0x91, 0x79, 0x06, - 0xfa, 0x1e, 0xe0, 0x4d, 0x6e, 0x6d, 0x49, 0xb2, 0xee, 0x9e, 0xca, 0xc8, 0xbc, 0xe8, 0xb3, 0xe4, 0xfc, 0xb8, 0x95, - 0xd8, 0x50, 0xdc, 0x49, 0xb9, 0x62, 0x3d, 0x71, 0x90, 0x5d, 0x9a, 0xbc, 0x2f, 0x51, 0x44, 0xc9, 0x4a, 0xa7, 0xff, - 0x39, 0x37, 0x1c, 0x77, 0x3a, 0x34, 0xd1, 0xea, 0xd8, 0x76, 0x68, 0x25, 0xe6, 0xe1, 0xd7, 0xe5, 0x9a, 0xaa, 0x05, - 0xb4, 0x80, 0x39, 0x22, 0x4a, 0xdd, 0x0c, 0x71, 0x93, 0xa4, 0xe3, 0x05, 0xc2, 0xdf, 0x6e, 0x33, 0x13, 0x5f, 0x76, - 0x7f, 0x97, 0x23, 0x34, 0x35, 0x94, 0xe4, 0x01, 0x14, 0x97, 0x6f, 0xc2, 0x9b, 0x31, 0x15, 0xf1, 0x0d, 0xfb, 0xcc, - 0x59, 0x6a, 0x0f, 0x5e, 0xa0, 0x4d, 0x4f, 0x82, 0xd5, 0x89, 0x1b, 0x40, 0x89, 0x4c, 0x10, 0x20, 0xbb, 0xc7, 0x00, - 0x96, 0x06, 0xd9, 0x8b, 0x26, 0xd3, 0x00, 0x22, 0x5b, 0x8c, 0xad, 0x61, 0x8e, 0xcd, 0x15, 0xa0, 0x05, 0x3b, 0xf3, - 0x4b, 0xa0, 0x6c, 0x60, 0x87, 0x77, 0xf4, 0x3f, 0x79, 0x43, 0x0c, 0x31, 0xa6, 0xa9, 0x4d, 0x3b, 0xeb, 0x55, 0x90, - 0xbd, 0xeb, 0x63, 0x16, 0x07, 0xe4, 0xc0, 0x75, 0x43, 0x69, 0xec, 0x8c, 0xd5, 0x35, 0x0d, 0x68, 0xa5, 0xa8, 0xae, - 0x08, 0x84, 0x24, 0x10, 0xf3, 0xf2, 0x50, 0x48, 0x4d, 0x42, 0xb5, 0x74, 0xd3, 0xa9, 0x6d, 0x82, 0xc2, 0xec, 0x78, - 0x6a, 0xf2, 0xd0, 0x4b, 0xe0, 0xed, 0xdb, 0x5b, 0x8b, 0x01, 0x47, 0xe1, 0x4a, 0x96, 0x32, 0xea, 0x57, 0xe6, 0xcd, - 0x7a, 0x58, 0xcb, 0x5f, 0x1c, 0xd0, 0x6e, 0x57, 0x8e, 0x19, 0xd5, 0x4e, 0xf5, 0x42, 0x70, 0x7a, 0x67, 0x80, 0x46, - 0x44, 0x02, 0x6c, 0xe0, 0x47, 0xfd, 0x8e, 0x54, 0x2c, 0x51, 0xd6, 0x56, 0x5e, 0xcd, 0xfa, 0x03, 0x16, 0x22, 0x8d, - 0x2b, 0x6c, 0x9c, 0xb3, 0x68, 0x55, 0x23, 0x99, 0x90, 0xa0, 0x07, 0x32, 0xb2, 0x73, 0x56, 0x93, 0xe0, 0xeb, 0x94, - 0x06, 0x5f, 0x70, 0x7a, 0xfc, 0xb5, 0x0e, 0x50, 0x8e, 0x7f, 0x71, 0xf6, 0xba, 0x57, 0xe1, 0x88, 0xeb, 0x11, 0xf3, - 0x45, 0x9d, 0x97, 0x3f, 0xdc, 0x19, 0x39, 0xfd, 0x7b, 0xcb, 0x0c, 0x40, 0x95, 0xbf, 0x58, 0x26, 0x80, 0x54, 0x1e, - 0xdc, 0x79, 0x23, 0x3a, 0x4b, 0x76, 0x14, 0x8d, 0x59, 0x3b, 0x69, 0x09, 0x3b, 0x98, 0x15, 0x47, 0x10, 0x2a, 0xfe, - 0xc5, 0x08, 0x20, 0x71, 0x14, 0xb4, 0xec, 0x68, 0xd0, 0x8a, 0xf6, 0x40, 0x9d, 0x93, 0x12, 0x61, 0x63, 0x5e, 0x88, - 0x0d, 0xf9, 0xfa, 0xe6, 0x04, 0x07, 0x59, 0x43, 0x12, 0x3c, 0xa8, 0xb7, 0x6f, 0x8a, 0x6c, 0x97, 0x1d, 0xa6, 0xde, - 0xf4, 0xf8, 0x3d, 0x97, 0x80, 0x90, 0x16, 0x0f, 0x91, 0x8f, 0xdd, 0x48, 0xcc, 0x6e, 0x3d, 0xde, 0x76, 0xc4, 0xa2, - 0x6f, 0x27, 0xa2, 0x54, 0xea, 0xb8, 0x36, 0x0f, 0x51, 0x10, 0x56, 0x18, 0x4a, 0x70, 0xf9, 0x55, 0x40, 0x6c, 0xa2, - 0xa0, 0xb1, 0x8f, 0xe5, 0x4c, 0x39, 0x6c, 0xb2, 0x0f, 0xe7, 0x2f, 0x75, 0xad, 0xff, 0x08, 0xb5, 0xce, 0x9e, 0xc0, - 0xaf, 0x18, 0xd8, 0x7b, 0x28, 0x83, 0x75, 0x4a, 0xdc, 0xb5, 0xe0, 0xa1, 0x0c, 0xca, 0x7d, 0x18, 0x48, 0x08, 0xc5, - 0xf5, 0x71, 0xd8, 0x14, 0xbb, 0x96, 0x18, 0x01, 0x3e, 0x4a, 0x66, 0xa5, 0x36, 0x1d, 0xc3, 0x95, 0x30, 0xe0, 0xf2, - 0x52, 0x8f, 0xe7, 0xa3, 0x9b, 0xdd, 0x95, 0x46, 0x1a, 0xfa, 0x6e, 0xe0, 0x78, 0xb9, 0x39, 0x4c, 0x95, 0x45, 0x5b, - 0x37, 0x25, 0x2c, 0x75, 0x81, 0xc8, 0x8c, 0x10, 0x31, 0xb7, 0x6c, 0xd2, 0x90, 0x38, 0xdb, 0xe9, 0x04, 0x7d, 0x6c, - 0x60, 0x38, 0x83, 0xd9, 0x54, 0xb5, 0xb5, 0x7b, 0x53, 0x58, 0xff, 0xcc, 0xb2, 0x0d, 0xfc, 0x7c, 0x39, 0x23, 0x21, - 0xa0, 0x61, 0xa1, 0x17, 0x11, 0xc2, 0x7a, 0x38, 0xca, 0xb3, 0x97, 0xd8, 0x70, 0x21, 0x43, 0x87, 0xe3, 0x87, 0x63, - 0xb3, 0x17, 0x34, 0xc7, 0xcf, 0xa7, 0xc7, 0xc6, 0xbe, 0x56, 0xd3, 0x24, 0x0b, 0x2e, 0x65, 0xe1, 0x74, 0xfd, 0xc8, - 0x11, 0xc5, 0x67, 0xda, 0x75, 0xdf, 0xc1, 0xe6, 0x33, 0x29, 0x73, 0x52, 0xe9, 0x26, 0x02, 0x95, 0x81, 0x4c, 0xde, - 0xed, 0x05, 0xc0, 0xb6, 0x01, 0xfa, 0xa2, 0xb9, 0xc8, 0x4c, 0x65, 0x9f, 0x74, 0x5e, 0x1e, 0x22, 0x65, 0x7b, 0x78, - 0x73, 0x58, 0x86, 0x80, 0xd7, 0xa7, 0x35, 0xfb, 0x37, 0x3c, 0x0d, 0xd2, 0x75, 0xb4, 0x31, 0x2a, 0x92, 0xe6, 0x82, - 0xc9, 0x35, 0x2a, 0xa6, 0xc1, 0x43, 0x38, 0x69, 0xc0, 0xa9, 0x52, 0x4e, 0xb8, 0x20, 0x79, 0x81, 0x49, 0x2a, 0xf6, - 0x04, 0x5a, 0xdb, 0x40, 0x44, 0x45, 0x0d, 0x3f, 0xba, 0x8c, 0x8b, 0x47, 0x69, 0x67, 0x4f, 0xa2, 0xa2, 0xfe, 0xda, - 0x7b, 0xd2, 0x0a, 0x61, 0x9f, 0x53, 0xdd, 0xf5, 0x1a, 0x8f, 0xcd, 0x08, 0x8a, 0x5e, 0xd3, 0xd4, 0xff, 0x65, 0x18, - 0x84, 0xbb, 0xcb, 0x76, 0x0e, 0x82, 0x82, 0x1c, 0x21, 0xc0, 0x4f, 0x5e, 0xd0, 0x97, 0x00, 0x6b, 0xe8, 0x88, 0x03, - 0x79, 0x7e, 0x6d, 0x8f, 0xa4, 0x73, 0xf5, 0xd5, 0xb9, 0xef, 0x7f, 0xc5, 0xd1, 0x1a, 0xef, 0x9f, 0x61, 0xec, 0x1f, - 0x9f, 0x29, 0x6d, 0xce, 0x1e, 0x33, 0xf1, 0xe8, 0x44, 0xf6, 0xb7, 0x8d, 0x49, 0x8a, 0xb7, 0xc7, 0x4a, 0x81, 0x7f, - 0xf8, 0x40, 0xf2, 0x36, 0x0b, 0xe7, 0x46, 0x12, 0xf3, 0xdb, 0xd9, 0xaa, 0x93, 0x9f, 0x1c, 0xd7, 0xca, 0x7d, 0xd6, - 0x24, 0x7f, 0xcc, 0xa5, 0x1d, 0xf0, 0x9d, 0xab, 0xce, 0xce, 0xad, 0xe4, 0xd6, 0x38, 0xe7, 0xf8, 0xcd, 0xb7, 0xbb, - 0x55, 0xea, 0xcd, 0xff, 0x95, 0xd5, 0xe2, 0x3a, 0x75, 0xc3, 0x25, 0xde, 0x40, 0x41, 0x50, 0xb8, 0xc3, 0x3a, 0xbd, - 0xcc, 0x5d, 0xe3, 0x0e, 0xa3, 0xc1, 0xda, 0xfa, 0xaa, 0xc8, 0x3c, 0x32, 0x17, 0x31, 0xce, 0x67, 0xe2, 0x65, 0x35, - 0x65, 0xdb, 0xa0, 0xdf, 0x35, 0x15, 0xe6, 0x3f, 0xbf, 0x86, 0x3a, 0xdb, 0xb1, 0xf9, 0x53, 0xe5, 0xdf, 0x80, 0x6b, - 0xe8, 0x50, 0x8e, 0xa2, 0xe0, 0xc4, 0x75, 0xcd, 0xb4, 0x4d, 0xce, 0xb5, 0x70, 0x5c, 0xbb, 0x1c, 0x78, 0xb5, 0x49, - 0x9c, 0x41, 0x94, 0x56, 0xc6, 0x3d, 0xa7, 0x4f, 0xbb, 0xfc, 0xce, 0xb8, 0x63, 0xd8, 0x75, 0x30, 0x0a, 0x82, 0x01, - 0x25, 0x16, 0x6d, 0x50, 0x77, 0x32, 0xba, 0x9a, 0xd8, 0xb3, 0x06, 0x62, 0x09, 0xac, 0x68, 0x7e, 0xab, 0x04, 0xa0, - 0xa5, 0x1d, 0x78, 0x59, 0xaf, 0x12, 0xc9, 0x92, 0xd5, 0x37, 0x0e, 0xe6, 0x7f, 0x88, 0x50, 0x04, 0xe7, 0xdb, 0x38, - 0xc4, 0x8b, 0x4a, 0x91, 0x98, 0x53, 0xec, 0xd1, 0x9b, 0xec, 0xa3, 0x5e, 0x82, 0x34, 0xff, 0x06, 0x18, 0x20, 0x60, - 0xc3, 0x71, 0x2c, 0x10, 0x94, 0xcc, 0xcf, 0xf1, 0xe5, 0xce, 0x5e, 0xbe, 0xf9, 0x04, 0x53, 0xfb, 0x37, 0x9e, 0xdb, - 0xc8, 0xfd, 0xdb, 0x50, 0xc9, 0xed, 0xcf, 0x2c, 0xee, 0xff, 0x2c, 0x9e, 0xdd, 0xbf, 0xe5, 0x1f, 0xbf, 0x6e, 0x5a, - 0xe0, 0x9d, 0xce, 0xfb, 0x48, 0x02, 0x35, 0x3f, 0x5f, 0x67, 0xa4, 0x60, 0x18, 0x11, 0x7c, 0xed, 0xf8, 0x30, 0xa2, - 0xfb, 0xad, 0x67, 0x03, 0x6b, 0x62, 0x1f, 0xe3, 0x16, 0xd5, 0xeb, 0x79, 0x81, 0xed, 0x6a, 0x5c, 0xcb, 0xf4, 0xb2, - 0xd0, 0x9a, 0xa7, 0xca, 0x2e, 0x15, 0x1a, 0x09, 0x07, 0x5b, 0xc0, 0x3b, 0xb8, 0xeb, 0xca, 0x9d, 0xbd, 0xb4, 0x66, - 0x36, 0xe5, 0x49, 0x82, 0x9c, 0x0e, 0x5c, 0x34, 0x7d, 0xf5, 0xd4, 0xb6, 0xc4, 0x18, 0xfe, 0x9c, 0x37, 0xd5, 0x18, - 0x15, 0x3d, 0x46, 0x23, 0x19, 0xb1, 0x72, 0x56, 0x46, 0xcb, 0x8b, 0x61, 0x68, 0x4b, 0xc6, 0xc5, 0xac, 0xb2, 0xa0, - 0x0c, 0xb8, 0x07, 0x42, 0xd6, 0x0b, 0xfa, 0xd6, 0x4e, 0x91, 0x0f, 0xd5, 0x27, 0x1c, 0xb0, 0x79, 0x3c, 0xc1, 0x71, - 0xe9, 0xa3, 0x7a, 0x40, 0x9a, 0xc4, 0x55, 0xb8, 0x86, 0xb3, 0xc9, 0xaa, 0x8a, 0xe7, 0xcd, 0x4f, 0xdb, 0x00, 0x73, - 0x5a, 0xb0, 0x7f, 0x73, 0x5b, 0xa2, 0xdc, 0x4f, 0x82, 0xda, 0x2e, 0x1a, 0x55, 0x8d, 0xb2, 0x00, 0xa2, 0x4c, 0x9f, - 0xde, 0x40, 0x02, 0xd1, 0x39, 0xd5, 0x8b, 0xe6, 0xdb, 0xd4, 0x76, 0x38, 0x37, 0x45, 0xa0, 0x16, 0x2e, 0x8d, 0xd1, - 0x6c, 0xa6, 0x70, 0xc2, 0x7b, 0x97, 0xf6, 0x3c, 0x5d, 0x20, 0x4f, 0xb6, 0x80, 0x49, 0xdf, 0x0b, 0x3c, 0x6b, 0x00, - 0x0f, 0x08, 0xf4, 0x28, 0xaa, 0xd0, 0xc0, 0x9a, 0x82, 0x1d, 0x8c, 0x8a, 0x34, 0x0e, 0x80, 0x64, 0x9f, 0x46, 0xdc, - 0x80, 0x83, 0x73, 0x34, 0x86, 0x8e, 0x6d, 0xcf, 0xe4, 0x95, 0x14, 0x82, 0xa0, 0xca, 0x66, 0x89, 0xcd, 0x68, 0xb2, - 0x17, 0x95, 0x6f, 0x70, 0xb3, 0x73, 0x27, 0x18, 0xfb, 0x9d, 0xce, 0x00, 0xa6, 0xac, 0xa2, 0x77, 0x48, 0xcd, 0x88, - 0x17, 0x3a, 0x29, 0x9a, 0x1c, 0x88, 0x48, 0x46, 0xcc, 0x75, 0xe3, 0xb7, 0x7f, 0x1e, 0xe5, 0x66, 0x63, 0x5b, 0x6c, - 0x56, 0x3c, 0x23, 0x58, 0xef, 0xe0, 0xea, 0x2c, 0xbc, 0xd6, 0x33, 0xb2, 0x50, 0xf8, 0xc7, 0x30, 0xb9, 0x53, 0xdf, - 0xf7, 0xc4, 0x88, 0xe6, 0xf2, 0x7f, 0x97, 0xb1, 0xab, 0xca, 0x69, 0x34, 0x86, 0x86, 0x48, 0x46, 0x36, 0x01, 0x48, - 0xe6, 0x59, 0xd3, 0x31, 0x9a, 0x8d, 0xd5, 0xb6, 0x73, 0x9a, 0x66, 0x3f, 0x7e, 0xe5, 0xf4, 0xd7, 0x06, 0xc7, 0x03, - 0xe4, 0xe7, 0xce, 0x8d, 0x72, 0xf6, 0x03, 0x5b, 0xcc, 0xa1, 0xc7, 0xb9, 0x5c, 0xd5, 0x37, 0x8a, 0x5c, 0x8d, 0x90, - 0x8b, 0x41, 0xdf, 0x0d, 0x2a, 0x1e, 0x10, 0x40, 0x7f, 0x02, 0x5f, 0x79, 0x79, 0xfe, 0x5f, 0xa3, 0xb9, 0xe3, 0x91, - 0x60, 0x63, 0xe5, 0x32, 0x9c, 0xc4, 0xcb, 0x61, 0x3c, 0xe0, 0xe8, 0x39, 0x91, 0xf8, 0xb4, 0x22, 0xe9, 0x11, 0x89, - 0x0c, 0xe3, 0x91, 0x59, 0x1a, 0x52, 0x9c, 0x61, 0x84, 0xe2, 0x2f, 0xab, 0xdf, 0xae, 0xbb, 0x6f, 0x20, 0xc5, 0xbf, - 0x71, 0x5d, 0x1d, 0xcf, 0x8d, 0x2a, 0x33, 0xe9, 0x65, 0x73, 0xdc, 0x92, 0xb3, 0x9a, 0x56, 0x33, 0x9f, 0xb5, 0x4b, - 0xa6, 0xed, 0xe6, 0xb1, 0x9c, 0x19, 0x3f, 0x4f, 0x13, 0xc9, 0xe0, 0x6f, 0xce, 0x61, 0x80, 0x16, 0x06, 0xda, 0x4b, - 0xec, 0xd4, 0xa0, 0xd3, 0xd5, 0x9b, 0x0d, 0x31, 0xda, 0xae, 0xd6, 0xe9, 0x07, 0x5c, 0x2f, 0xe8, 0x18, 0x76, 0xd6, - 0x74, 0xf2, 0x9c, 0x10, 0xbb, 0x28, 0xf8, 0xf1, 0xfb, 0xae, 0xa0, 0xd4, 0x34, 0xa0, 0x5f, 0xe7, 0xe5, 0xe5, 0xae, - 0x76, 0x91, 0x81, 0x9a, 0x09, 0xe8, 0x90, 0xdd, 0x30, 0xe6, 0x58, 0x17, 0x63, 0x0f, 0xd2, 0x85, 0x71, 0x6b, 0xf6, - 0x41, 0x68, 0x8c, 0xb2, 0x70, 0x65, 0x4c, 0x2e, 0x0a, 0x5f, 0x93, 0x93, 0x6b, 0xb8, 0xa0, 0x25, 0xb4, 0xcf, 0xbd, - 0x77, 0x0e, 0xd3, 0x3d, 0xc2, 0x51, 0x5b, 0x7a, 0x91, 0x16, 0xea, 0xcd, 0x42, 0x79, 0x56, 0x80, 0x16, 0x2c, 0x52, - 0x4f, 0xab, 0xe5, 0xc8, 0xe5, 0x5d, 0x3f, 0x3a, 0x3d, 0x75, 0xab, 0xb5, 0xdc, 0x63, 0x4a, 0x03, 0xe1, 0x1d, 0xad, - 0xec, 0xbf, 0xe2, 0x25, 0x47, 0x2a, 0x6c, 0xd5, 0x2c, 0x93, 0xaf, 0xc8, 0xf5, 0x3f, 0x6a, 0x7a, 0x13, 0xef, 0x13, - 0xae, 0x0a, 0x84, 0x3b, 0x12, 0xa1, 0x33, 0x9e, 0x32, 0xeb, 0x68, 0x1d, 0xaf, 0xa9, 0x13, 0x3b, 0x1e, 0x1e, 0x17, - 0x28, 0x86, 0xdf, 0x9a, 0xd1, 0x80, 0xe7, 0xbe, 0x78, 0x11, 0xec, 0x5e, 0xfb, 0x2e, 0x39, 0x33, 0x8b, 0x6c, 0x7f, - 0xd5, 0x6a, 0xdc, 0x85, 0xd8, 0xb4, 0xca, 0xdc, 0x15, 0xfb, 0xec, 0x30, 0x9c, 0x6b, 0xc6, 0x17, 0x07, 0xb7, 0x7b, - 0x23, 0x77, 0x07, 0x6f, 0x9e, 0x12, 0x47, 0xd7, 0x02, 0x1e, 0x97, 0x9b, 0xbc, 0x5b, 0x55, 0xba, 0x5f, 0x1b, 0xf5, - 0x6a, 0x5f, 0x23, 0xfb, 0x92, 0x80, 0xe4, 0x71, 0x3d, 0xa4, 0x88, 0xe3, 0xa9, 0xc8, 0xd6, 0x84, 0xb1, 0x0e, 0x0a, - 0x1e, 0x6a, 0x3d, 0xb7, 0xed, 0xa4, 0xf6, 0x83, 0x72, 0xb7, 0x2e, 0xca, 0xca, 0xc3, 0xe1, 0xb7, 0xcd, 0x8f, 0x07, - 0xee, 0x25, 0x85, 0xe2, 0xa1, 0xfa, 0x2a, 0x02, 0x03, 0xee, 0x57, 0xd4, 0x9a, 0xcc, 0x8a, 0xe3, 0x27, 0xec, 0x94, - 0xca, 0x14, 0xa3, 0x83, 0x9b, 0xb6, 0x3b, 0x4d, 0x03, 0x18, 0x1d, 0xd3, 0x75, 0xb2, 0x33, 0xf5, 0xf8, 0x84, 0x88, - 0x1c, 0x53, 0xfb, 0x9f, 0xca, 0x8b, 0xd3, 0x01, 0xde, 0x07, 0xfb, 0xaf, 0x98, 0x40, 0x6c, 0xad, 0xbb, 0x49, 0x9d, - 0x86, 0x48, 0xc1, 0x87, 0x35, 0x6d, 0xfc, 0x5f, 0x7c, 0x2e, 0x8c, 0x26, 0xa2, 0x77, 0xea, 0xad, 0x2b, 0x25, 0xb0, - 0x5d, 0xed, 0x52, 0xe8, 0xb6, 0xb7, 0xba, 0x89, 0x71, 0x59, 0xf0, 0x86, 0x4e, 0xef, 0xc8, 0xfa, 0x49, 0xd0, 0x86, - 0xdc, 0x3b, 0x75, 0x19, 0x71, 0x88, 0x91, 0x94, 0xb3, 0x8b, 0xb1, 0xd4, 0x86, 0xd0, 0xc5, 0x16, 0x65, 0x4d, 0xee, - 0xfa, 0xfb, 0x53, 0x74, 0x18, 0x5a, 0x22, 0x8d, 0xf2, 0xbc, 0x03, 0xb6, 0x5e, 0xdd, 0x29, 0xaa, 0xb8, 0x1d, 0xae, - 0x6c, 0x5d, 0x02, 0xbd, 0x4e, 0x0c, 0xbe, 0xd8, 0x51, 0x83, 0x02, 0xde, 0x08, 0xdd, 0x64, 0xd2, 0x35, 0xc4, 0x70, - 0x36, 0xce, 0x3e, 0xa6, 0x1c, 0xf3, 0x73, 0xbf, 0x34, 0x5f, 0x56, 0x52, 0x3b, 0x41, 0x4c, 0x18, 0x90, 0xeb, 0x59, - 0x71, 0xa4, 0xfa, 0xf6, 0xf4, 0xaf, 0x4d, 0xe1, 0xff, 0x8e, 0x0d, 0xdf, 0xea, 0x30, 0x22, 0x19, 0xf5, 0x0a, 0x3b, - 0x04, 0x3a, 0x83, 0xba, 0xa4, 0x30, 0xe9, 0x43, 0x40, 0x9d, 0xb8, 0xc6, 0xf5, 0x54, 0x1e, 0x21, 0xf4, 0x4d, 0x1a, - 0x54, 0xce, 0x6d, 0x3b, 0xd4, 0x5b, 0xdf, 0x13, 0x51, 0x02, 0x84, 0x47, 0xa3, 0x00, 0x5a, 0x64, 0x32, 0xb8, 0x37, - 0x38, 0x80, 0xb0, 0x2e, 0xa5, 0x9c, 0xd1, 0x5a, 0xd2, 0x75, 0x68, 0x3e, 0x6e, 0xb1, 0xbe, 0xd5, 0x09, 0x39, 0x82, - 0x54, 0xea, 0xe9, 0x53, 0x35, 0x5d, 0xa4, 0x97, 0x98, 0x2d, 0x9d, 0xf2, 0x79, 0x80, 0xd8, 0x86, 0x5e, 0x58, 0x74, - 0x9f, 0xcf, 0xe5, 0x21, 0x40, 0xa6, 0xb9, 0x04, 0x24, 0x5c, 0x52, 0x50, 0x3f, 0x02, 0x93, 0x72, 0xf9, 0x1f, 0x15, - 0xd2, 0xeb, 0xdc, 0x1d, 0xbe, 0x7a, 0xbd, 0x58, 0xd5, 0x1a, 0x59, 0xbf, 0xf1, 0x03, 0x5d, 0xe5, 0xf5, 0xaa, 0xd6, - 0x9e, 0x2f, 0xd8, 0x8c, 0x0e, 0xd2, 0x8d, 0xf4, 0x3f, 0xf9, 0x07, 0x63, 0xa9, 0xb3, 0x23, 0xfa, 0x16, 0x57, 0xe2, - 0xba, 0xaf, 0xa7, 0xf7, 0x50, 0x5e, 0x3c, 0x49, 0x93, 0x65, 0xca, 0x6a, 0xd3, 0xfa, 0xb0, 0x53, 0x04, 0x42, 0xd4, - 0xd1, 0xcb, 0xb8, 0xe4, 0xc0, 0x45, 0x59, 0xba, 0x5e, 0x80, 0x7f, 0xf6, 0x0f, 0xa3, 0x13, 0x68, 0xa0, 0xd8, 0xb0, - 0xfb, 0x7e, 0x47, 0x65, 0xe7, 0x42, 0x0e, 0x4d, 0xf4, 0xbe, 0xda, 0x29, 0x93, 0x33, 0x75, 0x67, 0x9f, 0x93, 0xe8, - 0x86, 0x3a, 0x90, 0x57, 0x06, 0x1c, 0xa7, 0x5e, 0xec, 0xf7, 0x60, 0x98, 0x15, 0xbe, 0xec, 0x59, 0xb7, 0xfd, 0x09, - 0x83, 0x29, 0xc8, 0x5a, 0xee, 0x2c, 0x8a, 0xe5, 0x5d, 0xc8, 0xab, 0xa8, 0xb1, 0x5c, 0x4c, 0xac, 0x10, 0xca, 0x02, - 0xb6, 0x9c, 0xdc, 0x8f, 0x42, 0x90, 0x7b, 0x9c, 0xe3, 0xcd, 0xce, 0x39, 0x52, 0xee, 0x12, 0xcc, 0xee, 0xb0, 0xc5, - 0x69, 0x22, 0xf5, 0xba, 0x8d, 0xe0, 0x52, 0x62, 0x4a, 0x54, 0x51, 0xe4, 0xa6, 0x98, 0xa8, 0xe2, 0xa8, 0x6b, 0x7b, - 0xa3, 0x6d, 0x23, 0x65, 0x90, 0xc8, 0x30, 0x23, 0xa4, 0xa7, 0xf9, 0xe1, 0xee, 0xcd, 0x3e, 0x99, 0x32, 0x06, 0x11, - 0xd0, 0xa8, 0x7a, 0x06, 0x10, 0x7a, 0xbe, 0xac, 0x5d, 0xf2, 0x58, 0x56, 0x30, 0x92, 0xbe, 0x02, 0x1a, 0x2e, 0x9a, - 0x74, 0xc3, 0x2f, 0xc1, 0x49, 0xac, 0x78, 0x9a, 0x00, 0x45, 0xa3, 0xad, 0xf2, 0x6c, 0x88, 0xaf, 0x3c, 0x0e, 0x3a, - 0x51, 0x4f, 0x1a, 0x14, 0xc1, 0x60, 0x9a, 0x8d, 0x24, 0xdc, 0x52, 0x93, 0x41, 0xac, 0x0c, 0xc4, 0xd1, 0xbf, 0xb4, - 0x52, 0xa6, 0x54, 0xbb, 0x5a, 0x30, 0x32, 0xa3, 0x07, 0xd3, 0xdf, 0x85, 0xa8, 0x61, 0xd5, 0x08, 0xfd, 0x45, 0xa6, - 0x4e, 0x75, 0xca, 0xc8, 0x4b, 0x8c, 0xd3, 0xc4, 0xc0, 0x18, 0x72, 0xa0, 0x71, 0xc0, 0x76, 0x03, 0x79, 0x5a, 0x73, - 0xb6, 0x8c, 0x9a, 0x49, 0xf7, 0xba, 0x76, 0xf4, 0x69, 0x6e, 0xe9, 0xfa, 0xcf, 0x65, 0xb6, 0x61, 0xc7, 0x9c, 0xbf, - 0xf4, 0xd3, 0x6e, 0xfa, 0x30, 0xc6, 0xbc, 0x19, 0x07, 0xc3, 0x0c, 0xae, 0xbf, 0x48, 0x8b, 0x47, 0x45, 0x83, 0x2c, - 0x5f, 0x6a, 0x8c, 0x23, 0xed, 0xef, 0x07, 0xaa, 0xb7, 0xbb, 0x8d, 0x49, 0xd2, 0x00, 0x28, 0x47, 0x68, 0x44, 0x70, - 0xec, 0x8a, 0xff, 0x38, 0xaa, 0xfc, 0xef, 0xee, 0x7a, 0x8b, 0x1e, 0x84, 0x2f, 0xf6, 0xa6, 0x4f, 0xa3, 0x80, 0x39, - 0x6b, 0xdd, 0xae, 0x3e, 0x8d, 0xa9, 0x21, 0xfd, 0x35, 0x01, 0xe3, 0xc6, 0xb1, 0xfa, 0xc7, 0x34, 0x25, 0xbf, 0xd7, - 0x63, 0x12, 0x5f, 0x2c, 0xfa, 0xca, 0x1a, 0x55, 0xea, 0xd1, 0x65, 0x38, 0x6d, 0xc9, 0x68, 0x4f, 0xca, 0xb7, 0xba, - 0xc3, 0xd3, 0xb6, 0x4b, 0x6a, 0x36, 0xef, 0x89, 0xf9, 0xec, 0xba, 0xda, 0x56, 0xe2, 0x88, 0xf4, 0x20, 0x5f, 0x4f, - 0x19, 0xa5, 0xa3, 0x4f, 0xd1, 0xde, 0xef, 0x8e, 0x03, 0x99, 0xa7, 0xc7, 0xa1, 0x56, 0xd7, 0xae, 0xec, 0xf8, 0x56, - 0x9c, 0x98, 0xd4, 0x58, 0x86, 0xec, 0xd7, 0xb8, 0x11, 0x0d, 0x3a, 0xee, 0x7d, 0xd5, 0x7a, 0xdd, 0xd4, 0x98, 0x0e, - 0x4e, 0x31, 0x04, 0xcd, 0x57, 0x5d, 0x12, 0x55, 0xc4, 0x82, 0x37, 0xc4, 0x07, 0x71, 0x01, 0x80, 0x9c, 0x93, 0x16, - 0xb5, 0xec, 0x18, 0x4b, 0xa2, 0x7c, 0x57, 0x81, 0x5a, 0xf2, 0xec, 0xa2, 0xa2, 0x53, 0x77, 0xa2, 0x57, 0xa7, 0x5e, - 0xa5, 0x39, 0x8d, 0xd0, 0xf5, 0xf0, 0x99, 0xe7, 0xa8, 0x64, 0x59, 0xf7, 0xae, 0x42, 0x5f, 0xb1, 0xd7, 0x5e, 0x49, - 0xc9, 0x3b, 0x52, 0x1a, 0x0a, 0x19, 0xc5, 0x1a, 0x34, 0xb6, 0xce, 0x5d, 0x62, 0x49, 0x27, 0xcb, 0xa3, 0x86, 0xc2, - 0x17, 0x73, 0x1f, 0xb7, 0xc6, 0x51, 0x39, 0xe6, 0x1c, 0xc0, 0x9e, 0x54, 0xe9, 0x64, 0xaa, 0x1c, 0xc0, 0xaf, 0x69, - 0x16, 0x11, 0x83, 0x94, 0xda, 0xe9, 0xb8, 0x8b, 0xb3, 0x64, 0x3b, 0x61, 0xd0, 0xb1, 0xe8, 0x39, 0x7a, 0x20, 0xd2, - 0x79, 0x1c, 0x44, 0xf7, 0x91, 0xc7, 0x0d, 0x32, 0x0c, 0xb6, 0x67, 0x2d, 0x79, 0x94, 0xb9, 0xe2, 0x28, 0xbb, 0x12, - 0x53, 0xcb, 0xb3, 0xa9, 0xdb, 0x33, 0xba, 0x62, 0xad, 0xac, 0xe9, 0xee, 0x88, 0x4c, 0x05, 0xf7, 0x7d, 0x7b, 0x86, - 0x4f, 0x47, 0x46, 0x8e, 0x33, 0x89, 0xa3, 0x3a, 0x84, 0xb9, 0x71, 0x22, 0x78, 0x82, 0xd1, 0xb2, 0x25, 0xf3, 0x94, - 0x53, 0x0a, 0xb5, 0xf7, 0xbf, 0x34, 0x1e, 0xa1, 0x6a, 0xae, 0x61, 0x7a, 0xcb, 0xd0, 0x1d, 0xc2, 0x76, 0xfd, 0x43, - 0x74, 0x32, 0xa2, 0x05, 0xef, 0x2f, 0x92, 0x0a, 0xc6, 0x5a, 0x5a, 0x95, 0xb6, 0xbe, 0xdd, 0x43, 0x02, 0x96, 0xa7, - 0x56, 0x9d, 0xa1, 0x80, 0x15, 0xa6, 0xcf, 0xf9, 0x9b, 0xb9, 0xc6, 0x21, 0x77, 0x2d, 0x11, 0x10, 0x1b, 0x81, 0xdd, - 0xd0, 0x09, 0x12, 0x18, 0xaa, 0x10, 0xfb, 0xac, 0x55, 0xf1, 0x9c, 0x37, 0x85, 0x1e, 0xf0, 0x23, 0x9f, 0xc4, 0x92, - 0xfa, 0x09, 0x92, 0xfc, 0x09, 0x97, 0x84, 0xd0, 0xa7, 0xfc, 0x22, 0xf6, 0xaa, 0xc9, 0x4d, 0xad, 0x34, 0xdb, 0x0e, - 0xc5, 0xcf, 0xfc, 0x62, 0xdc, 0xdd, 0x68, 0x88, 0x21, 0x62, 0x85, 0x11, 0x0a, 0xc6, 0x9c, 0xa0, 0x6e, 0xf2, 0x57, - 0xa4, 0xf8, 0x74, 0xce, 0xe6, 0x5b, 0xf8, 0x4e, 0xdb, 0xe9, 0x1d, 0x14, 0x0a, 0x31, 0xea, 0x0c, 0x2d, 0x61, 0xd8, - 0xc3, 0x93, 0xf9, 0xec, 0xc2, 0x9c, 0x84, 0x24, 0x15, 0x2d, 0x4a, 0x38, 0x43, 0xfc, 0x06, 0xc0, 0x04, 0x9a, 0xac, - 0x44, 0xa9, 0xa8, 0x81, 0x3d, 0x82, 0x5f, 0xb8, 0xd9, 0xe6, 0xf3, 0x56, 0xe4, 0xe1, 0x40, 0x1a, 0xe5, 0x0a, 0x6d, - 0x20, 0xa6, 0x7a, 0x6e, 0x23, 0xb1, 0x18, 0x19, 0x45, 0x6b, 0xc9, 0x97, 0x5a, 0x42, 0x5d, 0xec, 0x3c, 0x08, 0xd6, - 0x55, 0x77, 0x95, 0x9d, 0xa1, 0x59, 0x31, 0x83, 0x03, 0x39, 0x2e, 0xd0, 0x30, 0x44, 0xba, 0x31, 0xd9, 0xa6, 0x98, - 0x65, 0x23, 0x7c, 0x5f, 0xc5, 0xbc, 0xc9, 0x6b, 0x21, 0xf2, 0x5a, 0x9d, 0x49, 0xb0, 0x86, 0x09, 0x79, 0x6a, 0x60, - 0x96, 0x24, 0xa4, 0x61, 0x09, 0xcb, 0x13, 0x3e, 0x43, 0xbd, 0x64, 0x98, 0x66, 0x64, 0xfa, 0xe0, 0x49, 0xbf, 0x65, - 0xfd, 0x89, 0x37, 0xf2, 0xf3, 0x46, 0x13, 0x78, 0x51, 0x09, 0x55, 0x2e, 0xb6, 0x19, 0x22, 0xba, 0xd5, 0x52, 0xc3, - 0x73, 0xea, 0x96, 0x27, 0x40, 0xe2, 0x49, 0x9f, 0x19, 0x7e, 0xb4, 0xcd, 0x08, 0x81, 0x54, 0xe9, 0xad, 0x8b, 0x90, - 0xd9, 0x27, 0x65, 0xe5, 0xe1, 0xf0, 0xe4, 0xd2, 0x69, 0x09, 0x95, 0xc0, 0xf5, 0x9b, 0xd7, 0x05, 0x54, 0x81, 0x99, - 0xa1, 0x58, 0x63, 0x53, 0x3d, 0x1b, 0x6f, 0x90, 0x66, 0x30, 0x2e, 0x22, 0xa1, 0x42, 0xe6, 0xce, 0x25, 0x9a, 0xba, - 0x5e, 0xcc, 0x19, 0xcb, 0xcb, 0x3e, 0xec, 0xf9, 0xd2, 0x53, 0xcc, 0x2e, 0xbc, 0xd6, 0x2f, 0x99, 0xe3, 0xf6, 0x59, - 0x48, 0xb3, 0xdc, 0x9d, 0x22, 0x35, 0x7b, 0xac, 0x92, 0x9a, 0x07, 0xb0, 0xae, 0xb3, 0x2b, 0x3b, 0xd3, 0xa5, 0x1c, - 0x61, 0x7f, 0x82, 0x3b, 0x80, 0x63, 0x04, 0x43, 0x12, 0x70, 0x1b, 0xf9, 0x8d, 0x5b, 0x30, 0xf2, 0xcd, 0xc7, 0x41, - 0x1b, 0x82, 0xc8, 0x04, 0x89, 0x10, 0x31, 0x91, 0xc7, 0xf0, 0xf3, 0x01, 0xce, 0xbe, 0xba, 0x4c, 0x34, 0x51, 0xbc, - 0x11, 0x8a, 0x69, 0x78, 0x0d, 0x77, 0xeb, 0xc0, 0x8c, 0xce, 0x7b, 0x3a, 0x45, 0x57, 0xd0, 0x24, 0xa6, 0x56, 0x4f, - 0x9b, 0xf7, 0xdc, 0x23, 0xc2, 0x2f, 0x74, 0x51, 0xdc, 0xdd, 0xc1, 0x7a, 0xbd, 0x80, 0x25, 0x13, 0xf9, 0x96, 0x33, - 0xf3, 0x66, 0xca, 0x1e, 0x92, 0x63, 0x9f, 0x3e, 0x3c, 0x6e, 0x17, 0xfb, 0xe4, 0x39, 0x2e, 0xb2, 0x5e, 0x52, 0x85, - 0x3d, 0x29, 0xff, 0x9b, 0x32, 0xe6, 0x44, 0x04, 0x35, 0x95, 0xe9, 0xda, 0xa2, 0xe3, 0xcf, 0x2a, 0x9a, 0x2c, 0x8d, - 0x60, 0x6b, 0x54, 0x91, 0x7e, 0x69, 0x94, 0x52, 0x1d, 0x51, 0x0f, 0x43, 0x9b, 0x48, 0xb1, 0xd0, 0x28, 0x70, 0x74, - 0xa9, 0x4d, 0xf0, 0x6c, 0x15, 0xf4, 0x48, 0x7c, 0xa4, 0x9d, 0xd8, 0xe6, 0xfc, 0x7c, 0x4f, 0xf1, 0x4d, 0xf2, 0x5b, - 0xfa, 0x5b, 0x70, 0x93, 0x42, 0x93, 0xc5, 0xb5, 0xbc, 0xb5, 0x72, 0x8b, 0xdf, 0xe5, 0x63, 0x5f, 0xa3, 0x8b, 0x83, - 0x51, 0x3a, 0x99, 0xf3, 0x5e, 0x78, 0xc8, 0xb5, 0x93, 0x57, 0x62, 0xaf, 0x66, 0x2b, 0xc5, 0x95, 0x60, 0xe1, 0xc1, - 0xa9, 0x2b, 0x99, 0x8a, 0x56, 0x10, 0xc8, 0xbc, 0x71, 0xdb, 0xaf, 0x7f, 0x20, 0xa3, 0x6d, 0x08, 0x19, 0xcc, 0xda, - 0xea, 0x25, 0xa6, 0xf3, 0xbe, 0x45, 0xbe, 0x64, 0x6f, 0x6c, 0x59, 0xf7, 0x10, 0x88, 0xfe, 0xe4, 0x78, 0x37, 0x64, - 0x05, 0x16, 0x0a, 0xfb, 0x12, 0xd0, 0x93, 0xa8, 0xaa, 0xd4, 0x5e, 0xea, 0x50, 0xae, 0xab, 0x19, 0x2a, 0x54, 0xcb, - 0xeb, 0x1f, 0x40, 0x22, 0x8e, 0x52, 0x06, 0xed, 0xe9, 0xa2, 0x2a, 0x23, 0x82, 0xc0, 0xb8, 0x08, 0x0d, 0x2b, 0xc4, - 0x14, 0x76, 0x59, 0xc5, 0x38, 0x4e, 0x57, 0xf7, 0xf5, 0x8b, 0x53, 0x88, 0xbd, 0xee, 0x86, 0xac, 0x12, 0x74, 0xee, - 0xba, 0xec, 0xd3, 0x1c, 0xfa, 0x99, 0xae, 0x7f, 0x04, 0x37, 0x39, 0x87, 0x35, 0x29, 0x38, 0x35, 0xf5, 0x39, 0x8b, - 0x25, 0xdf, 0x08, 0x15, 0x4e, 0x5b, 0x32, 0xda, 0xb1, 0x63, 0x56, 0xe5, 0xc7, 0x2e, 0x4b, 0x69, 0x60, 0x48, 0xa2, - 0xba, 0x36, 0xe8, 0x18, 0xb4, 0x24, 0x72, 0xf3, 0xea, 0x70, 0x49, 0x7b, 0x43, 0xc8, 0x0f, 0x37, 0xa7, 0xfb, 0x94, - 0xd0, 0xc6, 0x6a, 0xf1, 0xca, 0xc5, 0x97, 0x44, 0xa4, 0xbc, 0xe0, 0x1e, 0x01, 0xeb, 0x77, 0x22, 0xfc, 0xbb, 0xe8, - 0xc1, 0x81, 0x47, 0x00, 0x8b, 0xf0, 0x56, 0xdc, 0x57, 0xde, 0x26, 0x94, 0x56, 0xa0, 0x2e, 0xd7, 0xa6, 0x51, 0x82, - 0x37, 0xa4, 0xff, 0xf0, 0xc8, 0xbe, 0xcc, 0x13, 0xb6, 0x51, 0x21, 0xf1, 0x0e, 0xbf, 0xf3, 0x77, 0x4f, 0xc7, 0x9c, - 0x00, 0xbb, 0xa5, 0xd3, 0xbc, 0x6a, 0x0b, 0x90, 0x16, 0x5d, 0x0c, 0x62, 0x9c, 0x82, 0xe5, 0x95, 0x00, 0xe9, 0x87, - 0x57, 0x61, 0xa1, 0xeb, 0xf9, 0x7b, 0x4d, 0xf7, 0xca, 0x5e, 0x87, 0x69, 0xf2, 0x65, 0xef, 0x88, 0x46, 0xd0, 0xcb, - 0xd5, 0xc9, 0xe5, 0xfb, 0x94, 0x72, 0xe1, 0x5f, 0xd2, 0xd5, 0xcf, 0x54, 0x89, 0xe6, 0xaa, 0x6f, 0xaa, 0xf8, 0x94, - 0xab, 0xf1, 0x09, 0xa4, 0xda, 0x9c, 0x57, 0x13, 0xe6, 0xca, 0x55, 0x9f, 0xdc, 0x77, 0x17, 0x98, 0x56, 0x6f, 0xfd, - 0xdb, 0xfd, 0x30, 0xd0, 0x9f, 0xdd, 0xdf, 0x1c, 0x7c, 0x9d, 0x5d, 0x74, 0x76, 0x3a, 0xf6, 0x2f, 0xe4, 0xc7, 0x11, - 0xba, 0xac, 0x87, 0xa2, 0x26, 0x72, 0xc2, 0x7b, 0xea, 0xa8, 0x61, 0x2f, 0xb7, 0x94, 0x79, 0x31, 0x7d, 0x2f, 0x59, - 0x05, 0x94, 0xdf, 0xb7, 0xd9, 0xa5, 0xd5, 0x84, 0xe2, 0x02, 0x92, 0x2e, 0x73, 0x9a, 0x95, 0x6e, 0xa4, 0x50, 0xb3, - 0xc9, 0x5e, 0x46, 0x56, 0xe9, 0xb5, 0x12, 0xec, 0x57, 0x8b, 0x60, 0x58, 0x56, 0xe9, 0x2a, 0x8f, 0x3a, 0x6c, 0xd6, - 0xae, 0xad, 0xd3, 0x7f, 0xb9, 0xb9, 0x9c, 0x09, 0xa2, 0xec, 0xa0, 0x56, 0xf2, 0x8c, 0x2b, 0x7d, 0xce, 0xb5, 0x52, - 0x77, 0x3a, 0xde, 0xab, 0x3f, 0x57, 0xcd, 0x27, 0x4b, 0x4b, 0xf7, 0xbd, 0x0e, 0xff, 0xd9, 0x95, 0xb5, 0x14, 0x41, - 0x16, 0x43, 0xea, 0x3d, 0x63, 0x67, 0x25, 0x53, 0x42, 0x01, 0xc4, 0x2f, 0x3c, 0xae, 0x5d, 0x07, 0xcd, 0xbb, 0xd2, - 0xed, 0xa7, 0xab, 0xd6, 0xaa, 0x90, 0xf2, 0x78, 0x63, 0x19, 0x51, 0x98, 0xb8, 0xaa, 0x95, 0x61, 0x9a, 0x37, 0x7f, - 0xef, 0x9e, 0xf4, 0x57, 0xc5, 0xcb, 0x6a, 0x22, 0x8a, 0x98, 0xae, 0x79, 0xbc, 0xb1, 0x7a, 0x37, 0x87, 0xb5, 0x79, - 0xf1, 0x5c, 0x8d, 0x2f, 0x5a, 0xff, 0x5c, 0x75, 0x6c, 0x0d, 0x63, 0x52, 0x39, 0xcf, 0x3b, 0xbf, 0x29, 0x29, 0x8d, - 0xb4, 0x8c, 0x36, 0x4e, 0x8a, 0x99, 0x0a, 0x2f, 0x57, 0xef, 0x54, 0x27, 0xaa, 0x10, 0x19, 0x1c, 0x3c, 0x23, 0xb8, - 0xbf, 0xfd, 0xf3, 0x51, 0x59, 0xb7, 0xb6, 0x8d, 0xe5, 0x8d, 0xbc, 0x92, 0x68, 0xfc, 0x2e, 0x96, 0xcb, 0x16, 0xe6, - 0x5b, 0xfb, 0xa6, 0x29, 0x97, 0xb5, 0x89, 0xa4, 0x8e, 0xd2, 0x27, 0xc5, 0x65, 0xa4, 0x2a, 0xbd, 0x4f, 0x40, 0xa2, - 0x97, 0x46, 0xfa, 0x0c, 0x23, 0xa5, 0x1e, 0xc9, 0x8e, 0x10, 0x21, 0x40, 0x80, 0x66, 0xe7, 0xaa, 0xbd, 0x4c, 0x97, - 0x0c, 0xce, 0xc8, 0xa4, 0x40, 0x9f, 0x61, 0x76, 0x35, 0x17, 0x09, 0xc1, 0x19, 0xa1, 0x03, 0xe9, 0x26, 0x63, 0x25, - 0xf8, 0x67, 0xa4, 0x1a, 0x35, 0x6d, 0xa6, 0xae, 0xb1, 0xf3, 0xb5, 0xb0, 0xe6, 0x87, 0x0e, 0xd9, 0xc5, 0x89, 0x45, - 0x89, 0xbe, 0x70, 0x24, 0x66, 0xe9, 0x49, 0x5d, 0x69, 0x0d, 0xdd, 0x85, 0x5b, 0x5c, 0xef, 0x5c, 0x76, 0xc9, 0x2f, - 0xe3, 0x4d, 0x2b, 0xd2, 0x1c, 0x53, 0x74, 0xf9, 0x26, 0x58, 0x0b, 0x70, 0xa0, 0xcc, 0xcb, 0x57, 0x3d, 0x02, 0x57, - 0x7e, 0x80, 0x8b, 0xe8, 0x65, 0x3e, 0x82, 0x08, 0xce, 0x4d, 0x95, 0x16, 0x5a, 0x98, 0x3d, 0x02, 0x3c, 0xd6, 0x6b, - 0xfe, 0x14, 0xfa, 0x99, 0x29, 0x5e, 0x0a, 0x27, 0xcf, 0x5a, 0xa3, 0x76, 0x0f, 0x31, 0xf8, 0x94, 0xac, 0xd6, 0xc4, - 0x22, 0xa7, 0x71, 0x9d, 0x53, 0x8f, 0x8f, 0x66, 0xcb, 0x7c, 0x90, 0x98, 0x15, 0xc0, 0xe4, 0x34, 0xae, 0x51, 0xe2, - 0x6d, 0xa6, 0xaa, 0x76, 0x46, 0x39, 0x8d, 0x2f, 0xc4, 0x90, 0x4c, 0x52, 0x31, 0xdf, 0x3e, 0x90, 0x51, 0x86, 0xc4, - 0x45, 0xc9, 0xad, 0xd5, 0x14, 0xa7, 0xad, 0x79, 0x43, 0x52, 0x7e, 0xc1, 0x28, 0xeb, 0xe6, 0xef, 0x52, 0x5f, 0xef, - 0xfe, 0x28, 0xa6, 0x4b, 0x8f, 0xab, 0xc3, 0x9b, 0x79, 0x75, 0x34, 0x91, 0x9e, 0xe6, 0xd4, 0x20, 0xf1, 0x5b, 0x0b, - 0xfe, 0x98, 0x1f, 0x2f, 0x35, 0xa6, 0x1a, 0x9a, 0xf8, 0xc8, 0x66, 0x8b, 0x2e, 0x2b, 0xbc, 0x71, 0x6e, 0x85, 0x2f, - 0xb5, 0x29, 0x16, 0xe3, 0xb3, 0xcf, 0x3b, 0x0d, 0xae, 0xa3, 0x78, 0x0f, 0x87, 0xd4, 0xd5, 0x8b, 0xa2, 0xa5, 0x3f, - 0x36, 0xfb, 0x3c, 0x8e, 0xf8, 0xc3, 0x9b, 0xfd, 0xb0, 0x04, 0xe6, 0xee, 0xd0, 0x4a, 0x8b, 0x03, 0x69, 0x2b, 0x39, - 0x5a, 0xef, 0xda, 0x7e, 0x8a, 0xd6, 0x44, 0x56, 0x63, 0x53, 0x41, 0xa9, 0x5e, 0x90, 0xff, 0xef, 0x6d, 0x6c, 0x55, - 0x32, 0x55, 0x3a, 0xe8, 0x3d, 0x24, 0xbd, 0x34, 0xf4, 0x15, 0x7d, 0xee, 0xe9, 0xb1, 0xde, 0xa9, 0x44, 0xbc, 0x8b, - 0xcb, 0x9c, 0x61, 0x36, 0x1b, 0xe6, 0xe6, 0x11, 0xbd, 0x95, 0x5e, 0xb1, 0xdb, 0x98, 0xf4, 0x34, 0x88, 0x65, 0x79, - 0x99, 0x53, 0xf7, 0x39, 0x09, 0x24, 0xfe, 0x39, 0x3c, 0x00, 0xff, 0xa4, 0x6b, 0xd0, 0x1c, 0x49, 0xe5, 0x72, 0x53, - 0xaf, 0x43, 0xbc, 0x6b, 0x77, 0x3c, 0x16, 0xe9, 0xeb, 0x26, 0x1a, 0xdf, 0xd0, 0x0d, 0xa5, 0xa8, 0xa9, 0x8c, 0x3a, - 0x8e, 0x0c, 0x97, 0x8c, 0xbc, 0x59, 0x91, 0x6b, 0xbf, 0x02, 0x79, 0x55, 0x00, 0x21, 0x48, 0x6b, 0x11, 0x4d, 0x4c, - 0xf6, 0x57, 0x43, 0xcd, 0x51, 0x9a, 0xd9, 0x26, 0x7f, 0xda, 0xc4, 0xee, 0xba, 0x05, 0xdc, 0xad, 0x1c, 0xa2, 0x8b, - 0xed, 0x31, 0x0f, 0x79, 0x04, 0xa3, 0x2d, 0x24, 0x0a, 0x59, 0x15, 0xa2, 0x85, 0xd3, 0xfc, 0x49, 0x3e, 0x55, 0x9e, - 0x02, 0x3c, 0x44, 0x41, 0x13, 0x96, 0xb2, 0x9b, 0xee, 0x4b, 0xb2, 0x74, 0xf4, 0x3c, 0x82, 0x0f, 0x50, 0x09, 0x0e, - 0xd0, 0x45, 0xce, 0xeb, 0xee, 0xc5, 0xb6, 0x11, 0xd9, 0xc8, 0xd6, 0x75, 0x4f, 0x07, 0x59, 0x6e, 0x2d, 0x2d, 0xfc, - 0xef, 0x8f, 0xbd, 0xaf, 0xee, 0x82, 0x1d, 0x60, 0x28, 0xef, 0x3e, 0x84, 0x16, 0xee, 0x38, 0xd4, 0xea, 0xc5, 0x8a, - 0x12, 0x05, 0x4f, 0x22, 0xf3, 0xc7, 0x4a, 0x76, 0xba, 0xc7, 0x56, 0x24, 0xde, 0x53, 0x37, 0xa8, 0xdb, 0xe5, 0x56, - 0x5d, 0x35, 0x7b, 0xb9, 0x2a, 0xec, 0x3f, 0x1b, 0xfa, 0xd1, 0x54, 0xc1, 0x07, 0x4c, 0x2f, 0x6e, 0x23, 0x2e, 0x0a, - 0x85, 0x35, 0x72, 0xfe, 0x01, 0x8c, 0xca, 0x9a, 0x17, 0x6e, 0x52, 0x2c, 0x83, 0xcb, 0xd0, 0x28, 0xee, 0xc4, 0x2d, - 0x86, 0x1a, 0x0f, 0x06, 0x3d, 0x0b, 0x4b, 0x90, 0x46, 0xf7, 0xe9, 0x3d, 0xce, 0x70, 0x12, 0xa4, 0xd5, 0xe7, 0xcd, - 0x09, 0x72, 0x8d, 0x77, 0x52, 0x6b, 0x44, 0x22, 0xcd, 0x1e, 0x47, 0x65, 0x6d, 0xf8, 0x18, 0xa6, 0xd1, 0x39, 0xa0, - 0xa8, 0x4d, 0x85, 0xad, 0x76, 0x8a, 0x50, 0xaa, 0xe3, 0x20, 0xb0, 0x69, 0xe9, 0xe3, 0x24, 0x2d, 0xe2, 0x40, 0x4f, - 0x25, 0x78, 0x5e, 0xd2, 0xfc, 0x96, 0x8a, 0xbc, 0x9f, 0x77, 0x82, 0x66, 0xfa, 0xbd, 0x82, 0x48, 0x79, 0xac, 0x44, - 0x1a, 0x46, 0x1d, 0x0c, 0x76, 0x6c, 0xc3, 0xab, 0x03, 0x18, 0xcf, 0x91, 0x4a, 0x46, 0x0d, 0x5c, 0xb9, 0xe2, 0xee, - 0x4b, 0x9b, 0x32, 0xe5, 0xda, 0x2a, 0xfc, 0xc8, 0x7c, 0xc9, 0x10, 0x2b, 0x5f, 0x35, 0x43, 0x89, 0xab, 0xd4, 0xb0, - 0xf6, 0x8b, 0xa9, 0x9b, 0x58, 0x5b, 0xc8, 0xe7, 0x8b, 0xbf, 0x46, 0x87, 0xb0, 0x0f, 0x20, 0xab, 0x9f, 0x2e, 0xc4, - 0x94, 0x5c, 0xc2, 0x04, 0x39, 0x57, 0x8c, 0x89, 0x77, 0x93, 0xc9, 0xa5, 0x3f, 0xcd, 0x16, 0xd8, 0x67, 0xd3, 0x4a, - 0xba, 0x5f, 0x92, 0x42, 0xfc, 0x1e, 0x0f, 0x1a, 0xd2, 0x13, 0x84, 0x98, 0x3d, 0x05, 0x8f, 0x6e, 0x56, 0x6e, 0xd4, - 0x1b, 0x49, 0xd0, 0x8e, 0xad, 0xd8, 0x02, 0x24, 0x38, 0xa0, 0x9e, 0xa8, 0xf1, 0x7d, 0xf0, 0x52, 0xe5, 0x97, 0x2f, - 0x0f, 0x11, 0x6a, 0x06, 0xe3, 0x89, 0xa4, 0x19, 0x3b, 0x54, 0x24, 0xf3, 0x15, 0x34, 0xf3, 0xe1, 0xae, 0xa7, 0x23, - 0xde, 0xec, 0xd0, 0x2b, 0x6d, 0xdc, 0xba, 0x27, 0xba, 0xb8, 0x22, 0x61, 0x68, 0xf5, 0xf1, 0xa0, 0xf2, 0xfe, 0x7c, - 0x39, 0x94, 0x57, 0xb6, 0xf2, 0x83, 0x70, 0x98, 0xb5, 0x3b, 0x78, 0xbe, 0x8e, 0x8c, 0x0f, 0x33, 0x92, 0xb3, 0x0c, - 0x16, 0x81, 0x07, 0x73, 0x96, 0xa2, 0x85, 0x6f, 0xca, 0x32, 0x1b, 0x64, 0x6a, 0xb4, 0x80, 0xc5, 0x8b, 0xfc, 0x1b, - 0x1b, 0x5f, 0x96, 0xd9, 0x58, 0xc1, 0xec, 0x75, 0x20, 0x3b, 0x25, 0x90, 0x98, 0xa3, 0xda, 0x9d, 0x0d, 0xa8, 0xa2, - 0x87, 0x27, 0x00, 0x57, 0xf0, 0x87, 0xc7, 0x2c, 0xd0, 0x39, 0x35, 0xce, 0xd7, 0xb0, 0x56, 0x1e, 0x35, 0x36, 0x59, - 0xd7, 0x44, 0x50, 0xa4, 0x16, 0xab, 0xd0, 0x4b, 0x92, 0x48, 0x1d, 0xaa, 0xfc, 0x8f, 0x2f, 0x9c, 0x53, 0x73, 0xef, - 0x68, 0xeb, 0x31, 0xd7, 0x13, 0xd2, 0x56, 0x51, 0x93, 0x33, 0x3d, 0x2e, 0xa1, 0xa0, 0xfc, 0x9c, 0x0a, 0x95, 0xe2, - 0xcb, 0x74, 0xe7, 0x66, 0x55, 0xc1, 0x00, 0x6a, 0x06, 0x30, 0xfa, 0x51, 0x40, 0x46, 0x6a, 0xfc, 0x78, 0xa2, 0x5e, - 0xf7, 0x31, 0x37, 0x74, 0xd0, 0xe2, 0x4c, 0x37, 0xb0, 0x47, 0xb2, 0x1d, 0x8e, 0x6a, 0x80, 0xf2, 0x21, 0x9e, 0x04, - 0x9e, 0x22, 0x9a, 0xa4, 0x59, 0x5f, 0xf1, 0xb7, 0xe9, 0xdc, 0xf6, 0x64, 0x1d, 0x00, 0x2d, 0x2c, 0x98, 0x41, 0xb3, - 0x49, 0xdf, 0x4b, 0xd0, 0x80, 0x1f, 0xc6, 0xce, 0x30, 0xdf, 0x3b, 0xa1, 0xf6, 0xce, 0x0e, 0xbd, 0x9e, 0x7d, 0xe5, - 0x9c, 0x70, 0x3e, 0x53, 0x2f, 0xc6, 0x51, 0xd8, 0x45, 0x9d, 0xc5, 0x93, 0x3f, 0x7c, 0xa6, 0xc2, 0x78, 0x4c, 0xc4, - 0x45, 0x2c, 0x35, 0xb8, 0x20, 0x89, 0x2d, 0x9b, 0xcd, 0x32, 0x0e, 0x7e, 0x26, 0xc3, 0x41, 0xc6, 0x04, 0x2f, 0x27, - 0xf4, 0xfe, 0x96, 0x48, 0x08, 0xb2, 0x21, 0x94, 0x4c, 0xd3, 0x90, 0x1a, 0xaf, 0x36, 0x97, 0x71, 0x99, 0xd1, 0x25, - 0xe3, 0xff, 0x66, 0x17, 0x14, 0xea, 0xb5, 0xa2, 0xe0, 0xfb, 0x2d, 0xdc, 0xf6, 0x1a, 0x9d, 0xb1, 0x67, 0xc8, 0xf4, - 0xa1, 0x39, 0x4c, 0x19, 0x29, 0x0c, 0x77, 0xed, 0x29, 0x48, 0x90, 0x99, 0x97, 0xe1, 0xfd, 0x86, 0xfd, 0x36, 0x60, - 0x0a, 0x1e, 0xdf, 0xfb, 0x66, 0x05, 0xd8, 0x1c, 0x69, 0xa8, 0x7b, 0xee, 0x29, 0xa0, 0x1c, 0xe6, 0xc2, 0xc3, 0x1c, - 0xba, 0x42, 0xb5, 0x0f, 0xb9, 0xab, 0xa7, 0x7a, 0x15, 0x0b, 0xcb, 0xc1, 0xa6, 0x6e, 0x54, 0x9b, 0x84, 0xea, 0xb8, - 0x5c, 0x03, 0xd2, 0x9e, 0xd0, 0x0c, 0xb4, 0x1e, 0x46, 0x54, 0xeb, 0x64, 0x97, 0xde, 0x4a, 0x30, 0xba, 0x24, 0x91, - 0x06, 0x26, 0xcb, 0x9c, 0xd4, 0x00, 0xa6, 0x45, 0x98, 0x03, 0xbf, 0x23, 0x39, 0xae, 0x91, 0x80, 0xce, 0x71, 0xd8, - 0x35, 0xac, 0x26, 0xa5, 0xf3, 0x5c, 0xb5, 0x24, 0x15, 0xa4, 0x22, 0x42, 0x25, 0x53, 0x25, 0xa5, 0x63, 0xc2, 0x39, - 0xae, 0x06, 0x24, 0xc3, 0x94, 0x0a, 0x6a, 0x6f, 0xa3, 0x52, 0x1a, 0xcb, 0x59, 0x18, 0x3e, 0x71, 0xf9, 0x33, 0xaa, - 0xf9, 0xb2, 0x65, 0x23, 0x89, 0xec, 0x35, 0xd3, 0xc5, 0x82, 0x3b, 0xf3, 0x27, 0x70, 0x07, 0xbe, 0xfb, 0x8a, 0x9a, - 0xf2, 0xbe, 0x3c, 0x18, 0x25, 0x26, 0x32, 0x7e, 0x4d, 0xf5, 0x15, 0xcc, 0x65, 0x3e, 0x43, 0x28, 0xd3, 0x6f, 0x3d, - 0x56, 0x67, 0x0b, 0x61, 0x53, 0x49, 0xec, 0xfe, 0xfd, 0xe4, 0x87, 0x02, 0x5e, 0xf0, 0x43, 0x8f, 0xcf, 0x56, 0x13, - 0x04, 0x89, 0x45, 0xb3, 0x0a, 0x7b, 0x8b, 0x9c, 0x18, 0x40, 0x54, 0xf6, 0x68, 0x6e, 0x2f, 0xa9, 0xa1, 0x23, 0x52, - 0x8f, 0x3b, 0x27, 0xac, 0xec, 0x6d, 0x4b, 0x9e, 0xbd, 0x5a, 0xd5, 0x53, 0xaa, 0x63, 0xc2, 0x80, 0x9b, 0xbf, 0xf1, - 0x75, 0x6e, 0xeb, 0xbb, 0x1b, 0x30, 0xd8, 0x12, 0xed, 0xc7, 0x91, 0x22, 0x80, 0x9d, 0x60, 0x8a, 0x43, 0xce, 0xf9, - 0xf1, 0xa6, 0x7a, 0xf9, 0xbf, 0x27, 0x47, 0x15, 0xae, 0xcf, 0x11, 0xb8, 0xc4, 0xe8, 0x74, 0x33, 0x76, 0xee, 0xee, - 0xa8, 0xf4, 0x0d, 0x1f, 0x80, 0x5d, 0x67, 0x10, 0xf4, 0x90, 0x7b, 0x12, 0xde, 0x52, 0x2a, 0x9c, 0xf6, 0x4d, 0x67, - 0xa4, 0xc7, 0xa2, 0xe5, 0x43, 0x78, 0x6c, 0x77, 0x10, 0xac, 0x67, 0xcb, 0xb2, 0xa0, 0xa9, 0x04, 0x68, 0xa6, 0x18, - 0xe0, 0xc8, 0x43, 0xec, 0x1c, 0xc9, 0xac, 0x1c, 0xe3, 0x6e, 0x8e, 0xf7, 0x32, 0x88, 0x0e, 0xd1, 0x11, 0x4f, 0xa5, - 0x25, 0xb2, 0x8c, 0x6d, 0xa9, 0xc2, 0xb5, 0x4f, 0x71, 0x5a, 0xd8, 0xa2, 0xab, 0xca, 0x44, 0xbf, 0xfc, 0x08, 0xc4, - 0xd3, 0x37, 0x7a, 0x5e, 0xbb, 0xd9, 0x24, 0xb2, 0x37, 0x74, 0xb5, 0xfc, 0x92, 0x5b, 0x94, 0x56, 0xae, 0xc6, 0x00, - 0x2b, 0xf6, 0x3a, 0xef, 0x09, 0x84, 0x33, 0x25, 0x0e, 0xb7, 0xf9, 0x9d, 0x61, 0xb6, 0xb4, 0xb1, 0xba, 0x19, 0x9d, - 0x62, 0xdc, 0xd6, 0xdb, 0xfd, 0x40, 0x67, 0x37, 0x24, 0x7c, 0x78, 0xe3, 0xfb, 0xd0, 0x03, 0xa9, 0x24, 0xb8, 0xe2, - 0xee, 0xca, 0x7b, 0x0b, 0xc2, 0xec, 0x81, 0x9c, 0x3e, 0x7a, 0x42, 0x82, 0x5e, 0xc0, 0xfe, 0x7c, 0x1e, 0x1e, 0xf3, - 0x92, 0x38, 0x36, 0xca, 0xc7, 0x1f, 0xd6, 0x58, 0xe1, 0x96, 0xe8, 0x70, 0x89, 0x48, 0xdf, 0xc3, 0xe0, 0xc5, 0x13, - 0x86, 0xa4, 0xea, 0x7f, 0xbc, 0x91, 0x50, 0xc5, 0x33, 0x85, 0xce, 0xed, 0xb4, 0x39, 0x27, 0x88, 0xe5, 0xce, 0x55, - 0x66, 0xaf, 0x1d, 0x85, 0xbd, 0xe3, 0xea, 0x96, 0x74, 0x5f, 0xf6, 0x95, 0x9a, 0x5b, 0x8d, 0x48, 0xb0, 0x91, 0xe1, - 0x79, 0x6e, 0xf5, 0x22, 0xb3, 0x43, 0xd6, 0x45, 0x0e, 0xb0, 0xe9, 0x4c, 0x16, 0x47, 0xed, 0xcd, 0xe8, 0x8b, 0xea, - 0x8a, 0xed, 0xaa, 0x6a, 0x95, 0xc6, 0x25, 0x1d, 0xb4, 0xe6, 0x54, 0xc9, 0x0f, 0xc0, 0x36, 0x22, 0x7f, 0xa7, 0x47, - 0x9d, 0xa9, 0x87, 0x4a, 0x39, 0xad, 0x75, 0x20, 0x8c, 0x87, 0x91, 0xde, 0xcf, 0xc0, 0x40, 0x51, 0x09, 0x6c, 0xb7, - 0x43, 0xe7, 0xa7, 0x5b, 0x93, 0x92, 0x91, 0x53, 0x50, 0x52, 0x56, 0x03, 0xd3, 0x44, 0x91, 0x75, 0x9e, 0x63, 0xf6, - 0x77, 0xc7, 0xd3, 0x9f, 0x3b, 0x0e, 0x1a, 0x31, 0xb3, 0x0b, 0x63, 0xff, 0xb8, 0x77, 0x4d, 0x36, 0xa4, 0x70, 0xea, - 0xc0, 0x24, 0xce, 0x92, 0xb4, 0xfe, 0x7d, 0xad, 0x99, 0xfe, 0xba, 0xe9, 0x7a, 0x09, 0x1f, 0xe8, 0xe1, 0xd8, 0x6e, - 0xb5, 0xf9, 0xa1, 0x7c, 0x60, 0x25, 0xbe, 0xf0, 0xdb, 0x35, 0x1d, 0xbe, 0x0c, 0x3d, 0xf7, 0x39, 0xcc, 0x61, 0x6d, - 0xc5, 0xde, 0x48, 0xa6, 0x05, 0x81, 0xde, 0x6e, 0x77, 0x12, 0xc6, 0x80, 0xfb, 0x7a, 0x8e, 0xa8, 0x7c, 0xe6, 0x72, - 0xf4, 0x4c, 0xa2, 0x04, 0xa6, 0xa3, 0xc1, 0xa3, 0x16, 0xa0, 0xe2, 0x13, 0x8b, 0xd3, 0xe1, 0x01, 0x36, 0x38, 0xb8, - 0x3b, 0x8c, 0xd1, 0x8f, 0x75, 0xf7, 0x6d, 0xea, 0xb3, 0x6c, 0xf8, 0x1a, 0x8e, 0x45, 0x5d, 0xfe, 0x70, 0x55, 0x1b, - 0xc7, 0xa2, 0xc7, 0xea, 0x2a, 0x3e, 0x1a, 0x17, 0xf5, 0x06, 0x43, 0xac, 0xce, 0x03, 0x1c, 0x55, 0xa4, 0x6c, 0xce, - 0x6c, 0xa1, 0x24, 0x81, 0xea, 0xad, 0xd5, 0xfc, 0x32, 0xb0, 0x5b, 0x83, 0x0d, 0xd1, 0xfc, 0x6c, 0xbd, 0x87, 0xef, - 0xe2, 0x27, 0x9f, 0x6d, 0xc9, 0x7c, 0x9b, 0x9d, 0x00, 0x77, 0x96, 0x5d, 0x79, 0x92, 0xd5, 0x8a, 0x77, 0x5b, 0x5f, - 0xbc, 0xef, 0x5f, 0x58, 0x2f, 0x84, 0x84, 0xf3, 0x4b, 0x7a, 0xbb, 0x96, 0x43, 0x1a, 0xc4, 0xf6, 0xaf, 0x26, 0x90, - 0x7f, 0x4a, 0x33, 0x77, 0xfe, 0x58, 0x19, 0x82, 0x63, 0x84, 0x1a, 0x6f, 0x09, 0x16, 0x5c, 0x7a, 0x45, 0x4a, 0xb1, - 0xcd, 0x6a, 0xa7, 0x95, 0x8c, 0xb5, 0xe6, 0xbe, 0xd2, 0x96, 0xb4, 0xca, 0x9d, 0x55, 0x40, 0x5c, 0x5d, 0x9a, 0xf8, - 0x50, 0x60, 0x35, 0x7b, 0x52, 0x96, 0xa4, 0x50, 0x1a, 0x2f, 0xfe, 0x91, 0x78, 0x47, 0x40, 0xe5, 0xea, 0x25, 0x42, - 0x30, 0xae, 0xbf, 0xef, 0xec, 0x2c, 0x3b, 0xcd, 0x1e, 0x32, 0xd5, 0x23, 0x2f, 0x2f, 0xc3, 0x39, 0x8a, 0x52, 0xa5, - 0xf1, 0x1d, 0x9c, 0x71, 0x23, 0x46, 0xbd, 0x7b, 0xf6, 0x14, 0x21, 0xef, 0xc8, 0x6f, 0x64, 0x92, 0xc3, 0x30, 0xef, - 0xbe, 0x3a, 0x19, 0x91, 0xe6, 0xf6, 0x0e, 0xe8, 0x62, 0x93, 0x29, 0xeb, 0x2c, 0xd8, 0x92, 0x0a, 0x12, 0x09, 0xd1, - 0xed, 0x90, 0x90, 0xbd, 0xb4, 0x6f, 0x48, 0x51, 0x54, 0xa7, 0x7a, 0xc8, 0x50, 0x4f, 0x3b, 0x7e, 0x5c, 0x47, 0x4c, - 0xc7, 0xda, 0x22, 0x1d, 0x91, 0x0c, 0x20, 0x18, 0x33, 0x5e, 0x42, 0xa6, 0x5a, 0x33, 0xbc, 0x56, 0x13, 0x4f, 0x19, - 0xdd, 0x59, 0x0f, 0x0c, 0x13, 0xa9, 0x20, 0x76, 0xde, 0xd7, 0x24, 0x52, 0x76, 0xeb, 0xc5, 0x67, 0xa5, 0x0c, 0xcb, - 0x7b, 0x78, 0xd6, 0xb5, 0xa7, 0x6c, 0x52, 0x06, 0x24, 0x96, 0xfb, 0x95, 0x8d, 0x5f, 0xeb, 0xe8, 0x4a, 0x9e, 0xd1, - 0xce, 0x03, 0x00, 0xa6, 0x96, 0xf8, 0x7d, 0xea, 0x32, 0x5f, 0xba, 0xd5, 0x8b, 0xed, 0x35, 0xba, 0x05, 0xb8, 0xf6, - 0xa8, 0x66, 0x9e, 0xf6, 0x16, 0xbb, 0xa7, 0x42, 0x07, 0x74, 0xd5, 0x30, 0x5b, 0xfc, 0xe5, 0x8d, 0x0f, 0xb7, 0xc4, - 0xbd, 0x3a, 0x95, 0xe8, 0x63, 0x7e, 0x2d, 0x2e, 0xfc, 0xa7, 0xdc, 0x91, 0x80, 0xd1, 0x31, 0x3e, 0x29, 0xa4, 0x0d, - 0xab, 0x22, 0x64, 0x42, 0x75, 0xbf, 0x38, 0x4d, 0xe0, 0x00, 0x03, 0xd3, 0xb9, 0xc9, 0x62, 0x96, 0xee, 0xae, 0x9c, - 0xea, 0x3e, 0x18, 0xc0, 0xaa, 0x76, 0xda, 0x9c, 0x7a, 0xea, 0x6e, 0x43, 0xeb, 0x18, 0x17, 0xdf, 0x42, 0x4d, 0x86, - 0xb0, 0xb5, 0x5e, 0xa8, 0x48, 0xd3, 0xbc, 0xc5, 0xca, 0x9f, 0x64, 0xdb, 0x1b, 0x60, 0xe8, 0x42, 0x62, 0x6b, 0x3e, - 0x28, 0x41, 0x7c, 0x50, 0x17, 0xc2, 0xbe, 0xa3, 0x81, 0x68, 0x71, 0x86, 0x75, 0x93, 0x2a, 0xd3, 0x7e, 0x46, 0x8e, - 0x26, 0xd4, 0xfa, 0x3e, 0xf6, 0xcf, 0xba, 0x73, 0xfa, 0x57, 0x24, 0xb5, 0x4c, 0xd3, 0x1c, 0xc9, 0xe8, 0x44, 0xd8, - 0xd8, 0x60, 0x20, 0x8d, 0x11, 0x2f, 0x3d, 0xfd, 0x9c, 0xbb, 0x75, 0xcd, 0x28, 0xb0, 0x7e, 0x83, 0xf1, 0x7a, 0xe0, - 0xe4, 0x9a, 0x5c, 0x04, 0x7a, 0x26, 0x46, 0x59, 0x0f, 0xa9, 0x67, 0x5e, 0x2f, 0xd5, 0xfb, 0x9c, 0x8b, 0x09, 0x42, - 0x85, 0xd7, 0x1c, 0x87, 0xf4, 0x13, 0xc0, 0xe3, 0x26, 0x5b, 0x24, 0x3f, 0x6a, 0x70, 0x1e, 0xf6, 0x49, 0xac, 0x2c, - 0x0e, 0x2f, 0x68, 0x7a, 0xf6, 0xbc, 0x0a, 0xf3, 0x03, 0xf9, 0xd3, 0xb9, 0x32, 0xc0, 0x18, 0xc9, 0xdd, 0xc4, 0xae, - 0x08, 0x4d, 0x01, 0x1c, 0x2a, 0xb5, 0x8e, 0x83, 0x68, 0x80, 0x39, 0xec, 0xfb, 0x72, 0x4b, 0x94, 0x91, 0x02, 0x58, - 0x9d, 0xe1, 0x0c, 0x60, 0x07, 0x25, 0xd9, 0x31, 0xd6, 0x62, 0x64, 0x01, 0x8f, 0x86, 0xab, 0x89, 0xd3, 0xa2, 0xda, - 0x8b, 0x8b, 0x31, 0x31, 0xf0, 0x18, 0xd1, 0x32, 0x69, 0xdc, 0x0c, 0xa6, 0xb9, 0x21, 0xe8, 0x66, 0x87, 0xce, 0xdc, - 0xdc, 0xb6, 0xb3, 0x08, 0x4e, 0x6f, 0x7f, 0x06, 0xce, 0x0f, 0xe2, 0xbe, 0x76, 0x45, 0xc4, 0xfd, 0x2b, 0x19, 0x70, - 0x25, 0x85, 0xe7, 0x6c, 0x82, 0xa0, 0x1f, 0xad, 0x7d, 0xa6, 0x41, 0x3c, 0x63, 0xcf, 0xa5, 0x4e, 0x05, 0x0c, 0xfe, - 0xa2, 0x11, 0xaf, 0x53, 0x4f, 0x4c, 0x87, 0x45, 0xf4, 0x3d, 0xd1, 0x6c, 0xa0, 0x31, 0x32, 0xdd, 0x6d, 0xef, 0x9a, - 0x21, 0x44, 0x9f, 0x98, 0x52, 0x96, 0x08, 0x80, 0xf3, 0x2f, 0x2b, 0x84, 0xfb, 0xb7, 0x82, 0x84, 0x05, 0x92, 0xe7, - 0x6a, 0xd7, 0xc4, 0x0d, 0xb0, 0x56, 0xcb, 0x19, 0x77, 0x24, 0x82, 0xd9, 0x98, 0xcb, 0x4c, 0xf4, 0x48, 0x12, 0x67, - 0x90, 0xca, 0x66, 0x5b, 0xc3, 0xdc, 0xdb, 0x06, 0x33, 0x21, 0xca, 0x11, 0x0c, 0xde, 0xbd, 0x85, 0x0d, 0x26, 0xb5, - 0x29, 0x25, 0x4e, 0x43, 0x35, 0x24, 0xf9, 0xb2, 0x17, 0xdb, 0xd5, 0x9d, 0x74, 0x1b, 0x68, 0x32, 0x7f, 0xf7, 0xc5, - 0xc1, 0x7d, 0x64, 0xfb, 0xbc, 0x55, 0xec, 0x85, 0x49, 0xb5, 0x7c, 0xda, 0xba, 0x74, 0xae, 0xbd, 0xb8, 0x46, 0x2f, - 0x4d, 0x5f, 0xb5, 0xdf, 0x58, 0x9f, 0xe7, 0x20, 0x47, 0x45, 0x9f, 0xf7, 0x97, 0x0b, 0x08, 0x9a, 0xba, 0x8c, 0x3b, - 0x01, 0x2e, 0x18, 0x51, 0x7a, 0xae, 0x33, 0x02, 0x5b, 0xc2, 0x3c, 0x2d, 0x9b, 0x2b, 0xbc, 0x3c, 0x3f, 0x38, 0x4d, - 0xa8, 0x54, 0xe8, 0x35, 0xbf, 0xaf, 0xde, 0xab, 0xb5, 0xc7, 0xe5, 0x61, 0xff, 0xbd, 0x48, 0xce, 0x40, 0x91, 0x76, - 0x46, 0x7e, 0xb4, 0xac, 0x83, 0x78, 0xdb, 0x9a, 0xbe, 0xbd, 0x96, 0x3f, 0x4c, 0x48, 0xa6, 0xca, 0x6d, 0x08, 0x16, - 0x93, 0xbe, 0xdf, 0x65, 0xf0, 0x93, 0x6c, 0x45, 0x4a, 0x0c, 0x34, 0x8a, 0x5d, 0xc6, 0x3c, 0xd9, 0xa4, 0x5e, 0x37, - 0x15, 0xdd, 0xf8, 0x50, 0xcf, 0x76, 0x18, 0x6f, 0xe0, 0xb1, 0x9e, 0x7c, 0x34, 0x77, 0xaa, 0xee, 0x5a, 0xf8, 0xba, - 0xba, 0x13, 0xda, 0xed, 0xed, 0xeb, 0x45, 0x69, 0x5e, 0x77, 0x27, 0xda, 0x3a, 0x45, 0xcf, 0xeb, 0xff, 0xeb, 0x39, - 0xe3, 0xe0, 0x6d, 0x0a, 0xef, 0x05, 0xf8, 0x76, 0x7c, 0xf6, 0x3c, 0x03, 0x8a, 0x96, 0x59, 0xb4, 0x32, 0xb9, 0xc6, - 0x39, 0x0e, 0x18, 0x55, 0xa8, 0xf3, 0x9a, 0xa9, 0x36, 0x4e, 0x6c, 0x58, 0xef, 0x78, 0x79, 0x55, 0x00, 0x71, 0x87, - 0x6b, 0x59, 0x6e, 0xe2, 0xc2, 0xfc, 0xe6, 0x99, 0x12, 0x92, 0xcd, 0x63, 0x6d, 0xd5, 0xe9, 0x77, 0x49, 0x49, 0x0e, - 0x03, 0x6e, 0x73, 0xe9, 0xc3, 0x4d, 0xe5, 0xa1, 0x0b, 0xdd, 0x2e, 0xca, 0x09, 0x22, 0x95, 0xba, 0x13, 0xa8, 0x70, - 0x6c, 0x8b, 0x15, 0x75, 0xa9, 0xed, 0x1b, 0xdf, 0x17, 0xfc, 0xb2, 0x10, 0x7c, 0x63, 0x27, 0x36, 0x31, 0x5b, 0xa9, - 0x66, 0x24, 0xe1, 0x67, 0x10, 0xcc, 0x71, 0xe5, 0x99, 0xda, 0xed, 0xf0, 0x7f, 0x14, 0x4d, 0x45, 0x0a, 0xe8, 0x12, - 0x87, 0x08, 0x99, 0x99, 0x63, 0x8a, 0x1e, 0xac, 0x10, 0x3a, 0x8b, 0x94, 0x0f, 0x76, 0x73, 0xf0, 0x7d, 0xeb, 0xe7, - 0xb6, 0xae, 0xda, 0xe5, 0x5e, 0xd1, 0xd3, 0x34, 0x25, 0x5a, 0x52, 0xa8, 0xa4, 0x91, 0xb5, 0x43, 0x7d, 0xad, 0xaf, - 0xdd, 0x48, 0x41, 0x2d, 0xb2, 0x20, 0x7a, 0x9d, 0xaf, 0x0d, 0x20, 0x4d, 0x2a, 0xfe, 0xc2, 0xbf, 0x7f, 0x16, 0x89, - 0x37, 0xb5, 0x88, 0x86, 0xfa, 0x3a, 0x6d, 0x5d, 0x7d, 0x15, 0x8f, 0x0d, 0xd7, 0x56, 0xfd, 0x18, 0xe5, 0xe6, 0x46, - 0xca, 0xfb, 0x89, 0xf9, 0xf3, 0xaf, 0x36, 0x0d, 0x8d, 0xc0, 0x49, 0xf3, 0xe6, 0x76, 0xee, 0x30, 0xe7, 0x9e, 0x23, - 0x35, 0x1c, 0xb2, 0x6f, 0x40, 0x6e, 0x91, 0x2f, 0xb5, 0x6b, 0x22, 0x71, 0x81, 0xb0, 0x89, 0x60, 0x43, 0xdc, 0x47, - 0xc6, 0x8c, 0x6e, 0x5d, 0xe3, 0xe0, 0xdd, 0xa5, 0x4c, 0x9d, 0x96, 0x6a, 0x2e, 0xa7, 0x42, 0x99, 0x49, 0x2a, 0xfa, - 0xd5, 0x46, 0x7f, 0x76, 0xe5, 0x94, 0xb8, 0x0e, 0x2a, 0xbf, 0x8d, 0x38, 0x75, 0x1b, 0xcd, 0xb4, 0xbf, 0x95, 0xaf, - 0x7a, 0x5c, 0xd4, 0x5f, 0xd2, 0xe3, 0xbd, 0xb5, 0x47, 0x6e, 0x4d, 0x2d, 0x3d, 0xe2, 0xfe, 0x6a, 0xbb, 0xaf, 0xf2, - 0x39, 0x0e, 0x22, 0x54, 0x3b, 0x21, 0xc6, 0xa5, 0x88, 0x02, 0x0e, 0xe0, 0x15, 0xf1, 0x5f, 0x90, 0xeb, 0xf1, 0xac, - 0x4e, 0xd1, 0x8f, 0x3d, 0xd0, 0xde, 0x6d, 0x9e, 0x03, 0x4e, 0xa9, 0x72, 0xca, 0xbe, 0x23, 0x6b, 0xb3, 0x2c, 0xd2, - 0xae, 0x77, 0x66, 0xb3, 0xa8, 0x58, 0x11, 0x00, 0xc8, 0xde, 0xe9, 0x53, 0x97, 0x75, 0x28, 0xb7, 0x1b, 0x08, 0xb7, - 0x4a, 0x66, 0xc2, 0x4c, 0x14, 0xfe, 0xba, 0x63, 0xc0, 0x0b, 0x21, 0xce, 0x0d, 0x5f, 0x79, 0x49, 0xe3, 0x44, 0x45, - 0x6c, 0x88, 0x1f, 0x94, 0x07, 0xc7, 0xe1, 0xd6, 0xfe, 0xb0, 0x6d, 0x64, 0x22, 0x87, 0xe8, 0x60, 0x35, 0x4a, 0xf7, - 0xc6, 0x77, 0x44, 0x76, 0x3f, 0xde, 0x5f, 0x6b, 0x89, 0x40, 0x4b, 0xcb, 0xf7, 0x6a, 0x57, 0x13, 0xce, 0x9f, 0xde, - 0x76, 0x15, 0x9b, 0x94, 0x19, 0xc5, 0xb7, 0x54, 0xb6, 0xaf, 0xbe, 0xff, 0x8a, 0x7e, 0x16, 0x25, 0x99, 0xc2, 0xd7, - 0xb2, 0x85, 0xe7, 0xd6, 0x32, 0x23, 0x0d, 0x00, 0x55, 0x24, 0x46, 0x73, 0xc9, 0xd3, 0x2e, 0xe9, 0x30, 0x10, 0x6d, - 0xfc, 0x58, 0x6c, 0x9a, 0x45, 0xa8, 0x28, 0x7b, 0xc0, 0x66, 0xa3, 0x1b, 0x32, 0x08, 0x4f, 0xd0, 0x8b, 0x7f, 0xa5, - 0x03, 0x2f, 0x2a, 0xe7, 0xca, 0xd2, 0xad, 0x2f, 0x6f, 0xeb, 0x6f, 0xd2, 0xf5, 0xa4, 0xd6, 0xbb, 0x32, 0x5c, 0x2c, - 0x68, 0x46, 0xbe, 0xf2, 0x5f, 0x0d, 0xe0, 0x75, 0x48, 0xd3, 0x19, 0x0b, 0x7f, 0x62, 0xea, 0x1e, 0x79, 0x5b, 0x99, - 0xf7, 0xdb, 0x65, 0x73, 0x3e, 0x68, 0x1f, 0xbc, 0xa4, 0xaa, 0x3f, 0xe0, 0xf8, 0xc8, 0x79, 0xb8, 0xbf, 0x8a, 0x69, - 0x6e, 0x45, 0xc1, 0x80, 0xe7, 0xa3, 0x15, 0x4d, 0xba, 0xab, 0x47, 0x2b, 0x22, 0x8c, 0x25, 0x4e, 0x2d, 0x6e, 0x75, - 0x21, 0x93, 0xa3, 0xdc, 0x42, 0xdf, 0xc9, 0xcb, 0xdc, 0xe2, 0x3a, 0xda, 0xcb, 0xcc, 0xf4, 0x94, 0x55, 0xf7, 0x1b, - 0xc2, 0xa8, 0x8f, 0xcc, 0x2e, 0x5a, 0x05, 0xa7, 0x95, 0x46, 0xb8, 0xa1, 0x5e, 0x6b, 0x8a, 0x05, 0xce, 0x8d, 0x82, - 0x5a, 0xd5, 0x3b, 0x4f, 0xbb, 0xc6, 0x41, 0xb6, 0x99, 0xd3, 0x8a, 0xd0, 0xed, 0x57, 0xb8, 0xa7, 0xb0, 0xae, 0xf3, - 0xe0, 0x6a, 0x4e, 0x34, 0x38, 0x8d, 0xdb, 0x6d, 0xb3, 0x88, 0x16, 0xb2, 0x8b, 0x15, 0xfd, 0x7a, 0x00, 0xfe, 0x8b, - 0x1d, 0x8a, 0x0f, 0x5b, 0xa9, 0xb1, 0x15, 0x23, 0x2b, 0x34, 0xf5, 0x76, 0x8e, 0x08, 0xff, 0xc2, 0xb7, 0xe4, 0x76, - 0x5b, 0xaa, 0x08, 0x35, 0x75, 0xb3, 0x6a, 0x7b, 0xed, 0x64, 0xbf, 0x34, 0x49, 0xfb, 0x61, 0x9e, 0x9e, 0x10, 0x2a, - 0x51, 0x7b, 0x73, 0x68, 0x88, 0x25, 0xd7, 0x46, 0x9c, 0x1b, 0x4c, 0x48, 0xe3, 0xbf, 0xbf, 0x11, 0x90, 0x13, 0x29, - 0xe9, 0x70, 0x39, 0xf6, 0x2c, 0xc5, 0x48, 0xa2, 0xf9, 0xc8, 0xe0, 0x75, 0x0a, 0x8b, 0xb4, 0x95, 0x27, 0xd7, 0x2d, - 0x75, 0x43, 0xdd, 0x35, 0x7d, 0x92, 0xaa, 0xe3, 0xbc, 0x38, 0xc2, 0xdd, 0xa9, 0x82, 0x46, 0xf5, 0xe6, 0xe4, 0x0c, - 0x49, 0xdb, 0x99, 0x17, 0x42, 0xf2, 0x41, 0xbc, 0x96, 0x44, 0x8a, 0xed, 0x27, 0x59, 0xea, 0x3e, 0xbe, 0x39, 0x88, - 0x0a, 0x84, 0x8b, 0x70, 0x8c, 0xc4, 0xfe, 0x14, 0x63, 0x8a, 0xee, 0x2c, 0x4a, 0x82, 0x4d, 0xd5, 0xc9, 0x19, 0x3a, - 0xd3, 0x7c, 0x02, 0x81, 0x65, 0x37, 0xc8, 0xe8, 0xa0, 0x2e, 0x62, 0x7e, 0xf4, 0xed, 0x10, 0x37, 0xbf, 0xe5, 0xe0, - 0x1a, 0x6d, 0xcf, 0x8c, 0x33, 0xa5, 0xb6, 0xf8, 0xa7, 0x39, 0x5c, 0x9f, 0xc0, 0xec, 0xee, 0x50, 0xc2, 0x89, 0x38, - 0x92, 0x50, 0xaf, 0x3f, 0x57, 0x3f, 0x6c, 0x22, 0x85, 0xce, 0x09, 0xad, 0x0d, 0xb4, 0xf8, 0x34, 0xa7, 0xab, 0x05, - 0x1f, 0xc6, 0x61, 0xc7, 0x90, 0xa9, 0x92, 0xfc, 0x2e, 0xfa, 0xdc, 0xcf, 0x05, 0x18, 0xde, 0x43, 0x5c, 0xe7, 0x7b, - 0x67, 0x47, 0xcd, 0xc2, 0x2d, 0x84, 0xed, 0x4f, 0xa3, 0x84, 0x1c, 0xf4, 0x6b, 0xe5, 0xe7, 0x88, 0x5f, 0x7d, 0xa4, - 0x67, 0xb2, 0xe1, 0x87, 0x43, 0xb4, 0xb8, 0x96, 0xb0, 0x24, 0xc3, 0xe8, 0xfd, 0x8b, 0x57, 0x18, 0xf6, 0x12, 0x18, - 0x3c, 0x83, 0xbd, 0x05, 0x02, 0xe0, 0xf6, 0xe8, 0x27, 0x0c, 0xb5, 0x54, 0x0a, 0xc2, 0xb9, 0xe4, 0x21, 0x41, 0x62, - 0x5c, 0xca, 0xd5, 0xda, 0xa4, 0x4f, 0xc0, 0x5a, 0x3b, 0x4e, 0x1d, 0x34, 0x26, 0x3d, 0xcf, 0x92, 0xe6, 0xcb, 0x98, - 0x3f, 0x0b, 0x14, 0x2c, 0x3f, 0x34, 0x35, 0xdd, 0x83, 0xa0, 0xea, 0xca, 0x18, 0x6b, 0xba, 0xa3, 0x1d, 0x04, 0xef, - 0xaf, 0xd5, 0x33, 0xa2, 0xfc, 0xdd, 0x1a, 0x93, 0x1d, 0x04, 0x85, 0x82, 0x2d, 0x6e, 0xc8, 0xa1, 0x10, 0x62, 0x57, - 0xe3, 0xce, 0xbe, 0x8b, 0x4e, 0x65, 0xa9, 0x99, 0xdc, 0x6e, 0x94, 0x4d, 0x33, 0x4c, 0x98, 0x62, 0x87, 0x56, 0xf2, - 0x05, 0x45, 0x89, 0x5d, 0xbb, 0x5a, 0x94, 0x33, 0xbf, 0xdb, 0xfa, 0x38, 0xb6, 0x50, 0x58, 0xf5, 0x39, 0x98, 0xe5, - 0xc4, 0xb4, 0x6d, 0x97, 0x81, 0xdc, 0xd9, 0x1b, 0x64, 0xaa, 0x29, 0x1b, 0x43, 0x98, 0x77, 0xcc, 0x47, 0xe6, 0xf0, - 0x3d, 0xb2, 0x3b, 0x0f, 0x99, 0xbb, 0xc9, 0x65, 0x2f, 0x3f, 0xeb, 0xf5, 0xcf, 0x1c, 0xa0, 0x90, 0xc6, 0xc0, 0xb1, - 0x79, 0xde, 0x10, 0x6b, 0x99, 0x98, 0x2e, 0x3b, 0x56, 0xba, 0x83, 0x41, 0xc1, 0xeb, 0x1c, 0x28, 0x54, 0xd2, 0x94, - 0x38, 0x71, 0x3d, 0x84, 0x35, 0xa2, 0x1c, 0xde, 0x3b, 0x31, 0xd7, 0xc9, 0x44, 0xb2, 0xf1, 0xaf, 0xf6, 0x3e, 0x5c, - 0xb4, 0x01, 0xfa, 0x78, 0x66, 0xa3, 0x16, 0x16, 0x89, 0x38, 0x85, 0x21, 0x3f, 0xaa, 0x79, 0xac, 0x49, 0xe8, 0x03, - 0x27, 0x03, 0xa9, 0xa0, 0x97, 0x0a, 0xfc, 0x6f, 0x91, 0x9c, 0xb1, 0x72, 0x4a, 0x01, 0x2a, 0xa2, 0xb5, 0x6b, 0xfe, - 0x75, 0xef, 0x7a, 0x4c, 0x82, 0x7a, 0xb5, 0x00, 0x6e, 0x2d, 0x25, 0x92, 0x9f, 0xfb, 0xdb, 0x30, 0x3a, 0xcc, 0x8c, - 0x93, 0xce, 0xf3, 0xea, 0x57, 0x4f, 0x2e, 0x22, 0x99, 0xa2, 0x2d, 0x04, 0x4f, 0x5d, 0x0c, 0x4c, 0xe4, 0x21, 0x9e, - 0x9b, 0x76, 0xd0, 0xa5, 0xc6, 0xa1, 0xfc, 0xcb, 0xae, 0xe3, 0x68, 0x6c, 0x16, 0xe3, 0x04, 0x42, 0x95, 0xea, 0xf2, - 0x3c, 0x73, 0x5d, 0xd6, 0x8b, 0x3d, 0x69, 0xa2, 0xae, 0xac, 0xf4, 0x5b, 0xa8, 0x98, 0x37, 0xba, 0x3a, 0x45, 0x6d, - 0x31, 0xad, 0x93, 0x97, 0x6d, 0x56, 0x66, 0xd5, 0x04, 0x6f, 0x43, 0xb6, 0x11, 0x4e, 0x76, 0xc1, 0x7e, 0x3a, 0xc7, - 0x4b, 0x77, 0x0d, 0x8d, 0x12, 0xbc, 0x84, 0x54, 0xd1, 0xdf, 0x99, 0x16, 0x0e, 0x24, 0x5a, 0x51, 0xb2, 0xf6, 0xa5, - 0xff, 0x66, 0x37, 0x9c, 0xe4, 0x5c, 0x47, 0xef, 0x50, 0x7b, 0x1c, 0x8a, 0x66, 0x3c, 0x26, 0x6b, 0x9c, 0xe7, 0x74, - 0x29, 0x70, 0xc9, 0x92, 0x72, 0xee, 0x05, 0xbb, 0x2b, 0x90, 0xf2, 0xfa, 0xcb, 0x16, 0x09, 0x99, 0x70, 0xfb, 0x3c, - 0x19, 0xb8, 0x8c, 0x09, 0xd2, 0x83, 0xde, 0xf5, 0x03, 0xd5, 0x58, 0xe0, 0xee, 0x97, 0x39, 0xe7, 0x7f, 0xae, 0x48, - 0x92, 0x86, 0x78, 0x68, 0x11, 0x1c, 0xa6, 0xda, 0xaf, 0xc0, 0xad, 0x63, 0xc0, 0xb5, 0x59, 0x99, 0x3e, 0xf8, 0xf5, - 0xf8, 0x40, 0x34, 0x02, 0xff, 0xf9, 0xf8, 0x2b, 0xe2, 0xd0, 0x83, 0x67, 0x2b, 0x42, 0xb2, 0xae, 0x87, 0x8b, 0x34, - 0xff, 0xd5, 0xee, 0x13, 0x80, 0x45, 0xb8, 0x91, 0x74, 0x2c, 0x01, 0x1d, 0xdf, 0x0d, 0x0b, 0xcc, 0x53, 0x60, 0x97, - 0xd1, 0x1f, 0xb3, 0x87, 0x95, 0x6b, 0x1c, 0x2a, 0x4e, 0xb4, 0x85, 0x71, 0xb8, 0x24, 0x58, 0xde, 0xd2, 0xb9, 0x8a, - 0x40, 0x06, 0x07, 0xa4, 0x5e, 0xde, 0x19, 0xc7, 0x23, 0xf7, 0x91, 0x15, 0x1c, 0xf8, 0x66, 0x58, 0xb6, 0x47, 0x06, - 0x1c, 0xea, 0x88, 0x1e, 0xd0, 0xe1, 0xd6, 0x20, 0x43, 0x4d, 0x14, 0x63, 0x0b, 0x3e, 0x3e, 0xaa, 0xc7, 0x0c, 0xf2, - 0x5c, 0x0e, 0xf8, 0xfa, 0xc0, 0xa0, 0xe2, 0x30, 0x81, 0xfc, 0x5d, 0x08, 0x83, 0x3a, 0xec, 0xad, 0x05, 0x80, 0xd2, - 0x27, 0x88, 0xa1, 0x13, 0x87, 0xd4, 0x1b, 0xd0, 0xe4, 0x7b, 0x90, 0xd2, 0x08, 0xfe, 0xa2, 0x22, 0x33, 0x1a, 0x3d, - 0x15, 0xb3, 0x50, 0x18, 0x45, 0x2b, 0x64, 0xa8, 0x4d, 0x88, 0x34, 0x75, 0xf7, 0x96, 0x11, 0xf9, 0xb1, 0x3d, 0xf0, - 0x65, 0x69, 0xaf, 0x45, 0x22, 0x55, 0xce, 0x78, 0x1f, 0x40, 0xa9, 0x39, 0xb8, 0x0a, 0xd4, 0x3d, 0x53, 0x7d, 0x4e, - 0xc5, 0xda, 0xcc, 0xb2, 0x58, 0x78, 0x20, 0x1f, 0xe2, 0x62, 0x7c, 0x15, 0x5d, 0xbe, 0xad, 0x28, 0x9e, 0xc5, 0xdf, - 0xfd, 0xba, 0xe9, 0x43, 0x0a, 0xff, 0x52, 0xf0, 0xd5, 0x59, 0x73, 0xe5, 0x84, 0x75, 0x9e, 0x20, 0x5d, 0x37, 0x18, - 0x74, 0x5b, 0x4b, 0x2c, 0x4f, 0xce, 0xf4, 0xeb, 0x76, 0x31, 0xa5, 0x6a, 0xaa, 0xde, 0xce, 0x03, 0x08, 0xa4, 0xf6, - 0x9d, 0x49, 0x67, 0xd0, 0x2c, 0x24, 0xcb, 0x0c, 0x13, 0xfc, 0xb9, 0xc3, 0x6e, 0x50, 0x91, 0x06, 0x5e, 0xb6, 0x96, - 0x5e, 0xe1, 0x73, 0x3c, 0xae, 0xe8, 0x25, 0xa7, 0xf1, 0xb7, 0x77, 0xa4, 0x3c, 0x6d, 0xfd, 0x54, 0x2e, 0xe7, 0x65, - 0xd1, 0x97, 0xa6, 0x5f, 0xd1, 0x6f, 0x52, 0xb7, 0x3c, 0xee, 0x22, 0x02, 0xe9, 0xff, 0x2a, 0xd7, 0x35, 0x8d, 0xbe, - 0x0a, 0x7b, 0xb1, 0x8b, 0x60, 0xf4, 0xec, 0xb6, 0x6e, 0x0e, 0x39, 0x53, 0x5a, 0x94, 0x1a, 0x0c, 0x4d, 0x3a, 0xfe, - 0x32, 0x0a, 0x4b, 0xd7, 0x94, 0x76, 0xee, 0xa7, 0x3b, 0xbd, 0x54, 0x47, 0x26, 0xfe, 0x6d, 0x2f, 0x7f, 0xc8, 0x3a, - 0x6a, 0x44, 0x17, 0xfe, 0x0f, 0xfe, 0x3c, 0xa2, 0x9c, 0x2f, 0x75, 0x4a, 0xa5, 0x1d, 0xd4, 0x47, 0x55, 0x72, 0x3c, - 0x1d, 0x07, 0xca, 0x68, 0x14, 0xcf, 0xd4, 0xf1, 0xcc, 0x69, 0x26, 0xe8, 0x89, 0x6e, 0x90, 0xac, 0xe5, 0x00, 0x2d, - 0x80, 0x9a, 0x92, 0x11, 0xa7, 0xea, 0x04, 0x37, 0x13, 0xa7, 0xd7, 0x45, 0x27, 0x48, 0x4e, 0x0b, 0xc7, 0xe8, 0x73, - 0x59, 0x0c, 0x51, 0x29, 0xeb, 0xdb, 0xab, 0x23, 0xaa, 0x1e, 0x65, 0xdb, 0xd2, 0xb7, 0x8a, 0x8d, 0x76, 0xa8, 0x83, - 0x15, 0x73, 0x60, 0x97, 0x97, 0xcc, 0xd4, 0x72, 0xe6, 0x60, 0xe6, 0xa7, 0x67, 0xc0, 0x9e, 0x03, 0x66, 0xe7, 0x0c, - 0x31, 0x47, 0x11, 0xaa, 0xc4, 0xd2, 0x18, 0x14, 0x17, 0x76, 0x92, 0x48, 0x7d, 0x3e, 0xef, 0x8e, 0x52, 0x15, 0x73, - 0x6a, 0x2a, 0xaf, 0x07, 0xb0, 0x2d, 0xb1, 0xf2, 0x57, 0x34, 0xa1, 0x1f, 0xe9, 0x16, 0x23, 0xfc, 0x8d, 0x8a, 0xe3, - 0xfc, 0x7e, 0x7e, 0x9b, 0x9a, 0x29, 0x01, 0x13, 0x43, 0x4e, 0x5d, 0x9d, 0x60, 0x5d, 0xa5, 0x98, 0x96, 0xc5, 0x99, - 0x96, 0xe7, 0x7c, 0x36, 0xb6, 0x25, 0xd6, 0x42, 0x38, 0x5b, 0xde, 0xf6, 0xc6, 0x5d, 0x5e, 0x30, 0x26, 0x92, 0x24, - 0x96, 0x6d, 0x5e, 0x4d, 0x07, 0x20, 0xc1, 0x1d, 0x62, 0x9b, 0x7e, 0xc1, 0xb7, 0xa2, 0x88, 0x07, 0xb0, 0x9b, 0xcc, - 0xce, 0x62, 0xab, 0x4c, 0x07, 0xe3, 0xe0, 0x96, 0xff, 0xd5, 0xb6, 0x86, 0x02, 0x21, 0x11, 0x9f, 0x08, 0x70, 0x49, - 0x74, 0x36, 0x83, 0x3a, 0x85, 0x0c, 0x37, 0xf1, 0x9d, 0xa2, 0xc9, 0x77, 0xb4, 0xfa, 0x8e, 0x88, 0xec, 0xdb, 0xab, - 0x88, 0x28, 0x4a, 0xb9, 0x3c, 0x6a, 0xc5, 0x49, 0x8e, 0x68, 0x4e, 0xc6, 0x97, 0x8e, 0xa4, 0x9d, 0x34, 0xe3, 0x4a, - 0x4d, 0x6f, 0x8f, 0xdf, 0x65, 0x10, 0xe9, 0x57, 0xe7, 0xb9, 0x15, 0xc7, 0x79, 0x21, 0x0a, 0xca, 0x07, 0xdc, 0x86, - 0x51, 0x8d, 0x56, 0xbe, 0x99, 0xf3, 0x80, 0x76, 0x66, 0x78, 0x38, 0x9d, 0xb5, 0x6f, 0xb6, 0x2d, 0xf8, 0x72, 0x1c, - 0x0e, 0x63, 0xd3, 0xbe, 0x7f, 0xfe, 0xae, 0x7e, 0x6f, 0xc6, 0x87, 0x57, 0xde, 0x49, 0xea, 0x1d, 0x0f, 0x60, 0xea, - 0xda, 0x98, 0xad, 0x73, 0x70, 0x9e, 0xc6, 0xd8, 0x22, 0xfa, 0x5f, 0xda, 0xc6, 0x67, 0xa5, 0x7f, 0x02, 0xee, 0xc1, - 0x9d, 0x64, 0x59, 0xfa, 0xc5, 0x99, 0x46, 0x8b, 0xfc, 0x89, 0xe5, 0x49, 0xad, 0x1e, 0x74, 0x5c, 0x9a, 0x5c, 0xbc, - 0x42, 0xba, 0x3c, 0x4b, 0xbf, 0x9c, 0x2d, 0xf4, 0x8f, 0xd3, 0x55, 0x00, 0xff, 0x8f, 0x73, 0xa4, 0xb8, 0x3f, 0xa6, - 0xd9, 0x8b, 0x74, 0xe3, 0xfb, 0xb3, 0x9b, 0xd5, 0xeb, 0x82, 0x3d, 0x3a, 0x4f, 0xb6, 0x6c, 0x5d, 0x0b, 0xad, 0xa9, - 0x1b, 0x17, 0xd4, 0x9d, 0xdd, 0x66, 0xed, 0x1b, 0xeb, 0x53, 0x6b, 0xe8, 0xbb, 0x98, 0x48, 0x3f, 0x7f, 0x44, 0x3f, - 0x5d, 0x7b, 0x8a, 0x0b, 0xc3, 0x7e, 0xa7, 0xba, 0x1e, 0x35, 0x33, 0x9d, 0x0a, 0x12, 0x9a, 0x97, 0x3c, 0xdd, 0x37, - 0x39, 0xaf, 0xe5, 0xf8, 0x72, 0xf4, 0x34, 0xa2, 0xa6, 0x7d, 0x47, 0x19, 0xdd, 0x4b, 0x82, 0x31, 0xea, 0x2a, 0x35, - 0x30, 0xfa, 0xe2, 0x55, 0x05, 0x06, 0x01, 0xaa, 0xf3, 0xfa, 0x40, 0x8a, 0xc0, 0xe0, 0xc3, 0x21, 0x8f, 0xe5, 0x06, - 0x03, 0x27, 0x4b, 0xeb, 0x20, 0xf5, 0xf2, 0x20, 0x1c, 0xa9, 0xea, 0xe2, 0x6d, 0x26, 0xa0, 0xc0, 0xeb, 0xa9, 0xfe, - 0x1b, 0xdd, 0x9c, 0x1b, 0xe7, 0x69, 0xc6, 0x87, 0x73, 0x43, 0xe9, 0x52, 0x71, 0xf1, 0xda, 0xae, 0x62, 0x1c, 0x16, - 0xd5, 0x56, 0x25, 0x53, 0x32, 0x65, 0x0e, 0x13, 0xf3, 0x33, 0x41, 0x7a, 0xde, 0xa8, 0x43, 0xee, 0x97, 0x4f, 0xf2, - 0x9a, 0x2e, 0x71, 0x65, 0x92, 0x8d, 0x42, 0xf8, 0x3f, 0x34, 0x55, 0x6b, 0x0e, 0xa4, 0x46, 0xe0, 0x72, 0x70, 0xb5, - 0x54, 0xde, 0xb6, 0xb4, 0x9f, 0x3f, 0x2e, 0xdf, 0xa7, 0xb7, 0x95, 0x24, 0xf9, 0x2f, 0x4d, 0xd8, 0x98, 0xf3, 0xc9, - 0x28, 0xb4, 0x29, 0xc4, 0x0d, 0x4c, 0x45, 0x3b, 0xc6, 0x4f, 0x0a, 0x2f, 0x08, 0xea, 0xf3, 0x0e, 0x45, 0x03, 0xb0, - 0x79, 0x95, 0x8a, 0xdc, 0x19, 0x68, 0x59, 0xa2, 0x6c, 0xdd, 0xe8, 0x6b, 0xc3, 0xf7, 0x38, 0x78, 0xd5, 0x70, 0xeb, - 0xde, 0xcb, 0xa6, 0x0a, 0x94, 0x4d, 0x5b, 0x59, 0xbc, 0x0a, 0x25, 0xcf, 0xd4, 0x4b, 0x9d, 0x2b, 0x69, 0x17, 0x0e, - 0x7e, 0xa6, 0xe2, 0xe8, 0x57, 0x12, 0x81, 0x5d, 0x39, 0xc8, 0x00, 0xc7, 0xed, 0x36, 0xc7, 0x19, 0x02, 0x11, 0x94, - 0x85, 0x56, 0x20, 0xd4, 0x22, 0x55, 0xa7, 0xbe, 0x33, 0x62, 0x35, 0x01, 0xe4, 0x8a, 0xbd, 0x8b, 0x56, 0xc8, 0x9f, - 0x65, 0x06, 0x3a, 0xb0, 0xa3, 0x3d, 0x37, 0x2e, 0xbe, 0x3e, 0x25, 0xe8, 0xd7, 0x12, 0x7b, 0x67, 0x54, 0xc7, 0xc8, - 0x69, 0x3e, 0x3f, 0x58, 0x26, 0xc6, 0x6d, 0x31, 0xde, 0xb6, 0x91, 0x39, 0x81, 0x29, 0x50, 0x89, 0x99, 0xd6, 0xaa, - 0x65, 0x04, 0x39, 0x4c, 0xb2, 0x13, 0x8f, 0x34, 0x19, 0x2b, 0x96, 0xf7, 0x40, 0x60, 0xce, 0x30, 0x6e, 0xd3, 0x98, - 0x55, 0x2b, 0xa4, 0x60, 0x04, 0xc3, 0xd0, 0xf8, 0x60, 0x31, 0x12, 0xe6, 0x95, 0x80, 0x0c, 0x1c, 0x29, 0x52, 0x10, - 0xdf, 0xed, 0x68, 0x7e, 0x30, 0xa5, 0x47, 0x9c, 0xa8, 0x70, 0x8f, 0xca, 0x29, 0xdd, 0x60, 0xa8, 0xe7, 0x82, 0x05, - 0x4c, 0x31, 0xc5, 0x46, 0x72, 0xa0, 0x32, 0xdc, 0xaa, 0x90, 0xb1, 0x5c, 0xf7, 0xb6, 0x3f, 0xbd, 0x97, 0x34, 0x6c, - 0xfa, 0x4a, 0x48, 0x1a, 0xd4, 0x5a, 0x71, 0xe1, 0x03, 0x76, 0xd1, 0xb3, 0xf7, 0x4d, 0x76, 0xc8, 0x34, 0x91, 0x31, - 0xda, 0x4b, 0xa2, 0x7c, 0x69, 0x7f, 0xac, 0x15, 0x5b, 0x63, 0x80, 0xab, 0xde, 0xe9, 0xfa, 0x84, 0x9c, 0xf2, 0x4e, - 0x8b, 0x82, 0x0c, 0x32, 0x2c, 0x23, 0xfa, 0xf0, 0x9f, 0x2e, 0xf2, 0xcd, 0x58, 0x3f, 0x4b, 0xa8, 0x53, 0x93, 0xd6, - 0x2f, 0x7a, 0xb3, 0xcd, 0xce, 0xc9, 0x6c, 0x01, 0xa0, 0xf0, 0x5f, 0xad, 0x3f, 0xb1, 0x35, 0x22, 0xd4, 0x50, 0x04, - 0x2f, 0x01, 0x57, 0x1c, 0xf0, 0xa8, 0xf6, 0x34, 0x42, 0xe1, 0x20, 0x09, 0x4d, 0x99, 0xb3, 0xdd, 0xdf, 0x10, 0xb4, - 0x71, 0x4d, 0x41, 0x87, 0x3e, 0x85, 0xa6, 0xff, 0xea, 0xb3, 0x5f, 0xa0, 0x5a, 0x45, 0xd1, 0x26, 0x76, 0x4d, 0xb1, - 0x38, 0xa4, 0x70, 0x93, 0x6b, 0x87, 0x77, 0x89, 0x10, 0xe0, 0xec, 0x5f, 0xcc, 0x29, 0x4e, 0x16, 0xd6, 0x9d, 0x4d, - 0x08, 0x96, 0x0a, 0x46, 0x52, 0xa2, 0x43, 0x19, 0x73, 0x9d, 0x39, 0x1e, 0x56, 0xe3, 0x97, 0x2e, 0xe8, 0xe1, 0x10, - 0x5e, 0xc7, 0xf8, 0xfc, 0xe1, 0x79, 0xc7, 0x3b, 0x56, 0x68, 0x99, 0xb5, 0x84, 0x29, 0xa4, 0x87, 0x7c, 0x0f, 0x83, - 0xca, 0x63, 0xcf, 0x05, 0xd3, 0xea, 0xfe, 0xa1, 0x54, 0x68, 0xe7, 0x39, 0xa8, 0xa9, 0x17, 0xc0, 0xc4, 0xc2, 0x4d, - 0x29, 0x0d, 0xbb, 0x92, 0x40, 0x6a, 0x53, 0x04, 0x30, 0xfe, 0xe4, 0x13, 0x22, 0x1e, 0xc4, 0x41, 0xa9, 0x96, 0xd0, - 0xf1, 0xe6, 0x68, 0xa3, 0x56, 0x77, 0xb1, 0x30, 0xbe, 0x05, 0x2b, 0x80, 0xb6, 0xc4, 0x86, 0xe1, 0x61, 0xf1, 0xa9, - 0x94, 0x37, 0x21, 0x01, 0xb5, 0xab, 0x20, 0x85, 0x95, 0x83, 0xb5, 0x1f, 0x4c, 0x80, 0xaa, 0x5d, 0x93, 0x28, 0xfd, - 0xb6, 0x52, 0x44, 0x0a, 0x8b, 0x42, 0x35, 0x8f, 0xec, 0xde, 0x96, 0x75, 0xda, 0x50, 0x35, 0x4f, 0x91, 0x2e, 0x95, - 0xda, 0x2e, 0x71, 0x6d, 0xff, 0xa7, 0x99, 0x42, 0xe6, 0x3e, 0x3b, 0x61, 0xf5, 0xb6, 0xf6, 0x14, 0xea, 0x64, 0x54, - 0x4f, 0xf1, 0xf2, 0x51, 0xb5, 0xc2, 0xdf, 0x56, 0xe6, 0xa0, 0x01, 0x0f, 0xc6, 0x45, 0xfa, 0x67, 0xef, 0xc3, 0x35, - 0xe4, 0x9e, 0xbc, 0x6f, 0x55, 0xa1, 0x48, 0x8e, 0x07, 0x33, 0xec, 0x2f, 0x3a, 0x81, 0xe3, 0x09, 0xdb, 0x36, 0x09, - 0x58, 0xeb, 0xf8, 0x1e, 0x49, 0x41, 0x8a, 0xfc, 0x36, 0xd6, 0x86, 0xc4, 0xdc, 0xf0, 0xa3, 0x44, 0x71, 0x4b, 0x29, - 0x5d, 0x25, 0x4f, 0x4e, 0x6d, 0xbb, 0x82, 0x52, 0x13, 0x47, 0xe0, 0xd8, 0xfa, 0xca, 0x11, 0xff, 0x6c, 0xfb, 0x6a, - 0x97, 0x2b, 0x89, 0x43, 0xb1, 0xc8, 0x4f, 0x75, 0x3f, 0x29, 0x0f, 0x93, 0x01, 0xac, 0x26, 0xd4, 0x19, 0x0b, 0x47, - 0x95, 0x24, 0x80, 0xc0, 0x04, 0xa8, 0x25, 0x3c, 0x17, 0x6a, 0x91, 0xdb, 0x30, 0xa9, 0xd9, 0x56, 0xce, 0x55, 0xfb, - 0x64, 0x6b, 0x6a, 0xcd, 0xc0, 0xcd, 0xc5, 0xc6, 0x71, 0x75, 0x67, 0x07, 0xb2, 0xd2, 0x43, 0x02, 0x9d, 0x7b, 0x53, - 0x62, 0x41, 0x53, 0xe0, 0xc3, 0xd1, 0xee, 0xbe, 0x03, 0x10, 0x45, 0x23, 0x46, 0xff, 0x59, 0xc1, 0xe4, 0xa4, 0xdf, - 0xe8, 0x06, 0x7c, 0x4b, 0x95, 0x79, 0x41, 0xd9, 0xe0, 0x72, 0x77, 0x7e, 0x63, 0xd5, 0x03, 0xcf, 0x4c, 0x98, 0x93, - 0x72, 0xed, 0xc1, 0x46, 0x66, 0x89, 0x9a, 0x70, 0xfd, 0x7f, 0x35, 0xd7, 0x90, 0x1c, 0x80, 0x5c, 0x8c, 0x7d, 0xab, - 0xac, 0xc0, 0xd5, 0x2c, 0x74, 0x40, 0x61, 0x1f, 0x0c, 0x9c, 0x0a, 0x1b, 0x76, 0x03, 0x6e, 0x7e, 0x90, 0xa6, 0x95, - 0xef, 0x12, 0xe8, 0xfe, 0xa7, 0x58, 0x63, 0xdf, 0xbb, 0x25, 0x6b, 0xd8, 0xe8, 0x4d, 0x41, 0xd3, 0xe8, 0x5e, 0x33, - 0x59, 0xd3, 0xd9, 0xca, 0x0c, 0x55, 0x23, 0xaf, 0xd6, 0x8f, 0x45, 0xdc, 0x1a, 0x9e, 0x9f, 0xc9, 0x79, 0xbd, 0x8f, - 0xe9, 0xa5, 0x6e, 0x3c, 0xf6, 0x45, 0xdd, 0xe1, 0xab, 0x1b, 0xd1, 0x86, 0xb3, 0xa2, 0x88, 0x9d, 0x0f, 0xeb, 0x9e, - 0x8a, 0xb4, 0x5b, 0x47, 0xbb, 0x78, 0x5b, 0x30, 0xa7, 0x64, 0x54, 0x67, 0xd0, 0x14, 0xba, 0x0a, 0xc7, 0xba, 0x7e, - 0xbe, 0xb8, 0x02, 0xeb, 0x8e, 0x96, 0x55, 0x82, 0x37, 0x26, 0x5d, 0xd4, 0x61, 0xd7, 0xf7, 0x8c, 0x0f, 0x89, 0xaa, - 0x8f, 0x46, 0xeb, 0x34, 0xf7, 0x05, 0x34, 0xa0, 0xe5, 0x0b, 0x3a, 0xb4, 0x21, 0xab, 0xd1, 0x5d, 0x69, 0xf2, 0xa4, - 0x56, 0xd5, 0x6f, 0x79, 0x0c, 0xde, 0xb1, 0x7c, 0x35, 0x56, 0x9d, 0x8e, 0x7f, 0x19, 0xbe, 0xbc, 0xac, 0xee, 0x90, - 0xf4, 0xb9, 0x97, 0x01, 0x60, 0xda, 0xe6, 0x93, 0x41, 0xbf, 0x2f, 0x02, 0x91, 0x91, 0xdf, 0x62, 0x3c, 0x7b, 0x29, - 0x4b, 0x00, 0x1d, 0x57, 0x05, 0xbd, 0x33, 0x4d, 0x7a, 0x79, 0x4f, 0x24, 0xe2, 0xc6, 0xc0, 0x78, 0x83, 0x42, 0x15, - 0x52, 0xef, 0x34, 0x81, 0xb8, 0x47, 0x1d, 0x13, 0xe9, 0x45, 0xd5, 0xf7, 0xab, 0x04, 0x07, 0xcf, 0xa2, 0xd5, 0x6e, - 0xff, 0xb3, 0x68, 0x0a, 0xe4, 0xc4, 0xc1, 0x44, 0x5d, 0xe1, 0x84, 0xc7, 0x3f, 0x9e, 0x68, 0xbf, 0x64, 0x47, 0xaa, - 0xe9, 0x30, 0x5f, 0xc5, 0x57, 0x76, 0xaa, 0x5a, 0xf1, 0xcb, 0xbc, 0xef, 0x67, 0x8b, 0xb4, 0xf1, 0x32, 0xd2, 0xab, - 0xd9, 0x5e, 0xed, 0x6c, 0xa2, 0xba, 0x53, 0x58, 0x1e, 0x35, 0x59, 0x51, 0x1d, 0x13, 0xab, 0x56, 0x5f, 0x1d, 0x7a, - 0xe5, 0xed, 0x65, 0xf6, 0x9b, 0x25, 0x61, 0xe6, 0xec, 0x69, 0xe1, 0xde, 0xec, 0x6c, 0xc9, 0x83, 0xee, 0xe7, 0xe0, - 0xbf, 0xc7, 0x46, 0x8a, 0x2d, 0x53, 0xbb, 0x28, 0x47, 0x02, 0x00, 0x0e, 0x12, 0xbf, 0x3a, 0xbd, 0xf9, 0x7b, 0x2d, - 0x2a, 0x75, 0xb3, 0xda, 0x69, 0x71, 0xe9, 0x1f, 0x23, 0x4a, 0x4b, 0xe3, 0x38, 0xe5, 0x08, 0xa2, 0x71, 0x6d, 0x7e, - 0xc1, 0x24, 0x73, 0xdf, 0x62, 0xb5, 0x12, 0x6b, 0xc9, 0x09, 0x14, 0x18, 0xb9, 0xd7, 0x35, 0xcf, 0x5d, 0xab, 0x53, - 0x58, 0xa6, 0xb6, 0xe6, 0xb0, 0xb5, 0xc3, 0xbe, 0x83, 0xaa, 0xaf, 0xa9, 0xc3, 0x55, 0x16, 0xbe, 0xad, 0x78, 0x21, - 0x5f, 0x4a, 0x79, 0x72, 0xea, 0xe6, 0x8d, 0x60, 0x29, 0x3e, 0x0a, 0x54, 0x73, 0x46, 0xf0, 0xa2, 0x56, 0x5f, 0x59, - 0xc4, 0x4a, 0x7e, 0x28, 0x09, 0xbd, 0xd8, 0x3d, 0x17, 0xd9, 0x80, 0xab, 0xb2, 0xfe, 0xbe, 0xfa, 0xdc, 0x23, 0x95, - 0x7d, 0x74, 0x61, 0x95, 0xda, 0x8e, 0x63, 0x6e, 0xa3, 0xa6, 0x2a, 0x78, 0xf3, 0xde, 0x35, 0xd8, 0x35, 0xb0, 0x3c, - 0x19, 0xcb, 0x25, 0x46, 0x06, 0x3e, 0xd6, 0xd6, 0x53, 0x7d, 0x65, 0x5e, 0x3d, 0x5a, 0xc9, 0x58, 0x48, 0xca, 0x2b, - 0x04, 0xa2, 0xd7, 0x7f, 0x96, 0x2b, 0x35, 0xac, 0xd5, 0xd9, 0x0a, 0x25, 0x1a, 0x31, 0xda, 0xbb, 0x54, 0x4e, 0x76, - 0x4d, 0x8f, 0x8c, 0xe9, 0xf3, 0xee, 0x47, 0xd5, 0xd2, 0x92, 0xd9, 0x86, 0x16, 0xff, 0x54, 0x64, 0x2c, 0xa9, 0x88, - 0x6d, 0xc5, 0x16, 0x7b, 0x16, 0x77, 0x01, 0x4c, 0x3e, 0x5d, 0x30, 0x77, 0x9f, 0x50, 0x0e, 0xc6, 0xea, 0x57, 0xaa, - 0x2a, 0x37, 0x3e, 0x4f, 0xbc, 0xbe, 0x6f, 0x60, 0x26, 0x91, 0x45, 0x1e, 0x05, 0x36, 0x2b, 0xeb, 0x69, 0x4f, 0x6f, - 0x73, 0x52, 0xa3, 0x5f, 0x18, 0x85, 0x56, 0x79, 0xc0, 0xe7, 0x9a, 0x79, 0xf2, 0xe6, 0x7d, 0xa7, 0x1b, 0x41, 0x86, - 0xa3, 0x8d, 0xb4, 0x41, 0xdb, 0x6d, 0x48, 0x7a, 0x8b, 0xf8, 0x0f, 0x29, 0xd3, 0x5f, 0x94, 0xf9, 0xea, 0xfb, 0xe1, - 0x7c, 0x5d, 0x4e, 0x50, 0x35, 0x7b, 0xc0, 0xe1, 0xd1, 0x58, 0x02, 0x2c, 0x20, 0x8e, 0x3e, 0x12, 0xb2, 0xb6, 0x26, - 0x28, 0x27, 0x3c, 0x52, 0xe5, 0x6c, 0x94, 0x77, 0x26, 0x7a, 0x42, 0xab, 0xca, 0xd9, 0x00, 0x5b, 0xd0, 0x8d, 0x5d, - 0xf2, 0x6d, 0x2c, 0x3c, 0x5d, 0xee, 0x77, 0x8e, 0xed, 0x81, 0x2b, 0xd7, 0x5c, 0xc0, 0x17, 0xde, 0x03, 0x77, 0xa1, - 0x5a, 0x40, 0x6b, 0x10, 0xff, 0x51, 0x54, 0xd9, 0xdd, 0xe6, 0xdc, 0x48, 0x24, 0x59, 0x28, 0x13, 0x2a, 0x6b, 0xf1, - 0x73, 0x83, 0x9c, 0xeb, 0x71, 0xe0, 0x1c, 0x29, 0x01, 0x82, 0x63, 0xc4, 0x24, 0xf6, 0xa6, 0x34, 0x54, 0x70, 0x8e, - 0x3e, 0x79, 0x2d, 0xbf, 0x60, 0xca, 0xd0, 0x05, 0x3a, 0x3d, 0xcf, 0x42, 0xc1, 0x7e, 0x60, 0x5d, 0x38, 0x3a, 0x69, - 0x39, 0xeb, 0x9f, 0x1d, 0x18, 0x01, 0xf2, 0x58, 0x79, 0x99, 0x24, 0x6c, 0x2d, 0x5a, 0xbd, 0xc9, 0xfb, 0x71, 0xa5, - 0x10, 0x2d, 0x16, 0xa8, 0xba, 0xfd, 0x02, 0x17, 0xa7, 0x25, 0x95, 0xac, 0x14, 0xb7, 0x0a, 0x15, 0x88, 0xd6, 0x9b, - 0x50, 0xf5, 0x3a, 0x7d, 0x6d, 0xdb, 0x51, 0x79, 0x69, 0x28, 0x36, 0x31, 0x04, 0x86, 0x58, 0x1f, 0x7e, 0xaa, 0xb6, - 0xe1, 0xb6, 0xb3, 0x2e, 0xee, 0x73, 0xdb, 0x7e, 0x0d, 0x5f, 0x8f, 0xc4, 0x9b, 0xca, 0xdb, 0xa6, 0x78, 0x58, 0x20, - 0xe2, 0x44, 0xd7, 0x6b, 0x0d, 0x9b, 0x93, 0x4f, 0x7f, 0x55, 0xa7, 0x4c, 0x8e, 0xe8, 0x63, 0x0f, 0x21, 0xe6, 0x42, - 0x44, 0x85, 0x48, 0xbf, 0x6f, 0x47, 0xe7, 0xca, 0x3d, 0x23, 0x44, 0xe7, 0xd8, 0x88, 0x75, 0x6c, 0x27, 0x81, 0xa7, - 0xb6, 0x14, 0xc4, 0x09, 0xdc, 0x7d, 0x27, 0x16, 0x7c, 0xf2, 0x85, 0x34, 0xe7, 0xe1, 0xf9, 0xcb, 0xdf, 0xfe, 0x4a, - 0x56, 0xea, 0xb9, 0xee, 0x2c, 0xba, 0xa6, 0xbb, 0xa4, 0x52, 0x97, 0xcf, 0x71, 0x17, 0xb3, 0xf0, 0x26, 0x6b, 0xff, - 0x7a, 0x78, 0x4b, 0x37, 0x6d, 0x48, 0x91, 0x4c, 0x51, 0xee, 0xfe, 0x4d, 0x3c, 0x35, 0x22, 0xc3, 0x5f, 0xf0, 0x8c, - 0xb1, 0xee, 0xcb, 0xaa, 0xf9, 0x60, 0xac, 0x04, 0xec, 0x3d, 0x27, 0x23, 0x73, 0x51, 0x70, 0xa3, 0x28, 0x44, 0x2b, - 0xd6, 0x83, 0xed, 0x40, 0x53, 0xf9, 0x80, 0xf1, 0x0f, 0x53, 0x6a, 0xb9, 0x8b, 0xcd, 0xf5, 0xfd, 0xd8, 0xf4, 0x38, - 0x26, 0x25, 0x23, 0x9d, 0x39, 0x1a, 0xa8, 0x15, 0xd8, 0xf6, 0x58, 0x7e, 0x39, 0x44, 0xe7, 0xb4, 0x2d, 0xb0, 0x4d, - 0xcb, 0xc7, 0x37, 0x87, 0x6c, 0x2e, 0x5f, 0x9a, 0xf1, 0x5e, 0xba, 0x79, 0xf2, 0x62, 0x99, 0xde, 0x0a, 0x7b, 0xc2, - 0x40, 0x14, 0x51, 0x05, 0x9d, 0x84, 0x22, 0xec, 0xb4, 0x5b, 0x7b, 0x82, 0x94, 0x16, 0x83, 0xf0, 0x13, 0x5c, 0x9f, - 0xb4, 0xaf, 0x45, 0x6d, 0x6a, 0x1d, 0x35, 0x41, 0xed, 0x72, 0x9e, 0x06, 0x48, 0x47, 0xc5, 0x73, 0x0b, 0xa1, 0xcf, - 0x28, 0xd0, 0x16, 0x34, 0x59, 0x29, 0xd2, 0x08, 0x43, 0x91, 0x73, 0x63, 0xaa, 0x26, 0x73, 0xb1, 0x5c, 0xf8, 0xb3, - 0x46, 0x9b, 0x34, 0xc4, 0x24, 0xe4, 0xda, 0x96, 0x5d, 0xe6, 0xeb, 0x04, 0xeb, 0x2b, 0x6b, 0x7f, 0x3d, 0xf9, 0x5b, - 0x41, 0x32, 0x05, 0xec, 0x47, 0x16, 0xaf, 0xdb, 0x97, 0xb8, 0x02, 0xde, 0xd4, 0x40, 0x51, 0x03, 0xca, 0xac, 0x3a, - 0xad, 0xdb, 0xf0, 0x80, 0x32, 0xfb, 0xcd, 0x40, 0x95, 0x9a, 0x5c, 0x59, 0xc5, 0xb7, 0xba, 0x8d, 0xb8, 0x5e, 0xb2, - 0x81, 0xb4, 0xf1, 0x76, 0xca, 0xad, 0xd3, 0x54, 0xb9, 0x12, 0xe7, 0x41, 0x25, 0xa9, 0x71, 0x0f, 0x30, 0x98, 0xe6, - 0xe9, 0x3b, 0x34, 0xe6, 0xdf, 0x8a, 0x83, 0x49, 0x5f, 0x18, 0x24, 0xab, 0xb4, 0x63, 0x01, 0x20, 0x40, 0xb7, 0x5d, - 0x72, 0xd3, 0xe4, 0x08, 0x46, 0xe4, 0x1f, 0xd0, 0xbb, 0xe0, 0x88, 0xec, 0x1d, 0xd8, 0x9d, 0xe9, 0xc3, 0xc0, 0xcc, - 0xbb, 0x9a, 0x92, 0x5d, 0x8a, 0xc2, 0x37, 0xd1, 0x37, 0xdb, 0xc5, 0x4a, 0x00, 0xdc, 0x30, 0xbb, 0xcc, 0x17, 0xaa, - 0x4c, 0xe6, 0x62, 0xab, 0xf2, 0x90, 0x9b, 0xa9, 0x4c, 0x71, 0x55, 0x13, 0x3c, 0x08, 0x82, 0x80, 0x82, 0xbc, 0x81, - 0x5c, 0xc7, 0x17, 0x0d, 0x20, 0xe8, 0x41, 0x58, 0x0c, 0x13, 0xcf, 0x8d, 0xb2, 0xbb, 0x3e, 0xaa, 0x98, 0xc2, 0xf8, - 0x54, 0x4a, 0x72, 0x72, 0xee, 0xf5, 0xc9, 0x64, 0xd8, 0x6a, 0x36, 0xec, 0xe4, 0x24, 0x21, 0xb4, 0x24, 0xb6, 0x90, - 0x53, 0xea, 0xf6, 0xae, 0x0e, 0xbd, 0x3c, 0x96, 0x05, 0x8c, 0xb6, 0x11, 0xac, 0x3b, 0x1c, 0xed, 0x3d, 0x25, 0xc2, - 0x8b, 0x65, 0x63, 0xbe, 0x13, 0xf1, 0x45, 0xaa, 0x8f, 0x81, 0x06, 0xcc, 0x9b, 0x3f, 0x07, 0xb2, 0x9a, 0xe0, 0x6f, - 0xc2, 0x8b, 0x65, 0x71, 0x9f, 0x39, 0x21, 0x2a, 0x36, 0x8b, 0xfb, 0x67, 0x1b, 0x14, 0x98, 0xae, 0x09, 0xdd, 0x40, - 0xaa, 0x81, 0x45, 0xc3, 0x1d, 0x3d, 0x58, 0xb4, 0x3f, 0xa2, 0xab, 0x62, 0x59, 0x61, 0xf4, 0xe8, 0xc1, 0x51, 0x3d, - 0x95, 0x1d, 0x4b, 0x6b, 0xa4, 0x39, 0xe2, 0x37, 0xcf, 0x11, 0x2d, 0xea, 0x36, 0xc6, 0x44, 0x63, 0x69, 0xe6, 0x1f, - 0x44, 0x79, 0xb4, 0xaf, 0x41, 0xd8, 0x3f, 0x83, 0x4d, 0xe2, 0x63, 0xc6, 0x20, 0x6f, 0x8e, 0x06, 0xf6, 0x72, 0x40, - 0x1d, 0x07, 0xcb, 0x93, 0x92, 0x82, 0x9b, 0x8b, 0x95, 0x2a, 0xcd, 0x32, 0x8a, 0x3d, 0xaf, 0x13, 0x59, 0xcb, 0x1e, - 0xe1, 0x24, 0x23, 0x26, 0xfa, 0x3c, 0x50, 0x90, 0x77, 0x5a, 0x2f, 0xff, 0xd3, 0x0a, 0xcc, 0x3a, 0x74, 0x32, 0xd6, - 0x64, 0x94, 0x2c, 0x84, 0x08, 0x6d, 0x08, 0xb7, 0x36, 0x24, 0xd7, 0x22, 0xb4, 0x1d, 0x99, 0x43, 0x1f, 0xe6, 0x4b, - 0xc1, 0x19, 0x5e, 0x82, 0x9e, 0x76, 0xb9, 0x7d, 0x71, 0xfa, 0xcd, 0x85, 0x72, 0x67, 0x83, 0x83, 0x08, 0xa4, 0xe8, - 0x5c, 0x9f, 0x1e, 0x8a, 0x17, 0x85, 0x83, 0x88, 0xb8, 0x39, 0xbc, 0x1e, 0xac, 0x3e, 0x26, 0x74, 0x56, 0x75, 0x4f, - 0x7b, 0xff, 0x85, 0x0b, 0xdf, 0xb5, 0xb5, 0x22, 0x8a, 0xd3, 0x1b, 0xb6, 0x1e, 0xa5, 0x79, 0x26, 0xad, 0x1e, 0xbb, - 0x62, 0xe0, 0x51, 0xa6, 0x22, 0xc7, 0x6f, 0xd6, 0x63, 0x8c, 0x6d, 0x08, 0x28, 0x43, 0xaa, 0xb7, 0x18, 0x02, 0x20, - 0x63, 0x9e, 0x8e, 0xfd, 0x71, 0xce, 0x26, 0xc8, 0x7b, 0x8d, 0x31, 0x17, 0xf1, 0x76, 0xed, 0x4f, 0xe0, 0xa1, 0x50, - 0x36, 0x12, 0xd7, 0xf2, 0x28, 0xc5, 0xb9, 0x07, 0x15, 0x9f, 0x46, 0xc4, 0xd6, 0x61, 0xea, 0x7c, 0x42, 0x18, 0xb2, - 0x07, 0x31, 0x66, 0x17, 0x26, 0x74, 0x7a, 0x89, 0xbe, 0x32, 0xbd, 0x0d, 0xa8, 0xbe, 0x15, 0x5b, 0x24, 0x9a, 0x97, - 0x44, 0xa2, 0xe8, 0xec, 0x84, 0xd8, 0x6c, 0x45, 0x8e, 0x1a, 0xab, 0xbd, 0x83, 0xc1, 0xe5, 0x33, 0x4e, 0x6b, 0xeb, - 0x0a, 0x30, 0xf9, 0x63, 0x98, 0x0a, 0x06, 0x9c, 0x1b, 0x4e, 0x2c, 0x79, 0x37, 0xa2, 0x1f, 0x7d, 0x20, 0xe3, 0xb7, - 0xd2, 0x22, 0xd8, 0xa3, 0x81, 0x18, 0xa9, 0x8a, 0x61, 0x05, 0xd3, 0x47, 0x21, 0xc1, 0x53, 0x17, 0x8e, 0xaa, 0x3a, - 0xf4, 0x0b, 0x22, 0xaa, 0x9d, 0x0b, 0xbb, 0x56, 0x0c, 0x98, 0x68, 0xcc, 0x1c, 0x1a, 0x2d, 0x5c, 0xc0, 0xf3, 0x74, - 0xfc, 0x7e, 0xe2, 0x7e, 0x76, 0xfe, 0xa0, 0x19, 0xf4, 0xbf, 0x26, 0xa3, 0xce, 0xf1, 0xd3, 0xfb, 0xdb, 0xf6, 0x69, - 0xbf, 0xb6, 0x33, 0xf7, 0x07, 0xea, 0xfb, 0x4f, 0xfc, 0xcb, 0x24, 0x80, 0xfc, 0x52, 0xc7, 0x6e, 0xc3, 0xf5, 0x53, - 0xe2, 0x35, 0x78, 0xb8, 0x7e, 0x72, 0x89, 0x50, 0xef, 0x00, 0xed, 0x8d, 0x4a, 0xdb, 0x86, 0x4b, 0x4c, 0xe2, 0x91, - 0xf2, 0x64, 0x35, 0x56, 0x64, 0x49, 0xad, 0x60, 0x65, 0x92, 0x6f, 0xe4, 0xae, 0xcf, 0x2e, 0xc1, 0x3d, 0xbe, 0x15, - 0xd9, 0x53, 0xae, 0x3e, 0x00, 0x17, 0xa5, 0xf3, 0x57, 0xcc, 0x3b, 0xff, 0x13, 0x55, 0xd6, 0x1d, 0xd4, 0x0c, 0xb5, - 0x96, 0x44, 0xad, 0x9a, 0x59, 0x5d, 0xb0, 0xb7, 0x04, 0xb4, 0xa6, 0x56, 0x1f, 0xca, 0xcd, 0x21, 0x7f, 0x6c, 0xb1, - 0x2e, 0x8c, 0x13, 0x4d, 0x93, 0x01, 0x79, 0x6a, 0x7e, 0xe9, 0x12, 0x43, 0x92, 0x41, 0xfd, 0xbf, 0xbe, 0x7b, 0x77, - 0x74, 0xd0, 0x4c, 0x5a, 0xde, 0x85, 0x3d, 0xde, 0xab, 0xa2, 0x62, 0x49, 0xe7, 0x1b, 0x7d, 0x7c, 0x90, 0x3c, 0xc9, - 0xc3, 0xf6, 0x79, 0xea, 0xcf, 0x0e, 0xfc, 0xd8, 0x80, 0xbe, 0xe3, 0x6d, 0xd3, 0x8e, 0xd2, 0xc7, 0x21, 0x04, 0x2c, - 0xd5, 0x2e, 0x68, 0x56, 0xc3, 0x23, 0x34, 0x58, 0xb7, 0x49, 0x55, 0x38, 0x8a, 0xa7, 0x1c, 0x1f, 0xfe, 0x03, 0xd0, - 0x41, 0x02, 0x3c, 0x6a, 0x2e, 0xcb, 0xc3, 0xb4, 0x03, 0x6b, 0x23, 0x6c, 0xf7, 0x26, 0x44, 0x2f, 0x16, 0x47, 0x6b, - 0xa7, 0x36, 0x61, 0x11, 0x69, 0xbc, 0x2b, 0x09, 0x5d, 0xdd, 0x07, 0xbd, 0x1e, 0x75, 0x9a, 0x35, 0x09, 0xa1, 0x9d, - 0x6c, 0xeb, 0xb8, 0x7a, 0xd0, 0xab, 0xac, 0xcf, 0x5f, 0x30, 0xfd, 0x58, 0xdf, 0xe3, 0x23, 0x86, 0xf5, 0x1b, 0xde, - 0x1f, 0x5c, 0x4a, 0x70, 0xb1, 0x69, 0xec, 0x7c, 0x33, 0x27, 0x0e, 0xbb, 0x59, 0x0a, 0x0b, 0x09, 0xa6, 0x97, 0x48, - 0x1b, 0xc6, 0x6a, 0x70, 0x7c, 0x11, 0x1f, 0xeb, 0xf9, 0x62, 0x40, 0x20, 0x52, 0xd9, 0x29, 0xf2, 0xc2, 0x00, 0x13, - 0xf5, 0xed, 0xcd, 0x87, 0xd4, 0xff, 0x10, 0xdf, 0xec, 0x03, 0xa9, 0x93, 0xe4, 0xd1, 0x8b, 0x45, 0x51, 0x24, 0x54, - 0xf4, 0x14, 0xff, 0xe2, 0x10, 0xca, 0xf0, 0x32, 0xd1, 0xd9, 0xa4, 0xe8, 0xf6, 0xce, 0x2d, 0x5f, 0x24, 0xbc, 0x71, - 0xe5, 0x72, 0xe9, 0x61, 0x60, 0xda, 0x02, 0x36, 0x50, 0x41, 0xc6, 0x31, 0x4b, 0xf1, 0x63, 0xe4, 0x2a, 0x43, 0x94, - 0xdd, 0xea, 0x31, 0xd4, 0x70, 0x11, 0x98, 0x3b, 0x94, 0x49, 0xc2, 0x68, 0xa1, 0x9e, 0xdb, 0xc0, 0xd3, 0x73, 0x66, - 0xe7, 0xe9, 0xdc, 0xde, 0xab, 0x62, 0xc7, 0x84, 0x89, 0x0c, 0x8a, 0xe8, 0xb1, 0xc2, 0x86, 0x5a, 0xcd, 0x97, 0x99, - 0x53, 0x6c, 0x7a, 0xe4, 0x0f, 0x43, 0x2d, 0x37, 0xe9, 0x96, 0x1d, 0xbd, 0xd2, 0x47, 0xfd, 0xd1, 0xa2, 0x13, 0x0c, - 0xd1, 0xe2, 0xee, 0xac, 0x8d, 0x72, 0xc4, 0x28, 0x6c, 0xde, 0x17, 0x80, 0xd9, 0xb7, 0x6e, 0x4b, 0xba, 0xfa, 0xc4, - 0xd5, 0xf7, 0xc2, 0xdc, 0xf3, 0x00, 0x62, 0x24, 0xcf, 0xe5, 0xc8, 0x45, 0x91, 0x03, 0x92, 0xbc, 0xab, 0x23, 0x2d, - 0x91, 0x8a, 0x10, 0x4e, 0x67, 0x1c, 0x0c, 0x8b, 0xd3, 0xb9, 0x6a, 0xea, 0xbb, 0x2c, 0x10, 0x09, 0x65, 0xb2, 0x9f, - 0xa2, 0x67, 0x7b, 0xa3, 0x71, 0x47, 0x87, 0xb5, 0x76, 0xeb, 0x20, 0x14, 0xae, 0x4c, 0xb5, 0x99, 0x70, 0xf7, 0x8c, - 0xfe, 0xeb, 0x8d, 0x97, 0x14, 0xab, 0x8e, 0x7b, 0xef, 0x53, 0x7d, 0xf9, 0x6b, 0x5e, 0x00, 0xf5, 0xfb, 0x81, 0xb3, - 0x21, 0x7f, 0xcb, 0x7d, 0xb0, 0x98, 0x32, 0x40, 0x80, 0x8f, 0x68, 0x86, 0x3a, 0xed, 0xab, 0xd9, 0x8d, 0x6f, 0x88, - 0xd9, 0xb3, 0xda, 0xd0, 0x77, 0x7e, 0xf8, 0xae, 0xae, 0xa0, 0xc1, 0x45, 0x65, 0xf4, 0x7f, 0x9e, 0x2a, 0x20, 0x98, - 0x0a, 0xfe, 0x3e, 0x6e, 0x87, 0xe3, 0x5b, 0xf0, 0x1c, 0x46, 0x7d, 0x1c, 0x69, 0xa2, 0x7b, 0x27, 0xee, 0x52, 0xaf, - 0x32, 0x4d, 0x32, 0xaf, 0x68, 0x97, 0x35, 0x2e, 0x58, 0xd6, 0x35, 0x5d, 0x5b, 0x76, 0xb0, 0x66, 0x5f, 0x02, 0x20, - 0x23, 0xdb, 0x9b, 0xaa, 0xa6, 0xf0, 0xeb, 0x4b, 0xb1, 0x08, 0x24, 0xf0, 0x9d, 0xb2, 0xbf, 0xba, 0x76, 0x7d, 0xd3, - 0xae, 0x16, 0xf1, 0xc1, 0xc0, 0x81, 0x50, 0xae, 0xf3, 0x86, 0x7b, 0x59, 0xa1, 0xcd, 0xf3, 0x25, 0xe7, 0xc6, 0xcb, - 0x88, 0x4a, 0x43, 0x21, 0x89, 0xda, 0x40, 0x9f, 0x8e, 0x3d, 0x0d, 0x10, 0x1e, 0x12, 0x4b, 0x1d, 0x64, 0x65, 0xfa, - 0x47, 0xd2, 0xde, 0x9b, 0x1b, 0xc3, 0xeb, 0xe1, 0x16, 0x97, 0x19, 0x45, 0x14, 0x76, 0x0c, 0x3c, 0x72, 0x2b, 0x56, - 0x7b, 0xb7, 0xfe, 0xe1, 0xc1, 0xd3, 0xbb, 0xab, 0x8f, 0x9f, 0x16, 0xb7, 0x43, 0xaa, 0xd5, 0x4f, 0xa7, 0xd6, 0xb2, - 0xe6, 0x93, 0x76, 0x98, 0xf7, 0x8e, 0x15, 0xbb, 0x84, 0x13, 0x69, 0x07, 0x31, 0x56, 0x6e, 0x26, 0x55, 0xa7, 0x09, - 0x70, 0x20, 0x35, 0x75, 0x9f, 0x3d, 0x73, 0xcd, 0x94, 0x3c, 0x36, 0xe8, 0x25, 0x51, 0x57, 0xa5, 0x11, 0x58, 0xa6, - 0xfd, 0xe3, 0xf1, 0xd2, 0x4b, 0x3d, 0x4d, 0xb0, 0x89, 0x6e, 0x18, 0x88, 0xc3, 0xdf, 0xb1, 0xc1, 0xaf, 0x67, 0xf7, - 0x64, 0x49, 0xa0, 0x70, 0x69, 0xeb, 0x86, 0x32, 0x0d, 0xda, 0x52, 0x21, 0x38, 0x2e, 0x5e, 0xdc, 0x2a, 0xc6, 0x93, - 0xac, 0xa9, 0x16, 0xc5, 0x43, 0x24, 0x1a, 0x70, 0x19, 0x5b, 0xca, 0x4d, 0xbe, 0x8d, 0x01, 0x0e, 0xd2, 0x11, 0xca, - 0xf5, 0xb2, 0x0a, 0xb0, 0xdb, 0xf0, 0xd7, 0xe3, 0x69, 0x1e, 0x80, 0x98, 0x1e, 0x1b, 0xf6, 0x74, 0x6f, 0xa3, 0xc9, - 0xad, 0xb9, 0x93, 0x12, 0x3f, 0x4a, 0xd9, 0x62, 0x4b, 0x0c, 0x83, 0x73, 0xa5, 0x13, 0x20, 0xa0, 0xe5, 0x6e, 0x09, - 0x20, 0xb5, 0x2c, 0x39, 0x89, 0x83, 0x85, 0x13, 0xd9, 0xde, 0x62, 0xbc, 0xdd, 0x93, 0x9e, 0x1a, 0xcf, 0xdc, 0x46, - 0xc6, 0xa4, 0xac, 0x7c, 0xbf, 0x20, 0x93, 0xf7, 0x79, 0x06, 0x16, 0xcd, 0x65, 0xf4, 0xf4, 0x5d, 0x71, 0x2a, 0x7e, - 0x98, 0x45, 0xe7, 0xe1, 0x69, 0xd4, 0xcd, 0x61, 0x96, 0x78, 0xc0, 0x4e, 0x38, 0xd3, 0x6a, 0x60, 0xac, 0x5d, 0xda, - 0x8e, 0xb4, 0x3b, 0xfb, 0x02, 0x29, 0x61, 0xcd, 0x6e, 0x76, 0x82, 0x63, 0x46, 0x5c, 0x0e, 0xba, 0xd7, 0x1b, 0x30, - 0xac, 0x6c, 0x17, 0x73, 0x73, 0x4f, 0xee, 0xa4, 0xd5, 0x53, 0x81, 0x9c, 0x81, 0x2c, 0x59, 0xd7, 0xf0, 0xbe, 0x40, - 0xb5, 0xbe, 0x78, 0x70, 0xf0, 0x76, 0x6f, 0x19, 0x77, 0x4d, 0x19, 0x65, 0x3b, 0xa2, 0x08, 0x7e, 0x65, 0x40, 0xba, - 0x56, 0xf6, 0x23, 0x77, 0x37, 0x4b, 0x1d, 0xa4, 0x14, 0xfa, 0x74, 0x3d, 0x5d, 0x1b, 0x09, 0x6f, 0x66, 0xaa, 0xe3, - 0x08, 0xf9, 0x44, 0x87, 0x41, 0x59, 0x49, 0x8a, 0xfe, 0x9f, 0xb1, 0xdf, 0x81, 0x82, 0x7a, 0xe0, 0xd7, 0xbf, 0x0b, - 0x12, 0x1c, 0xd8, 0x9d, 0xa0, 0xb5, 0xe2, 0x63, 0x09, 0xb2, 0x5b, 0x85, 0xb9, 0xa0, 0x44, 0xad, 0x7f, 0xcf, 0xcd, - 0xf5, 0xfa, 0xfb, 0x4b, 0x56, 0xaa, 0x75, 0x27, 0xdb, 0xb6, 0xf5, 0x4d, 0xae, 0x19, 0x3b, 0x62, 0x5f, 0x0e, 0x7c, - 0x70, 0x9a, 0xc9, 0xb5, 0x80, 0xa4, 0x21, 0xd3, 0xe7, 0x6e, 0x8d, 0xba, 0x91, 0x8c, 0xdc, 0x01, 0x09, 0x44, 0x08, - 0x34, 0x18, 0x94, 0x75, 0xbb, 0x2f, 0xc6, 0xe1, 0xbc, 0xb3, 0x78, 0xff, 0x73, 0x41, 0x97, 0xe8, 0xb0, 0xae, 0xce, - 0x62, 0xc9, 0x7f, 0xbf, 0x53, 0x8c, 0x64, 0x7b, 0xb4, 0xbd, 0x7f, 0x31, 0x9a, 0xe2, 0x4a, 0xa6, 0xfd, 0x83, 0xbf, - 0xbf, 0xe8, 0x2d, 0xbc, 0xd9, 0xd1, 0xc1, 0xea, 0x3e, 0x4f, 0xb9, 0x79, 0xd2, 0x17, 0x33, 0xf9, 0xae, 0x8c, 0x4c, - 0x6e, 0x10, 0x18, 0x75, 0x67, 0x75, 0xa9, 0xcb, 0xc3, 0xc8, 0xc5, 0x7a, 0xb8, 0x19, 0x4e, 0xe1, 0x36, 0x13, 0x59, - 0xb5, 0x50, 0x05, 0xe0, 0x11, 0x3a, 0x29, 0x51, 0x24, 0x49, 0x62, 0x80, 0xe8, 0xde, 0xc6, 0x3c, 0x80, 0x17, 0x35, - 0x9f, 0x35, 0xd4, 0x76, 0x46, 0x36, 0xc7, 0x01, 0xad, 0xcd, 0x1c, 0xd3, 0xb2, 0x29, 0x41, 0xe7, 0xee, 0x74, 0x84, - 0x0e, 0xbd, 0xa5, 0xf5, 0x54, 0x97, 0x8a, 0x7d, 0xd3, 0xb3, 0xb6, 0x8e, 0xc8, 0x27, 0x71, 0x6b, 0x75, 0x90, 0xe6, - 0x2a, 0xa7, 0xe2, 0x66, 0xaa, 0x9e, 0xa3, 0x77, 0x16, 0x8e, 0x40, 0xdf, 0x5a, 0x1e, 0xac, 0x71, 0x11, 0x6c, 0x8a, - 0xee, 0x2c, 0x15, 0x55, 0xc5, 0x96, 0xfb, 0x9d, 0x4c, 0xa7, 0xed, 0x9d, 0x01, 0xb2, 0x4e, 0x98, 0xee, 0x1e, 0x12, - 0x48, 0x3c, 0x7a, 0x17, 0x60, 0xb0, 0x67, 0x52, 0x4c, 0xab, 0xea, 0xbc, 0x62, 0x82, 0x87, 0xa5, 0x3c, 0xf3, 0xbd, - 0xd9, 0xb3, 0xc3, 0xa8, 0x61, 0x3c, 0x76, 0xf8, 0x05, 0x25, 0x05, 0xb3, 0x37, 0xd1, 0xcd, 0xdf, 0xcb, 0xd7, 0xf5, - 0x09, 0xb7, 0x46, 0x0e, 0xb9, 0x75, 0x07, 0xd7, 0xef, 0xf4, 0x7d, 0xe6, 0x62, 0x56, 0xdf, 0x7e, 0x36, 0x2e, 0x66, - 0x46, 0xc9, 0x77, 0x6a, 0xa4, 0x3d, 0x24, 0xde, 0x6b, 0x00, 0xb6, 0x00, 0xca, 0x92, 0x09, 0xe8, 0x60, 0xb5, 0x2e, - 0x97, 0x4e, 0xd7, 0x29, 0x69, 0xaa, 0x3d, 0xf3, 0x6a, 0xbb, 0xb1, 0xcd, 0x85, 0x67, 0x4c, 0xb6, 0x58, 0x9b, 0x4e, - 0x5b, 0xc2, 0xe4, 0xb5, 0xae, 0xdf, 0xe8, 0xfa, 0x97, 0x15, 0x81, 0x9a, 0xa9, 0x5c, 0x71, 0x5f, 0x2b, 0x6b, 0x08, - 0x3e, 0x85, 0x45, 0x98, 0x0a, 0xf0, 0xac, 0x3a, 0x81, 0xaa, 0xf5, 0x43, 0xdb, 0xfb, 0x1b, 0x16, 0xdb, 0xb3, 0x9d, - 0x76, 0x01, 0x14, 0x9e, 0x25, 0xee, 0x9c, 0x2b, 0x77, 0x74, 0xe3, 0x74, 0x13, 0x53, 0xf6, 0xe3, 0x17, 0xa8, 0x93, - 0x83, 0x99, 0xdb, 0x0b, 0x1a, 0x4b, 0xc0, 0x93, 0x22, 0x13, 0xc4, 0xe4, 0xe7, 0x40, 0x08, 0x6d, 0xa4, 0xd2, 0x99, - 0x86, 0xc7, 0x68, 0xf7, 0x5a, 0x29, 0x0b, 0xa6, 0x76, 0xef, 0xc9, 0x6d, 0xd8, 0x74, 0x04, 0xaa, 0xb3, 0xef, 0xa4, - 0xdc, 0xa8, 0x97, 0x30, 0x02, 0x3a, 0xdd, 0x6b, 0xf5, 0xe3, 0x9f, 0x93, 0xb9, 0x86, 0x7d, 0x64, 0xc7, 0x1b, 0xd1, - 0x0d, 0x40, 0x0e, 0x87, 0x4b, 0x28, 0x65, 0xed, 0x93, 0xea, 0xdf, 0xfb, 0xb2, 0xd1, 0xf0, 0x09, 0xc3, 0x38, 0x49, - 0x54, 0x71, 0xda, 0xe6, 0xd6, 0x40, 0xe9, 0x4f, 0xee, 0x9d, 0x12, 0xa6, 0x20, 0x10, 0x4d, 0xb2, 0x72, 0x3e, 0x05, - 0x2c, 0x3c, 0xe5, 0x81, 0x4a, 0x98, 0x46, 0x2c, 0xd1, 0x0e, 0xad, 0x34, 0x1b, 0x5d, 0x82, 0x19, 0x06, 0x5c, 0xfb, - 0x0b, 0x8d, 0xd6, 0x9d, 0xec, 0xad, 0xa3, 0x06, 0x6f, 0xd0, 0xd2, 0xe8, 0x77, 0x43, 0x93, 0x00, 0x84, 0x9c, 0x1e, - 0xdc, 0xf7, 0x2c, 0x46, 0xc7, 0x15, 0x9d, 0x47, 0x0f, 0x64, 0x22, 0xd4, 0x94, 0xeb, 0x4e, 0x0e, 0x80, 0x39, 0xdd, - 0x72, 0x69, 0xa8, 0xc7, 0x60, 0x9a, 0x5e, 0x40, 0x54, 0xc0, 0x8a, 0x0e, 0xa0, 0xd3, 0xd8, 0x0d, 0x65, 0x79, 0xc3, - 0x0c, 0x05, 0x08, 0x82, 0xec, 0x1b, 0x84, 0xfd, 0xa9, 0x3a, 0xe6, 0x6a, 0x86, 0x5d, 0x86, 0x19, 0x1c, 0x87, 0x86, - 0xf6, 0x14, 0xfc, 0x04, 0x6c, 0xa2, 0x09, 0x08, 0x50, 0x6e, 0x12, 0x62, 0x0f, 0x6a, 0xfe, 0x2b, 0x0f, 0x49, 0x7b, - 0xdd, 0x34, 0xf5, 0x09, 0xa6, 0x38, 0x2c, 0x83, 0x75, 0x5b, 0xb6, 0x57, 0xb7, 0xaa, 0x8c, 0xe3, 0x1a, 0x60, 0x6c, - 0xe9, 0x1c, 0x47, 0x61, 0x1a, 0xe2, 0x7f, 0x0d, 0xa8, 0x0b, 0x73, 0xab, 0x76, 0x13, 0xfa, 0x66, 0x49, 0x53, 0x3e, - 0x9a, 0xdc, 0x1f, 0x1b, 0x9a, 0x13, 0xfd, 0xbe, 0xc0, 0x8c, 0x6b, 0x89, 0x4b, 0x16, 0x7a, 0xda, 0x06, 0x65, 0xa7, - 0x6b, 0x5c, 0x65, 0xfc, 0x6a, 0xf4, 0xcb, 0xb7, 0xab, 0x57, 0x31, 0xc4, 0x8c, 0x28, 0x60, 0x6b, 0xde, 0x59, 0xc7, - 0x27, 0xda, 0x7d, 0x37, 0xe6, 0x97, 0xa7, 0xa8, 0x71, 0xa3, 0x94, 0x58, 0x2c, 0x92, 0xf7, 0x15, 0x6e, 0x6b, 0x3e, - 0xd8, 0x5e, 0xf9, 0xf8, 0xeb, 0x79, 0x28, 0x04, 0x4f, 0xa8, 0x92, 0x20, 0xd1, 0x40, 0x37, 0xad, 0xd7, 0x82, 0x96, - 0xde, 0x97, 0x94, 0x66, 0x9e, 0xfb, 0xcb, 0xa6, 0x5d, 0x02, 0x42, 0x55, 0x03, 0x39, 0x3f, 0x45, 0x93, 0x2f, 0xa6, - 0xd3, 0x31, 0x82, 0x4f, 0x9a, 0x9c, 0x23, 0x0c, 0xa7, 0xdd, 0x7e, 0x97, 0x9f, 0xfe, 0x26, 0x47, 0x07, 0x9e, 0xfb, - 0x49, 0xea, 0xdb, 0xa1, 0xf0, 0xe9, 0x77, 0xbd, 0x18, 0x7d, 0xf5, 0x8d, 0x90, 0xbe, 0xed, 0xc4, 0xc6, 0x41, 0x90, - 0x37, 0xf2, 0x42, 0x84, 0x08, 0x76, 0x49, 0x20, 0xcc, 0x65, 0xfd, 0x46, 0xbc, 0x85, 0xaf, 0xe8, 0x2d, 0x35, 0x47, - 0x4f, 0xa3, 0x03, 0x3d, 0x9c, 0xb0, 0xbe, 0x8b, 0x0f, 0xa3, 0x2f, 0xb0, 0xe6, 0xe1, 0xb3, 0x00, 0x90, 0x8e, 0x61, - 0x15, 0xc0, 0xda, 0x60, 0xee, 0x18, 0x6e, 0xeb, 0xf4, 0xc4, 0x5a, 0xe6, 0x00, 0xbb, 0xdc, 0xc9, 0x71, 0x43, 0x77, - 0x0e, 0x41, 0xc1, 0xbc, 0x1d, 0x58, 0x23, 0xff, 0xcf, 0xb4, 0xa1, 0x3b, 0xab, 0x98, 0x58, 0x06, 0x22, 0xcd, 0x11, - 0x09, 0x0d, 0x5f, 0x77, 0x2f, 0x02, 0x07, 0xf0, 0x11, 0x83, 0xaf, 0x41, 0xc5, 0xf3, 0xdc, 0xe4, 0x57, 0xf5, 0xf3, - 0x4b, 0xc4, 0xde, 0x14, 0xaf, 0xeb, 0xa9, 0xbb, 0x2b, 0x0f, 0x7f, 0xa7, 0x14, 0x00, 0xbd, 0x54, 0x76, 0x15, 0x98, - 0xc9, 0xc1, 0x26, 0x32, 0xfc, 0x5c, 0x2f, 0xa1, 0x32, 0x6d, 0xf6, 0x84, 0x10, 0x2e, 0x48, 0x39, 0xb9, 0x1e, 0x2f, - 0x46, 0x7e, 0x02, 0x2a, 0x02, 0x6d, 0x02, 0xc8, 0xb2, 0x3f, 0x82, 0x45, 0xca, 0x01, 0x81, 0x78, 0x17, 0x17, 0x7d, - 0xea, 0x2d, 0x0d, 0x92, 0x98, 0xdd, 0x4b, 0x11, 0x30, 0x88, 0xcb, 0x84, 0x12, 0x34, 0x6c, 0x4d, 0xd9, 0xb7, 0x10, - 0xb9, 0x23, 0x74, 0xd8, 0x11, 0x66, 0x06, 0x5d, 0x25, 0xf2, 0x1f, 0x1d, 0x2d, 0xa9, 0x82, 0x87, 0xe9, 0xc9, 0xb3, - 0x15, 0xcd, 0x86, 0x93, 0x06, 0x12, 0x12, 0xf8, 0x50, 0x88, 0x03, 0x1b, 0x6f, 0x48, 0xa2, 0x60, 0x3d, 0x48, 0xfe, - 0x74, 0xd9, 0xf2, 0xba, 0xf3, 0x2c, 0x3b, 0xbe, 0x63, 0x68, 0x0e, 0x79, 0x8c, 0x44, 0x11, 0x94, 0xc2, 0xef, 0xa0, - 0xa4, 0x45, 0xa6, 0x12, 0x50, 0xae, 0x15, 0xd9, 0xe1, 0xa9, 0x2a, 0x31, 0x01, 0x5a, 0xa0, 0x07, 0xd1, 0xbd, 0xcb, - 0x42, 0xd3, 0x74, 0xf0, 0xda, 0xa1, 0x61, 0x2c, 0x83, 0xa9, 0x0e, 0x2e, 0x5b, 0xa1, 0x38, 0x32, 0xe9, 0x90, 0x51, - 0x70, 0x72, 0xeb, 0xac, 0xcb, 0x26, 0x37, 0xbf, 0xbd, 0x57, 0x74, 0x74, 0x0c, 0x64, 0x75, 0x9e, 0xde, 0x3c, 0xcf, - 0x66, 0x08, 0x06, 0xe9, 0xe3, 0x69, 0x57, 0xf1, 0x72, 0x9a, 0x81, 0x69, 0xd5, 0xf6, 0x6d, 0x19, 0x2e, 0x37, 0xb1, - 0xe4, 0xaa, 0xdb, 0x87, 0xb9, 0xcc, 0x67, 0xa7, 0x93, 0xec, 0xb7, 0xd6, 0xe0, 0xc8, 0x69, 0x66, 0xfd, 0x46, 0xf9, - 0x3c, 0x3f, 0x32, 0x95, 0x6f, 0x0e, 0x93, 0x44, 0x6a, 0xd7, 0x50, 0xad, 0xc2, 0x0c, 0x3f, 0x19, 0x44, 0xbd, 0x06, - 0x54, 0x1b, 0xad, 0x18, 0x6e, 0xe0, 0xc5, 0xe6, 0xc9, 0xd2, 0x75, 0xcc, 0x8c, 0xab, 0xc0, 0x2c, 0x26, 0x95, 0x18, - 0xdf, 0x8b, 0x04, 0x19, 0xb4, 0xa7, 0xfb, 0x54, 0x34, 0xd6, 0x97, 0x40, 0x27, 0x8b, 0x68, 0x03, 0x06, 0xe6, 0x10, - 0xea, 0x58, 0xa0, 0xb1, 0x71, 0x98, 0x45, 0x6d, 0x2b, 0x33, 0x9a, 0x2a, 0x83, 0x61, 0x0c, 0xb5, 0x01, 0x5c, 0xdd, - 0xaa, 0x85, 0x94, 0x8c, 0xb2, 0xee, 0x32, 0x1a, 0x28, 0xa1, 0x63, 0x19, 0x2b, 0x3c, 0x52, 0x32, 0x5c, 0x19, 0x71, - 0x1a, 0xe0, 0xcb, 0xd3, 0xff, 0xf7, 0x27, 0x32, 0x6a, 0xe9, 0xee, 0x48, 0xde, 0xb9, 0xec, 0xe8, 0x4a, 0x33, 0x9e, - 0xa7, 0xcb, 0xe9, 0x8b, 0xd4, 0x6d, 0xa9, 0x96, 0xf9, 0xc3, 0xe8, 0x18, 0x85, 0xaf, 0xed, 0xdb, 0x6d, 0x25, 0x1a, - 0xce, 0x30, 0x62, 0xae, 0x7c, 0xe3, 0x16, 0xf5, 0x5a, 0x8a, 0xee, 0xb7, 0x8c, 0x9c, 0x4a, 0x75, 0xc7, 0x1b, 0x97, - 0x1a, 0x5e, 0x29, 0xfd, 0x87, 0x79, 0x5e, 0xcc, 0xb1, 0xdd, 0xf6, 0x71, 0xb5, 0xb2, 0xcb, 0x89, 0xce, 0x9b, 0xe7, - 0x24, 0xe1, 0x6d, 0x8f, 0x63, 0x2b, 0x91, 0xe2, 0xb1, 0x34, 0x7f, 0x57, 0x4d, 0xb6, 0x9b, 0x5f, 0x7d, 0x2e, 0xc8, - 0x5a, 0x4c, 0xba, 0xd5, 0x56, 0xa5, 0x35, 0xf3, 0xe0, 0xdd, 0x9e, 0x39, 0xd2, 0x53, 0x12, 0x34, 0xdc, 0x2e, 0xe4, - 0xd3, 0x20, 0xd2, 0x5b, 0x66, 0x74, 0x58, 0x93, 0x57, 0xbe, 0xb1, 0x09, 0x0e, 0xe1, 0x78, 0x6b, 0x34, 0x0f, 0x93, - 0x9d, 0x4e, 0xea, 0xc5, 0xff, 0x33, 0x5b, 0xf8, 0x36, 0x75, 0xf2, 0x57, 0x5c, 0xe9, 0x20, 0xc5, 0xc5, 0x14, 0x1f, - 0x8e, 0x11, 0xcc, 0x97, 0xf4, 0x1d, 0x0a, 0x8f, 0xa6, 0x9c, 0x06, 0x06, 0x21, 0x66, 0xcf, 0xbe, 0x73, 0xf7, 0x76, - 0xbc, 0x25, 0xce, 0xa8, 0x2c, 0x6b, 0x8a, 0x25, 0x18, 0xa4, 0x79, 0x1d, 0x10, 0x00, 0x57, 0x2e, 0x88, 0x5d, 0x81, - 0xc8, 0x96, 0x17, 0xd1, 0xe2, 0xdd, 0x2f, 0x4b, 0xa3, 0xb8, 0x29, 0xf1, 0x99, 0xec, 0xf6, 0x44, 0x30, 0xc0, 0x2d, - 0xc8, 0x0e, 0xc7, 0x0e, 0x16, 0xc4, 0x1c, 0x09, 0xde, 0x15, 0x65, 0x58, 0x92, 0x3a, 0x50, 0x2c, 0x5a, 0xd4, 0x05, - 0x13, 0x13, 0x29, 0x64, 0x6b, 0xab, 0x84, 0x00, 0x69, 0xa2, 0xf6, 0x12, 0x58, 0xd4, 0x34, 0x7b, 0xa2, 0xea, 0x62, - 0x92, 0xbb, 0x21, 0xf7, 0x34, 0x1e, 0xfc, 0x3c, 0x94, 0xcc, 0xb1, 0x37, 0x89, 0x90, 0x7f, 0xbd, 0xd9, 0xfa, 0x05, - 0xf6, 0x0e, 0x7e, 0xd1, 0x10, 0xbe, 0x9a, 0xc2, 0x1a, 0x92, 0x30, 0xab, 0x5c, 0x78, 0xab, 0x24, 0x40, 0x81, 0xb2, - 0xac, 0x4f, 0x8b, 0x03, 0x46, 0x1f, 0x0a, 0xca, 0x16, 0xcb, 0x39, 0x89, 0xd9, 0x71, 0x11, 0x5b, 0x72, 0x2f, 0xfa, - 0xfc, 0xfc, 0x65, 0x1c, 0xef, 0x11, 0x81, 0xca, 0xad, 0xf2, 0xfe, 0x48, 0xc9, 0x01, 0x03, 0x33, 0xfd, 0x29, 0x8d, - 0xe8, 0xdc, 0x7f, 0x3f, 0xd5, 0x0f, 0x39, 0xf0, 0x4b, 0xe0, 0x38, 0xd0, 0xa7, 0x2c, 0x5a, 0xce, 0x06, 0xaa, 0x7b, - 0x9c, 0x53, 0xfb, 0x58, 0x5c, 0x22, 0xae, 0x4c, 0x42, 0xa0, 0xbb, 0x5c, 0x48, 0x82, 0xc5, 0xa7, 0x60, 0x48, 0x36, - 0x01, 0xd3, 0x58, 0x61, 0x73, 0xad, 0x79, 0x77, 0x80, 0x2e, 0x36, 0x58, 0xc1, 0x2b, 0x1c, 0x0c, 0xbd, 0xb6, 0x66, - 0x4e, 0x6b, 0x35, 0xbd, 0x13, 0x25, 0x89, 0x6c, 0xb2, 0xdb, 0x8f, 0x23, 0x7b, 0x43, 0x1a, 0x22, 0xfc, 0xb9, 0x51, - 0x5a, 0x14, 0x8a, 0xe6, 0x6a, 0x85, 0x88, 0x7d, 0xaf, 0x52, 0x4e, 0x32, 0xa9, 0x5a, 0x6a, 0x72, 0x5b, 0x29, 0x21, - 0xd6, 0x85, 0x7f, 0x14, 0xd4, 0xcd, 0xa8, 0x3f, 0x25, 0x37, 0xd0, 0x37, 0xe2, 0x4d, 0x02, 0x6f, 0xac, 0xf5, 0x21, - 0x28, 0x9a, 0x68, 0xfc, 0x08, 0x8a, 0xc5, 0xc1, 0x04, 0x4f, 0x3c, 0x97, 0x61, 0x09, 0x48, 0xa7, 0x78, 0xe8, 0xc5, - 0x84, 0x8b, 0x40, 0x0b, 0xce, 0x59, 0xbe, 0x7b, 0xa7, 0x79, 0xa0, 0xd3, 0x7a, 0x21, 0x89, 0xd9, 0x5e, 0x75, 0xba, - 0xf4, 0x8a, 0x81, 0xf3, 0xeb, 0x4c, 0x59, 0x22, 0xee, 0x49, 0x5e, 0x6e, 0x37, 0x96, 0xd1, 0xc6, 0x22, 0xe6, 0x74, - 0xa6, 0x82, 0x3f, 0x9b, 0x7a, 0x5b, 0x27, 0x16, 0xbf, 0x1d, 0x4f, 0xad, 0x8c, 0xd7, 0x01, 0xee, 0x12, 0x6f, 0xca, - 0xa0, 0x54, 0xc4, 0xeb, 0x41, 0x84, 0x48, 0x90, 0x12, 0x9d, 0x46, 0x86, 0xd2, 0x63, 0x3f, 0x48, 0xcc, 0x06, 0xd4, - 0x88, 0x1d, 0xd8, 0x39, 0xca, 0x6e, 0x85, 0x9f, 0xfb, 0xbb, 0xf5, 0xf0, 0x7b, 0x95, 0x3e, 0xe9, 0x2d, 0xcc, 0x4a, - 0xf3, 0xa4, 0x1a, 0x56, 0x60, 0xd9, 0x71, 0xfb, 0x97, 0x67, 0xae, 0xc2, 0xe0, 0xdc, 0x56, 0xc3, 0x9d, 0x3e, 0x97, - 0xef, 0x2f, 0xe0, 0xef, 0x4f, 0xbf, 0xaf, 0xf9, 0x02, 0xb3, 0x8e, 0x94, 0x50, 0xd7, 0x6e, 0x23, 0x22, 0xee, 0xc5, - 0xab, 0xab, 0x14, 0x5a, 0x80, 0x2c, 0xbf, 0x80, 0x67, 0xc7, 0xb7, 0x46, 0xba, 0x4f, 0x8e, 0x54, 0x20, 0x11, 0xf2, - 0x56, 0x41, 0x58, 0x09, 0x38, 0x52, 0xd8, 0x2c, 0xb2, 0xa0, 0x4f, 0x25, 0x74, 0x0d, 0x3f, 0x25, 0xbe, 0xbc, 0x9a, - 0x2b, 0x7e, 0x0c, 0xe9, 0x04, 0x74, 0xd8, 0x9d, 0x0f, 0x22, 0xb0, 0x41, 0xce, 0x7a, 0xc9, 0x68, 0xde, 0xc9, 0x67, - 0xa3, 0xc8, 0xb4, 0x63, 0xa5, 0xfd, 0xda, 0xa8, 0xdb, 0x3e, 0x1e, 0xdf, 0x29, 0x06, 0x3c, 0x38, 0x6c, 0x6e, 0x37, - 0x69, 0x20, 0x6f, 0xd5, 0xde, 0xfb, 0x7a, 0xc7, 0xb5, 0x5d, 0x90, 0x7c, 0xb2, 0xb4, 0x83, 0x28, 0xa4, 0xdb, 0x8d, - 0x9c, 0x2a, 0xea, 0x17, 0x45, 0xfb, 0x22, 0x2d, 0xef, 0xee, 0x12, 0x8f, 0x7b, 0xf5, 0x24, 0x4e, 0x2e, 0x8e, 0x73, - 0x4d, 0x25, 0xe2, 0xc8, 0x97, 0xa8, 0x2f, 0x4f, 0xd1, 0x66, 0x5a, 0x53, 0x83, 0xab, 0x5c, 0xab, 0xa6, 0x44, 0x5e, - 0x8a, 0x9e, 0xd9, 0xcd, 0xe2, 0xaf, 0xb8, 0xa6, 0x42, 0x33, 0xe0, 0xfc, 0x59, 0xfb, 0xe6, 0xcf, 0x04, 0x0f, 0x2e, - 0x8b, 0x7f, 0x94, 0xfe, 0x97, 0x53, 0x4f, 0x64, 0xf9, 0x05, 0xfe, 0x6a, 0xbc, 0x59, 0xbc, 0xf9, 0xef, 0x2e, 0x72, - 0x9f, 0x2f, 0xd8, 0x51, 0xb0, 0xde, 0x5e, 0x8e, 0x2f, 0x72, 0x7d, 0x39, 0x49, 0x7c, 0x83, 0x30, 0x80, 0xd3, 0x21, - 0x2d, 0xeb, 0x9d, 0xd6, 0x04, 0x9f, 0x81, 0x80, 0x90, 0x6c, 0xe7, 0xec, 0xc4, 0xd6, 0x1f, 0x49, 0xb4, 0x19, 0xcc, - 0xe4, 0x65, 0x90, 0xec, 0x6b, 0x24, 0x00, 0x72, 0x6a, 0x33, 0xd2, 0x71, 0x3e, 0x6d, 0x02, 0x6d, 0x32, 0x49, 0xdd, - 0x6e, 0x01, 0x5c, 0x80, 0x54, 0x94, 0xaf, 0xd6, 0x4d, 0x14, 0x35, 0xf3, 0x2a, 0x14, 0x5f, 0xed, 0xf5, 0x0b, 0xb4, - 0x53, 0x4d, 0x7b, 0x35, 0xd7, 0x81, 0xc0, 0x7a, 0xd5, 0x21, 0x42, 0x4b, 0xb6, 0x82, 0x1e, 0x7f, 0x4f, 0x7c, 0xb7, - 0xf9, 0x80, 0x7a, 0x83, 0xe5, 0x6e, 0xaf, 0x39, 0x9d, 0xda, 0xcd, 0x90, 0x1a, 0xf4, 0x19, 0x54, 0x51, 0xb0, 0x04, - 0xbc, 0xfd, 0xcc, 0xee, 0x66, 0x4b, 0x45, 0x36, 0xb7, 0xf8, 0xe2, 0x60, 0x5b, 0x24, 0x90, 0x8e, 0x23, 0x60, 0x4d, - 0x66, 0xa4, 0x24, 0xb3, 0x53, 0x9a, 0x32, 0x0a, 0x40, 0x06, 0xf0, 0x62, 0x12, 0xf6, 0x98, 0xf4, 0xdf, 0x87, 0x57, - 0x69, 0xfd, 0xd5, 0xfb, 0x62, 0xe4, 0x3d, 0xff, 0x68, 0x7a, 0xe0, 0xf4, 0x7b, 0x6b, 0x97, 0xb3, 0xd7, 0xa9, 0x47, - 0x8d, 0x25, 0xdf, 0x38, 0x80, 0xff, 0x84, 0xa7, 0xce, 0xea, 0x30, 0xdf, 0x8e, 0x9c, 0x52, 0xe5, 0xca, 0xbd, 0x0a, - 0xee, 0xf7, 0x07, 0xe1, 0xfe, 0xff, 0x55, 0xed, 0x66, 0xf8, 0xf9, 0xdf, 0xd6, 0xf0, 0x7f, 0xd9, 0x75, 0x58, 0x6b, - 0xee, 0x7f, 0x6b, 0x70, 0xe9, 0x77, 0x14, 0xd4, 0x75, 0xed, 0xdf, 0x79, 0x10, 0x68, 0x05, 0x5e, 0x17, 0x77, 0x26, - 0x2b, 0x3d, 0x4d, 0xe9, 0xc1, 0x20, 0x3a, 0xf8, 0xff, 0xb3, 0x6c, 0x8e, 0xbd, 0x38, 0x61, 0xb2, 0xb2, 0xfd, 0x7e, - 0x1a, 0x0b, 0xb0, 0x9c, 0x44, 0x69, 0xe3, 0x80, 0xf7, 0x95, 0x1f, 0xd7, 0xe8, 0xe7, 0x80, 0x4e, 0xac, 0x53, 0x40, - 0xdf, 0xd5, 0xf4, 0x09, 0xe2, 0xb1, 0x87, 0xd7, 0x90, 0x7b, 0x47, 0x70, 0x5f, 0x6b, 0x1c, 0x2e, 0x28, 0x3f, 0x14, - 0x77, 0x72, 0x31, 0x95, 0xfc, 0x52, 0xfa, 0xbd, 0x66, 0xf7, 0x45, 0x29, 0x37, 0xc4, 0x50, 0x93, 0x5b, 0x7f, 0x13, - 0x21, 0xdd, 0x2b, 0x92, 0xc8, 0x6a, 0x51, 0x77, 0xae, 0x92, 0x4e, 0x9c, 0xdd, 0xb3, 0xad, 0xca, 0x0c, 0xc0, 0x8b, - 0x2a, 0x97, 0x92, 0xb7, 0xeb, 0x40, 0x19, 0xd3, 0x7b, 0xec, 0x7c, 0x1d, 0x0d, 0xa8, 0xa5, 0xf4, 0x55, 0xde, 0xf0, - 0xef, 0xfc, 0x02, 0xf3, 0xae, 0xc6, 0xba, 0xf1, 0xc4, 0x3e, 0x1a, 0xda, 0x4d, 0xe3, 0x3e, 0x42, 0x40, 0x1d, 0x6e, - 0xc8, 0xa9, 0x46, 0xa2, 0x6b, 0x3e, 0xcb, 0xc3, 0x88, 0xb2, 0x91, 0xb7, 0xe0, 0x4c, 0x9c, 0xb3, 0x4e, 0x41, 0x86, - 0x99, 0xa1, 0x61, 0x05, 0x4d, 0x6f, 0x31, 0xc6, 0xf6, 0xf2, 0x8e, 0xef, 0x8e, 0xb2, 0xb5, 0xfd, 0xfa, 0xcb, 0x02, - 0x81, 0x74, 0x5c, 0x04, 0xef, 0x14, 0x5f, 0xe0, 0x91, 0x34, 0x32, 0xf5, 0x60, 0xdf, 0x5f, 0xd2, 0x8b, 0xb0, 0xff, - 0xd6, 0x9c, 0x26, 0x97, 0x2f, 0xe7, 0x4c, 0x69, 0x51, 0xe7, 0x6c, 0xf1, 0xe2, 0xb6, 0x71, 0xff, 0xd3, 0xe4, 0xda, - 0x98, 0xf5, 0xe7, 0x9c, 0x14, 0x15, 0x4e, 0xb4, 0xce, 0xe6, 0x62, 0xef, 0xbd, 0xe7, 0xbc, 0x1e, 0x2c, 0xbb, 0x07, - 0x67, 0x32, 0x62, 0xeb, 0xad, 0x2e, 0xbc, 0x64, 0xdf, 0x26, 0x6e, 0x9b, 0x0a, 0xae, 0x29, 0x1e, 0xbc, 0x7a, 0x99, - 0xde, 0x5d, 0x9d, 0x2c, 0x59, 0xac, 0x59, 0x7e, 0x34, 0xa0, 0x9b, 0x7d, 0x58, 0xb7, 0x8d, 0xc6, 0xf1, 0x9a, 0x4a, - 0x6c, 0x9b, 0x58, 0xc6, 0xac, 0xa6, 0x13, 0xc1, 0xfd, 0x5f, 0x36, 0xb8, 0x76, 0xa6, 0x0e, 0xc5, 0xb5, 0x19, 0x85, - 0x52, 0xf0, 0xa3, 0x04, 0x24, 0x5c, 0x32, 0x96, 0x0c, 0x9c, 0xe0, 0xdb, 0x39, 0x9d, 0x4c, 0x99, 0xa6, 0xe1, 0x74, - 0xf3, 0xc3, 0x69, 0x07, 0xdf, 0x76, 0x12, 0x23, 0x20, 0xb9, 0x1f, 0x19, 0xb9, 0xc1, 0x64, 0x49, 0xa8, 0x11, 0x77, - 0xeb, 0xe4, 0x17, 0x74, 0xcc, 0x64, 0x89, 0xa7, 0x52, 0x93, 0x87, 0xf3, 0xf1, 0x09, 0xfb, 0xf9, 0x67, 0xeb, 0x6f, - 0xd6, 0x37, 0xed, 0x2a, 0xdc, 0x28, 0xeb, 0xd4, 0x7e, 0x86, 0x3d, 0xab, 0x12, 0x42, 0xde, 0x94, 0xf7, 0xf6, 0x96, - 0xda, 0xa7, 0xdf, 0x37, 0x22, 0xb8, 0xfa, 0xce, 0xd0, 0xea, 0xcf, 0x29, 0x82, 0xe5, 0x6e, 0xd7, 0x4a, 0xa5, 0xc9, - 0xea, 0x89, 0xef, 0xfd, 0x1a, 0x6f, 0x45, 0xce, 0x83, 0x97, 0xec, 0x8d, 0x38, 0x7f, 0x2c, 0x8a, 0xef, 0x9e, 0x3f, - 0x22, 0x16, 0x97, 0x77, 0x73, 0x0c, 0xb1, 0xc9, 0xe5, 0x21, 0x95, 0xc4, 0x34, 0xf8, 0x04, 0x6a, 0x3b, 0xab, 0xa1, - 0x44, 0x57, 0x52, 0xab, 0x2b, 0xbe, 0x94, 0x02, 0x96, 0xca, 0xa8, 0x92, 0xa1, 0xae, 0x0e, 0xa7, 0x8e, 0xd2, 0xf2, - 0xc3, 0xab, 0x0a, 0x2e, 0x95, 0x79, 0x68, 0x69, 0x0c, 0xbf, 0xf4, 0xe9, 0x05, 0x63, 0x18, 0xd9, 0x66, 0x03, 0x97, - 0xa7, 0xe8, 0x44, 0xef, 0xe0, 0x0b, 0xa1, 0xe3, 0x43, 0x33, 0xf9, 0x02, 0x8d, 0xd7, 0x50, 0x92, 0xe1, 0x90, 0x73, - 0x55, 0xdc, 0xa2, 0x96, 0xb8, 0x7f, 0x45, 0xd6, 0x53, 0x4d, 0x69, 0xd1, 0x9e, 0x96, 0xa1, 0xa0, 0xb4, 0x4e, 0x72, - 0x5d, 0x61, 0x7a, 0x99, 0x74, 0x52, 0x4f, 0x6b, 0x70, 0x2b, 0x77, 0x8e, 0x44, 0x66, 0xf7, 0x4d, 0xd3, 0x91, 0x42, - 0x6e, 0x57, 0x40, 0xd2, 0xae, 0x3f, 0x8f, 0x55, 0xc8, 0x3e, 0x24, 0x4d, 0x2d, 0xe9, 0xfe, 0xcc, 0x0e, 0x84, 0x96, - 0xf7, 0xdd, 0xd8, 0xa9, 0x23, 0xdd, 0x83, 0x60, 0xec, 0xfc, 0x56, 0x69, 0x37, 0xcd, 0x49, 0xbc, 0x71, 0xf1, 0x6b, - 0x8f, 0x02, 0xb2, 0xa5, 0x19, 0x7c, 0x6d, 0x4d, 0x40, 0xbb, 0x7c, 0x03, 0x6b, 0x2d, 0x76, 0x30, 0xde, 0xe7, 0x6d, - 0xe8, 0xa1, 0x0f, 0x6c, 0x94, 0x6a, 0x1e, 0x7e, 0xa3, 0x54, 0xfd, 0x8e, 0x9c, 0x42, 0xd5, 0x73, 0xfe, 0xba, 0x24, - 0x8e, 0x8d, 0xac, 0xb6, 0x8b, 0x23, 0x06, 0x1b, 0x18, 0xe3, 0x50, 0x5f, 0x20, 0x62, 0xde, 0x31, 0x32, 0x40, 0x87, - 0xa4, 0x43, 0x59, 0x27, 0xd3, 0x44, 0x42, 0xcc, 0x03, 0x12, 0xec, 0x1d, 0x40, 0x3d, 0x80, 0xff, 0xc9, 0xa4, 0x45, - 0xfe, 0xa9, 0x5d, 0xe5, 0xdc, 0x31, 0xc7, 0x5f, 0x6a, 0x76, 0xcd, 0x46, 0x99, 0xd5, 0xd4, 0xe0, 0x7e, 0xd1, 0x14, - 0x11, 0xd5, 0xf2, 0x5a, 0x36, 0x4a, 0x67, 0x8e, 0xa4, 0xf8, 0x8b, 0xd9, 0xd2, 0x93, 0xfe, 0xf6, 0xbe, 0x94, 0x3e, - 0x7b, 0xf7, 0xfc, 0xce, 0x22, 0x67, 0xaa, 0xdd, 0xfd, 0x14, 0x87, 0x4f, 0x7d, 0xb2, 0xe4, 0x5f, 0x3f, 0xfe, 0x8b, - 0x7f, 0x41, 0x6f, 0xf8, 0x0b, 0xd6, 0xa5, 0x11, 0xac, 0x5d, 0xf6, 0xf5, 0x25, 0xd8, 0xf1, 0xa1, 0xd1, 0xa7, 0x0c, - 0x2c, 0x05, 0x77, 0x41, 0x4b, 0xf0, 0x10, 0x60, 0xb2, 0xd5, 0x6a, 0xfd, 0x40, 0xdc, 0x3f, 0xbb, 0xae, 0x2b, 0x5f, - 0x2c, 0xdc, 0x57, 0xdb, 0xf7, 0x45, 0xaa, 0xd5, 0xe7, 0xdd, 0x4e, 0x26, 0xc7, 0xbb, 0xff, 0x42, 0x0d, 0xfa, 0x46, - 0x84, 0xc2, 0x33, 0x3b, 0x3d, 0x5d, 0x0d, 0x8b, 0xd7, 0x55, 0xbf, 0x98, 0xaa, 0xb6, 0xb8, 0xa9, 0xa6, 0xc5, 0x8b, - 0xea, 0xf4, 0xe0, 0xff, 0xae, 0x78, 0xc9, 0x33, 0x61, 0x98, 0x0f, 0x34, 0xc8, 0xd9, 0xe3, 0x97, 0xa1, 0x96, 0x1b, - 0x1f, 0x28, 0x76, 0xab, 0xa2, 0x70, 0xda, 0xa5, 0xef, 0x7f, 0xdc, 0x67, 0xf0, 0x46, 0x0a, 0x7a, 0x19, 0xd3, 0x1e, - 0x2d, 0x69, 0xd0, 0x4c, 0x22, 0x48, 0xec, 0xeb, 0x4e, 0x7b, 0x27, 0x1d, 0x48, 0x18, 0xf4, 0xeb, 0x6d, 0xcb, 0x35, - 0x1e, 0x45, 0x98, 0x30, 0x79, 0xcb, 0x40, 0x1c, 0x0a, 0xbe, 0x42, 0x35, 0x22, 0xda, 0x3b, 0x61, 0x9b, 0x24, 0x82, - 0x20, 0x86, 0x2e, 0x75, 0x52, 0x07, 0xbb, 0x4c, 0x73, 0xeb, 0x3c, 0x96, 0x00, 0x69, 0xc5, 0x9a, 0xf2, 0x53, 0x5f, - 0xb4, 0x62, 0x92, 0x8a, 0xda, 0x5c, 0x98, 0x2a, 0xa1, 0x9b, 0x11, 0x62, 0x7d, 0xcb, 0x05, 0xdf, 0xe6, 0x75, 0x2d, - 0x02, 0x2d, 0x37, 0x6b, 0x06, 0xe4, 0x8c, 0x2e, 0xe4, 0x8d, 0xb9, 0x90, 0x2d, 0x96, 0x69, 0xbd, 0x30, 0x4e, 0x35, - 0x6d, 0xda, 0x88, 0xc8, 0x5e, 0xfd, 0xfa, 0x16, 0x88, 0xec, 0x43, 0x5f, 0xd4, 0x99, 0xde, 0xd4, 0xad, 0xb0, 0x89, - 0x41, 0xa6, 0xa1, 0x2a, 0x51, 0x1a, 0xa2, 0x11, 0x17, 0xf1, 0x68, 0x57, 0x89, 0xb0, 0xf1, 0x93, 0xfc, 0x9a, 0x49, - 0x0a, 0x3a, 0x80, 0x58, 0xa0, 0xe2, 0xba, 0xf6, 0x02, 0xe2, 0x90, 0x95, 0xde, 0x37, 0xfd, 0x53, 0x6e, 0xee, 0x8c, - 0x72, 0x33, 0xf4, 0x93, 0x26, 0x57, 0x70, 0x09, 0x31, 0xea, 0x21, 0x8a, 0xc8, 0x47, 0xb1, 0xef, 0x50, 0x27, 0x90, - 0x02, 0x4e, 0x14, 0x67, 0xd0, 0x38, 0x53, 0x81, 0x03, 0xf6, 0x81, 0x16, 0x71, 0x0c, 0x45, 0xf6, 0xa3, 0xae, 0x3a, - 0xd7, 0x23, 0x93, 0x54, 0x77, 0xd2, 0x6f, 0xfe, 0x73, 0x55, 0x65, 0xb0, 0x97, 0x57, 0xab, 0x6c, 0x3e, 0x28, 0xf9, - 0x07, 0xf6, 0x37, 0x73, 0x85, 0x8a, 0xb5, 0xf3, 0x36, 0x9c, 0xd1, 0xe6, 0x88, 0xb1, 0x85, 0xe5, 0x71, 0x6e, 0xa9, - 0x27, 0x70, 0xad, 0xbf, 0x03, 0xcf, 0x70, 0xf6, 0x2d, 0x61, 0x64, 0x5e, 0x4e, 0x26, 0x0b, 0xdd, 0xcc, 0x6e, 0x77, - 0x79, 0xe4, 0x6c, 0x98, 0xd4, 0x9e, 0x2c, 0xea, 0xd7, 0x00, 0xe3, 0x47, 0xbc, 0xe6, 0xc1, 0x9e, 0x81, 0xeb, 0x91, - 0x48, 0xc1, 0x66, 0x80, 0x77, 0x32, 0x76, 0xc4, 0xca, 0x89, 0xb3, 0x34, 0x06, 0x9d, 0x0b, 0x57, 0xa5, 0xe9, 0xef, - 0x2d, 0x51, 0x4a, 0x00, 0x98, 0x41, 0xe8, 0xfd, 0xdc, 0x36, 0xf7, 0xad, 0xa8, 0x4d, 0x49, 0x1a, 0xe2, 0x0c, 0xa2, - 0x72, 0xa0, 0x62, 0x17, 0x34, 0x1d, 0xed, 0x5b, 0x2a, 0xc7, 0xc9, 0x0c, 0x12, 0xa6, 0x5e, 0x19, 0x77, 0xc5, 0x5f, - 0xfa, 0xac, 0x56, 0xff, 0xfc, 0xc8, 0xf4, 0x54, 0xff, 0x88, 0xec, 0xf6, 0x55, 0xf1, 0x3c, 0x57, 0x7e, 0x62, 0x4a, - 0xcd, 0xd5, 0x76, 0x27, 0x43, 0xbc, 0xb1, 0xf4, 0x56, 0xcc, 0x1c, 0xb0, 0xde, 0x1a, 0x9e, 0xd7, 0xbb, 0x5e, 0xcc, - 0x1d, 0xf5, 0x4b, 0xba, 0xaf, 0xcf, 0xff, 0x26, 0x03, 0xfb, 0x0d, 0x93, 0x9c, 0xfb, 0x9a, 0xf6, 0x3c, 0xa1, 0xaf, - 0x16, 0xf3, 0x9a, 0x26, 0x36, 0xf6, 0x99, 0x67, 0xde, 0xd2, 0x34, 0xc8, 0x9e, 0xee, 0xeb, 0x95, 0xd6, 0x99, 0x39, - 0xc7, 0x87, 0x83, 0xe6, 0xf3, 0x27, 0xdd, 0x1b, 0x07, 0xdd, 0x15, 0x28, 0x2e, 0xdd, 0x27, 0xdf, 0x51, 0xf8, 0xc2, - 0x5c, 0x30, 0xed, 0x5c, 0x22, 0xe4, 0xc1, 0xcd, 0xf2, 0x04, 0xa4, 0xbc, 0xcc, 0x27, 0x90, 0x34, 0xcf, 0xcf, 0x97, - 0x98, 0x95, 0x22, 0xc1, 0xcd, 0x84, 0x59, 0x4f, 0x9b, 0x81, 0xe9, 0x66, 0x37, 0x9b, 0x77, 0xf5, 0xe4, 0x49, 0xe7, - 0xb5, 0x83, 0xa6, 0xce, 0xbf, 0xb2, 0xd9, 0xa7, 0x2f, 0x2a, 0xf5, 0x34, 0xad, 0xdc, 0xf5, 0x24, 0x7f, 0xaf, 0x44, - 0x99, 0x05, 0xf0, 0x5e, 0x4a, 0xe4, 0x29, 0x9e, 0xee, 0xe4, 0xa4, 0x89, 0x6a, 0x80, 0x14, 0xab, 0xbb, 0x13, 0xc3, - 0x46, 0xd5, 0x42, 0xa7, 0x1c, 0x3d, 0xe3, 0xd5, 0xcf, 0xd8, 0x23, 0xbe, 0x88, 0xd8, 0x89, 0x8d, 0xc2, 0x8f, 0x8a, - 0xa1, 0xc2, 0xfa, 0xb4, 0x2e, 0x83, 0x57, 0x86, 0xd5, 0x65, 0x2c, 0x06, 0xe4, 0xe7, 0xa6, 0x5c, 0x48, 0xa1, 0xe5, - 0xe7, 0xe8, 0x3e, 0x34, 0x35, 0x50, 0x6f, 0x7c, 0x1e, 0xef, 0xf2, 0xd9, 0xa4, 0xfe, 0x0d, 0x04, 0x7c, 0x90, 0x01, - 0xb5, 0x2c, 0x74, 0x5a, 0xcf, 0x9e, 0xe2, 0xc7, 0x4f, 0xfb, 0xdf, 0x58, 0xf2, 0xf1, 0xc7, 0xff, 0x14, 0xcf, 0x1e, - 0xfb, 0xa8, 0x52, 0x68, 0x12, 0xbc, 0xa7, 0xb0, 0x6f, 0xb3, 0xff, 0xef, 0x91, 0x67, 0xd1, 0xc4, 0x8b, 0xe1, 0x51, - 0x9d, 0x5d, 0x20, 0x4a, 0x6a, 0x7d, 0xe8, 0x8b, 0xd8, 0x71, 0xdf, 0xf7, 0x60, 0x59, 0xba, 0x47, 0xdc, 0xe4, 0xc1, - 0xf5, 0x49, 0xec, 0xf6, 0x95, 0x89, 0x54, 0x6a, 0xfc, 0x8c, 0x5c, 0xc0, 0x58, 0x27, 0xed, 0x31, 0x5c, 0xda, 0xd2, - 0x48, 0xc1, 0xa6, 0x14, 0x67, 0x52, 0x80, 0xfb, 0x4c, 0x94, 0xbe, 0xb3, 0x8f, 0x20, 0xa9, 0xf7, 0x2f, 0x4d, 0x60, - 0x49, 0x5e, 0x97, 0x05, 0x12, 0x9f, 0x8f, 0x2b, 0xf7, 0xf9, 0x24, 0x20, 0x2e, 0x8a, 0x0b, 0x68, 0x0b, 0xc4, 0x18, - 0x15, 0xb9, 0x14, 0x3d, 0x64, 0x69, 0x2a, 0x26, 0xaa, 0x43, 0x7a, 0xc1, 0x6e, 0xdf, 0x0d, 0x94, 0x32, 0x2d, 0x74, - 0xfc, 0xd5, 0x89, 0x18, 0x28, 0x5d, 0x9e, 0xed, 0x6d, 0x61, 0x40, 0x2e, 0x17, 0x13, 0x82, 0x34, 0xbe, 0x9e, 0xc2, - 0xb2, 0xf3, 0x11, 0x5d, 0x25, 0x00, 0x29, 0xbc, 0x5b, 0xc4, 0xcd, 0xa0, 0xa0, 0xa4, 0x81, 0xaa, 0xa9, 0x8d, 0x1e, - 0x08, 0xb1, 0xec, 0x4c, 0xa9, 0xdc, 0x8a, 0x0a, 0x04, 0x01, 0x22, 0x1b, 0x7b, 0x90, 0xc8, 0xe9, 0x61, 0x7a, 0xb8, - 0xa3, 0x2d, 0x4a, 0xa6, 0x68, 0x04, 0x35, 0xda, 0xf4, 0x90, 0xa4, 0x07, 0xaf, 0x9b, 0x89, 0xc1, 0x89, 0xb3, 0xe1, - 0x17, 0xbc, 0xd7, 0x93, 0x7b, 0xbb, 0x36, 0xb2, 0xcf, 0x25, 0xe9, 0x10, 0x73, 0x78, 0xd8, 0xd5, 0xd3, 0xcd, 0x71, - 0xbb, 0xa7, 0x9c, 0x7b, 0x89, 0x9d, 0x80, 0xf6, 0xf6, 0xd4, 0x7d, 0x67, 0x25, 0x6a, 0x5d, 0xf0, 0x08, 0x29, 0xd7, - 0x49, 0xd7, 0x93, 0xef, 0x2f, 0xef, 0x6a, 0x53, 0x2a, 0x9b, 0x88, 0x54, 0x34, 0x99, 0x2a, 0x10, 0x53, 0x82, 0x34, - 0x96, 0x51, 0x2f, 0xb7, 0x73, 0xc4, 0x9e, 0x0e, 0xa3, 0xb8, 0x85, 0x37, 0xb3, 0x55, 0xf6, 0x26, 0xad, 0xaf, 0xb0, - 0x82, 0x69, 0x8a, 0x6a, 0xfe, 0xfb, 0x59, 0x56, 0xb4, 0x33, 0x10, 0x41, 0xa8, 0xe7, 0xb6, 0x25, 0xbb, 0x80, 0x46, - 0x39, 0x7f, 0xdb, 0x40, 0x9b, 0x0e, 0xfb, 0x20, 0xe4, 0x39, 0x32, 0xef, 0xe5, 0x5b, 0x22, 0xc4, 0x40, 0x4a, 0x90, - 0x81, 0xaf, 0x5d, 0x44, 0xd4, 0x1c, 0x16, 0xcd, 0x6d, 0xe0, 0xf0, 0x21, 0x5c, 0x91, 0x99, 0x60, 0x32, 0xc5, 0xdd, - 0x85, 0x38, 0xed, 0xb8, 0xc3, 0x3d, 0x3b, 0xea, 0x59, 0x93, 0x3b, 0x65, 0x1a, 0x69, 0x26, 0x79, 0x7a, 0xb7, 0x4a, - 0xa3, 0x6c, 0xe9, 0xc8, 0xc5, 0x26, 0x92, 0x4b, 0xb9, 0x85, 0x88, 0xdb, 0xb2, 0x76, 0xfa, 0xf6, 0xfb, 0xb2, 0x79, - 0x74, 0x2f, 0xbe, 0xf5, 0x3e, 0xec, 0xdc, 0xaa, 0xb7, 0x35, 0xdb, 0xd6, 0x4f, 0x4b, 0x81, 0x52, 0x06, 0xdc, 0x99, - 0xae, 0x64, 0x26, 0x55, 0x57, 0xbe, 0x68, 0xdb, 0x21, 0x5f, 0x98, 0xc0, 0xf4, 0x34, 0xbc, 0xcd, 0x6a, 0x9d, 0x50, - 0x94, 0xd2, 0x07, 0x62, 0x91, 0xf8, 0x30, 0x00, 0xc6, 0x07, 0xaf, 0x89, 0x5c, 0xf0, 0x33, 0xfc, 0x5c, 0x2a, 0xfd, - 0xae, 0xc9, 0x52, 0x14, 0x80, 0x3e, 0x88, 0xf6, 0xec, 0x34, 0xaa, 0xf9, 0x2a, 0x9b, 0xe9, 0xe4, 0x20, 0xa6, 0x7f, - 0xfc, 0xff, 0x5c, 0x05, 0xea, 0xb7, 0x23, 0x3d, 0x84, 0xf7, 0x9b, 0x04, 0xae, 0xd5, 0xc2, 0x98, 0x9e, 0xc4, 0xa8, - 0x7b, 0x58, 0x12, 0xe1, 0x40, 0x00, 0x5f, 0x45, 0x4d, 0xdc, 0x48, 0xe3, 0xad, 0xa2, 0xa7, 0xa8, 0x6f, 0xc3, 0x5b, - 0x77, 0xb2, 0x4f, 0xc6, 0xe1, 0x7e, 0x8e, 0xb9, 0x17, 0x25, 0xcb, 0x32, 0x88, 0x86, 0x41, 0xd1, 0x2d, 0x0c, 0xac, - 0x90, 0x9f, 0x2f, 0xe0, 0xcb, 0x30, 0xe7, 0x33, 0xa3, 0x64, 0xb4, 0x42, 0xf4, 0x2a, 0xa4, 0x0e, 0x12, 0xef, 0x66, - 0x98, 0x0d, 0x7a, 0x06, 0xc5, 0xfe, 0x60, 0xea, 0x54, 0x2a, 0x68, 0xaf, 0xaa, 0xd2, 0x64, 0x57, 0x92, 0x5b, 0x7b, - 0x15, 0x1d, 0xfd, 0x14, 0x44, 0x8e, 0x97, 0xa2, 0xc5, 0x17, 0x1e, 0x0b, 0xfb, 0x5d, 0xdc, 0x1c, 0xc7, 0x00, 0x3c, - 0x7f, 0xfa, 0xa9, 0x17, 0xb7, 0x22, 0x3b, 0xfd, 0x5b, 0xe2, 0xd2, 0x77, 0x8f, 0xa6, 0xf1, 0xff, 0x29, 0x64, 0x7f, - 0xe0, 0xb7, 0x08, 0xac, 0x3f, 0xed, 0x31, 0x38, 0xb8, 0x84, 0xeb, 0x2d, 0x62, 0xf3, 0x05, 0x2c, 0xcb, 0xdb, 0xad, - 0x79, 0x20, 0x24, 0xee, 0x0b, 0x63, 0x66, 0x4f, 0xca, 0x6a, 0x94, 0x08, 0xff, 0xba, 0x8f, 0x61, 0xff, 0x35, 0x71, - 0x09, 0xc2, 0x70, 0x6e, 0x5c, 0xe8, 0xef, 0xb4, 0xce, 0x9e, 0xe2, 0xfb, 0xa7, 0xfe, 0x66, 0xc9, 0xfb, 0x1f, 0xff, - 0x53, 0x9c, 0x79, 0x67, 0x5c, 0xa3, 0xb7, 0x4f, 0x6f, 0x6e, 0x22, 0x46, 0x4d, 0x5e, 0xcb, 0x0a, 0x67, 0x3f, 0x8b, - 0x9c, 0xcd, 0x84, 0x57, 0x27, 0x72, 0x81, 0x86, 0x91, 0x8f, 0x7b, 0x5e, 0xa2, 0x17, 0xec, 0xba, 0xa3, 0x58, 0x9a, - 0x68, 0xcb, 0x22, 0x54, 0xe8, 0xa7, 0x06, 0x49, 0x82, 0xf9, 0xfe, 0x27, 0x31, 0x3b, 0x6a, 0xab, 0x61, 0x66, 0x15, - 0x0f, 0xf1, 0x5d, 0x5a, 0x99, 0xa4, 0x9c, 0x57, 0xf5, 0x4e, 0x25, 0xca, 0xe6, 0x47, 0x64, 0x87, 0xc5, 0x60, 0xcc, - 0x4a, 0x08, 0xfb, 0x9d, 0x21, 0x32, 0x72, 0xd4, 0x97, 0x38, 0x49, 0xfc, 0x56, 0x47, 0x48, 0xbc, 0xb3, 0x34, 0x48, - 0x5f, 0x4b, 0x80, 0x7c, 0x2d, 0xbb, 0x3e, 0xf6, 0x62, 0x4a, 0x27, 0x1c, 0xee, 0x24, 0x7d, 0xeb, 0x3d, 0xf2, 0x8d, - 0x30, 0x6f, 0x95, 0xc6, 0x31, 0x20, 0xec, 0x5c, 0x03, 0x46, 0x46, 0xec, 0x40, 0x0e, 0x31, 0x17, 0x3b, 0x40, 0x30, - 0xeb, 0x6a, 0x9c, 0x03, 0xbf, 0xf7, 0x0e, 0xa5, 0xf4, 0x62, 0x2d, 0xb5, 0x4f, 0x72, 0x7e, 0x90, 0xc3, 0x31, 0x27, - 0x30, 0xde, 0x93, 0x39, 0x1d, 0x68, 0x1e, 0x93, 0x52, 0x2b, 0x91, 0x8a, 0x06, 0xe4, 0x57, 0xc9, 0xe0, 0x9e, 0xed, - 0xc9, 0x88, 0x93, 0x7f, 0xa1, 0x94, 0xfc, 0xe1, 0xc6, 0x3d, 0x9a, 0x09, 0xce, 0xcb, 0x03, 0x76, 0xb1, 0x59, 0x94, - 0x40, 0x3b, 0x53, 0xcd, 0x93, 0xb3, 0x05, 0x73, 0x69, 0x49, 0x49, 0xcb, 0xc2, 0x27, 0x64, 0x46, 0x6e, 0xfe, 0xc5, - 0xeb, 0x9b, 0xfe, 0x91, 0x61, 0x05, 0xc1, 0x5e, 0xeb, 0xaf, 0xf5, 0x7e, 0x4f, 0xa7, 0x83, 0x43, 0xd0, 0x9d, 0x03, - 0xb4, 0x4a, 0xe3, 0x61, 0x7f, 0xc6, 0x26, 0x80, 0x4c, 0x10, 0x3f, 0xdc, 0xb0, 0xee, 0xee, 0x07, 0x04, 0x66, 0x3f, - 0xf1, 0x8b, 0xe3, 0x94, 0x11, 0xf0, 0xad, 0xdd, 0xa2, 0x12, 0x22, 0x87, 0xff, 0xe7, 0xbe, 0x92, 0xc5, 0xea, 0x36, - 0x39, 0xd2, 0xec, 0xd7, 0xad, 0x33, 0xc0, 0x38, 0xfa, 0xe5, 0x3a, 0xa1, 0x12, 0x46, 0x66, 0x87, 0xa6, 0xd8, 0x15, - 0xce, 0x1e, 0xe1, 0x64, 0xc6, 0x7e, 0x0a, 0x8d, 0x48, 0xe3, 0x60, 0x25, 0x47, 0x5a, 0x80, 0x8b, 0xe5, 0x70, 0x68, - 0x98, 0x84, 0x0e, 0xb0, 0xc5, 0x45, 0x8e, 0xfb, 0xe1, 0xf9, 0x4c, 0xb2, 0xc3, 0x4b, 0x02, 0xe8, 0xc0, 0xb9, 0x88, - 0x89, 0x20, 0x07, 0x2d, 0x06, 0xa1, 0x1b, 0x72, 0xb0, 0x16, 0xaa, 0x61, 0x72, 0x04, 0xcf, 0xbe, 0xfe, 0x31, 0xfa, - 0x49, 0xc3, 0xe0, 0x25, 0x24, 0xc3, 0x28, 0x01, 0xe4, 0x98, 0xac, 0xf4, 0x1b, 0xf7, 0x76, 0x7b, 0xeb, 0xae, 0x0b, - 0x89, 0x3b, 0xfb, 0x69, 0xd7, 0x72, 0x31, 0x91, 0x7a, 0xf5, 0xd1, 0xc0, 0xb0, 0x73, 0xc0, 0x16, 0x78, 0x15, 0x44, - 0x67, 0xa2, 0xc7, 0x3d, 0xdc, 0x9f, 0x42, 0xaf, 0x30, 0x47, 0x60, 0x02, 0x7c, 0x63, 0xb2, 0x3b, 0x79, 0x84, 0xab, - 0xdb, 0x7d, 0xb4, 0xe7, 0xb1, 0x35, 0x8e, 0x0a, 0xa1, 0x34, 0xe2, 0x2d, 0xd3, 0x9d, 0x64, 0xc2, 0x3a, 0xac, 0xfe, - 0xa1, 0x29, 0xae, 0xd2, 0x77, 0xd2, 0x34, 0x82, 0x13, 0xb1, 0xfb, 0x36, 0xdc, 0x6a, 0xe0, 0x04, 0x47, 0x2e, 0x7a, - 0xf8, 0x8e, 0x08, 0x43, 0x0b, 0x7c, 0xc0, 0x49, 0xc5, 0x6c, 0x3c, 0x26, 0x06, 0x4e, 0xe3, 0x24, 0x57, 0x66, 0x39, - 0x37, 0x39, 0x24, 0xae, 0x58, 0xae, 0xb0, 0x9e, 0x5e, 0xb3, 0x6c, 0x93, 0x09, 0xf0, 0xde, 0x4f, 0xdd, 0x7b, 0x26, - 0xa4, 0x5c, 0x35, 0x6a, 0xcb, 0xdd, 0x59, 0xf1, 0x69, 0xb4, 0x92, 0xc9, 0xc9, 0x26, 0xfe, 0x30, 0xc0, 0x9d, 0xdd, - 0x12, 0xdd, 0xa9, 0xee, 0x2e, 0xb9, 0x0b, 0x8d, 0x27, 0xcc, 0x9c, 0xc2, 0x3e, 0x58, 0x4b, 0x75, 0x1e, 0x86, 0xd6, - 0xe3, 0xef, 0x9a, 0x99, 0x80, 0xc8, 0x4e, 0xa6, 0xb3, 0xf8, 0xa1, 0x1b, 0x90, 0xd2, 0x12, 0x47, 0x17, 0x25, 0xab, - 0x3f, 0xad, 0x7b, 0x93, 0x2a, 0xee, 0xd0, 0xf6, 0xf5, 0x8d, 0x1c, 0xef, 0x24, 0x2b, 0xb1, 0x04, 0xd5, 0x48, 0x7e, - 0x91, 0xa4, 0x81, 0x1d, 0x90, 0x0e, 0xb9, 0x46, 0xc3, 0x4c, 0x3d, 0x9b, 0x07, 0xaf, 0x23, 0xdd, 0x06, 0xab, 0x74, - 0x66, 0xf7, 0xf2, 0x03, 0x69, 0x85, 0xa6, 0x8c, 0x49, 0x31, 0xc9, 0xc7, 0x8b, 0x2e, 0x4e, 0x9c, 0xa2, 0x96, 0x7c, - 0x72, 0xe5, 0xa4, 0xe7, 0xb5, 0x3a, 0xe4, 0xea, 0x65, 0x0f, 0x31, 0x90, 0x24, 0xb3, 0x78, 0xa1, 0x7a, 0x72, 0x43, - 0xbc, 0x46, 0x03, 0x9c, 0xb6, 0xc7, 0xee, 0x2e, 0x1e, 0xe5, 0xad, 0x7f, 0xaa, 0xb7, 0x15, 0x5a, 0xfe, 0x94, 0x97, - 0xf7, 0x6a, 0xfd, 0x6d, 0x14, 0x21, 0xbf, 0x8b, 0x1f, 0x76, 0xeb, 0x9f, 0xb6, 0xab, 0x52, 0xa1, 0x53, 0xf9, 0x35, - 0x69, 0x8b, 0x05, 0xc0, 0x9f, 0xd7, 0xa6, 0x29, 0x24, 0x23, 0x8c, 0xa8, 0x4f, 0x10, 0x61, 0xaa, 0x13, 0xc6, 0xb7, - 0xa2, 0x86, 0xbc, 0x15, 0xad, 0x48, 0xaf, 0x19, 0x4d, 0xe3, 0xec, 0xe7, 0x8e, 0x11, 0x76, 0x32, 0x1c, 0xb1, 0x5b, - 0x92, 0x72, 0xfd, 0x14, 0xe9, 0x29, 0x24, 0x8e, 0x45, 0x70, 0x99, 0x50, 0x69, 0x29, 0xc7, 0x04, 0xd0, 0xad, 0xb6, - 0x86, 0x2c, 0x86, 0xd4, 0xa0, 0x8c, 0x59, 0xdd, 0x3c, 0x22, 0x70, 0xd4, 0xa0, 0x87, 0x8e, 0xa4, 0x70, 0x42, 0xb3, - 0x9d, 0x3e, 0x3e, 0x7f, 0xc6, 0xb4, 0x55, 0xdb, 0x20, 0x12, 0x03, 0xd0, 0xed, 0xee, 0x48, 0x0c, 0x41, 0xda, 0xdf, - 0x11, 0xd9, 0x5a, 0x2d, 0xca, 0xe8, 0xc8, 0x86, 0x6e, 0xa7, 0xc8, 0xfc, 0x5a, 0xcd, 0x15, 0xd9, 0xd4, 0xed, 0x06, - 0x65, 0x14, 0x19, 0xa4, 0xbc, 0xa3, 0xb4, 0xe5, 0x62, 0x19, 0x1d, 0xdd, 0xa2, 0x88, 0x50, 0x71, 0x1b, 0x14, 0x69, - 0xfa, 0x83, 0x14, 0x39, 0x0d, 0x11, 0xa7, 0x00, 0xef, 0x4e, 0x2d, 0x89, 0xda, 0x2d, 0x15, 0x0d, 0x9e, 0x42, 0x2f, - 0x83, 0x79, 0x77, 0xe1, 0x40, 0x42, 0x68, 0x73, 0x83, 0x53, 0x10, 0x6d, 0x41, 0xa7, 0x22, 0xbc, 0x15, 0xe9, 0x41, - 0x6a, 0x20, 0x00, 0xaf, 0xce, 0x7d, 0x1c, 0x1c, 0x00, 0xf4, 0xc9, 0x2a, 0xe8, 0x7c, 0x7f, 0xb4, 0xc8, 0x21, 0x96, - 0x66, 0x47, 0xea, 0x29, 0xe2, 0x52, 0x9a, 0x4f, 0xc4, 0xed, 0x82, 0x1c, 0x44, 0x9a, 0x56, 0xfc, 0x87, 0x5c, 0xd8, - 0xa4, 0x9d, 0x0f, 0x33, 0x04, 0x5f, 0x6a, 0xe2, 0x89, 0xd4, 0x7d, 0x8e, 0xc5, 0x94, 0x91, 0xc9, 0xd7, 0xba, 0x0b, - 0xab, 0x1d, 0xcc, 0x01, 0x31, 0x9e, 0x54, 0xf2, 0xd3, 0x29, 0xb2, 0xb3, 0xc9, 0x62, 0xa5, 0xa1, 0x02, 0x5a, 0x3a, - 0xa4, 0xcb, 0x65, 0xab, 0xc7, 0x01, 0x77, 0xfc, 0xa8, 0x09, 0x1f, 0x0d, 0x71, 0x5d, 0xfa, 0xf4, 0x2a, 0x48, 0x43, - 0xf8, 0x60, 0x29, 0xa4, 0x65, 0xd5, 0xb8, 0xf7, 0x66, 0x92, 0xda, 0xbf, 0xdb, 0x2c, 0xad, 0x69, 0xb0, 0xc3, 0xb6, - 0xe8, 0x19, 0x44, 0xe1, 0xeb, 0xaf, 0xa7, 0xd5, 0x47, 0xe7, 0x36, 0x2d, 0x88, 0xd0, 0x57, 0x05, 0x4e, 0x2c, 0xa7, - 0xbf, 0x2c, 0xe9, 0xe6, 0x96, 0xd0, 0x47, 0x2c, 0x7f, 0x94, 0x29, 0xc7, 0x67, 0x86, 0x17, 0x6b, 0xe8, 0x7e, 0x07, - 0x5a, 0x44, 0x8d, 0xb3, 0x59, 0x16, 0x8d, 0x6c, 0x09, 0xaf, 0xa9, 0x98, 0x98, 0xab, 0x1f, 0x0d, 0x19, 0x6b, 0x64, - 0x82, 0xc8, 0xa2, 0x1f, 0x3f, 0xea, 0xd2, 0x11, 0xe7, 0x61, 0x10, 0x27, 0x20, 0xcd, 0xbc, 0x64, 0xe4, 0x4d, 0x14, - 0xfc, 0xd6, 0x73, 0x60, 0x9b, 0xf7, 0x5b, 0xfb, 0xcc, 0x6e, 0xc4, 0x47, 0xfa, 0xda, 0xeb, 0x1d, 0x08, 0x25, 0x21, - 0x46, 0xe5, 0x9e, 0x8f, 0x8b, 0x25, 0x7b, 0x1a, 0x78, 0x53, 0x96, 0x2b, 0x06, 0xb7, 0xf8, 0x0d, 0xe8, 0x41, 0x0d, - 0xef, 0x20, 0xb4, 0x8f, 0x9c, 0x76, 0x84, 0x07, 0x68, 0x54, 0x0f, 0xd7, 0x72, 0x44, 0x17, 0x10, 0x64, 0x4e, 0xd0, - 0xa3, 0x81, 0x32, 0x50, 0xf0, 0x95, 0x64, 0xd0, 0x55, 0x66, 0xbb, 0xcc, 0xd6, 0xd0, 0x8c, 0x09, 0x10, 0xa9, 0xce, - 0x9f, 0x46, 0x70, 0x09, 0x5d, 0xc2, 0xa5, 0xa2, 0x0e, 0x64, 0xd4, 0xca, 0x70, 0x30, 0x0a, 0x68, 0xfa, 0x54, 0x1a, - 0xbf, 0x1a, 0x5d, 0x0a, 0xc0, 0xb1, 0xc6, 0xe7, 0x49, 0x06, 0x9f, 0x6d, 0x5c, 0xb1, 0xb8, 0x0c, 0x9a, 0x03, 0x83, - 0x6b, 0x5f, 0xdb, 0xab, 0xae, 0xc1, 0xce, 0xeb, 0xef, 0xa2, 0x33, 0x86, 0x3d, 0xa3, 0x10, 0x2b, 0x80, 0x0e, 0x90, - 0x28, 0xd7, 0xc0, 0xd9, 0x7b, 0xee, 0x7c, 0x6c, 0x23, 0x57, 0xd0, 0x85, 0x0e, 0x08, 0xae, 0x35, 0xb8, 0xdc, 0x3f, - 0x02, 0x5c, 0x68, 0x68, 0xe7, 0x59, 0x60, 0x45, 0x2e, 0x33, 0x50, 0x23, 0xfe, 0x4d, 0xee, 0x60, 0x59, 0x8d, 0x74, - 0x11, 0x8f, 0x15, 0xb5, 0xed, 0x64, 0x81, 0x4e, 0xb0, 0x4d, 0x0d, 0xb1, 0x03, 0x14, 0xbc, 0x50, 0x7e, 0xc2, 0xa9, - 0x47, 0x4b, 0x37, 0xa8, 0x2c, 0xf8, 0x1c, 0x98, 0xc5, 0xef, 0xa4, 0xde, 0xc2, 0xfd, 0x27, 0x47, 0x76, 0x10, 0x40, - 0xbc, 0x35, 0x2b, 0x84, 0x30, 0xf1, 0x72, 0x6c, 0x13, 0x76, 0x94, 0x81, 0x60, 0x37, 0x9a, 0x08, 0xd9, 0x08, 0x73, - 0x1b, 0xef, 0xa2, 0xf5, 0x7a, 0x1f, 0xbb, 0xbf, 0x18, 0x85, 0xc1, 0x76, 0x81, 0x61, 0xfc, 0x58, 0x8c, 0x22, 0xd4, - 0xf6, 0xf2, 0x5b, 0x91, 0x8c, 0xe4, 0x67, 0x15, 0xcc, 0x45, 0xdc, 0x0e, 0x6c, 0x5d, 0x99, 0x5a, 0x3f, 0x20, 0x2a, - 0xef, 0xf7, 0x92, 0x41, 0xbb, 0x85, 0x8f, 0x6f, 0x46, 0x76, 0xe2, 0x2b, 0xc7, 0xf5, 0xac, 0xfa, 0xfc, 0xfe, 0x39, - 0x22, 0x6b, 0x1e, 0x6f, 0xdd, 0x19, 0x8d, 0xce, 0x6a, 0x2d, 0x87, 0x8d, 0xda, 0xc0, 0x30, 0x21, 0xac, 0xf0, 0xfd, - 0xbc, 0x5c, 0xfb, 0x80, 0x30, 0xfc, 0xba, 0xe1, 0x37, 0x9e, 0x2d, 0xb0, 0x97, 0x1e, 0x1c, 0x2d, 0x6f, 0x5d, 0x57, - 0xd7, 0xd3, 0x4a, 0x34, 0x1e, 0x61, 0x12, 0xe2, 0x75, 0xde, 0x30, 0xa5, 0x03, 0x99, 0xe5, 0xf0, 0xa4, 0x7f, 0x70, - 0x4d, 0x67, 0x3e, 0xc4, 0x88, 0xf6, 0x17, 0x80, 0x7c, 0xd1, 0x94, 0x8f, 0x48, 0x9e, 0xd1, 0xe4, 0x06, 0x9b, 0x46, - 0xfa, 0x85, 0x33, 0xa5, 0x3a, 0x10, 0xdc, 0x77, 0x00, 0x00, 0x03, 0x75, 0x58, 0xf0, 0xfb, 0xd8, 0x56, 0xe2, 0xba, - 0x06, 0x63, 0x30, 0x30, 0xfd, 0x47, 0xbf, 0xa8, 0xf3, 0xa3, 0xcf, 0x50, 0x4d, 0xac, 0xf9, 0x52, 0x23, 0x32, 0xbb, - 0x92, 0x15, 0xd9, 0x4d, 0x90, 0x1f, 0xe7, 0xcb, 0x82, 0xdc, 0x84, 0xb8, 0x09, 0x41, 0xc5, 0x3d, 0xf1, 0xb5, 0xa8, - 0x02, 0x7d, 0x03, 0x94, 0x7f, 0x38, 0xeb, 0x40, 0xd0, 0x5f, 0x04, 0xc6, 0x9a, 0x1c, 0x84, 0xf3, 0xcf, 0x2d, 0x33, - 0x91, 0x3f, 0x8f, 0xc2, 0x65, 0xc9, 0x5f, 0xdd, 0xb2, 0x8d, 0xcc, 0xb8, 0xf1, 0x6d, 0x54, 0x99, 0x14, 0xf2, 0xba, - 0xf6, 0xac, 0x33, 0xbe, 0x90, 0x6a, 0x15, 0xc8, 0xd5, 0x45, 0xcc, 0x8c, 0x69, 0x0b, 0x39, 0xfd, 0x59, 0xfb, 0x5a, - 0xe5, 0x7f, 0xc6, 0x1d, 0x1c, 0xb3, 0xf0, 0xcf, 0xc7, 0x6d, 0x63, 0x0a, 0x88, 0xca, 0x1e, 0xf6, 0x45, 0xe5, 0x59, - 0xa7, 0xcb, 0x02, 0xd8, 0x26, 0x88, 0x2b, 0x19, 0xa1, 0xcc, 0x61, 0xa3, 0xf6, 0xfe, 0xbb, 0x7d, 0x1d, 0xcf, 0xac, - 0xb6, 0x1f, 0xd1, 0x4f, 0xfc, 0x11, 0xf3, 0x6f, 0x17, 0xf6, 0x65, 0x62, 0x9c, 0xbe, 0xce, 0x33, 0xd5, 0x9f, 0xba, - 0x49, 0x5d, 0xf0, 0xac, 0x4e, 0x40, 0x05, 0x09, 0xab, 0xe0, 0x67, 0xb0, 0x67, 0xff, 0xa3, 0xc2, 0xd5, 0x90, 0x4f, - 0xcb, 0x67, 0x67, 0xf6, 0x1a, 0xba, 0x56, 0x50, 0x75, 0xb8, 0x01, 0x22, 0x87, 0xc5, 0xad, 0xea, 0x62, 0xc7, 0x99, - 0xf9, 0x2f, 0x0b, 0xd8, 0xf8, 0x5a, 0x08, 0x4e, 0xd7, 0x1f, 0x5d, 0xbe, 0x44, 0xdb, 0x93, 0xb3, 0x89, 0x19, 0xc6, - 0x97, 0x34, 0xc4, 0x83, 0x7b, 0x4a, 0x87, 0x1f, 0x19, 0x93, 0x4f, 0xf1, 0xc7, 0xa7, 0xfd, 0x64, 0x2e, 0xf9, 0xf1, - 0xe3, 0x5f, 0xfa, 0xab, 0x7e, 0xcd, 0x33, 0xbf, 0x50, 0x67, 0xb2, 0xc3, 0x7b, 0x45, 0xd1, 0xf3, 0x8b, 0xb9, 0x18, - 0x11, 0x5b, 0x31, 0xfe, 0x30, 0x0b, 0x7d, 0xfa, 0xed, 0xed, 0xc3, 0x3d, 0x78, 0x33, 0x28, 0x69, 0xc6, 0x41, 0x9d, - 0x9b, 0xeb, 0x1c, 0x5b, 0x69, 0xca, 0x60, 0x12, 0xec, 0x0d, 0x2c, 0x91, 0x41, 0xda, 0x9d, 0x08, 0x11, 0xa8, 0x0c, - 0x4a, 0x05, 0x43, 0x69, 0x8e, 0xa3, 0xae, 0x83, 0x70, 0x20, 0x28, 0x97, 0x11, 0x5d, 0xd5, 0x2a, 0xce, 0x46, 0x07, - 0x0b, 0x01, 0x67, 0x10, 0x61, 0x08, 0xf0, 0xbd, 0x9a, 0x28, 0x81, 0x29, 0x24, 0x0d, 0x21, 0xfe, 0x54, 0x3a, 0x8e, - 0xa2, 0xb8, 0xae, 0xc2, 0xd7, 0xfb, 0x17, 0xd9, 0x38, 0xf9, 0x28, 0x81, 0xa2, 0xbc, 0x03, 0x81, 0x9a, 0x22, 0x05, - 0x9b, 0x8b, 0xcc, 0xe5, 0x88, 0x85, 0xf3, 0xa1, 0xa0, 0x17, 0x12, 0x56, 0x4b, 0x07, 0x3a, 0x1d, 0x7b, 0x38, 0x24, - 0xb4, 0x2a, 0xd8, 0x84, 0xa1, 0xc9, 0x6d, 0x7e, 0xad, 0x02, 0xca, 0xc9, 0x64, 0x17, 0xb7, 0xc0, 0x37, 0xbf, 0x3e, - 0x47, 0x77, 0x2d, 0x74, 0x9e, 0xf9, 0x9e, 0xf1, 0xf7, 0xef, 0x6f, 0x8e, 0xbf, 0xc2, 0xa3, 0x19, 0xab, 0x26, 0xac, - 0xff, 0xf5, 0x4d, 0x48, 0x08, 0xa5, 0x40, 0x15, 0x01, 0x42, 0xa9, 0xac, 0x81, 0x75, 0x1d, 0x32, 0x73, 0x68, 0xe8, - 0xfa, 0x47, 0x06, 0x39, 0x82, 0x1d, 0x36, 0xf6, 0x6d, 0x58, 0xf8, 0x5a, 0xa9, 0x83, 0xbd, 0x81, 0xa9, 0xb5, 0xb0, - 0x4f, 0x5b, 0xa0, 0xce, 0xe6, 0x9c, 0x39, 0x82, 0xa0, 0x5b, 0x66, 0x66, 0xaa, 0xd2, 0x59, 0x9e, 0x69, 0x7e, 0x46, - 0x7b, 0xc5, 0x7e, 0x5d, 0x02, 0x17, 0x64, 0xe9, 0x6c, 0x6e, 0x6d, 0x45, 0x81, 0xbb, 0x92, 0x2a, 0x9f, 0xb1, 0x75, - 0x3c, 0x04, 0xc2, 0xcf, 0x13, 0x63, 0xbb, 0xc6, 0xe7, 0xc1, 0xd6, 0x27, 0xf9, 0x80, 0x96, 0xae, 0x0f, 0x5d, 0x72, - 0xad, 0x8f, 0x11, 0xcc, 0x88, 0xa9, 0xfc, 0xf0, 0x86, 0xfa, 0x6e, 0x38, 0x70, 0x74, 0x9f, 0xe2, 0xa7, 0x4f, 0x3b, - 0xe9, 0x92, 0x4f, 0x3f, 0xfe, 0x4b, 0x5e, 0xd9, 0x75, 0x08, 0x55, 0x2e, 0x53, 0xf0, 0x63, 0x9f, 0xaf, 0xab, 0xc9, - 0xf6, 0xa6, 0xe2, 0x38, 0x10, 0x3f, 0xaa, 0x34, 0x99, 0xe8, 0x17, 0x77, 0x99, 0xc1, 0xf1, 0x3c, 0x44, 0x56, 0x7b, - 0xe2, 0x5c, 0xd7, 0x93, 0x39, 0x97, 0x6d, 0x70, 0xa1, 0x11, 0x32, 0x75, 0x79, 0xe6, 0x65, 0x9f, 0x13, 0x58, 0xd4, - 0x4c, 0xca, 0xee, 0x6b, 0x8c, 0xc7, 0xd7, 0x96, 0xd3, 0xf4, 0x2c, 0xa4, 0x50, 0x37, 0x1d, 0x2e, 0x68, 0x08, 0x15, - 0xd0, 0xe0, 0xe7, 0x6f, 0x4a, 0xd9, 0x98, 0xb8, 0x19, 0xc9, 0x7f, 0xf0, 0xc8, 0xa4, 0x99, 0x32, 0x1e, 0xe9, 0xb3, - 0x5a, 0xb3, 0x3c, 0xe8, 0x88, 0x2d, 0x65, 0xd9, 0xc7, 0xf8, 0x3a, 0xb5, 0x90, 0xfd, 0x35, 0xfd, 0x3e, 0xdd, 0x62, - 0x94, 0x5a, 0xca, 0x93, 0x8e, 0x84, 0xda, 0xfa, 0xb2, 0x1f, 0x44, 0xa4, 0x2e, 0xf0, 0x50, 0x13, 0xb1, 0x5e, 0xc6, - 0x74, 0x30, 0x98, 0x2a, 0xda, 0x6f, 0x4f, 0x77, 0xab, 0xd3, 0x37, 0x9b, 0x6a, 0x11, 0xe2, 0xfa, 0x40, 0xea, 0x63, - 0x58, 0x4d, 0x94, 0x9d, 0x1d, 0x7a, 0x09, 0x07, 0xc1, 0x03, 0xa3, 0x73, 0x05, 0x37, 0x19, 0x2e, 0x62, 0xd6, 0x99, - 0x07, 0xdc, 0x25, 0xe5, 0x70, 0x82, 0x38, 0xaf, 0xd0, 0xd5, 0xba, 0x0b, 0xe3, 0x5a, 0xbe, 0xc9, 0xbb, 0xd3, 0xa9, - 0xa4, 0x4e, 0x79, 0xb8, 0x01, 0x6d, 0xa4, 0x36, 0xbd, 0x53, 0x6c, 0x1f, 0xb8, 0x55, 0xfb, 0xef, 0x37, 0x20, 0x93, - 0xbd, 0xcf, 0x1f, 0x38, 0xa0, 0x21, 0x08, 0xd9, 0xe3, 0xe5, 0xf8, 0x02, 0x9d, 0x45, 0xdd, 0x5a, 0x77, 0x75, 0xda, - 0x5d, 0x6f, 0xa1, 0x2a, 0xeb, 0x23, 0x2e, 0x98, 0x9c, 0xa1, 0xc3, 0xb6, 0x95, 0x42, 0x3f, 0x86, 0x9e, 0xc4, 0xbc, - 0xbc, 0xb2, 0x36, 0xac, 0x93, 0x13, 0x7b, 0xc1, 0xd5, 0xbe, 0xf9, 0x83, 0xf8, 0x0c, 0x30, 0x8b, 0xb9, 0xe9, 0xcd, - 0xb3, 0xea, 0x0b, 0x31, 0x44, 0xc6, 0x35, 0x1c, 0x61, 0x02, 0x3e, 0xa0, 0xfa, 0x0f, 0x0e, 0x2d, 0x86, 0xab, 0xe3, - 0x52, 0x83, 0xe9, 0xf8, 0xc1, 0x17, 0xd5, 0x11, 0x12, 0xd3, 0x8e, 0xc7, 0x06, 0xac, 0x31, 0xd4, 0xa0, 0x83, 0x5b, - 0xd3, 0x28, 0x40, 0x10, 0xdb, 0xd7, 0xcf, 0x0d, 0x6e, 0xbf, 0x7b, 0xd7, 0xbb, 0x22, 0xe1, 0xdc, 0xbe, 0x6c, 0x80, - 0x39, 0x61, 0x30, 0x3a, 0x9d, 0xad, 0x6b, 0xa2, 0x2f, 0xea, 0xf7, 0x85, 0x2d, 0xf4, 0x40, 0x6e, 0x4c, 0x78, 0x04, - 0x0b, 0x15, 0x77, 0x90, 0x33, 0xa8, 0x80, 0xfb, 0x0b, 0x7a, 0xc0, 0x82, 0x94, 0xf1, 0x89, 0x78, 0x87, 0xd6, 0x31, - 0x42, 0x0d, 0x2c, 0x38, 0x56, 0x1a, 0x86, 0x03, 0x06, 0xc1, 0xf1, 0x59, 0xd6, 0x88, 0xbc, 0x53, 0x23, 0xf8, 0x2b, - 0x1a, 0x45, 0xeb, 0x58, 0x4a, 0x0a, 0x15, 0xac, 0xe9, 0xec, 0x6b, 0x45, 0xc4, 0xab, 0x29, 0xe8, 0x04, 0x18, 0xd2, - 0x9e, 0x38, 0xfb, 0x74, 0x17, 0xc9, 0xa2, 0x7a, 0xcf, 0x2e, 0x12, 0xe1, 0x2e, 0x03, 0x52, 0xc4, 0x81, 0x4f, 0x87, - 0xd5, 0x74, 0x55, 0x6e, 0xee, 0xa1, 0xad, 0xbd, 0x8b, 0x1d, 0x9d, 0x06, 0xc1, 0xe4, 0xd8, 0xb3, 0xe1, 0x46, 0x2f, - 0x0a, 0x0e, 0x5b, 0x49, 0xd9, 0xaf, 0x22, 0x9c, 0x70, 0xeb, 0xb9, 0x16, 0x2a, 0x1d, 0x34, 0x17, 0x7f, 0xba, 0x42, - 0x2f, 0x21, 0xd4, 0xf6, 0x4c, 0x23, 0x4e, 0x2f, 0xc1, 0xd8, 0x6e, 0xfe, 0x53, 0xb7, 0x0c, 0xda, 0xdc, 0xda, 0xbb, - 0xf4, 0xd6, 0x86, 0xb3, 0x49, 0x65, 0x56, 0x0e, 0xba, 0x17, 0xa5, 0xbb, 0x1c, 0xe3, 0x0c, 0x46, 0xf1, 0x49, 0x3e, - 0xd3, 0xaa, 0xf4, 0xd8, 0xef, 0x36, 0x78, 0xc4, 0x3e, 0xda, 0xc6, 0xd8, 0x21, 0x16, 0x58, 0xe4, 0x78, 0x76, 0x02, - 0x35, 0x0e, 0x8d, 0x78, 0x4d, 0x11, 0x5a, 0x52, 0x7b, 0x87, 0x8f, 0x3e, 0xf6, 0xd6, 0xca, 0x77, 0xe4, 0xc5, 0x5a, - 0x04, 0x50, 0x83, 0x9a, 0x56, 0x09, 0xdd, 0xa5, 0x9b, 0x67, 0xbc, 0x06, 0x2c, 0x3a, 0x0a, 0x87, 0x28, 0xdf, 0x39, - 0x57, 0xd0, 0x8e, 0xb6, 0x48, 0x64, 0x1d, 0xa3, 0xa9, 0x14, 0xb9, 0xfe, 0xc3, 0x32, 0x0d, 0x9a, 0x1f, 0xdb, 0x57, - 0x90, 0xbd, 0x39, 0x1f, 0xf3, 0x3f, 0x9e, 0xb3, 0x2f, 0xd9, 0x5e, 0x17, 0x00, 0xae, 0x9f, 0xfd, 0x63, 0x62, 0xf2, - 0x67, 0x16, 0x86, 0xf9, 0x0f, 0xf5, 0x09, 0x6f, 0x02, 0xff, 0x04, 0xcf, 0x59, 0x62, 0xbc, 0x97, 0xe2, 0x3c, 0xc5, - 0x33, 0x97, 0x40, 0x6f, 0x4b, 0xbe, 0x6c, 0x01, 0xbc, 0xb8, 0xd4, 0xbc, 0x5d, 0x70, 0x36, 0x46, 0x14, 0xa8, 0xbe, - 0xd5, 0x93, 0x5c, 0x0e, 0xba, 0x51, 0x8a, 0xb8, 0xe9, 0xb7, 0x0a, 0x34, 0xa3, 0x55, 0x62, 0x76, 0x19, 0x7a, 0xb1, - 0x74, 0x9c, 0xab, 0xf0, 0x33, 0x19, 0x6c, 0x83, 0x7d, 0x22, 0xc2, 0x65, 0xd2, 0x63, 0x84, 0xbf, 0x4d, 0x91, 0x7c, - 0xab, 0xc3, 0xf5, 0x83, 0xc6, 0xbf, 0xee, 0xfd, 0xfa, 0xd5, 0xe3, 0x8b, 0x9b, 0x86, 0xb0, 0x7a, 0xa0, 0x7c, 0x72, - 0xb6, 0xde, 0xed, 0x4c, 0x0f, 0x03, 0xc5, 0x43, 0x61, 0x34, 0x3a, 0xc6, 0x49, 0x61, 0x36, 0x9b, 0x75, 0xfd, 0xd0, - 0xf5, 0x1f, 0x39, 0x1d, 0x48, 0xb0, 0x0c, 0xe5, 0xde, 0x9d, 0x81, 0x79, 0xbd, 0x35, 0x90, 0xb5, 0x5c, 0xe5, 0xc0, - 0x9d, 0x9e, 0xa9, 0xde, 0x8e, 0x14, 0x8e, 0x78, 0xa4, 0x15, 0xee, 0xcc, 0x5e, 0x66, 0x03, 0xba, 0x8b, 0x73, 0x45, - 0x77, 0xce, 0x29, 0x59, 0x44, 0x96, 0x9f, 0xf4, 0x8e, 0xde, 0xec, 0xf8, 0xd8, 0xbd, 0x2b, 0x09, 0x2c, 0xff, 0x6f, - 0xd4, 0xa1, 0x7a, 0x48, 0x8c, 0x14, 0x9e, 0x45, 0xb1, 0xb1, 0x2a, 0x86, 0xef, 0xf0, 0x5b, 0xc9, 0x53, 0xed, 0x15, - 0xc3, 0x02, 0xdf, 0x35, 0xcc, 0xdd, 0x3a, 0x12, 0xbc, 0x4c, 0xc7, 0x80, 0x47, 0x62, 0xc0, 0x6f, 0x36, 0x8f, 0x08, - 0x5d, 0x27, 0x7b, 0x1c, 0x3f, 0x05, 0xe1, 0xc6, 0x15, 0x94, 0x33, 0x23, 0x7c, 0x83, 0x91, 0x83, 0xa7, 0x98, 0x3f, - 0xde, 0xdc, 0x41, 0xf5, 0xf1, 0xc3, 0xbe, 0x58, 0x7b, 0xf0, 0xd7, 0x02, 0xac, 0x81, 0x3c, 0xda, 0x50, 0x3d, 0x4b, - 0xf5, 0xce, 0xfd, 0x35, 0x4f, 0x0b, 0x7e, 0x46, 0x6e, 0x74, 0x5b, 0x9c, 0x23, 0x5f, 0xe2, 0xed, 0xb6, 0x13, 0x6f, - 0x77, 0x7d, 0x6b, 0x7e, 0xd4, 0x08, 0x10, 0x36, 0xbf, 0x2d, 0xdb, 0xfa, 0xc3, 0xc5, 0xed, 0x97, 0xf6, 0xce, 0x60, - 0x07, 0xb8, 0xc4, 0x80, 0x8d, 0xae, 0x8b, 0xd8, 0xe6, 0x8c, 0x1b, 0x63, 0x17, 0x71, 0xd8, 0x00, 0xa4, 0x8a, 0x98, - 0x08, 0x4d, 0xe5, 0x28, 0x04, 0x83, 0xa1, 0x37, 0x7d, 0x1f, 0xef, 0x33, 0x0f, 0xb0, 0x01, 0x9b, 0x4c, 0x6d, 0x42, - 0xd8, 0x98, 0x54, 0xb7, 0x7e, 0x1d, 0xa1, 0x2c, 0xc6, 0xc6, 0xd2, 0x9a, 0x2b, 0x0b, 0x42, 0x9f, 0xb7, 0xfe, 0x56, - 0xc3, 0x36, 0xd7, 0xf8, 0xb7, 0x58, 0x44, 0xfc, 0x98, 0x72, 0xd8, 0x5f, 0xc2, 0xa7, 0x0b, 0xc7, 0xe8, 0xe8, 0x93, - 0xc6, 0x99, 0x71, 0xaa, 0xae, 0x95, 0xfe, 0x56, 0xc6, 0x43, 0x1f, 0xdf, 0xdd, 0x98, 0x2a, 0x3b, 0xf4, 0x12, 0x2c, - 0x3a, 0x0a, 0xe3, 0x21, 0x9e, 0x06, 0x75, 0x1d, 0x47, 0x32, 0x98, 0xba, 0xc7, 0x99, 0xbe, 0xda, 0xce, 0xa2, 0xb8, - 0x8c, 0xd8, 0x79, 0x5f, 0x5a, 0x2d, 0xe3, 0xa0, 0x5a, 0xb8, 0x88, 0x8e, 0x19, 0xd4, 0x22, 0xe2, 0x9d, 0x7a, 0xf1, - 0x24, 0xf9, 0x98, 0xd3, 0x71, 0xa0, 0x74, 0x2d, 0x69, 0x8f, 0x05, 0x34, 0x44, 0x66, 0x14, 0x5e, 0xfa, 0xa9, 0x9b, - 0xfd, 0xd3, 0xf8, 0x7f, 0x5d, 0x6e, 0xb6, 0xdb, 0x63, 0xbb, 0x12, 0x85, 0x39, 0x4d, 0x0e, 0x81, 0xb6, 0xe0, 0x3b, - 0x6e, 0xf5, 0x31, 0x47, 0xc6, 0x78, 0xad, 0x4b, 0xfa, 0xa5, 0xad, 0x3a, 0x8f, 0xda, 0x35, 0x5a, 0xbf, 0xc0, 0x51, - 0x21, 0xb4, 0xd3, 0x6c, 0xb4, 0x8b, 0x0f, 0x7c, 0xde, 0x3c, 0x98, 0x86, 0x26, 0x14, 0x53, 0x4b, 0xf5, 0x90, 0x39, - 0x2a, 0x9f, 0xe3, 0xf4, 0x1e, 0x80, 0x8a, 0x48, 0x7b, 0xf7, 0x7e, 0xa6, 0xde, 0x5f, 0x6b, 0x86, 0xee, 0xa3, 0x56, - 0xca, 0x48, 0xf8, 0x6d, 0x87, 0x90, 0xb0, 0x0a, 0x49, 0x18, 0x3b, 0x27, 0xca, 0x59, 0xd6, 0x36, 0x86, 0x96, 0xf7, - 0x87, 0x83, 0xe7, 0x89, 0x56, 0xcb, 0xb8, 0xc5, 0x23, 0x72, 0xbb, 0xf7, 0x99, 0x48, 0xf5, 0xa2, 0xea, 0xf2, 0x08, - 0x82, 0x45, 0x27, 0x32, 0xd2, 0x5f, 0x8c, 0xda, 0x71, 0x02, 0xfd, 0x7b, 0xf9, 0x53, 0x50, 0x52, 0xd4, 0x0a, 0xa7, - 0x8c, 0x75, 0x13, 0x9d, 0x68, 0x29, 0xc2, 0xc8, 0xa6, 0xaf, 0x82, 0xff, 0x04, 0x37, 0x58, 0x79, 0x77, 0xcf, 0x33, - 0xa2, 0x6a, 0xe1, 0x11, 0x45, 0x32, 0x26, 0xee, 0x7e, 0x0e, 0xb3, 0x84, 0x5e, 0x7a, 0x77, 0xad, 0x75, 0xea, 0x9c, - 0x2e, 0xde, 0x04, 0x51, 0x0a, 0xa2, 0xbb, 0xcf, 0xf1, 0x13, 0xe3, 0x00, 0xe9, 0x06, 0xf8, 0xe7, 0x04, 0xc9, 0x29, - 0x4f, 0x54, 0x5e, 0x06, 0xd3, 0x36, 0xa4, 0x60, 0xf8, 0x58, 0xef, 0xc1, 0x8d, 0x37, 0x7c, 0xb9, 0x9c, 0xfa, 0xe6, - 0xcd, 0x23, 0x57, 0x3d, 0xc4, 0xd3, 0x38, 0xef, 0x6c, 0x5a, 0x26, 0xf8, 0x48, 0x12, 0xff, 0xac, 0x4d, 0xec, 0xb6, - 0x6c, 0xd2, 0xf3, 0xa6, 0xdb, 0xc2, 0xd9, 0xbd, 0x65, 0x0e, 0xb2, 0xd8, 0xf4, 0x05, 0x20, 0xe5, 0x80, 0xd6, 0xc5, - 0x2e, 0x0a, 0x05, 0x71, 0x1a, 0xe0, 0x02, 0x30, 0x42, 0x4b, 0x2c, 0x56, 0xe0, 0x89, 0xc6, 0xd3, 0x2c, 0xa7, 0xc5, - 0x36, 0x78, 0x37, 0x82, 0x67, 0x89, 0x5c, 0x4a, 0xd3, 0x90, 0x36, 0xb5, 0x94, 0xf0, 0xcc, 0xa9, 0xf5, 0x6d, 0x9a, - 0x6e, 0x6a, 0x93, 0xd9, 0x7c, 0xec, 0x8a, 0x15, 0x6d, 0xe8, 0xe0, 0x0e, 0x26, 0xd1, 0x16, 0x42, 0x36, 0x6a, 0x20, - 0xdb, 0xec, 0x4f, 0x59, 0xb2, 0xd3, 0x54, 0xa1, 0x27, 0xb5, 0x6e, 0x89, 0x16, 0x47, 0xa6, 0xde, 0xcd, 0x02, 0x49, - 0xb0, 0x85, 0x86, 0x63, 0x61, 0x45, 0xd3, 0xe3, 0x7b, 0xee, 0xa3, 0x63, 0x60, 0xa1, 0x96, 0x9e, 0xc6, 0x1c, 0xbd, - 0x33, 0x88, 0x69, 0xd6, 0x32, 0xb2, 0x65, 0xe3, 0xf3, 0x1e}; +const uint8_t INDEX_BR[] PROGMEM = { + 0x5b, 0x05, 0x7b, 0x53, 0xc1, 0xb6, 0x69, 0x3d, 0x41, 0xeb, 0x04, 0x30, 0xf6, 0xd6, 0x77, 0x35, 0xdb, 0xa3, 0x08, + 0x36, 0x0e, 0x04, 0x80, 0x90, 0x4f, 0xf1, 0xb2, 0x21, 0xa4, 0x82, 0xee, 0x00, 0xaa, 0x20, 0x7f, 0x3b, 0xff, 0x00, + 0xaa, 0x9a, 0x73, 0x74, 0x8c, 0xe1, 0xa6, 0x1f, 0xa0, 0xa2, 0x59, 0xf5, 0xaa, 0x92, 0x79, 0x50, 0x43, 0x1f, 0xe8, + 0x3c, 0x52, 0x68, 0x2c, 0xf6, 0x36, 0x31, 0x90, 0x4e, 0x2b, 0x91, 0x69, 0x38, 0xb4, 0x61, 0xa7, 0xbb, 0x57, 0x79, + 0x3b, 0x6d, 0x62, 0x85, 0x9f, 0x24, 0x24, 0xda, 0x45, 0xc1, 0xe2, 0x85, 0x44, 0x50, 0x6c, 0x8c, 0x1e, 0xab, 0xbb, + 0x7b, 0x1e, 0x30, 0x98, 0x59, 0xe5, 0xb9, 0xd1, 0x34, 0x03, 0x0b, 0x79, 0x37, 0x8c, 0x4b, 0x68, 0x2c, 0xbe, 0x21, + 0xcc, 0x30, 0xc4, 0x9c, 0x4d, 0x34, 0xd6, 0x01, 0xc1, 0xe1, 0x66, 0x48, 0x3c, 0x94, 0xf4, 0x3c, 0x5b, 0x12, 0xfd, + 0x15, 0xb4, 0x05, 0xdb, 0xaf, 0xc1, 0x59, 0x5d, 0x9f, 0xc5, 0x8d, 0xaf, 0xf7, 0xc6, 0x5f, 0x58, 0xfb, 0xc3, 0x74, + 0x45, 0x97, 0x23, 0xac, 0x2e, 0x4a, 0xb9, 0xab, 0xd7, 0x5b, 0xe5, 0x85, 0x6f, 0x21, 0xba, 0x7e, 0xa3, 0x57, 0xa5, + 0xb3, 0x24, 0x7e, 0x60, 0x30, 0x0e, 0x15, 0x1f, 0x04, 0x5e, 0xba, 0x06, 0x04, 0xff, 0x02, 0x26, 0x2d, 0x03, 0x6c, + 0x97, 0x1b, 0x23, 0x84, 0x28, 0x25, 0xb4, 0xe2, 0xca, 0xbd, 0xc8, 0xc3, 0x96, 0x4a, 0xef, 0xed, 0xe7, 0x65, 0x4f, + 0xb2, 0x2d, 0x6c, 0x02, 0x91, 0x04, 0x76, 0xae, 0xba, 0x67, 0x9c, 0xe3, 0x38, 0xd6, 0x96, 0x61, 0x0c, 0xd3, 0x80, + 0xe4, 0x4a, 0x43, 0x2e, 0x92, 0xff, 0xa7, 0xdb, 0xd2, 0xec, 0xf5, 0xf5, 0x90, 0x4a, 0x22, 0x74, 0x92, 0x40, 0x80, + 0xed, 0x2b, 0xb5, 0xbc, 0xb6, 0xe0, 0x71, 0xda, 0x35, 0x3b, 0xe9, 0xec, 0xeb, 0xac, 0xbe, 0x6a, 0x2f, 0xf0, 0xde, + 0x96, 0xf7, 0x43, 0x51, 0x4d, 0xce, 0xbb, 0xd3, 0x70, 0xc2, 0x08, 0x2c, 0xf0, 0xc8, 0xac, 0x25, 0x82, 0x67, 0xeb, + 0xcd, 0xf5, 0x7d, 0x7d, 0xb7, 0x8b, 0xca, 0x2a, 0x15, 0x66, 0x68, 0xf3, 0x6e, 0xb6, 0xda, 0x5b, 0x81, 0x7b, 0x2e, + 0xe6, 0xc1, 0xdc, 0x5e, 0x0a, 0x7a, 0xda, 0x4a, 0x2c, 0x68, 0xa4, 0xd0, 0x92, 0xe7, 0xb2, 0xb3, 0xef, 0xab, 0xda, + 0xd7, 0xef, 0x89, 0xe7, 0x22, 0xd0, 0x93, 0xe5, 0x08, 0xcc, 0xea, 0x82, 0xdb, 0xde, 0x1a, 0xdd, 0x49, 0x47, 0x26, + 0x23, 0xc1, 0x16, 0x7b, 0x2a, 0x99, 0x23, 0xe9, 0x4c, 0xf8, 0x65, 0xaa, 0xfe, 0xe9, 0x6a, 0x41, 0x0d, 0x8f, 0xfb, + 0x48, 0x9a, 0x49, 0x4e, 0x0b, 0x2e, 0x91, 0xfc, 0x76, 0x6e, 0x55, 0xee, 0xb4, 0xbb, 0x3c, 0x63, 0xc1, 0x95, 0x5a, + 0xf8, 0x37, 0x35, 0xb1, 0xaa, 0xcd, 0xa9, 0x99, 0x2f, 0x3d, 0x52, 0xd0, 0x01, 0xd9, 0x06, 0x27, 0x41, 0x92, 0xdd, + 0x6d, 0x4a, 0xee, 0xad, 0xa6, 0x33, 0xe1, 0xff, 0xeb, 0x6b, 0x5a, 0xff, 0xfd, 0xf3, 0x05, 0x0a, 0xbb, 0xc4, 0x55, + 0x1d, 0x28, 0x69, 0x66, 0x7f, 0x4f, 0x49, 0xce, 0xb2, 0xec, 0x8a, 0x0a, 0x15, 0x2d, 0x63, 0x1b, 0x6f, 0x28, 0x00, + 0xf5, 0x44, 0x47, 0xd6, 0xf5, 0xb5, 0xd7, 0x6f, 0xff, 0xf5, 0x0b, 0x3d, 0xd9, 0x1d, 0x42, 0xe9, 0xb3, 0x32, 0xa5, + 0xf6, 0xa3, 0x1b, 0xc3, 0x58, 0x8a, 0x7c, 0x24, 0xc7, 0x03, 0xbc, 0x92, 0xd5, 0x4f, 0xbf, 0xfc, 0xfa, 0x3d, 0x3b, + 0x6d, 0x8c, 0xc5, 0xdb, 0x9d, 0x77, 0x97, 0xd2, 0x5e, 0x82, 0x85, 0x53, 0x9a, 0xaf, 0xb6, 0x09, 0x45, 0x51, 0x25, + 0x90, 0x44, 0x85, 0xa4, 0xc6, 0x23, 0x18, 0xfb, 0xbb, 0x56, 0xef, 0xe9, 0x3c, 0x13, 0x62, 0xf0, 0xaf, 0x6b, 0xe9, + 0xa2, 0xa7, 0x26, 0x32, 0xa1, 0x8d, 0x5d, 0x56, 0x22, 0xa0, 0xe8, 0xb8, 0x06, 0xc1, 0x07, 0xb4, 0x7e, 0xf5, 0xbe, + 0xae, 0xff, 0xfa, 0x95, 0xa7, 0xb8, 0xe2, 0x34, 0x6a, 0xc9, 0x3c, 0xee, 0xb3, 0x45, 0x67, 0xae, 0x93, 0x6c, 0x20, + 0xbc, 0x42, 0xc5, 0xe1, 0x91, 0x52, 0xb9, 0x8c, 0x62, 0x0d, 0x46, 0xbb, 0x46, 0x72, 0x69, 0x26, 0xe0, 0xac, 0x67, + 0xec, 0xb5, 0x51, 0x5f, 0x9d, 0xae, 0x5d, 0x25, 0x15, 0x72, 0xf0, 0xc2, 0x2f, 0x67, 0xe6, 0x68, 0x08, 0xac, 0xdd, + 0xcb, 0xd5, 0xe3, 0x03, 0x9d, 0x49, 0x4d, 0xa1, 0xcd, 0x81, 0xbf, 0x09, 0xd9, 0xdd, 0x2d, 0xe1, 0x7b, 0x7f, 0x9a, + 0x7d, 0xfd, 0x92, 0xd9, 0x26, 0x19, 0x8d, 0x9d, 0x2b, 0x6d, 0x38, 0x99, 0x2b, 0x2d, 0xb5, 0xfa, 0xed, 0xe3, 0xb0, + 0x11, 0x2c, 0x17, 0x79, 0xe0, 0x24, 0xb1, 0x86, 0x68, 0x15, 0xb3, 0x7f, 0x4b, 0xf3, 0xeb, 0xd7, 0xc3, 0xdd, 0x24, + 0x64, 0x69, 0x0f, 0x4e, 0xae, 0xdb, 0xc7, 0x71, 0x4e, 0xaa, 0x64, 0x18, 0xec, 0xd1, 0x7b, 0x59, 0xa8, 0x31, 0x05, + 0x80, 0x2b, 0xcb, 0x0c, 0x91, 0x02, 0x85, 0x0d, 0x16, 0x1d, 0x1b, 0xa6, 0x6e, 0x48, 0x13, 0x04, 0xa1, 0xc5, 0xff, + 0x33, 0xa7, 0xdf, 0xb4, 0x67, 0x98, 0xb0, 0x60, 0xdb, 0x52, 0xfa, 0xaf, 0x26, 0xbf, 0x98, 0xed, 0xc1, 0x7b, 0x18, + 0x2e, 0x02, 0x9e, 0x60, 0x3c, 0xef, 0xff, 0xef, 0xad, 0xb4, 0xdc, 0xfe, 0x88, 0x74, 0x45, 0x88, 0xec, 0x01, 0xc8, + 0x71, 0x86, 0x23, 0xeb, 0xf7, 0x9d, 0x99, 0x55, 0xe0, 0x34, 0x40, 0xb2, 0xc7, 0x99, 0x95, 0xb4, 0xd6, 0x62, 0x53, + 0x71, 0xef, 0x7d, 0xef, 0x32, 0xbf, 0x8b, 0xce, 0xf8, 0x61, 0x58, 0x61, 0x32, 0x1b, 0x69, 0x87, 0x65, 0xbb, 0xb2, + 0x9c, 0x06, 0x00, 0xc1, 0xfb, 0xde, 0xfb, 0x51, 0xf8, 0xff, 0x47, 0x16, 0xe7, 0x47, 0x64, 0x81, 0x8a, 0xcc, 0x2a, + 0xce, 0xc9, 0x2c, 0x60, 0x46, 0x55, 0x00, 0x67, 0x54, 0x00, 0xfb, 0xe8, 0x80, 0x1c, 0x0b, 0x82, 0x46, 0xd3, 0x64, + 0xb3, 0x8f, 0x86, 0x6c, 0x79, 0xbf, 0xd2, 0x62, 0x05, 0x51, 0xae, 0x67, 0x64, 0x5b, 0xf2, 0xbb, 0x1d, 0x28, 0x6b, + 0x96, 0xd2, 0x4a, 0x47, 0xff, 0xeb, 0x96, 0xeb, 0x89, 0x6e, 0x15, 0x9f, 0x68, 0xa7, 0xdc, 0x30, 0x8c, 0xfc, 0x9f, + 0xc0, 0x23, 0xe1, 0x0c, 0x4e, 0xc3, 0x69, 0xa8, 0xc2, 0xd5, 0xa0, 0xa6, 0x5b, 0xc7, 0x8e, 0xb3, 0xe9, 0x34, 0x54, + 0xfd, 0x31, 0xfc, 0x1e, 0x4a, 0x0b, 0x83, 0xa9, 0x33, 0x4d, 0xd3, 0x7f, 0x77, 0x8d, 0x72, 0x55, 0x71, 0x4d, 0x74, + 0xe3, 0xaa, 0x55, 0x19, 0xf8, 0x9b, 0xa6, 0x69, 0xf8, 0x87, 0xe4, 0x6e, 0x5f, 0xac, 0xf9, 0x3d, 0xda, 0x61, 0xa0, + 0x50, 0xca, 0x2e, 0x93, 0x38, 0x3e, 0xe4, 0x5b, 0x96, 0x25, 0xd9, 0xf0, 0x75, 0x2c, 0xad, 0x37, 0x37, 0x2a, 0x15, + 0x48, 0xe8, 0xbd, 0xf6, 0x19, 0xb5, 0x55, 0xf0, 0x10, 0x5d, 0x14, 0x49, 0xa4, 0xa7, 0xff, 0xa7, 0xaa, 0x7a, 0xbc, + 0x43, 0xa6, 0x66, 0xdd, 0xd8, 0xc4, 0x05, 0xf2, 0xe9, 0x1b, 0x34, 0x4f, 0x12, 0x1a, 0x1b, 0xac, 0xf3, 0x32, 0x32, + 0xad, 0x2b, 0xeb, 0xd8, 0x29, 0x4e, 0xfe, 0x6f, 0x80, 0xa1, 0x0d, 0xad, 0x4a, 0xc2, 0xb6, 0x83, 0xa0, 0xff, 0x50, + 0x68, 0x62, 0xad, 0x71, 0x00, 0x77, 0x5a, 0x66, 0xb4, 0x05, 0x9d, 0x01, 0x51, 0x9c, 0xbb, 0xf8, 0xd3, 0x64, 0x82, + 0x69, 0xb6, 0x87, 0x82, 0xef, 0x73, 0x14, 0x62, 0x0c, 0x22, 0xcb, 0x73, 0xdb, 0xb3, 0x0f, 0x4c, 0xfe, 0x77, 0x60, + 0x45, 0xff, 0x14, 0xea, 0x9f, 0xb0, 0x02, 0xa0, 0x6e, 0xbd, 0xe4, 0xd7, 0x0a, 0xbc, 0xe7, 0xf7, 0x00, 0x44, 0x26, + 0xc2, 0x96, 0xe5, 0x37, 0xda, 0x32, 0xca, 0xef, 0x42, 0xb4, 0xdd, 0xe2, 0x70, 0xf0, 0xe0, 0xc1, 0x93, 0xd4, 0xed, + 0x18, 0xbf, 0x54, 0xdd, 0xb0, 0x5b, 0x86, 0x3e, 0x46, 0x7f, 0xbe, 0x1b, 0x61, 0x83, 0x52, 0xe1, 0x6a, 0x62, 0x7a, + 0xa6, 0x7e, 0x6f, 0xc4, 0x5b, 0x63, 0x55, 0x1a, 0xb8, 0xa5, 0x87, 0xd9, 0x84, 0xd4, 0xcf, 0xd7, 0xd4, 0x62, 0x26, + 0x28, 0x13, 0x41, 0x44, 0xca, 0x76, 0xf6, 0x21, 0x63, 0x4b, 0xae, 0xaf, 0xe7, 0xc7, 0x4d, 0x79, 0x5d, 0x8f, 0xc3, + 0x04, 0x0d, 0xc9, 0x06, 0x39, 0xa0, 0xd8, 0xde, 0x87, 0xb3, 0xd7, 0x4b, 0x65, 0x4e, 0xa3, 0xa6, 0xff, 0x79, 0xab, + 0x0c, 0xed, 0xb4, 0x01, 0x69, 0xdc, 0xa6, 0x15, 0x98, 0x98, 0x82, 0xc8, 0x86, 0x4d, 0x06, 0x77, 0xbd, 0x39, 0x6c, + 0x73, 0x52, 0x73, 0x48, 0xd6, 0xe4, 0x8a, 0x4a, 0x83, 0x9a, 0xf5, 0xc9, 0xeb, 0xa5, 0x2e, 0xa9, 0x70, 0x4b, 0x9d, + 0xb9, 0xbe, 0xc8, 0xe4, 0x9e, 0x17, 0xf2, 0xca, 0x95, 0x97, 0xd3, 0x14, 0xd8, 0x9b, 0xef, 0xb1, 0xaf, 0x13, 0x9a, + 0xf5, 0x3d, 0x8f, 0x74, 0xe3, 0x5d, 0x47, 0x07, 0x26, 0xd2, 0x60, 0xa2, 0xef, 0x11, 0x34, 0x2f, 0x6e, 0xb3, 0xfe, + 0xf0, 0xa3, 0x42, 0x7d, 0xfb, 0x47, 0x5c, 0x75, 0x98, 0x0f, 0xe6, 0x0f, 0x4e, 0xe5, 0x67, 0x1a, 0x4f, 0x93, 0x47, + 0x5f, 0x04, 0x7c, 0xff, 0x7a, 0xf9, 0x79, 0x6b, 0x46, 0x47, 0xc6, 0x0c, 0xd5, 0x90, 0x30, 0xd5, 0xfc, 0xde, 0x8b, + 0xd5, 0x65, 0x7f, 0x87, 0x2d, 0x9a, 0xd5, 0xe4, 0x3f, 0xf9, 0xed, 0x07, 0xfb, 0xa2, 0xb6, 0xd3, 0xe1, 0xbf, 0xbd, + 0x40, 0x78, 0x7a, 0x67, 0x24, 0x0b, 0x6b, 0x0e, 0xed, 0xcf, 0x7a, 0x6a, 0x7c, 0xdb, 0x36, 0x61, 0x5b, 0xc3, 0x7c, + 0x5d, 0xfc, 0xf6, 0x1c, 0xc2, 0xa9, 0xba, 0x12, 0x15, 0x35, 0x11, 0x87, 0x41, 0x13, 0xa5, 0xe5, 0x73, 0x07, 0xfa, + 0xc6, 0xe3, 0x96, 0x43, 0x01, 0x76, 0x4b, 0x53, 0x28, 0xad, 0x09, 0x7e, 0x88, 0x0f, 0x26, 0x90, 0x04, 0xfd, 0x2a, + 0x0d, 0x4c, 0xcd, 0x5c, 0xbf, 0x10, 0xd6, 0x47, 0xaf, 0xe2, 0x12, 0x80, 0x00, 0x59, 0xaa, 0x9b, 0x18, 0x58, 0x96, + 0xc8, 0x80, 0x67, 0xc2, 0xb9, 0x4e, 0x5d, 0x86, 0x1e, 0x79, 0xf5, 0xcf, 0xb0, 0x81, 0x1f, 0x9e, 0x4f, 0x34, 0x1d, + 0x7c, 0xf2, 0x4a, 0x4d, 0xbd, 0x42, 0x06, 0xbc, 0x73, 0x56, 0xbc, 0x9d, 0x43, 0xa9, 0xe6, 0x44, 0x0c, 0xcd, 0xcd, + 0xe4, 0x4e, 0xde, 0xb3, 0x0e, 0x25, 0x35, 0xb6, 0xb6, 0xf6, 0xcc, 0xae, 0x6f, 0x91, 0x82, 0x59, 0xa1, 0xdc, 0x8b, + 0xaa, 0x4f, 0x64, 0x26, 0xd0, 0xa5, 0xe7, 0x38, 0xf3, 0xf5, 0xcd, 0x4f, 0x05, 0x62, 0x8c, 0x38, 0xc3, 0x96, 0x13, + 0x68, 0xb2, 0xe4, 0xd9, 0xcf, 0x4a, 0x5f, 0x44, 0x57, 0xf6, 0x49, 0x47, 0xae, 0x16, 0x81, 0xa1, 0xa7, 0x2d, 0xd8, + 0xb3, 0x35, 0x74, 0x6a, 0xc2, 0xbc, 0xc0, 0x7d, 0xae, 0xf0, 0x88, 0xe4, 0xd0, 0x28, 0x7c, 0x22, 0x98, 0x95, 0xa3, + 0x2a, 0x81, 0x16, 0x0b, 0xc7, 0x4a, 0xf3, 0x07, 0xb8, 0xa1, 0x56, 0xbf, 0xdf, 0x36, 0x6b, 0xa3, 0x84, 0x8b, 0xbf, + 0x24, 0x99, 0xc1, 0x09, 0x7e, 0xff, 0x99, 0x8c, 0x1c, 0xd1, 0x43, 0x7c, 0xb1, 0x46, 0x9d, 0x2e, 0x65, 0x92, 0xa9, + 0xa0, 0xd0, 0x45, 0x92, 0x47, 0x37, 0x9c, 0x3c, 0x5f, 0xf1, 0xf3, 0x0d, 0x1e, 0x37, 0xeb, 0x3d, 0xb6, 0x7c, 0x33, + 0x35, 0xaf, 0xf3, 0x08, 0x54, 0x33, 0x56, 0x02, 0x4f, 0x18, 0xda, 0xc0, 0xbb, 0xc5, 0x4a, 0x42, 0xd7, 0xef, 0xbd, + 0xa4, 0xec, 0x61, 0x77, 0x1b, 0xa2, 0x57, 0x47, 0x00, 0xee, 0x8b, 0xd3, 0x56, 0xd4, 0xbd, 0x01, 0xa2, 0x8f, 0xee, + 0xef, 0xb1, 0xac, 0xe1, 0x03, 0x87, 0x8d, 0x2b, 0x3c, 0x8e, 0x15, 0x84, 0x96, 0xb4, 0xfe, 0x56, 0xd5, 0x1e, 0xc0, + 0x83, 0x66, 0x79, 0x15, 0x8a, 0x60, 0xb7, 0x15, 0x21, 0x3b, 0xce, 0x44, 0x71, 0x9f, 0x6f, 0xe0, 0x70, 0xde, 0xb8, + 0x7a, 0x74, 0x43, 0xcd, 0x4d, 0x7a, 0x90, 0xd2, 0x4b, 0x9b, 0xc8, 0x6a, 0xa2, 0xc6, 0xdf, 0x1a, 0x55, 0x85, 0x34, + 0xdd, 0x1d, 0x1a, 0x7c, 0x88, 0x7c, 0x81, 0x3d, 0xd8, 0x12, 0xf4, 0x32, 0x8d, 0xc6, 0xc1, 0x56, 0x0d, 0xe5, 0x8d, + 0x75, 0x00, 0x03, 0x61, 0x93, 0xa0, 0x44, 0x06, 0x5b, 0x67, 0x8b, 0x58, 0xf5, 0x9c, 0xb0, 0x79, 0x7f, 0xbd, 0x2e, + 0xb1, 0x17, 0xb3, 0x85, 0xcd, 0x54, 0x5f, 0xc8, 0x38, 0xeb, 0xa7, 0xcd, 0xfe, 0xbf, 0x2d, 0x80, 0x83, 0x12, 0xc3, + 0x0b, 0x02, 0x41, 0x44, 0xd5, 0x07, 0xe5, 0xcd, 0xb0, 0x24, 0x2c, 0x0a, 0x6c, 0x1b, 0x1f, 0xb9, 0x7b, 0x48, 0x9e, + 0x55, 0x42, 0x7c, 0x2b, 0x63, 0xd3, 0xd1, 0x76, 0x18, 0x61, 0xa8, 0x86, 0x2d, 0x11, 0x5a, 0x41, 0x04, 0x6c, 0xea, + 0xcf, 0x34, 0xf6, 0x71, 0xe7, 0xda, 0x41, 0xba, 0x28, 0xcb, 0x2c, 0x1c, 0x47, 0x50, 0xa7, 0x83, 0x41, 0xad, 0x84, + 0x9e, 0xec, 0x1e, 0xfc, 0xc6, 0xc6, 0xb8, 0xa0, 0xb8, 0xa1, 0x70, 0xeb, 0x5a, 0x9f, 0x46, 0x06, 0xa6, 0xf8, 0x72, + 0xa5, 0xff, 0xfe, 0x80, 0x1e, 0x07, 0xbb, 0xd4, 0x48, 0xf9, 0x6c, 0xd6, 0x13, 0xf8, 0xee, 0x86, 0x06, 0x67, 0x78, + 0x38, 0xf2, 0x83, 0xc3, 0x3b, 0x25, 0xf0, 0xa0, 0x60, 0x56, 0xbe, 0x7d, 0x10, 0x8a, 0xd4, 0x17, 0x81, 0x0e, 0x17, + 0x5f, 0x53, 0xaf, 0x87, 0x23, 0xe4, 0x56, 0xe4, 0xb1, 0xc0, 0x9d, 0xc8, 0x38, 0x25, 0x47, 0x18, 0x18, 0x27, 0x57, + 0xdf, 0x84, 0xcd, 0x7c, 0xdb, 0x31, 0xff, 0xc2, 0xe5, 0x83, 0x03, 0xd1, 0xac, 0x9f, 0x2d, 0xd8, 0xa5, 0xa4, 0x40, + 0xe0, 0x1e, 0xe9, 0x18, 0x3d, 0x85, 0xa9, 0x1b, 0x9c, 0xa6, 0x14, 0x51, 0x9a, 0x88, 0xd9, 0x42, 0x70, 0x6c, 0x6b, + 0x63, 0x2e, 0xfd, 0x46, 0xd9, 0xd5, 0xb4, 0xd9, 0x8f, 0x2c, 0xf8, 0x42, 0xf9, 0xb6, 0x27, 0xb8, 0x69, 0xc5, 0xed, + 0x5c, 0xea, 0xff, 0x76, 0x9d, 0x49, 0x1b, 0x5a, 0xf7, 0xaa, 0x2d, 0x04, 0x36, 0x45, 0x05, 0xa8, 0x99, 0x5e, 0x24, + 0x53, 0x3b, 0x89, 0xd9, 0x0f, 0x4d, 0x24, 0x27, 0x44, 0x2b, 0xfb, 0x3b, 0xd9, 0x8b, 0x36, 0xe9, 0x18, 0x4a, 0x30, + 0xfc, 0xc8, 0xa5, 0xf4, 0xd5, 0xd5, 0x72, 0x2d, 0x3b, 0x5f, 0xc3, 0x4e, 0x98, 0x0c, 0x08, 0xb2, 0xff, 0x59, 0x68, + 0x6b, 0xc0, 0xe4, 0x52, 0x8f, 0x29, 0x78, 0x74, 0x6d, 0xbe, 0xfb, 0x13, 0x8a, 0x25, 0x0b, 0x31, 0xe5, 0xd0, 0xae, + 0xbf, 0x1e, 0x13, 0x23, 0xa0, 0x2c, 0x89, 0x10, 0x6e, 0xa5, 0x1c, 0xa8, 0x7f, 0xbf, 0x62, 0x52, 0xb0, 0xa5, 0xf6, + 0x46, 0x9c, 0x5d, 0xbd, 0xac, 0x81, 0xc4, 0x46, 0xf3, 0x61, 0x62, 0x47, 0x08, 0x27, 0x4d, 0xed, 0x4b, 0x45, 0x91, + 0x48, 0xcf, 0x52, 0xc4, 0x20, 0xe3, 0x8a, 0xe9, 0x12, 0x2d, 0xac, 0x99, 0xb0, 0x1c, 0x1b, 0x91, 0xa4, 0xcc, 0x6d, + 0x11, 0x3f, 0xbe, 0xe9, 0x06, 0x24, 0x40, 0x3d, 0x62, 0x90, 0x0f, 0xbe, 0x25, 0x20, 0xd7, 0x25, 0x09, 0xca, 0xb5, + 0xcf, 0x25, 0x64, 0x42, 0x3b, 0x19, 0x09, 0x13, 0xf3, 0x46, 0x90, 0x72, 0xf7, 0x74, 0x4a, 0xd7, 0x00, 0x4b, 0x39, + 0x59, 0xcd, 0x21, 0x62, 0xe4, 0x78, 0x5d, 0x75, 0xb5, 0x80, 0x58, 0x0a, 0xb7, 0xa3, 0xed, 0xc8, 0xe4, 0x5c, 0xdc, + 0xa1, 0xf3, 0xce, 0x99, 0x5f, 0x18, 0xa7, 0x1c, 0x9c, 0x1e, 0xe6, 0x2e, 0x20, 0x20, 0xa7, 0xae, 0xfa, 0xc1, 0x19, + 0x19, 0xa4, 0xb8, 0x9a, 0x77, 0x5a, 0x24, 0x9a, 0x11, 0xf9, 0xac, 0x18, 0xaa, 0xdb, 0x2a, 0x37, 0xc2, 0x62, 0xad, + 0x5c, 0x82, 0x29, 0x72, 0x72, 0x1b, 0x7c, 0xb3, 0x83, 0xc7, 0xcd, 0x13, 0x16, 0xc0, 0x59, 0x8f, 0xe5, 0x62, 0xc2, + 0xa1, 0xea, 0x36, 0x7e, 0x0d, 0x64, 0x0a, 0xbc, 0x72, 0xd4, 0x59, 0x92, 0xe3, 0x0b, 0x0d, 0xaa, 0x81, 0xbf, 0xf6, + 0x91, 0xe7, 0x41, 0x6e, 0x50, 0x35, 0xd5, 0x34, 0x8b, 0x42, 0x4f, 0x31, 0xcf, 0x84, 0xcc, 0x5f, 0x35, 0x8a, 0x4e, + 0xc2, 0x8c, 0xa7, 0xc9, 0x96, 0x3a, 0xdd, 0xa7, 0x32, 0xa1, 0x80, 0xd8, 0x43, 0xe0, 0x14, 0xb8, 0xf7, 0xa6, 0xc2, + 0x3c, 0x9d, 0x92, 0x49, 0x1c, 0x9e, 0xcc, 0xb3, 0x59, 0x03, 0x66, 0xc0, 0x8d, 0x12, 0xe8, 0xe6, 0x8c, 0xfc, 0x70, + 0x0b, 0xb7, 0x55, 0x71, 0x1e, 0x93, 0x15, 0x68, 0xc9, 0x4d, 0x04, 0xc9, 0xf0, 0xca, 0xb8, 0x80, 0xd2, 0x7b, 0x13, + 0x67, 0xc6, 0xdd, 0xe2, 0xab, 0x8a, 0x4f, 0xc0, 0x79, 0xdc, 0x97, 0xdb, 0x8a, 0x53, 0x9a, 0x2a, 0x1c, 0x80, 0x92, + 0x17, 0xc4, 0x63, 0xe1, 0x9b, 0xd3, 0x2b, 0x99, 0x61, 0xe0, 0x62, 0x46, 0x35, 0x15, 0xdd, 0x85, 0x74, 0xc2, 0x74, + 0x90, 0xf0, 0x96, 0x34, 0x06, 0x77, 0x40, 0xf1, 0xbe, 0x00, 0x14, 0x11, 0x8e, 0xc2, 0x77, 0x76, 0x4c, 0x47, 0xab, + 0x92, 0xf0, 0x68, 0x99, 0x2d, 0xda, 0x79, 0xf9, 0x46, 0x25, 0xab, 0x1c, 0x70, 0x34, 0x00, 0x6c, 0x5e, 0x7f, 0x48, + 0x7c, 0x06, 0x81, 0x1c, 0x1f, 0x27, 0x76, 0x3e, 0x34, 0x4d, 0x95, 0xc2, 0x9f, 0x8d, 0xf6, 0x26, 0x2c, 0x70, 0xc7, + 0x29, 0x13, 0x3a, 0x1e, 0x1b, 0xd1, 0x0d, 0x41, 0xe7, 0x5f, 0xb0, 0x03, 0xb6, 0xda, 0x66, 0x7b, 0xbf, 0x7a, 0xbd, + 0x2c, 0x4e, 0x0e, 0x98, 0xe4, 0x9d, 0xcd, 0xbd, 0xb7, 0xdb, 0xf9, 0x2f, 0x27, 0x1f, 0x99, 0xb0, 0x40, 0xe1, 0x55, + 0x4e, 0x59, 0x64, 0x24, 0x3a, 0xa0, 0xc4, 0x2b, 0x4d, 0xe7, 0x62, 0x74, 0x2d, 0x12, 0xaf, 0x4a, 0xb1, 0x2b, 0x24, + 0xa9, 0x61, 0xe6, 0x0d, 0x38, 0xc8, 0x66, 0x9d, 0xa6, 0x46, 0x41, 0x11, 0xb2, 0xcc, 0xc5, 0xc6, 0x2c, 0xb1, 0x58, + 0xf3, 0x96, 0x33, 0x6d, 0x4e, 0x61, 0x44, 0xe0, 0xe4, 0x80, 0xa8, 0xfe, 0xac, 0xd6, 0xd8, 0xe0, 0xd6, 0xf3, 0x6a, + 0x18, 0x61, 0xe0, 0xdd, 0x50, 0x92, 0xf2, 0xc4, 0x18, 0x2b, 0x21, 0xc9, 0xa9, 0x23, 0x8e, 0xfd, 0xc8, 0xf2, 0x15, + 0xdf, 0xef, 0xb9, 0xc6, 0x14, 0x97, 0x07, 0x13, 0x63, 0x16, 0x43, 0xa6, 0x76, 0x83, 0xca, 0x22, 0xa9, 0x9f, 0x8e, + 0x6a, 0x59, 0x39, 0x93, 0x7b, 0x0c, 0xf7, 0x51, 0xe7, 0x92, 0xbc, 0x7b, 0x74, 0x05, 0x01, 0xd9, 0x5d, 0x82, 0xd5, + 0x27, 0x87, 0x24, 0x8a, 0x9c, 0xb0, 0x9f, 0xeb, 0x5f, 0x56, 0x23, 0x7f, 0x95, 0x63, 0x29, 0x5f, 0x0f, 0x79, 0x4b, + 0x19, 0x62, 0x2a, 0xad, 0xe5, 0x9e, 0x53, 0x90, 0x71, 0xe9, 0xb2, 0x6c, 0xf1, 0x40, 0x2c, 0x21, 0x7c, 0xb0, 0x98, + 0x7d, 0xde, 0x2e, 0x1c, 0xc8, 0xa4, 0x50, 0x7e, 0xdf, 0xe5, 0xf2, 0xfc, 0xf0, 0xc9, 0x46, 0x2d, 0xc6, 0x18, 0xa7, + 0x54, 0x5b, 0xdf, 0x38, 0xa8, 0x15, 0xa3, 0x0d, 0x3c, 0xbf, 0xb0, 0xdd, 0x6e, 0x6f, 0xd9, 0x2b, 0x20, 0x2b, 0x6b, + 0x2b, 0x70, 0x53, 0x27, 0x9d, 0x1f, 0x36, 0xc2, 0x49, 0x13, 0xca, 0x40, 0x7a, 0x39, 0x43, 0x2d, 0x90, 0x44, 0x37, + 0x45, 0x2d, 0x8c, 0x16, 0xeb, 0x5b, 0x8e, 0xbe, 0xf5, 0x6b, 0x18, 0x32, 0x4a, 0xe9, 0x98, 0xa6, 0xc3, 0xbd, 0x2e, + 0xd9, 0x77, 0x11, 0x44, 0x8b, 0x6c, 0xd6, 0x6c, 0xc3, 0x2f, 0x52, 0xae, 0xbd, 0x10, 0xde, 0x92, 0xe6, 0x28, 0xf2, + 0xce, 0x11, 0xa9, 0xa5, 0x70, 0xaa, 0xa9, 0xc7, 0x24, 0x1c, 0xbb, 0x04, 0x5a, 0x29, 0x98, 0xee, 0xe1, 0xc5, 0x4e, + 0xb6, 0xa1, 0xc2, 0x22, 0x2d, 0x04, 0x69, 0x14, 0x73, 0xf8, 0xfd, 0x20, 0x11, 0x59, 0x06, 0xd8, 0x79, 0xd4, 0xe7, + 0xc0, 0x1e, 0x0e, 0xe3, 0xd1, 0x55, 0x11, 0xfb, 0xee, 0x0f, 0x13, 0x14, 0x62, 0x9d, 0xa6, 0x09, 0x0a, 0xf7, 0x7c, + 0x84, 0x8e, 0xcd, 0x31, 0x3f, 0x9d, 0x85, 0xb6, 0x7d, 0xb3, 0x7a, 0x28, 0xbc, 0xfc, 0x2e, 0xc5, 0x3e, 0xa5, 0xb7, + 0xf3, 0x2f, 0xd3, 0x39, 0xc5, 0x3c, 0xb3, 0xeb, 0x14, 0x53, 0xa1, 0x89, 0xcd, 0x85, 0x97, 0x28, 0x12, 0xe7, 0xc3, + 0x4b, 0x69, 0xe0, 0xcd, 0xd0, 0x29, 0x91, 0x60, 0xdc, 0x08, 0x4f, 0x62, 0x14, 0x61, 0x0d, 0x98, 0xee, 0xe6, 0x3d, + 0x3b, 0xa9, 0x38, 0x77, 0xca, 0x92, 0x2b, 0xba, 0xfb, 0xb9, 0x41, 0x00, 0x40, 0xc5, 0xce, 0xe3, 0x0b, 0x9f, 0x2c, + 0xaf, 0xe5, 0xf4, 0x1c, 0x57, 0x86, 0x10, 0x5a, 0x1a, 0x71, 0x42, 0xb0, 0x91, 0x4a, 0x60, 0x5b, 0x61, 0xb9, 0x53, + 0x25, 0x4a, 0x06, 0x7d, 0x60, 0x8c, 0xec, 0x0e, 0x8a, 0x13, 0xd9, 0x10, 0xda, 0x9a, 0x8e, 0x08, 0x62, 0xe6, 0x7f, + 0x1f, 0xe4, 0x0c, 0xf0, 0xf8, 0xad, 0x64, 0x48, 0x85, 0x00, 0xcd, 0x1c, 0x2f, 0x2f, 0x03, 0x76, 0x31, 0xc4, 0x95, + 0xb2, 0xb8, 0x00, 0xc4, 0x66, 0x1f, 0x9b, 0x71, 0x90, 0xfb, 0x41, 0xea, 0x1c, 0xd6, 0x65, 0x07, 0xa2, 0xd2, 0x0b, + 0xe1, 0xf9, 0x35, 0x0d, 0xd5, 0xa4, 0x09, 0xe3, 0x75, 0xcc, 0x20, 0x66, 0x45, 0x69, 0x23, 0x05, 0x61, 0x95, 0x82, + 0x43, 0x60, 0x8e, 0x34, 0xcb, 0x84, 0xfa, 0xd2, 0x22, 0xc1, 0x1b, 0x0a, 0x01, 0xdb, 0xf1, 0x5a, 0xa2, 0x01, 0x1c, + 0x18, 0xb3, 0x61, 0x87, 0xa4, 0x95, 0x05, 0xf5, 0x40, 0xf9, 0x59, 0x5f, 0x78, 0x3c, 0x23, 0x99, 0xf0, 0xc1, 0x03, + 0x47, 0xf3, 0xa5, 0xa8, 0xe7, 0xe6, 0x44, 0x4b, 0xda, 0xe4, 0x10, 0x7f, 0x22, 0xaa, 0xb5, 0x68, 0xc5, 0x03, 0x5f, + 0x01, 0xa8, 0x90, 0xa6, 0x82, 0x4a, 0x64, 0x49, 0x59, 0x54, 0x64, 0x1e, 0x4c, 0xcc, 0xb5, 0x05, 0x56, 0xd6, 0xa8, + 0x77, 0xfd, 0x08, 0x7e, 0xa6, 0x84, 0x4a, 0xad, 0x3b, 0xc4, 0x3f, 0x65, 0xfc, 0x35, 0x84, 0x14, 0x99, 0x9f, 0x19, + 0xb9, 0xcb, 0x63, 0x9e, 0x3d, 0xb2, 0xfa, 0xb7, 0x7d, 0x1b, 0x8e, 0x2e, 0xdc, 0x63, 0x92, 0xab, 0xf7, 0x48, 0x64, + 0x64, 0x33, 0x01, 0x41, 0xb7, 0xf1, 0x7a, 0x38, 0x4a, 0xc5, 0x1f, 0x9b, 0x14, 0x6f, 0xab, 0x0a, 0xa2, 0xf6, 0xa2, + 0x85, 0x54, 0x93, 0x9e, 0x03, 0x69, 0xd0, 0x9c, 0x34, 0x6a, 0xd0, 0xb5, 0x07, 0x2a, 0x54, 0x04, 0xe5, 0x8f, 0x0e, + 0x27, 0x26, 0xca, 0xf0, 0x14, 0xee, 0x2f, 0x4c, 0x46, 0x98, 0x39, 0x02, 0x55, 0xed, 0xa2, 0xe5, 0xf3, 0x16, 0x63, + 0x02, 0x79, 0xef, 0xfe, 0x0d, 0xf3, 0x23, 0xaa, 0x61, 0xb3, 0xe5, 0xbf, 0xf9, 0x33, 0x4f, 0x29, 0x2a, 0x27, 0xc2, + 0x54, 0x48, 0xa8, 0xb1, 0xb3, 0xa4, 0xd0, 0xa3, 0x8b, 0x58, 0xc3, 0x2c, 0x3f, 0x59, 0x73, 0x75, 0x63, 0x08, 0x19, + 0x23, 0x10, 0x9c, 0x21, 0xc4, 0xa8, 0xf2, 0x44, 0x39, 0x78, 0x9b, 0x00, 0xb8, 0x04, 0xf5, 0x18, 0x2c, 0x73, 0xfb, + 0x12, 0xa1, 0xb9, 0x9c, 0xf7, 0x1f, 0x71, 0x29, 0x19, 0x29, 0x7e, 0x96, 0x97, 0x59, 0xf7, 0x52, 0x79, 0x2a, 0x6e, + 0x5d, 0xd1, 0x56, 0xdc, 0x06, 0x0f, 0x98, 0x82, 0x3e, 0xca, 0x4a, 0x6d, 0xf4, 0x69, 0x16, 0x35, 0xbb, 0x64, 0xdb, + 0x63, 0x77, 0x63, 0x79, 0x52, 0x70, 0x27, 0xbc, 0x84, 0x69, 0x19, 0x4a, 0xdd, 0x36, 0x5e, 0x8a, 0x7d, 0x38, 0xc7, + 0x79, 0xf9, 0x2d, 0x53, 0xbc, 0xff, 0x8e, 0xe2, 0xd3, 0xd7, 0x2c, 0xcd, 0x30, 0x3f, 0x3a, 0x2a, 0x94, 0xc0, 0xcc, + 0x7a, 0xac, 0xe6, 0x51, 0x12, 0x6b, 0x28, 0x40, 0x87, 0x05, 0x43, 0x7b, 0xbc, 0xde, 0x82, 0x78, 0xa8, 0xb2, 0x1e, + 0x2c, 0xbc, 0x27, 0x33, 0x74, 0xb5, 0xa4, 0x0d, 0xad, 0x6b, 0xa2, 0xa2, 0x4f, 0xef, 0x91, 0xc5, 0xdd, 0xfa, 0x38, + 0x4e, 0x9a, 0x18, 0x15, 0x97, 0xa2, 0xdd, 0x59, 0x37, 0x5e, 0x84, 0x99, 0x78, 0x8c, 0x9c, 0x11, 0x19, 0x6b, 0xe9, + 0x8c, 0x96, 0xdc, 0xba, 0x04, 0x99, 0x4f, 0x06, 0x7a, 0x27, 0x50, 0x7e, 0xfe, 0x28, 0x07, 0x8e, 0x08, 0x00, 0xb5, + 0xdb, 0x16, 0x84, 0x2c, 0x38, 0xf0, 0xbf, 0x2c, 0xb7, 0x7d, 0x9f, 0xbc, 0xb3, 0x7c, 0x31, 0x2b, 0xe0, 0x7c, 0x63, + 0xa3, 0xbd, 0x11, 0xb7, 0xb3, 0x91, 0x0d, 0x1d, 0x4f, 0x24, 0xd4, 0x1f, 0x66, 0xe6, 0xd9, 0x31, 0x1f, 0x18, 0x09, + 0xce, 0x46, 0x47, 0x60, 0xbb, 0x2a, 0x73, 0xfd, 0x37, 0x49, 0xde, 0x59, 0x12, 0x23, 0x5c, 0x51, 0x81, 0xac, 0x1e, + 0x64, 0x41, 0xb0, 0xb8, 0xa5, 0x2b, 0xba, 0xde, 0xe7, 0x15, 0x89, 0x36, 0x91, 0x29, 0xb9, 0x1e, 0x20, 0xa0, 0xab, + 0x57, 0x3d, 0x3e, 0x55, 0xf7, 0xc6, 0xfb, 0x52, 0xe3, 0x85, 0xf6, 0xf7, 0xb5, 0x9d, 0x3a, 0xdc, 0xbc, 0xe7, 0x7a, + 0x9e, 0xf6, 0x8f, 0x12, 0x75, 0x7a, 0x2d, 0x32, 0x9e, 0x87, 0xfb, 0x35, 0x94, 0xd3, 0x48, 0x35, 0x69, 0x25, 0xa1, + 0xb8, 0x11, 0xcb, 0xbc, 0xe3, 0x96, 0x07, 0xbc, 0x0a, 0xbf, 0x69, 0xb5, 0x10, 0xef, 0x7d, 0x64, 0x58, 0x9e, 0x7a, + 0x77, 0x34, 0x61, 0xf6, 0x50, 0x2b, 0xbb, 0x29, 0x26, 0x60, 0x3f, 0xad, 0xfe, 0xc9, 0xae, 0xca, 0x17, 0x17, 0xf3, + 0x3f, 0xbb, 0xae, 0xb2, 0x91, 0xf1, 0x64, 0x9a, 0xf5, 0xdd, 0xf3, 0xf9, 0x87, 0x3f, 0x7b, 0x1a, 0x4e, 0xe6, 0xc3, + 0x2c, 0xda, 0x7f, 0x29, 0xbf, 0x2c, 0x1a, 0x42, 0x5b, 0xfe, 0xe8, 0x2c, 0xf5, 0xcc, 0x42, 0xef, 0x85, 0x00, 0x3e, + 0x2d, 0xda, 0x1d, 0x1c, 0xd5, 0x74, 0x3d, 0x8c, 0xf7, 0x41, 0x0b, 0xb7, 0x2e, 0x77, 0x20, 0xce, 0x6c, 0xc4, 0x49, + 0x7e, 0x51, 0xb3, 0xeb, 0x5d, 0x36, 0xb3, 0x69, 0x97, 0xbf, 0x23, 0x08, 0x9f, 0x4d, 0x90, 0xd1, 0x3a, 0xd6, 0x36, + 0xa9, 0xbc, 0x0a, 0x2c, 0xff, 0x8d, 0xe4, 0xd3, 0xb9, 0x36, 0x8c, 0x6e, 0x05, 0xfb, 0xf9, 0xa7, 0xf0, 0x6d, 0xd5, + 0x77, 0xc7, 0x9d, 0xff, 0x7c, 0xba, 0x5f, 0xfd, 0x3b, 0xd5, 0x93, 0xb9, 0x59, 0xf4, 0xde, 0xf3, 0xe0, 0xfb, 0xd3, + 0xa0, 0xdb, 0xb6, 0x7a, 0xb2, 0x7d, 0xfc, 0x7f, 0x17, 0xef, 0xff, 0x27, 0xfa, 0xfa, 0x15, 0x7b, 0x64, 0x3e, 0xf4, + 0xe2, 0xf8, 0x6c, 0xea, 0xe2, 0xf2, 0xff, 0x5c, 0xab, 0xdb, 0x3b, 0xcf, 0x6a, 0xb4, 0x6b, 0x6e, 0xb9, 0xee, 0xb5, + 0xed, 0x32, 0xaa, 0x9c, 0xc0, 0xad, 0xff, 0x7a, 0xea, 0x2b, 0x08, 0xf2, 0x79, 0xe3, 0xe5, 0x7e, 0xf6, 0xf0, 0xb6, + 0x26, 0xb3, 0xcf, 0x96, 0x7b, 0x27, 0x2f, 0xfc, 0xbc, 0x1e, 0x6c, 0xfb, 0x54, 0xea, 0x28, 0xd5, 0xac, 0xf8, 0x61, + 0x4d, 0x72, 0xb6, 0x2b, 0xe7, 0xfc, 0xd3, 0xf2, 0x79, 0xfd, 0xff, 0xc5, 0xf3, 0x6f, 0x76, 0xba, 0x47, 0xd8, 0x6f, + 0x61, 0x5f, 0x63, 0x3f, 0xab, 0x4f, 0xe6, 0x70, 0xf5, 0x29, 0xde, 0xba, 0xee, 0x6d, 0xb5, 0x6b, 0xee, 0xe2, 0x8a, + 0x39, 0x75, 0xc9, 0x95, 0xb3, 0x7a, 0x2a, 0x88, 0x0b, 0x62, 0x11, 0x48, 0x7c, 0x57, 0xff, 0x1d, 0xdc, 0xd5, 0x43, + 0xef, 0x95, 0x8b, 0x6d, 0xcf, 0x6d, 0x02, 0xec, 0x9a, 0xc8, 0x8c, 0x11, 0x2b, 0x62, 0x1b, 0xed, 0x28, 0xf0, 0x01, + 0x8b, 0xb6, 0xcf, 0x9f, 0x12, 0x9f, 0xf5, 0xbb, 0x90, 0x6b, 0x70, 0x7d, 0x7f, 0xfd, 0x5a, 0x3c, 0x93, 0x3f, 0x08, + 0x29, 0xfa, 0xf9, 0xc0, 0x53, 0xfd, 0x8b, 0x54, 0x98, 0x42, 0x54, 0x27, 0x2c, 0x6b, 0x86, 0x7d, 0x65, 0xdf, 0x47, + 0x8b, 0x43, 0x05, 0x6a, 0x3d, 0xde, 0x27, 0x44, 0x3e, 0xaa, 0x48, 0x77, 0xf9, 0x77, 0x2a, 0x82, 0xc0, 0x88, 0x28, + 0x30, 0x34, 0xc8, 0xa7, 0x9d, 0x03, 0x7f, 0x81, 0xe5, 0xfe, 0xa2, 0xb8, 0x7a, 0xb7, 0x4b, 0x4a, 0x35, 0x5c, 0xcd, + 0x1b, 0xeb, 0x14, 0x20, 0x25, 0x36, 0x11, 0x01, 0xc3, 0x7f, 0x72, 0xe5, 0x0b, 0xb6, 0x5e, 0xe7, 0x69, 0x3e, 0x19, + 0x55, 0xa7, 0x26, 0x37, 0x5b, 0x0b, 0x23, 0x5d, 0x0b, 0xaf, 0x3a, 0x3a, 0xe1, 0x94, 0x63, 0xcc, 0x57, 0x94, 0x36, + 0x1e, 0x71, 0xa7, 0xfe, 0xf6, 0x2a, 0xfe, 0xdc, 0x80, 0x84, 0x72, 0xcb, 0x6c, 0x70, 0x95, 0x99, 0xbd, 0x56, 0x70, + 0xa3, 0x24, 0x4c, 0xf1, 0x12, 0xf2, 0x8c, 0xdf, 0x73, 0xb1, 0x32, 0x05, 0xb6, 0x69, 0x7a, 0xff, 0xc7, 0xf6, 0x24, + 0x28, 0x04, 0x3a, 0xf1, 0x52, 0x80, 0x04, 0xaa, 0xbe, 0x21, 0xc8, 0x37, 0x3d, 0xe2, 0xa0, 0xad, 0xa9, 0x8d, 0xf3, + 0x4c, 0xb3, 0x68, 0x33, 0xde, 0xd5, 0x95, 0x92, 0xb9, 0x9e, 0x11, 0x8d, 0xbc, 0xb6, 0xed, 0xf5, 0x66, 0xcc, 0xec, + 0x7a, 0x38, 0x07, 0x74, 0xbe, 0x0e, 0xa0, 0x9f, 0x55, 0x07, 0x96, 0x2a, 0x4e, 0x7f, 0xb9, 0x03, 0x32, 0xb0, 0xa4, + 0x43, 0x0f, 0x53, 0x0a, 0x9f, 0xa5, 0xf4, 0x1f, 0x01, 0x3b, 0x05, 0x0e, 0x4b, 0x39, 0x16, 0xd9, 0x8d, 0xd9, 0xcc, + 0xda, 0xd6, 0xe4, 0xa4, 0x33, 0x17, 0x51, 0xf6, 0x7c, 0x6d, 0x57, 0xff, 0x5d, 0xac, 0xfa, 0x78, 0xc9, 0xc6, 0x29, + 0x20, 0x05, 0x05, 0x05, 0xb1, 0xc7, 0xf2, 0xf3, 0xd0, 0x2f, 0x19, 0x91, 0x41, 0x34, 0xbd, 0x1a, 0x74, 0xfa, 0x79, + 0xd2, 0x5f, 0x58, 0x94, 0xd4, 0x4a, 0xe8, 0x0f, 0xb8, 0xe1, 0x03, 0x77, 0xe7, 0x8c, 0x38, 0x37, 0x84, 0xfc, 0x34, + 0xf5, 0x23, 0xe6, 0x6e, 0xe6, 0x64, 0x16, 0x3f, 0x07, 0x72, 0xd2, 0x82, 0xd5, 0xf8, 0x13, 0x36, 0x82, 0xdc, 0x37, + 0x7e, 0x69, 0xa9, 0x02, 0x47, 0x97, 0x2b, 0xe9, 0xe9, 0xd2, 0xc9, 0x62, 0xa1, 0x75, 0x70, 0x77, 0x8a, 0xe0, 0x8b, + 0xb7, 0xc2, 0x3a, 0xff, 0xe5, 0x72, 0xeb, 0xae, 0x38, 0x1b, 0x36, 0xa2, 0x7e, 0x76, 0xbf, 0xba, 0x75, 0x1a, 0xbe, + 0x47, 0xbf, 0x8b, 0x57, 0xdf, 0xf5, 0x78, 0x21, 0x47, 0xb7, 0x6e, 0x0d, 0x56, 0xf3, 0x74, 0x05, 0x45, 0x33, 0xb0, + 0x2f, 0x5e, 0x94, 0x83, 0x2f, 0x60, 0x34, 0xee, 0x78, 0x11, 0x6c, 0x0a, 0xed, 0xf3, 0xce, 0xf3, 0xe5, 0x15, 0x55, + 0x93, 0x85, 0xf4, 0x76, 0xcd, 0xc6, 0xf0, 0xfe, 0xba, 0xbd, 0xf9, 0x59, 0xfc, 0x54, 0x7c, 0x85, 0x46, 0xe8, 0xb7, + 0x0b, 0x40, 0xba, 0xfa, 0x92, 0x15, 0x8f, 0x3e, 0x6f, 0x85, 0x58, 0xcd, 0xf7, 0x8e, 0x67, 0xb1, 0x42, 0xe0, 0xe3, + 0x5b, 0xd1, 0xeb, 0x25, 0x3c, 0xb3, 0x9a, 0x75, 0x82, 0xb9, 0x03, 0x88, 0x50, 0x44, 0x1a, 0x34, 0x11, 0xe4, 0xbb, + 0xb3, 0x61, 0x2e, 0x54, 0x67, 0x35, 0x2f, 0xff, 0xbc, 0xbc, 0xa2, 0x1e, 0x51, 0x2f, 0x7e, 0x73, 0xbd, 0x81, 0x44, + 0xab, 0xae, 0x85, 0x1a, 0xbf, 0x4b, 0x8d, 0x53, 0xe6, 0x41, 0x03, 0x24, 0x69, 0xe8, 0x50, 0xcb, 0xfb, 0x10, 0x8f, + 0x8b, 0xed, 0x6b, 0x34, 0x7e, 0xdf, 0xc4, 0x5a, 0x11, 0x15, 0x79, 0x4f, 0x81, 0x2c, 0x0e, 0x94, 0x50, 0x5a, 0x5e, + 0xc8, 0xdf, 0x15, 0xa6, 0x5a, 0x64, 0xce, 0xc3, 0xc4, 0x59, 0xa0, 0xfe, 0x7a, 0xf4, 0xd4, 0xfb, 0x52, 0x07, 0x7c, + 0x63, 0x61, 0x03, 0xd7, 0x73, 0x43, 0x8d, 0x60, 0x1c, 0xa4, 0x48, 0x35, 0xa8, 0x0d, 0x33, 0x6a, 0x30, 0x79, 0x4d, + 0xc7, 0x96, 0x62, 0xaf, 0xa4, 0x3f, 0xe4, 0x99, 0x2e, 0xac, 0xf2, 0x6d, 0xd5, 0x23, 0x3b, 0xbd, 0x8d, 0x0b, 0xfe, + 0x59, 0xf3, 0xde, 0x7c, 0x8d, 0x78, 0xba, 0x6c, 0x48, 0xb0, 0xa4, 0xe7, 0xc9, 0x71, 0xeb, 0xfc, 0xa2, 0xba, 0x9e, + 0xab, 0x78, 0x04, 0x99, 0x12, 0x2e, 0x09, 0xb9, 0x8e, 0x73, 0xbd, 0x97, 0x30, 0x10, 0x21, 0x5f, 0xd7, 0x67, 0x09, + 0x50, 0xda, 0xd6, 0x97, 0x50, 0x0b, 0x1f, 0x4a, 0xb3, 0xaf, 0xdd, 0x12, 0x8e, 0xcc, 0x7d, 0x57, 0xec, 0x5d, 0x1d, + 0x39, 0x5d, 0x38, 0x24, 0x15, 0xa0, 0xef, 0xb9, 0xe8, 0xf0, 0x8d, 0xb1, 0xb0, 0x48, 0xc4, 0xa6, 0x37, 0xd1, 0x8d, + 0x66, 0xdd, 0xe0, 0x57, 0x27, 0x9d, 0x06, 0x5f, 0x1b, 0x38, 0x15, 0xb1, 0xa6, 0x00, 0x3b, 0xf4, 0xb8, 0xb1, 0x3b, + 0x40, 0x88, 0x6f, 0x5a, 0xed, 0x84, 0xce, 0xd6, 0x8d, 0x23, 0x14, 0x04, 0x03, 0xea, 0x45, 0xd4, 0x8a, 0xf2, 0xfb, + 0x4a, 0x27, 0x11, 0xf5, 0x06, 0xbf, 0x7b, 0x7d, 0xef, 0xe5, 0xa9, 0x2a, 0xbe, 0xde, 0xab, 0x07, 0x7a, 0xb2, 0x1d, + 0x90, 0xd8, 0x34, 0x15, 0x6b, 0x40, 0x93, 0x67, 0x4e, 0x81, 0xb8, 0xc1, 0x3c, 0xf7, 0x61, 0x3f, 0xc6, 0x7c, 0x8d, + 0x40, 0x8d, 0x4e, 0x84, 0x6a, 0x49, 0xa4, 0x2f, 0x57, 0x2a, 0x63, 0xdd, 0x2c, 0xe2, 0xd9, 0x45, 0x93, 0xf7, 0x7f, + 0x6f, 0x1c, 0x5c, 0xd7, 0x54, 0x50, 0x6e, 0xda, 0x33, 0xff, 0xeb, 0x9a, 0xc2, 0x06, 0xc0, 0x03, 0x7c, 0x5d, 0x42, + 0x72, 0x65, 0xc0, 0x87, 0x6f, 0x0a, 0x75, 0x5e, 0xdd, 0xe6, 0x2d, 0x64, 0x65, 0x40, 0xe4, 0xb4, 0x7d, 0x80, 0xf0, + 0x36, 0x60, 0x0c, 0x23, 0x2e, 0x40, 0xf4, 0xd1, 0x31, 0xdd, 0xaa, 0x69, 0x20, 0xee, 0xca, 0x56, 0x1f, 0xcf, 0x04, + 0xbc, 0x12, 0x7e, 0x63, 0x18, 0xc7, 0x10, 0xe2, 0x33, 0x8e, 0xed, 0xf1, 0x25, 0x54, 0x0e, 0x34, 0xca, 0x83, 0x55, + 0x79, 0xfc, 0x39, 0xf7, 0xfe, 0xea, 0xab, 0x66, 0x64, 0xd3, 0x80, 0xb9, 0xd1, 0xb4, 0xa1, 0x63, 0xc5, 0xba, 0xe4, + 0x6f, 0xbd, 0x43, 0x63, 0xb0, 0x2f, 0x5b, 0x5f, 0xc2, 0xfd, 0x4e, 0x46, 0xc3, 0x0d, 0x8c, 0xc0, 0x18, 0x0c, 0x54, + 0x95, 0x52, 0x90, 0xfe, 0xe2, 0xa5, 0x9d, 0x8b, 0x92, 0xf7, 0xa4, 0x93, 0xbd, 0x11, 0xca, 0x83, 0x42, 0x8b, 0x81, + 0x8b, 0x7e, 0x53, 0x6b, 0x25, 0xee, 0x63, 0xf9, 0xae, 0x8f, 0xd6, 0x54, 0xba, 0x99, 0x11, 0xd9, 0x52, 0x87, 0x51, + 0xdc, 0x9c, 0x3b, 0x69, 0xe6, 0xa1, 0x53, 0x60, 0x91, 0xe6, 0x66, 0x79, 0x00, 0xe2, 0x5b, 0xd4, 0xc8, 0xe6, 0x94, + 0xff, 0x89, 0x47, 0xdd, 0x40, 0x88, 0xc8, 0xb2, 0xbe, 0x6b, 0x8a, 0x33, 0x28, 0x94, 0xe4, 0x06, 0x85, 0xf0, 0xde, + 0xc8, 0xa0, 0x40, 0xc9, 0x52, 0xd9, 0x48, 0xfa, 0xfd, 0x27, 0x1e, 0x54, 0xe8, 0xe9, 0xce, 0x91, 0x64, 0xeb, 0x36, + 0x0b, 0x6b, 0x28, 0x8d, 0x32, 0x31, 0xbb, 0xd9, 0xc9, 0xb7, 0x05, 0x05, 0x45, 0x49, 0x39, 0x51, 0xa4, 0x19, 0x0e, + 0x77, 0xfa, 0x5f, 0xee, 0x51, 0xde, 0xb1, 0x40, 0xb9, 0xcd, 0x9c, 0x96, 0x00, 0x01, 0x44, 0xfd, 0x5c, 0x40, 0x34, + 0x51, 0xa4, 0x14, 0x72, 0x79, 0x23, 0x2f, 0xf3, 0xd1, 0xad, 0x79, 0xca, 0x41, 0xfb, 0xca, 0xfe, 0x94, 0x30, 0xe7, + 0xb6, 0x92, 0x3e, 0x92, 0x31, 0x31, 0x52, 0x17, 0xdc, 0xd0, 0x81, 0x61, 0xbd, 0x77, 0xe8, 0xe5, 0x53, 0x63, 0xc2, + 0xef, 0x2f, 0x82, 0x22, 0x88, 0x42, 0x00, 0x80, 0x69, 0x59, 0xb6, 0xa4, 0xf0, 0x49, 0x12, 0x05, 0x90, 0xf5, 0xb8, + 0xf4, 0xc0, 0xb5, 0x24, 0x30, 0x3c, 0xaa, 0x09, 0x68, 0xde, 0x2e, 0x50, 0x38, 0xa0, 0x85, 0x95, 0xeb, 0xb0, 0x76, + 0x42, 0xaa, 0x26, 0x45, 0xab, 0x9b, 0xd5, 0x92, 0x94, 0x67, 0x06, 0x4c, 0x15, 0x91, 0xa7, 0xf5, 0x3f, 0x64, 0xbe, + 0xb4, 0x40, 0xf4, 0xc6, 0x7c, 0x16, 0x5c, 0x3f, 0x56, 0x3b, 0x8e, 0x5e, 0x37, 0x4c, 0x6b, 0x37, 0x48, 0x02, 0x44, + 0x3e, 0x95, 0xd5, 0xd5, 0x2b, 0x15, 0xa4, 0xa1, 0xc6, 0x8f, 0x7c, 0xaf, 0x14, 0xe4, 0x4a, 0xe9, 0xa5, 0xa0, 0x00, + 0xdd, 0x78, 0xe9, 0x88, 0xa7, 0x6c, 0xe9, 0xc5, 0xa6, 0xb0, 0x71, 0xc2, 0xd8, 0xeb, 0xd9, 0x8a, 0x93, 0x7a, 0xec, + 0xaa, 0x4e, 0xb2, 0x04, 0x5d, 0x5c, 0x4b, 0x5e, 0xc5, 0x91, 0xe9, 0xd2, 0xd4, 0x31, 0xf5, 0xef, 0x1a, 0xed, 0x89, + 0x15, 0xba, 0xff, 0x2d, 0x91, 0x3b, 0xaf, 0x4c, 0xd3, 0x02, 0x41, 0xd6, 0x82, 0x90, 0xe0, 0x7c, 0x27, 0x44, 0x1e, + 0x95, 0xc7, 0xa4, 0x65, 0xee, 0xf1, 0xb5, 0x6e, 0xc6, 0x53, 0xda, 0x03, 0x51, 0x3e, 0xcc, 0x71, 0x97, 0x12, 0xe6, + 0x9e, 0x3d, 0xb0, 0x32, 0x4c, 0x4c, 0xec, 0x83, 0x1e, 0x3d, 0xae, 0x59, 0x01, 0xc1, 0x30, 0xfd, 0xda, 0xa5, 0xdd, + 0xed, 0xfa, 0x61, 0x0b, 0xe0, 0x5d, 0x2e, 0x84, 0xfa, 0xb9, 0x3a, 0x71, 0x53, 0x78, 0x75, 0x83, 0xb6, 0x8a, 0xd5, + 0x1a, 0x54, 0xb4, 0xdc, 0xa1, 0x6d, 0xeb, 0xcf, 0x69, 0x06, 0x1f, 0x3b, 0x39, 0x10, 0x6a, 0x3a, 0x22, 0x98, 0x89, + 0x72, 0x24, 0x0d, 0x9e, 0xb8, 0xea, 0x6c, 0x91, 0xaa, 0x77, 0x73, 0x02, 0x64, 0x48, 0xea, 0x0b, 0x32, 0x84, 0x36, + 0x20, 0x74, 0xac, 0xa9, 0xf2, 0xb5, 0x41, 0xed, 0xd6, 0x13, 0x63, 0x6f, 0xdf, 0x84, 0x16, 0x45, 0x85, 0xbe, 0x2d, + 0x16, 0xbb, 0x64, 0x8c, 0xbe, 0xe0, 0x6f, 0xcc, 0x7e, 0x92, 0xd1, 0xc3, 0x67, 0xb5, 0xd1, 0x45, 0x3b, 0x88, 0x5d, + 0x60, 0xfc, 0xa3, 0xc9, 0xdb, 0x9a, 0x55, 0x5c, 0x7e, 0x75, 0x41, 0x55, 0xab, 0xd9, 0x62, 0xfa, 0xb3, 0xae, 0x70, + 0x89, 0x64, 0xa2, 0xc4, 0x0c, 0x7f, 0x10, 0x68, 0xf5, 0x7d, 0xe0, 0xdc, 0xe7, 0xb9, 0x9e, 0xa0, 0xff, 0xe2, 0xa5, + 0x77, 0x28, 0xa3, 0x42, 0xc8, 0xc7, 0x91, 0x2f, 0xa4, 0x88, 0x55, 0xba, 0x7b, 0xb4, 0xd1, 0x12, 0x39, 0x0b, 0x64, + 0xcd, 0x6a, 0xca, 0x34, 0xd4, 0x85, 0x63, 0x8b, 0x2e, 0xd3, 0x6c, 0x17, 0xd0, 0x32, 0xac, 0xa4, 0x63, 0xeb, 0x9d, + 0x05, 0x84, 0x22, 0x22, 0x80, 0x29, 0x69, 0xf0, 0x9f, 0x5d, 0x69, 0x8b, 0xc5, 0xdc, 0xb4, 0x94, 0x7d, 0x2e, 0x23, + 0x31, 0xd7, 0x13, 0xb2, 0x1b, 0xb8, 0x5e, 0xdc, 0x08, 0x4d, 0x6b, 0x84, 0xe4, 0x24, 0xd1, 0xa3, 0x5e, 0xa8, 0x37, + 0x55, 0x59, 0x34, 0x7a, 0x40, 0x54, 0x03, 0xd7, 0xbb, 0x69, 0x27, 0x52, 0x12, 0x2c, 0x15, 0xdd, 0x07, 0x6e, 0xdd, + 0xad, 0x75, 0x98, 0x70, 0x85, 0x9c, 0x97, 0x50, 0x23, 0x86, 0x84, 0xfb, 0x80, 0x3d, 0x94, 0x4c, 0x00, 0x98, 0x82, + 0x13, 0x68, 0x09, 0xb0, 0xed, 0x56, 0x50, 0x02, 0x06, 0xac, 0xcc, 0x34, 0xa2, 0x30, 0xf3, 0xd0, 0x15, 0x26, 0xe4, + 0x38, 0x37, 0x8f, 0x3a, 0x58, 0x90, 0x2a, 0x44, 0xdb, 0xef, 0x4d, 0x0f, 0xe6, 0x38, 0x33, 0xce, 0x91, 0x0b, 0x80, + 0xe3, 0x2d, 0x28, 0xd5, 0x30, 0x34, 0x6c, 0xff, 0xaa, 0xc9, 0x2a, 0x67, 0x44, 0x62, 0xd5, 0x2b, 0x9b, 0xfd, 0x2a, + 0xfe, 0x18, 0x0a, 0x2a, 0x69, 0x3a, 0xbe, 0x49, 0x4c, 0x3d, 0x5b, 0x5e, 0x7d, 0x65, 0x78, 0xd2, 0xb3, 0x7d, 0xc0, + 0x15, 0x8f, 0xc0, 0xba, 0x29, 0xf9, 0x51, 0x27, 0x83, 0x06, 0xe0, 0xa8, 0x45, 0x3b, 0x54, 0x9d, 0x62, 0x10, 0x30, + 0xe2, 0x74, 0x5a, 0x96, 0xfc, 0x25, 0x8a, 0x0d, 0x34, 0xf1, 0x18, 0x2f, 0x58, 0x3a, 0xb1, 0xbd, 0xa3, 0xf9, 0xaa, + 0x44, 0x23, 0xcb, 0xac, 0x0d, 0x92, 0xfc, 0x36, 0xbd, 0xd6, 0x2a, 0x23, 0xed, 0x6d, 0xd9, 0x21, 0xfe, 0x11, 0xc8, + 0x82, 0x31, 0xa3, 0x22, 0x51, 0x31, 0x45, 0xc6, 0xa5, 0xf1, 0x56, 0xb2, 0x00, 0x5d, 0xa6, 0x67, 0x6b, 0xf3, 0x8a, + 0xbc, 0x7d, 0x12, 0xcd, 0x7d, 0x30, 0x55, 0x61, 0xff, 0x72, 0x34, 0x5b, 0x1e, 0xab, 0xf0, 0x8f, 0x55, 0x75, 0x04, + 0x9a, 0xb6, 0xab, 0xa7, 0x40, 0x8e, 0x4e, 0xd5, 0xc5, 0x21, 0x39, 0xf6, 0xc2, 0x8c, 0x43, 0x12, 0x72, 0xb2, 0x78, + 0x1b, 0xac, 0x4f, 0x32, 0xb4, 0x46, 0x80, 0x2f, 0x17, 0x61, 0xd5, 0x2b, 0xcd, 0x1e, 0x65, 0xb2, 0x1a, 0x59, 0x2b, + 0x28, 0x4d, 0x10, 0x45, 0xf3, 0x14, 0x09, 0x03, 0xcf, 0x72, 0xa2, 0x30, 0x61, 0x38, 0x25, 0xec, 0x43, 0xa2, 0x8b, + 0xf6, 0x0f, 0x33, 0xcb, 0x87, 0x12, 0xb0, 0xa5, 0x79, 0x12, 0x20, 0x46, 0x80, 0x51, 0xa5, 0x58, 0xd1, 0x3f, 0x38, + 0x4f, 0x1c, 0x0f, 0x73, 0x49, 0x22, 0x3f, 0xe3, 0xfd, 0x91, 0x79, 0xd3, 0xcd, 0xcb, 0x23, 0xdb, 0x90, 0x26, 0x66, + 0xaa, 0xa7, 0x70, 0x8d, 0xd8, 0x6e, 0xbb, 0x80, 0x2d, 0x54, 0xba, 0x41, 0xb5, 0x2f, 0x8a, 0x20, 0xf4, 0x2f, 0x75, + 0x90, 0xd6, 0xfc, 0x37, 0x47, 0x1b, 0x4c, 0x8c, 0xde, 0x64, 0x07, 0x8c, 0xfb, 0x66, 0xaa, 0xba, 0x96, 0x40, 0xc7, + 0xa6, 0x2a, 0xfc, 0x76, 0x70, 0x09, 0x89, 0xb9, 0x32, 0x16, 0xbd, 0xd5, 0x19, 0x59, 0xe5, 0xfe, 0xdf, 0x36, 0x1d, + 0x41, 0xb7, 0x7f, 0x9d, 0x5d, 0xcd, 0xce, 0x03, 0x64, 0x91, 0x07, 0x8e, 0x88, 0xa5, 0x7a, 0x6a, 0xf3, 0x68, 0x58, + 0x58, 0xaa, 0x2b, 0xc7, 0xfb, 0xb8, 0x92, 0x36, 0x9f, 0x97, 0x86, 0x03, 0x22, 0x72, 0x30, 0xbd, 0x35, 0xf0, 0x5b, + 0x24, 0x32, 0xaf, 0x6a, 0x1c, 0xd1, 0xa9, 0x8b, 0x71, 0x31, 0xae, 0x15, 0x94, 0x46, 0x7e, 0xdc, 0x49, 0x3f, 0x46, + 0x47, 0x4b, 0x1f, 0x9f, 0x6e, 0xad, 0x8a, 0xee, 0xd5, 0x2f, 0x72, 0x28, 0xe6, 0x65, 0x19, 0x1d, 0x08, 0x19, 0x24, + 0x7b, 0x4f, 0xbe, 0xf3, 0x9e, 0xb8, 0xcc, 0x45, 0x4f, 0x8d, 0x0a, 0x0e, 0xbd, 0xbd, 0x8d, 0x2c, 0x53, 0x39, 0x72, + 0x07, 0xcc, 0xce, 0xf8, 0xda, 0xde, 0x40, 0x6c, 0xef, 0x85, 0xc8, 0xad, 0xf0, 0x48, 0x61, 0xfa, 0x71, 0x65, 0x84, + 0xab, 0x31, 0xe9, 0x50, 0x99, 0x4c, 0xf3, 0xc2, 0x2e, 0x57, 0x59, 0xd0, 0x61, 0x19, 0x54, 0x33, 0x99, 0x99, 0x66, + 0xb2, 0x69, 0xa4, 0xe1, 0x0a, 0xc5, 0x34, 0x06, 0x2e, 0x97, 0x2a, 0x52, 0xf6, 0xbc, 0x92, 0xa5, 0xe7, 0x38, 0x0b, + 0x1d, 0xa6, 0x4d, 0x07, 0xcf, 0x53, 0xe2, 0x92, 0x70, 0x84, 0x35, 0x13, 0x4c, 0x93, 0xac, 0xb4, 0x40, 0xb9, 0xa8, + 0xa4, 0x18, 0xba, 0x3e, 0xaf, 0x24, 0x65, 0xee, 0x68, 0x19, 0x4f, 0x69, 0xf4, 0x8c, 0xf2, 0x15, 0xb5, 0x66, 0xfe, + 0xc9, 0xf2, 0xef, 0x20, 0x85, 0xd6, 0x57, 0x40, 0x05, 0xa6, 0x14, 0xac, 0x04, 0xf9, 0xfb, 0xc5, 0x8d, 0x56, 0x11, + 0x97, 0x82, 0xf3, 0x2a, 0xe6, 0x65, 0x53, 0x0d, 0x69, 0xbe, 0xfe, 0xe4, 0x7f, 0xa6, 0x93, 0x83, 0x4a, 0x1c, 0x6e, + 0x00, 0x33, 0x86, 0x5c, 0x2c, 0xe8, 0x4f, 0xa5, 0x57, 0x5f, 0xa9, 0x97, 0xa2, 0x46, 0x5d, 0xe8, 0xee, 0x96, 0xdc, + 0x5a, 0xcf, 0x46, 0x9a, 0x68, 0x56, 0x2a, 0xdf, 0x0f, 0x92, 0x66, 0x86, 0x1a, 0xe1, 0x62, 0x2f, 0x36, 0x60, 0xdc, + 0x1a, 0xa7, 0x50, 0x7b, 0x2f, 0x59, 0xc2, 0x67, 0x8b, 0xcb, 0x41, 0x95, 0xc2, 0x18, 0xdf, 0x81, 0xb9, 0x21, 0xf7, + 0xc1, 0x93, 0xde, 0x7e, 0xbb, 0xf3, 0x53, 0xbc, 0x0c, 0xec, 0x12, 0x11, 0x0f, 0xa2, 0xdf, 0xdc, 0x2a, 0x6d, 0xaf, + 0x37, 0x16, 0x36, 0x57, 0xc5, 0x0f, 0x2a, 0x55, 0xe0, 0xce, 0x2b, 0x77, 0x61, 0x50, 0x1e, 0x41, 0x0e, 0xfa, 0x4d, + 0xe3, 0xe6, 0x7e, 0x27, 0x54, 0x61, 0xd8, 0xa5, 0x87, 0x49, 0x59, 0xe7, 0x4b, 0x7a, 0x18, 0x33, 0xc4, 0xce, 0xcc, + 0x32, 0xa9, 0xd0, 0xae, 0x65, 0x41, 0xe3, 0xa7, 0xe0, 0x8f, 0x28, 0xa3, 0x48, 0x2b, 0x26, 0xb0, 0x0f, 0x32, 0x01, + 0xc7, 0x07, 0xc1, 0xa8, 0x2e, 0xe2, 0x13, 0x9c, 0xee, 0xce, 0x0b, 0x0e, 0x54, 0x32, 0xb4, 0x48, 0xb0, 0xc4, 0x1e, + 0xf1, 0xb0, 0xa9, 0x1f, 0xec, 0x9d, 0xda, 0x55, 0x38, 0x6f, 0x16, 0xeb, 0x31, 0x48, 0xf5, 0xfc, 0xb6, 0xf9, 0x84, + 0x03, 0xfc, 0x51, 0x9d, 0xea, 0xf1, 0x4d, 0x1d, 0xaf, 0x71, 0x08, 0xab, 0x43, 0xe5, 0x16, 0x7f, 0x52, 0x90, 0xce, + 0xb8, 0xa0, 0x87, 0xfd, 0x2b, 0x69, 0xf1, 0x05, 0x65, 0x37, 0x01, 0x1b, 0xbd, 0xf5, 0xa0, 0x04, 0xa1, 0xf3, 0xfe, + 0xe1, 0xd1, 0x7d, 0x16, 0x14, 0x6b, 0x44, 0x1d, 0x35, 0xf1, 0x6e, 0xb4, 0x9b, 0x54, 0x5c, 0x10, 0xab, 0x36, 0x5b, + 0xed, 0xb0, 0x0c, 0xd1, 0xfb, 0x37, 0x19, 0x59, 0x80, 0xa2, 0xbd, 0xe9, 0x79, 0x19, 0xac, 0x56, 0x4f, 0x13, 0x12, + 0x86, 0x6f, 0x20, 0xab, 0x29, 0x6c, 0x33, 0xdd, 0xca, 0xe8, 0x73, 0x60, 0x8e, 0x9e, 0x74, 0xd6, 0xd4, 0x82, 0xb1, + 0x65, 0xd4, 0x9f, 0x29, 0x0b, 0x27, 0x1f, 0xcb, 0xe0, 0xe7, 0x85, 0x29, 0x75, 0x07, 0x0d, 0xc9, 0x62, 0xc4, 0xca, + 0x4d, 0x3c, 0x74, 0xe8, 0xaa, 0x04, 0x83, 0xf5, 0xdb, 0x7a, 0xe3, 0xac, 0xd7, 0x38, 0x20, 0xf4, 0xde, 0x0f, 0x5c, + 0x2d, 0xfc, 0x10, 0x89, 0x11, 0xde, 0x90, 0x36, 0x47, 0x9d, 0xf1, 0xe2, 0x37, 0xde, 0x1b, 0x43, 0xb9, 0xbd, 0xae, + 0xf8, 0xa3, 0x5f, 0xd7, 0x95, 0x2a, 0x74, 0x25, 0x71, 0x66, 0xee, 0x63, 0x49, 0xd1, 0x23, 0x53, 0xda, 0xc5, 0x3d, + 0x00, 0x58, 0x98, 0x8d, 0x8a, 0xd0, 0xa4, 0x91, 0xb8, 0xfc, 0x54, 0x61, 0xa5, 0x52, 0x9f, 0x50, 0x72, 0x22, 0x30, + 0x0c, 0xbe, 0xff, 0x28, 0xd2, 0x15, 0x47, 0x3f, 0xc0, 0x3f, 0x22, 0x20, 0x50, 0x6b, 0x16, 0x69, 0xa8, 0x1d, 0x90, + 0x8c, 0x9f, 0x0e, 0x17, 0xce, 0xce, 0xcc, 0x88, 0x20, 0x53, 0x77, 0x03, 0x02, 0x84, 0xe1, 0x1a, 0x81, 0x2e, 0xff, + 0x4a, 0x49, 0xdb, 0x96, 0x3b, 0xd4, 0x61, 0x90, 0x5d, 0xe8, 0x20, 0x5a, 0x2d, 0xfa, 0xa5, 0xca, 0xf8, 0x16, 0xd1, + 0xc9, 0xfa, 0xfa, 0xfd, 0xc7, 0xe5, 0x5e, 0xe4, 0x0f, 0x6e, 0x2d, 0x00, 0x98, 0x8d, 0x38, 0x1b, 0x27, 0xbc, 0x6a, + 0x1d, 0x8b, 0x8f, 0xd2, 0x35, 0x86, 0xed, 0x14, 0xb4, 0xe2, 0x41, 0xab, 0x46, 0x54, 0x8a, 0x75, 0x7e, 0xdc, 0x2b, + 0x0f, 0xed, 0x76, 0xef, 0x87, 0xc0, 0xb9, 0x60, 0x47, 0xcc, 0x13, 0xc0, 0xbc, 0xac, 0x5c, 0x15, 0x32, 0x4d, 0xb0, + 0x11, 0x07, 0x39, 0xc8, 0xb4, 0xeb, 0x1e, 0x98, 0xb2, 0x4d, 0x8b, 0xdd, 0x2d, 0x66, 0x61, 0x03, 0x19, 0x21, 0x05, + 0x9b, 0x84, 0x0f, 0xd9, 0x32, 0x09, 0xa5, 0x07, 0x0e, 0x33, 0xd0, 0xd6, 0x7a, 0x14, 0xfb, 0x35, 0x6d, 0x13, 0x5d, + 0xb2, 0xc0, 0xa5, 0x46, 0xb6, 0x6f, 0xfa, 0x84, 0x8e, 0xc5, 0xe4, 0x86, 0xef, 0x77, 0xe7, 0xd5, 0x18, 0x96, 0xed, + 0x63, 0x65, 0x2f, 0xeb, 0xaf, 0x57, 0x2b, 0x50, 0x33, 0x9d, 0xb9, 0x7c, 0xab, 0xe4, 0xdf, 0xf5, 0x61, 0xa0, 0xf6, + 0x42, 0xe1, 0xa3, 0x98, 0x40, 0x59, 0x4b, 0xea, 0x14, 0xbc, 0x35, 0xde, 0xaf, 0xda, 0x61, 0xdc, 0xbf, 0xb9, 0x0b, + 0x15, 0x37, 0xbe, 0xfe, 0xa7, 0x9b, 0xda, 0xe0, 0xe8, 0x8d, 0x70, 0x49, 0xe7, 0x7e, 0xbd, 0x82, 0x00, 0x51, 0x6f, + 0x61, 0xca, 0x3a, 0x6f, 0xaf, 0xae, 0x6b, 0xf2, 0x68, 0x4b, 0x3f, 0xee, 0x82, 0x0e, 0x9b, 0xac, 0x64, 0x6d, 0xa1, + 0x7b, 0x64, 0x35, 0xee, 0x7e, 0x46, 0x38, 0x74, 0xb0, 0x83, 0xd4, 0x2d, 0x3e, 0xf4, 0x0e, 0xd3, 0xeb, 0x94, 0x54, + 0x6f, 0xf5, 0x9b, 0xfa, 0xcb, 0x97, 0xc6, 0xb9, 0x1a, 0x35, 0xac, 0x5d, 0xd4, 0xa4, 0x64, 0x66, 0x0a, 0xa6, 0xdb, + 0x20, 0x85, 0xab, 0xbe, 0xfa, 0xca, 0xe0, 0xc8, 0xf7, 0x73, 0x42, 0x05, 0x9b, 0x10, 0xaa, 0xc7, 0x2f, 0x88, 0xae, + 0x64, 0xfe, 0x71, 0xbb, 0x32, 0x06, 0x49, 0xe8, 0xdb, 0x11, 0x6f, 0xa5, 0xa9, 0xb3, 0x43, 0x3e, 0xe6, 0x6c, 0x82, + 0x5f, 0xd2, 0x84, 0x40, 0xb3, 0xf0, 0x2f, 0x0d, 0xd8, 0xee, 0x70, 0x6c, 0x3d, 0xd0, 0xb8, 0xf8, 0x0f, 0x94, 0x1b, + 0xd1, 0x99, 0x85, 0x1d, 0xef, 0x66, 0xe6, 0x4b, 0x87, 0xc3, 0x9e, 0x61, 0x09, 0x54, 0x65, 0x18, 0xd0, 0x0f, 0xdd, + 0x90, 0xed, 0x52, 0x1d, 0x3b, 0x07, 0x09, 0xeb, 0x3d, 0x14, 0x62, 0x3f, 0x9a, 0xab, 0xcb, 0xeb, 0xe5, 0x81, 0xfb, + 0x0a, 0xc3, 0xe5, 0xc1, 0xb0, 0xf8, 0x98, 0x15, 0x52, 0x75, 0xe9, 0xba, 0x8e, 0x3d, 0xad, 0x31, 0x20, 0x1f, 0x33, + 0x0c, 0x7f, 0x0e, 0x06, 0x8b, 0x76, 0x64, 0xdb, 0x32, 0x38, 0xc3, 0xca, 0xdb, 0x32, 0x65, 0xa6, 0xec, 0x2e, 0xd8, + 0x9e, 0x1a, 0xf8, 0xb3, 0x93, 0x94, 0x11, 0x14, 0x2a, 0xd3, 0x11, 0x34, 0xfa, 0xc7, 0x57, 0x45, 0xad, 0xc8, 0x46, + 0xd6, 0xfc, 0xb6, 0x78, 0x67, 0x8c, 0x53, 0x5a, 0x97, 0xb3, 0x7a, 0x17, 0xab, 0x46, 0x1f, 0x9a, 0x44, 0x2b, 0x92, + 0xb5, 0xaf, 0xb1, 0xc7, 0xc0, 0x10, 0x46, 0xc8, 0x72, 0xb3, 0xd6, 0x66, 0x36, 0x38, 0x89, 0xe3, 0x51, 0x07, 0xd6, + 0xdb, 0x79, 0xe5, 0x15, 0x0c, 0x02, 0xe0, 0x5f, 0x43, 0xcc, 0xb3, 0x0d, 0xfd, 0xce, 0x74, 0x53, 0xd5, 0xcb, 0x25, + 0x14, 0x46, 0x7f, 0x8c, 0x49, 0x07, 0xe5, 0xa5, 0x6a, 0x2a, 0x0d, 0x92, 0x51, 0x3d, 0x16, 0xa4, 0xa3, 0xcb, 0x73, + 0xc6, 0x67, 0x1d, 0xec, 0x69, 0xd2, 0xcd, 0x00, 0x10, 0x69, 0x87, 0x32, 0x26, 0x2a, 0x84, 0x15, 0x1e, 0x19, 0xa1, + 0xca, 0x1d, 0xf8, 0x28, 0xe0, 0xf3, 0xee, 0xb4, 0x20, 0x98, 0xd5, 0x25, 0x87, 0xb7, 0x42, 0x54, 0x14, 0xb7, 0xb2, + 0x9f, 0x90, 0xcc, 0xc7, 0x66, 0x26, 0xda, 0x6b, 0xc6, 0x9a, 0x77, 0x7f, 0x0e, 0x41, 0xa3, 0x20, 0x74, 0x58, 0x44, + 0xf3, 0x43, 0x0e, 0x83, 0xe4, 0x95, 0xd5, 0xd5, 0xc9, 0xf0, 0x5b, 0x81, 0x64, 0x05, 0x42, 0x74, 0xe2, 0x12, 0x84, + 0xde, 0x7e, 0x32, 0xec, 0x82, 0x57, 0xa0, 0x70, 0x28, 0x1c, 0x2f, 0x81, 0xcd, 0x27, 0x46, 0xc7, 0x72, 0xec, 0x74, + 0xc8, 0x55, 0x85, 0x3a, 0x59, 0x45, 0xd0, 0xda, 0x92, 0x9f, 0xf4, 0x95, 0x82, 0x98, 0x64, 0xcb, 0x96, 0x20, 0xa6, + 0x26, 0xc7, 0xc7, 0x09, 0xbd, 0x3f, 0xb1, 0x48, 0x1a, 0x92, 0x84, 0xef, 0x3e, 0xc1, 0x19, 0x23, 0x18, 0x63, 0x95, + 0x12, 0x63, 0x43, 0x59, 0x66, 0x7f, 0x38, 0x7d, 0x33, 0xc1, 0x81, 0x5f, 0x42, 0x91, 0xf2, 0x2a, 0x39, 0xe1, 0x19, + 0xc3, 0x5c, 0xca, 0xf1, 0xac, 0xe8, 0x1b, 0x75, 0xf8, 0x4b, 0x84, 0x22, 0x90, 0xe0, 0x2e, 0x2f, 0x67, 0xea, 0x8b, + 0xca, 0x4c, 0x69, 0x11, 0x6e, 0xf1, 0x1c, 0x6a, 0x8f, 0x1b, 0xb2, 0x13, 0x6f, 0xba, 0xf7, 0x7a, 0x84, 0x0f, 0x54, + 0x8a, 0x70, 0xde, 0x15, 0x83, 0xe5, 0x6e, 0x2c, 0xcc, 0xa5, 0x2f, 0x26, 0x7f, 0xa0, 0xe7, 0xb3, 0xb4, 0x9c, 0xf8, + 0xaa, 0xf9, 0xe6, 0xa8, 0x8b, 0x3f, 0xe2, 0x69, 0x89, 0x6d, 0xb9, 0xbd, 0x49, 0x2f, 0x3e, 0xdf, 0xe7, 0x7f, 0xd7, + 0x78, 0xb0, 0x50, 0xfd, 0x6c, 0x96, 0xe2, 0x06, 0xab, 0x07, 0x3e, 0x04, 0xb9, 0xc8, 0x4c, 0xe5, 0xda, 0xa6, 0xc7, + 0x9a, 0x3c, 0x6c, 0xc6, 0x3a, 0xb1, 0x5a, 0xf9, 0x36, 0xd8, 0xf4, 0x6a, 0x5b, 0xab, 0x5b, 0x13, 0xfa, 0x43, 0xad, + 0x64, 0x5b, 0xfd, 0x82, 0x07, 0x75, 0x34, 0x5f, 0x76, 0x86, 0xd5, 0xc5, 0xfe, 0x94, 0xc3, 0xa4, 0xf8, 0xbb, 0xb4, + 0xa2, 0x88, 0xfb, 0x4b, 0x06, 0x35, 0x75, 0x7e, 0x8c, 0x5f, 0xac, 0x0e, 0xa1, 0xdf, 0xc7, 0x11, 0xb5, 0x54, 0xfe, + 0x87, 0x13, 0xa5, 0x93, 0x49, 0x1e, 0xed, 0xf9, 0x34, 0x2d, 0x5e, 0xd1, 0xa5, 0xdb, 0xf1, 0x32, 0xf9, 0x56, 0x14, + 0x79, 0xbc, 0x58, 0x65, 0xc0, 0xec, 0x75, 0x68, 0xfa, 0xc9, 0xd3, 0x28, 0x39, 0x16, 0x57, 0x0c, 0x6e, 0x3d, 0x0e, + 0x1e, 0x91, 0x37, 0x35, 0xec, 0x4d, 0x28, 0x58, 0xec, 0xc0, 0x20, 0x3f, 0x06, 0x87, 0xa1, 0x43, 0x3d, 0x22, 0xde, + 0x35, 0xbe, 0x9c, 0xd4, 0x47, 0x58, 0x30, 0xab, 0x89, 0xf0, 0xdb, 0x82, 0xbc, 0x47, 0x22, 0x5a, 0x9f, 0x24, 0x8d, + 0xad, 0x74, 0xe0, 0xed, 0x2b, 0x41, 0x36, 0x39, 0xd0, 0x93, 0xde, 0xc2, 0x6c, 0xe7, 0x7c, 0xb4, 0xf2, 0xf7, 0x22, + 0xf9, 0x29, 0x24, 0xd2, 0x55, 0x15, 0x34, 0x75, 0x64, 0x1f, 0x91, 0x61, 0xed, 0x4b, 0xdd, 0xaf, 0x7d, 0x2d, 0xa4, + 0x24, 0xf8, 0xff, 0x69, 0x14, 0x0b, 0xba, 0x0b, 0x98, 0x77, 0x57, 0xc3, 0x30, 0x91, 0x93, 0xd5, 0xc4, 0x65, 0x89, + 0x6e, 0xea, 0x40, 0x61, 0x0c, 0xfb, 0x6d, 0xb4, 0x38, 0x5c, 0xd8, 0x97, 0x3c, 0x50, 0xa9, 0x5b, 0x8e, 0xe7, 0xb7, + 0x32, 0xa7, 0xf2, 0x62, 0x39, 0xa8, 0x0c, 0x5b, 0x03, 0xf0, 0x0c, 0x61, 0x18, 0x10, 0x0d, 0x39, 0x2e, 0x49, 0xb8, + 0xc2, 0xb0, 0x46, 0xf6, 0x58, 0x34, 0x52, 0x86, 0xd5, 0xc5, 0x8d, 0x2c, 0x4e, 0xa6, 0x89, 0x18, 0xce, 0xa7, 0x69, + 0x71, 0x02, 0xfc, 0xc0, 0x84, 0x1b, 0xec, 0xfa, 0xaa, 0xb0, 0x1c, 0xd0, 0x6c, 0x13, 0x1a, 0x2d, 0x52, 0x93, 0x66, + 0x95, 0x74, 0x52, 0xfe, 0x2f, 0xc7, 0x34, 0xd6, 0x19, 0xe9, 0x9c, 0x30, 0x22, 0xf2, 0x0f, 0x8d, 0x52, 0x76, 0x10, + 0xb6, 0x3a, 0x3c, 0x9c, 0x4d, 0x7e, 0x40, 0xd5, 0x46, 0xf7, 0x83, 0xaf, 0x2e, 0x64, 0xb0, 0x07, 0xc1, 0x91, 0xeb, + 0xe6, 0xa8, 0x49, 0xfe, 0x97, 0xa3, 0xfc, 0x73, 0x2e, 0x4e, 0x3a, 0x92, 0xb4, 0xb1, 0x62, 0x44, 0x64, 0x63, 0xfa, + 0x83, 0x4d, 0x9b, 0xc2, 0xa1, 0x0f, 0x0b, 0x3a, 0x9e, 0xa2, 0x40, 0x1a, 0xbd, 0xaa, 0xe4, 0xa0, 0x30, 0x19, 0x20, + 0x7b, 0x25, 0x65, 0xde, 0x2f, 0xe4, 0x32, 0xc8, 0x7c, 0xab, 0x56, 0x66, 0xcb, 0x67, 0x0c, 0xc4, 0x08, 0x37, 0xc2, + 0xe0, 0x57, 0x8a, 0xa9, 0xaf, 0x59, 0xa6, 0xb8, 0x9f, 0x86, 0x90, 0x1d, 0x12, 0x86, 0x1f, 0x68, 0x53, 0x26, 0xa7, + 0x4d, 0x13, 0x2f, 0xf5, 0xd5, 0xd5, 0x30, 0x79, 0x81, 0x4c, 0xde, 0xe0, 0x3e, 0x72, 0xdd, 0x8f, 0xfa, 0x6d, 0x93, + 0x60, 0x7f, 0x95, 0xe0, 0xff, 0x0e, 0x21, 0x5b, 0x0b, 0x60, 0x06, 0x89, 0x8d, 0xbe, 0x5d, 0xf0, 0x71, 0x51, 0xe4, + 0xeb, 0x44, 0xac, 0xa8, 0x8c, 0x4b, 0xb2, 0x5b, 0x07, 0xbb, 0xd2, 0x36, 0x18, 0x6c, 0x90, 0x68, 0x68, 0x32, 0x0d, + 0x9d, 0x2c, 0xdf, 0x2c, 0x0d, 0xc9, 0xb7, 0x65, 0x9d, 0x3b, 0x14, 0x1a, 0x49, 0x0d, 0x6e, 0xd3, 0xf2, 0x39, 0xf5, + 0x94, 0x6c, 0x78, 0x6c, 0x2b, 0x79, 0x50, 0x61, 0x3a, 0x36, 0xb3, 0x79, 0xb4, 0x62, 0x3d, 0xaf, 0xd6, 0x39, 0x24, + 0x76, 0x5b, 0x56, 0x0e, 0x56, 0x64, 0x88, 0x4f, 0x5c, 0x8f, 0xd6, 0xf6, 0xa3, 0xef, 0x63, 0x44, 0x25, 0x39, 0x18, + 0x96, 0x2d, 0x95, 0xa7, 0x16, 0x27, 0x9e, 0xda, 0xfd, 0x60, 0xfa, 0xb2, 0x48, 0x85, 0x17, 0x4d, 0xe6, 0x8b, 0xd4, + 0xa4, 0x38, 0xcc, 0xc5, 0x48, 0xcc, 0x81, 0xa5, 0x7d, 0x84, 0x8c, 0xc1, 0x52, 0x7a, 0xa4, 0xb1, 0x8d, 0x91, 0xb7, + 0xe8, 0x50, 0x2e, 0x3a, 0xfb, 0x9f, 0x59, 0x7a, 0x0a, 0x43, 0xda, 0x2c, 0x8d, 0x6b, 0xb7, 0x54, 0x5e, 0xd9, 0x0f, + 0xae, 0xb2, 0x6d, 0x1b, 0x8b, 0xa0, 0x7e, 0xb2, 0xde, 0x92, 0x4c, 0x2a, 0x70, 0x30, 0x31, 0xd0, 0x43, 0x65, 0x37, + 0x5a, 0x6b, 0x7a, 0x3c, 0x4a, 0x5b, 0xa4, 0x89, 0xb0, 0x36, 0x32, 0xaa, 0xba, 0x8a, 0xb8, 0xbd, 0xd2, 0x97, 0x7c, + 0x05, 0x18, 0x6f, 0xbc, 0xb9, 0xb9, 0x46, 0xc3, 0x1d, 0x73, 0x12, 0xc5, 0x20, 0xde, 0xad, 0x8c, 0x27, 0x4d, 0xeb, + 0xf2, 0xdb, 0x72, 0xe2, 0x53, 0xbd, 0x6e, 0x31, 0x81, 0x0c, 0xce, 0x82, 0x78, 0x3d, 0x03, 0x58, 0x65, 0x57, 0x11, + 0xd5, 0x66, 0x41, 0x82, 0x60, 0x1a, 0x5c, 0xc7, 0x1a, 0xb7, 0x39, 0xca, 0x36, 0x09, 0x7a, 0xe6, 0x68, 0x2c, 0xff, + 0x9d, 0x59, 0xe0, 0xe5, 0x45, 0x81, 0xf6, 0xc4, 0x81, 0xef, 0xc6, 0x33, 0xf6, 0xfa, 0x9f, 0x8f, 0x8c, 0x01, 0x1c, + 0xd4, 0x98, 0x93, 0xd9, 0xe1, 0x55, 0xda, 0xaa, 0xf4, 0x2a, 0xa9, 0xb2, 0x3d, 0xc4, 0xab, 0xde, 0x8c, 0x24, 0x4a, + 0xa9, 0x0b, 0xb6, 0x6c, 0xe6, 0x41, 0xe2, 0x35, 0x47, 0x7f, 0xac, 0x25, 0x92, 0xbd, 0x82, 0x86, 0x91, 0x33, 0xf1, + 0x8b, 0x4f, 0x89, 0x79, 0xa7, 0x7d, 0xc5, 0xe4, 0x79, 0x83, 0x8f, 0xe0, 0x8b, 0x4a, 0x5d, 0x34, 0xde, 0x38, 0x84, + 0x3a, 0xd6, 0x30, 0x41, 0x0a, 0x30, 0x73, 0x80, 0x8b, 0x62, 0xe5, 0x93, 0x51, 0x52, 0xec, 0x9d, 0x16, 0xdb, 0xbc, + 0x16, 0x8a, 0x57, 0xfe, 0xa2, 0x94, 0xa6, 0xf6, 0xf2, 0xa9, 0x0d, 0xe5, 0xfb, 0x0d, 0x74, 0x1a, 0xeb, 0x8d, 0xe8, + 0xa3, 0x94, 0xf2, 0xec, 0x3a, 0xe1, 0x0c, 0x8f, 0xad, 0x8a, 0x59, 0x3c, 0x58, 0x35, 0xce, 0x3e, 0x84, 0x12, 0x1e, + 0x2d, 0x5b, 0x52, 0x76, 0xf3, 0x14, 0x86, 0xbf, 0xc3, 0x4a, 0xa1, 0xa8, 0x45, 0x80, 0xc0, 0xba, 0x8d, 0x7f, 0xdc, + 0x69, 0x3a, 0x6f, 0xa7, 0xb3, 0xc1, 0xc6, 0xe2, 0x68, 0xd8, 0x1e, 0xa9, 0x32, 0xf0, 0xcb, 0xc7, 0x33, 0x6b, 0x4d, + 0x0a, 0x17, 0x78, 0xa8, 0xc2, 0xf6, 0xaf, 0xa3, 0x3d, 0x69, 0xbe, 0x23, 0xe0, 0x8e, 0x41, 0x3a, 0x01, 0xdf, 0x79, + 0x08, 0x5d, 0x00, 0xed, 0x14, 0x62, 0x17, 0x21, 0x75, 0x09, 0x72, 0x97, 0xa1, 0xed, 0x5a, 0x78, 0x40, 0x4c, 0xf0, + 0xb0, 0x32, 0xc3, 0xa3, 0xca, 0x02, 0x8f, 0x2b, 0x7b, 0x78, 0x42, 0x1c, 0xe0, 0x69, 0x65, 0x85, 0x85, 0x8a, 0xea, + 0xb2, 0xba, 0xaa, 0xae, 0xab, 0xa5, 0x32, 0xf4, 0x2b, 0x96, 0x05, 0xc6, 0xbf, 0x20, 0x46, 0xb0, 0xf8, 0xa0, 0x31, + 0xe5, 0xf6, 0xc1, 0xc3, 0x47, 0x8f, 0x9f, 0x3c, 0x0d, 0x55, 0xa6, 0xf3, 0x75, 0xd6, 0xa4, 0xe6, 0x71, 0x6b, 0xa8, + 0x23, 0xef, 0x7c, 0x39, 0xf4, 0x2d, 0x03, 0xea, 0xec, 0x69, 0x1a, 0x86, 0x2f, 0x5d, 0x7e, 0x3d, 0xf1, 0x4b, 0xc5, + 0xeb, 0xc6, 0x37, 0x66, 0x7c, 0xef, 0x8b, 0x4f, 0x13, 0x5a, 0x3c, 0xbd, 0xe0, 0x82, 0x15, 0x3c, 0x98, 0x8f, 0xf3, + 0x2e, 0x0b, 0x11, 0x75, 0xaa, 0x87, 0xc2, 0x5a, 0xf1, 0xfc, 0x83, 0x0e, 0x63, 0x2f, 0x4e, 0x11, 0xd9, 0x37, 0xe7, + 0x86, 0x88, 0xa6, 0xc9, 0xbb, 0x88, 0xaa, 0x7f, 0xb0, 0x7b, 0x29, 0x1a, 0x5d, 0x52, 0xee, 0xd3, 0xdf, 0x9f, 0xad, + 0xc8, 0xbe, 0xa9, 0xe8, 0x41, 0x71, 0x70, 0x56, 0x2d, 0x4c, 0x66, 0x93, 0xa6, 0x4e, 0x36, 0xc0, 0xf7, 0x4c, 0x0a, + 0xaa, 0x92, 0xa1, 0xf6, 0xcf, 0x1e, 0xf8, 0x4f, 0x5a, 0x77, 0xe1, 0xa7, 0xb2, 0x7e, 0xbb, 0xff, 0x9a, 0xbe, 0x2d, + 0x4e, 0x58, 0x9c, 0x58, 0x94, 0x54, 0xd2, 0x99, 0xc9, 0xa2, 0x9e, 0xd7, 0x57, 0xce, 0xcd, 0x3c, 0xc9, 0x2c, 0x9f, + 0xf6, 0x56, 0xd0, 0xd9, 0x6d, 0x80, 0x98, 0x04, 0xea, 0x35, 0x3e, 0x65, 0xf5, 0x66, 0x3c, 0xfd, 0xc4, 0xb2, 0x29, + 0x85, 0x00, 0xa7, 0xc5, 0x17, 0xff, 0xdb, 0xf1, 0x2d, 0xf9, 0x6e, 0xa6, 0x97, 0xf9, 0xfd, 0x9a, 0x6c, 0xe6, 0x46, + 0x0a, 0xdc, 0xf1, 0xcb, 0xd9, 0xe8, 0xb9, 0xc6, 0x7f, 0x33, 0xc8, 0xdb, 0xe4, 0xe9, 0x52, 0xca, 0xd5, 0xc6, 0x0e, + 0x5d, 0x6e, 0xfe, 0xa9, 0x2a, 0x8f, 0x5d, 0x5b, 0xea, 0xf8, 0xe7, 0x5d, 0x1c, 0x8f, 0x5f, 0xf4, 0x3f, 0xde, 0xe7, + 0x1e, 0xef, 0xa2, 0x66, 0x51, 0xfe, 0x3d, 0x54, 0xa9, 0xe3, 0xf7, 0xa7, 0xf9, 0xd2, 0x51, 0x3e, 0x4e, 0x4d, 0xf3, + 0x41, 0x5e, 0x46, 0x2c, 0x8f, 0xd6, 0xf4, 0xf6, 0xcf, 0x9f, 0x12, 0xff, 0x6b, 0x73, 0x9d, 0xed, 0xe1, 0xfe, 0xa4, + 0x5e, 0x6c, 0x63, 0x31, 0x22, 0x66, 0x63, 0xc1, 0x0e, 0xa8, 0xde, 0x3f, 0x3d, 0x87, 0x94, 0x65, 0xfc, 0xa7, 0xc7, + 0x0b, 0x2c, 0x9d, 0xc0, 0x9a, 0xbf, 0xed, 0x98, 0x96, 0xbc, 0xcb, 0x96, 0x63, 0x0c, 0x7d, 0xc6, 0xff, 0xe6, 0x2e, + 0xee, 0xb0, 0x5a, 0x12, 0xa5, 0x18, 0x11, 0x16, 0xfa, 0xf9, 0x8d, 0x0f, 0x42, 0xf4, 0x61, 0xe5, 0xc1, 0x54, 0xaa, + 0x4d, 0xa6, 0x9c, 0x38, 0x81, 0x0f, 0xbe, 0x1d, 0x78, 0x5a, 0x81, 0x37, 0x7f, 0xdb, 0x37, 0x2d, 0xa3, 0x63, 0xf7, + 0x92, 0xbe, 0xbc, 0x9c, 0xb7, 0x4c, 0x4b, 0xbc, 0xd4, 0xc5, 0xed, 0x49, 0x13, 0x7a, 0x67, 0x41, 0x35, 0xc9, 0xc9, + 0xa7, 0xe5, 0x13, 0x0c, 0xfb, 0x45, 0x49, 0xac, 0x9a, 0x56, 0x43, 0x63, 0xd4, 0x9b, 0x26, 0x53, 0x37, 0xb8, 0xd6, + 0xa8, 0x57, 0x81, 0x7f, 0x66, 0x40, 0xee, 0x39, 0x13, 0x7b, 0xce, 0xa0, 0x97, 0x6f, 0x7e, 0x4c, 0x9a, 0xb7, 0xa6, + 0xde, 0x16, 0x5f, 0x2b, 0xa6, 0x52, 0xa2, 0xf0, 0x7d, 0x5d, 0xb8, 0x10, 0x1f, 0x0b, 0x97, 0xab, 0x07, 0xa6, 0xe9, + 0xd3, 0x29, 0xe4, 0xfb, 0x56, 0xe0, 0x9c, 0x91, 0xa3, 0xa0, 0x0d, 0x86, 0x3c, 0x62, 0xde, 0x0f, 0x21, 0xe5, 0x73, + 0x1f, 0xc6, 0xe8, 0xf7, 0xd8, 0x08, 0x28, 0x96, 0xa3, 0xec, 0x2e, 0xc4, 0x3c, 0xcf, 0x62, 0x76, 0xd0, 0xd5, 0x72, + 0xb3, 0xee, 0x98, 0x63, 0x26, 0x56, 0xfc, 0x8c, 0xeb, 0x58, 0xc1, 0x8c, 0x7c, 0xd0, 0xa2, 0x3b, 0xfa, 0x23, 0x2b, + 0xfb, 0x3a, 0x50, 0x77, 0x12, 0x5a, 0x3b, 0x4c, 0xdf, 0x66, 0x19, 0x0e, 0x28, 0x96, 0x08, 0x12, 0x85, 0xe6, 0xf7, + 0x2a, 0x31, 0xb1, 0x52, 0x9c, 0x1a, 0xcd, 0xf1, 0x86, 0xaa, 0xc4, 0x98, 0xa0, 0xe5, 0xed, 0xea, 0x8b, 0xcc, 0x74, + 0x09, 0x1b, 0x37, 0x58, 0xd2, 0x8a, 0xfd, 0xa2, 0xa4, 0x7e, 0xdf, 0x96, 0x85, 0x8a, 0x77, 0xd9, 0xd5, 0x51, 0x5d, + 0xda, 0xa1, 0xa3, 0x22, 0x66, 0xa0, 0xe8, 0xbb, 0x9d, 0x57, 0xce, 0x46, 0xf6, 0x81, 0xdb, 0x09, 0x9c, 0x05, 0x55, + 0x79, 0x9d, 0xea, 0x50, 0x0a, 0x97, 0x05, 0xe0, 0x16, 0x38, 0x3d, 0x3d, 0x09, 0xca, 0xb3, 0xae, 0xac, 0x44, 0x57, + 0x7a, 0x37, 0xe4, 0x3f, 0xd2, 0xe8, 0xc7, 0x39, 0xe2, 0xd9, 0xe7, 0xdd, 0xe9, 0x46, 0xa7, 0xf9, 0x5d, 0xfe, 0x26, + 0xe6, 0xef, 0x2a, 0xfa, 0xf2, 0x97, 0x2d, 0x6f, 0x5f, 0x07, 0xc2, 0x60, 0x69, 0x86, 0xf8, 0xda, 0x52, 0x79, 0x8d, + 0xb9, 0x0f, 0x31, 0x4d, 0xc2, 0xc6, 0xc8, 0xa3, 0x29, 0xe0, 0x3c, 0xdf, 0xbb, 0x51, 0x7a, 0x6d, 0x4b, 0x82, 0x50, + 0xe2, 0x3d, 0xdf, 0x4a, 0xbf, 0xe7, 0x71, 0x11, 0x0b, 0x32, 0xdb, 0xd0, 0xe9, 0xf9, 0xa8, 0x8e, 0x21, 0xe6, 0xc6, + 0xd3, 0x2e, 0xec, 0xa9, 0x5a, 0xab, 0x05, 0x49, 0xd2, 0xeb, 0x3c, 0xdf, 0xfd, 0xce, 0xa4, 0x8c, 0xe0, 0x51, 0x13, + 0x24, 0x7e, 0xee, 0xeb, 0x3f, 0xef, 0x4c, 0x0d, 0x7a, 0x0b, 0x78, 0x5a, 0x3a, 0xe8, 0xc9, 0x27, 0x8e, 0x5f, 0x0b, + 0xcb, 0x6d, 0xa3, 0x4b, 0xff, 0x7d, 0x9e, 0xad, 0x4b, 0x26, 0x4c, 0xba, 0x45, 0x32, 0x1f, 0x2b, 0x13, 0xe8, 0xa9, + 0x69, 0x9d, 0x90, 0xaa, 0xfe, 0x89, 0x15, 0xd4, 0x28, 0x70, 0xa0, 0x94, 0xba, 0x9a, 0xb0, 0x10, 0xdc, 0x09, 0x8f, + 0xcf, 0x4b, 0x01, 0xd1, 0xdc, 0xbd, 0x08, 0x92, 0x8b, 0xc4, 0x6b, 0x41, 0x25, 0x56, 0x65, 0xc5, 0x65, 0x55, 0x52, + 0x09, 0xa6, 0xf0, 0x03, 0x00, 0xcd, 0x7b, 0x05, 0x4c, 0x72, 0xa0, 0x47, 0x54, 0x3a, 0xf9, 0xa8, 0x11, 0x1f, 0xf0, + 0x64, 0x93, 0xfe, 0xa1, 0xa7, 0xb8, 0x24, 0x4e, 0x9c, 0xe9, 0x50, 0x82, 0xfb, 0x63, 0xd2, 0xfa, 0x53, 0xc5, 0x7c, + 0x8b, 0x89, 0x0e, 0xea, 0xf2, 0xf6, 0xfa, 0x3a, 0xab, 0x89, 0x84, 0x1a, 0x70, 0xc3, 0x9a, 0xd6, 0x94, 0xba, 0x6e, + 0x03, 0x4d, 0x1f, 0x40, 0xdf, 0xb3, 0xea, 0xf6, 0x47, 0x85, 0xe5, 0x40, 0x6e, 0x72, 0x5b, 0x8d, 0xa2, 0x8d, 0xff, + 0x38, 0xb8, 0xa9, 0xc6, 0x29, 0x70, 0x5d, 0xcd, 0x64, 0x99, 0x80, 0xab, 0x6a, 0x8a, 0x00, 0x2e, 0xab, 0xf9, 0x09, + 0xf5, 0xbd, 0xfa, 0x82, 0x8c, 0x5f, 0xea, 0xf0, 0xf5, 0x7e, 0xae, 0x20, 0xf4, 0xce, 0x4f, 0x59, 0x5b, 0x39, 0xf3, + 0x9c, 0xf7, 0xeb, 0x94, 0x83, 0xf7, 0x1b, 0xba, 0xee, 0xf0, 0xa9, 0xce, 0xbc, 0x0d, 0xd0, 0xfe, 0x29, 0x6f, 0xde, + 0xe9, 0x29, 0x62, 0xef, 0xe4, 0x6a, 0xee, 0xee, 0xff, 0x69, 0xe8, 0x79, 0x6f, 0x78, 0xaa, 0xb4, 0x95, 0xe3, 0x4a, + 0x8f, 0xde, 0xd1, 0xbf, 0x96, 0xda, 0x02, 0x87, 0xd5, 0x54, 0x0a, 0x1c, 0x54, 0x93, 0x2b, 0xb0, 0x5f, 0xcd, 0xab, + 0x5a, 0x3b, 0x00, 0x9b, 0xd5, 0x20, 0x03, 0x7b, 0xd5, 0x64, 0x0d, 0xd8, 0xaa, 0x46, 0xbb, 0x4f, 0x1e, 0xd8, 0xae, + 0x06, 0x6b, 0x60, 0x67, 0x64, 0x5e, 0xe7, 0xed, 0xfe, 0x33, 0xf1, 0xdc, 0xc0, 0x58, 0xaf, 0xdb, 0xcb, 0x3e, 0x33, + 0xef, 0x75, 0x0d, 0x4c, 0x97, 0x65, 0x5b, 0xac, 0x5a, 0x80, 0x0d, 0xfa, 0xef, 0x48, 0xd3, 0xd6, 0x0a, 0x7e, 0x23, + 0x90, 0xc0, 0xfc, 0xec, 0x2c, 0x60, 0x01, 0xa3, 0xff, 0xd0, 0xe3, 0xd8, 0xc0, 0x50, 0x4b, 0xac, 0x4f, 0xd6, 0x31, + 0x6d, 0xd3, 0xff, 0xc7, 0xc6, 0x66, 0x1b, 0x25, 0x48, 0xb7, 0xed, 0x35, 0xbe, 0xbc, 0x65, 0x47, 0x68, 0x3f, 0x52, + 0xd4, 0xa5, 0xb4, 0x0a, 0xf0, 0x57, 0x4b, 0x72, 0xf1, 0xe8, 0xb2, 0x3d, 0x13, 0xbe, 0x57, 0x67, 0xb1, 0xce, 0x94, + 0x90, 0x88, 0x1d, 0xe2, 0xea, 0xfe, 0xbf, 0x3c, 0xf5, 0x95, 0x42, 0x65, 0x11, 0x27, 0xc5, 0x47, 0x7c, 0x4c, 0xb6, + 0x0a, 0x4e, 0x11, 0xc7, 0x42, 0x8c, 0x43, 0x37, 0x4f, 0xce, 0x61, 0xd5, 0x24, 0x0a, 0xfb, 0x47, 0xc2, 0x6b, 0xd0, + 0xf0, 0x9b, 0x6c, 0x39, 0xc0, 0xce, 0x19, 0x86, 0xd0, 0x8c, 0xbe, 0xb3, 0x9c, 0x49, 0x95, 0x93, 0x57, 0x30, 0x8e, + 0xe8, 0x58, 0xfd, 0x8e, 0x94, 0x97, 0xf7, 0xa5, 0xca, 0x97, 0x43, 0x50, 0xb6, 0x1f, 0xe0, 0x1d, 0x14, 0xc5, 0xf4, + 0xb9, 0x0c, 0x16, 0xf8, 0x5e, 0x77, 0x0f, 0x0f, 0xf0, 0xc2, 0x26, 0xfc, 0x58, 0x2b, 0x4f, 0x78, 0x67, 0xab, 0x96, + 0xd1, 0x63, 0xe1, 0x60, 0x06, 0xeb, 0x20, 0xfe, 0xaf, 0x79, 0xdd, 0x32, 0x80, 0x12, 0x38, 0xe1, 0xe6, 0x4c, 0x7b, + 0x7d, 0x9d, 0x3e, 0xb0, 0x32, 0x69, 0x8a, 0x5d, 0xf4, 0x07, 0xe6, 0x2a, 0xca, 0x6b, 0xa8, 0xe2, 0x34, 0xbb, 0x91, + 0xba, 0xc0, 0xe3, 0x7b, 0xa5, 0x3b, 0x8f, 0x3b, 0x3c, 0x74, 0xed, 0x0a, 0x8a, 0x5c, 0x23, 0x06, 0xda, 0x5d, 0x00, + 0xdd, 0x2e, 0xc3, 0xc6, 0xbf, 0xea, 0x27, 0xa9, 0x08, 0x44, 0x34, 0xe3, 0x5e, 0xf8, 0x17, 0xb0, 0xad, 0x1b, 0xdd, + 0xa2, 0x94, 0x8e, 0x2e, 0x72, 0xeb, 0xad, 0xc4, 0x9d, 0xa9, 0x64, 0xa3, 0x41, 0x0d, 0x58, 0xce, 0xbd, 0xff, 0x3b, + 0xd5, 0x5c, 0x52, 0x7f, 0xc8, 0xc4, 0x41, 0xdf, 0x9a, 0xad, 0x90, 0x05, 0xff, 0x32, 0xc9, 0x7f, 0xc6, 0x3d, 0x3e, + 0xf1, 0xd5, 0xf3, 0x90, 0x36, 0xdd, 0xb4, 0x02, 0x76, 0xd7, 0x14, 0x59, 0x9f, 0xf2, 0x71, 0x11, 0x46, 0xce, 0x82, + 0x47, 0xf8, 0x03, 0x3a, 0x09, 0x71, 0xd9, 0xfb, 0xda, 0x8b, 0xbd, 0xe8, 0xb9, 0x4c, 0xfa, 0xc7, 0xac, 0x5e, 0xa4, + 0xe3, 0x3c, 0xd4, 0x66, 0xef, 0xa8, 0x5f, 0xa3, 0x0c, 0xd5, 0x9b, 0xb3, 0x9e, 0xc3, 0x36, 0x30, 0xea, 0x4e, 0x89, + 0x56, 0x11, 0xa5, 0x1b, 0x31, 0x68, 0x28, 0x39, 0x19, 0x5f, 0xe9, 0x6c, 0xbf, 0xbb, 0xbc, 0xe3, 0x56, 0x53, 0x0a, + 0x14, 0xfb, 0x46, 0xb9, 0xc6, 0x87, 0x9f, 0x25, 0x2b, 0x22, 0xcb, 0x7a, 0xc1, 0x78, 0xf6, 0x1a, 0xd6, 0x82, 0x69, + 0x8a, 0xb6, 0x50, 0x7b, 0x71, 0x71, 0xd1, 0x84, 0x6b, 0x9d, 0xdc, 0xb8, 0x3b, 0x1f, 0x16, 0x76, 0x41, 0x09, 0xb7, + 0xbd, 0xc2, 0xd3, 0xc1, 0xdc, 0x67, 0x46, 0x28, 0x89, 0x06, 0xe0, 0x04, 0x5c, 0x3a, 0x86, 0xeb, 0xc6, 0xf1, 0x5e, + 0x03, 0x78, 0x58, 0x2f, 0xe0, 0x0c, 0x50, 0xa9, 0x78, 0x45, 0x77, 0xab, 0x1e, 0xbf, 0x3b, 0x4b, 0xf3, 0xeb, 0x44, + 0x19, 0x14, 0x96, 0x83, 0x87, 0x96, 0x32, 0xcf, 0x34, 0xa8, 0x51, 0x83, 0x8d, 0x54, 0xac, 0x90, 0x17, 0xaf, 0x79, + 0xd0, 0xba, 0xea, 0x85, 0x3d, 0x41, 0x4a, 0x3f, 0x2f, 0x73, 0x0b, 0xaa, 0x81, 0x81, 0x84, 0x58, 0x36, 0x99, 0xd5, + 0x1f, 0x1d, 0x3c, 0x77, 0xaf, 0xa6, 0x9a, 0x6c, 0xb2, 0x05, 0x99, 0xf7, 0xeb, 0xef, 0xad, 0x24, 0x5b, 0x7e, 0x71, + 0x27, 0xdb, 0x5f, 0xef, 0x97, 0x42, 0xc9, 0xe8, 0xcd, 0x38, 0x3b, 0xd6, 0x83, 0x72, 0xd6, 0x81, 0x40, 0x44, 0x7e, + 0x5b, 0x1d, 0x0a, 0xc4, 0x62, 0x32, 0x82, 0x32, 0x85, 0x19, 0x61, 0xdf, 0xd0, 0x5d, 0x1e, 0xc6, 0xdb, 0xb7, 0x13, + 0xd8, 0x4c, 0x2c, 0x28, 0xc0, 0x58, 0xdc, 0x4e, 0x4e, 0x27, 0xd7, 0x50, 0x09, 0xe6, 0x31, 0xc7, 0x50, 0x09, 0xc9, + 0xbd, 0x72, 0x66, 0xfd, 0x58, 0x64, 0x15, 0x35, 0x06, 0xc9, 0x95, 0x55, 0xc0, 0xb2, 0xb7, 0x90, 0x90, 0x71, 0xc8, + 0x28, 0x72, 0x02, 0xfb, 0x45, 0x6a, 0x46, 0x41, 0x49, 0xfc, 0x6f, 0xe6, 0x9e, 0xb9, 0x99, 0x7b, 0xe5, 0xc3, 0xb2, + 0x34, 0x29, 0x49, 0x56, 0x69, 0x13, 0xf4, 0x13, 0x3a, 0x7e, 0x89, 0xd8, 0xe9, 0xb4, 0x21, 0x34, 0xb0, 0xc7, 0x38, + 0x32, 0x82, 0xa9, 0x30, 0xa1, 0x7a, 0x57, 0x6f, 0x40, 0x12, 0x78, 0x80, 0xd1, 0xce, 0x44, 0x4e, 0x68, 0xe0, 0xc3, + 0xd9, 0x68, 0xe3, 0xe6, 0xe1, 0x77, 0x4e, 0x54, 0xaa, 0x85, 0x9d, 0xe5, 0xdf, 0x5b, 0x70, 0xa5, 0xfd, 0x4d, 0x40, + 0x95, 0xc0, 0xc5, 0xe4, 0xdf, 0xbf, 0xfb, 0x71, 0xe8, 0xdf, 0x2a, 0xe7, 0x8f, 0x16, 0x8b, 0x6f, 0x23, 0x3b, 0x5c, + 0x82, 0xb5, 0xae, 0x23, 0xe7, 0x22, 0x3f, 0x33, 0x1b, 0xaa, 0xe1, 0xf7, 0x43, 0xdd, 0xee, 0xe4, 0x42, 0xa9, 0x5a, + 0x7c, 0x32, 0x72, 0xdc, 0x51, 0xc9, 0x13, 0x89, 0x86, 0x58, 0xd1, 0xf2, 0x2b, 0x26, 0x9e, 0xd1, 0xf8, 0x52, 0xf1, + 0x48, 0x16, 0xee, 0x1f, 0xb9, 0xf6, 0x92, 0xa9, 0x83, 0x6c, 0xc0, 0x64, 0x64, 0xae, 0xfc, 0x76, 0xdc, 0x68, 0x1f, + 0xe6, 0x0f, 0x7f, 0xaa, 0x4f, 0xc4, 0xde, 0xff, 0x9a, 0x51, 0x76, 0xbd, 0x2c, 0x24, 0x3f, 0x61, 0xe1, 0x09, 0x7f, + 0x1c, 0xa1, 0xe0, 0xe5, 0xf4, 0xf1, 0x82, 0x38, 0x8e, 0x9e, 0x2b, 0x76, 0x20, 0x4b, 0x25, 0x2a, 0xee, 0x22, 0x48, + 0x42, 0x14, 0xe1, 0x09, 0xa0, 0xe4, 0x7d, 0x5c, 0x89, 0x4a, 0xb3, 0x98, 0x98, 0x4f, 0x46, 0x2a, 0x00, 0x67, 0x07, + 0xd1, 0xd0, 0x4a, 0xa4, 0x27, 0xea, 0x24, 0xa2, 0x21, 0x47, 0x67, 0x83, 0xfe, 0x9d, 0x78, 0x16, 0x1b, 0xa2, 0x22, + 0x40, 0x4c, 0x41, 0xd4, 0xb1, 0x15, 0x6f, 0x94, 0x81, 0x2b, 0xea, 0xa1, 0x44, 0x82, 0xd6, 0xd4, 0x19, 0xa0, 0x29, + 0x21, 0x20, 0xc7, 0x5c, 0xee, 0xa1, 0x87, 0x19, 0xa6, 0x6d, 0xb7, 0xab, 0xa8, 0x30, 0xde, 0x1f, 0xce, 0xcb, 0xa8, + 0xd4, 0x76, 0x0a, 0x44, 0xa9, 0x7a, 0x1a, 0xc6, 0x38, 0x1d, 0x2b, 0x84, 0x77, 0x01, 0xc5, 0x25, 0xc9, 0x4a, 0x8c, + 0xfa, 0x6e, 0x34, 0x6d, 0xaa, 0x11, 0x12, 0x38, 0xe9, 0xdd, 0xf4, 0x3a, 0x40, 0x49, 0x0c, 0x26, 0x58, 0x1c, 0xc1, + 0x66, 0xf0, 0xc9, 0xb1, 0x46, 0x90, 0x94, 0x50, 0xa0, 0x54, 0x99, 0xf1, 0x47, 0xad, 0xa4, 0x20, 0xf1, 0x3e, 0xfc, + 0xfc, 0x53, 0x65, 0xf1, 0xc4, 0x0a, 0x1b, 0xef, 0x63, 0x7e, 0x5f, 0xc9, 0xab, 0x1e, 0x84, 0x89, 0x07, 0xc4, 0xb7, + 0x09, 0x1c, 0x61, 0x99, 0xca, 0x65, 0x83, 0x0c, 0x05, 0x28, 0x38, 0xd5, 0x9a, 0xb3, 0x38, 0x13, 0x9b, 0x46, 0xaa, + 0x30, 0xe8, 0x11, 0x4c, 0x91, 0xfc, 0x8b, 0xb8, 0x7f, 0xdf, 0x06, 0xc4, 0xf8, 0x14, 0x1f, 0xfa, 0xc9, 0xeb, 0x9d, + 0x85, 0x2a, 0x98, 0x10, 0xf5, 0xe2, 0x3f, 0x8f, 0x97, 0xc5, 0xae, 0x1f, 0x1f, 0x36, 0x15, 0x48, 0x20, 0xf6, 0x3c, + 0x0d, 0xce, 0x7f, 0xc6, 0x2c, 0x09, 0x03, 0x69, 0x71, 0x6f, 0x4a, 0xe0, 0x4d, 0x2f, 0x58, 0x9a, 0x4d, 0x80, 0x09, + 0x52, 0x9c, 0x8c, 0xb6, 0x16, 0xac, 0x53, 0x4f, 0x8d, 0xd2, 0x99, 0x13, 0x60, 0x52, 0xa9, 0x87, 0x90, 0x9e, 0x7a, + 0x81, 0xde, 0xb1, 0xa2, 0x1f, 0xe3, 0xad, 0x86, 0xb7, 0xec, 0x6a, 0x91, 0x0a, 0x8b, 0xd0, 0x19, 0x08, 0x2e, 0x0b, + 0x4e, 0xf1, 0xfb, 0x08, 0x75, 0x0d, 0xc3, 0x9a, 0xb1, 0xc9, 0x89, 0x1a, 0x2a, 0xc8, 0xb4, 0xee, 0xe9, 0x60, 0xef, + 0x65, 0xde, 0x24, 0xcc, 0x9c, 0x0f, 0x47, 0x94, 0x6c, 0x78, 0xe3, 0x1e, 0x4f, 0x3f, 0x20, 0xdd, 0x19, 0xa4, 0x4f, + 0x30, 0x3d, 0xa6, 0x50, 0x12, 0x41, 0x73, 0x5f, 0x5e, 0x92, 0xab, 0x47, 0x87, 0xbd, 0x52, 0x37, 0xdd, 0x2f, 0xf7, + 0x72, 0xd2, 0xb8, 0x85, 0xe9, 0x2a, 0xec, 0x76, 0xf7, 0xa3, 0x7e, 0x10, 0x38, 0xea, 0xa5, 0x9a, 0x66, 0xa0, 0x66, + 0x92, 0x62, 0x67, 0xf2, 0x56, 0xca, 0x37, 0x8e, 0xa5, 0x17, 0xd4, 0x79, 0x5a, 0x33, 0x6f, 0x72, 0x9f, 0x15, 0x8a, + 0xb2, 0xc2, 0xda, 0xa1, 0x8c, 0x61, 0xa8, 0x92, 0xe1, 0x71, 0x1a, 0xc1, 0xba, 0x28, 0x8a, 0xb6, 0xe8, 0x3a, 0x73, + 0x34, 0xa7, 0x5d, 0xa5, 0x4b, 0xb2, 0xc4, 0xf7, 0xe8, 0x47, 0x13, 0x95, 0xfd, 0x25, 0x7a, 0x91, 0x5a, 0x5a, 0x99, + 0xb6, 0x77, 0xd7, 0xd4, 0xf1, 0x3b, 0x1b, 0x85, 0x3f, 0xba, 0xc1, 0x67, 0xa6, 0xf3, 0xd1, 0x06, 0x37, 0x96, 0xee, + 0x08, 0x67, 0xb0, 0x6d, 0x63, 0x3f, 0xef, 0x03, 0x46, 0x85, 0x0f, 0xa8, 0xbe, 0x9e, 0xe6, 0x1d, 0xae, 0x1d, 0x71, + 0x53, 0x5b, 0xcf, 0xff, 0x66, 0x3a, 0x07, 0xe5, 0x07, 0x7c, 0xc0, 0xa6, 0xcf, 0xf6, 0x16, 0x2c, 0x95, 0xaf, 0x3b, + 0x01, 0x63, 0x49, 0xd5, 0x55, 0x98, 0xad, 0xd9, 0xd2, 0x60, 0x80, 0xe2, 0x22, 0x3f, 0x73, 0x2a, 0x88, 0x70, 0x89, + 0x4c, 0x70, 0x03, 0x45, 0x1f, 0x50, 0x42, 0x99, 0xb1, 0x08, 0x0f, 0xc6, 0x57, 0x98, 0x48, 0x8c, 0xf4, 0x27, 0x14, + 0xc6, 0xe3, 0x8e, 0x7f, 0x7e, 0xce, 0xcf, 0xbb, 0x66, 0xa7, 0xbd, 0xc2, 0xd8, 0xc4, 0xee, 0x59, 0xe8, 0xf8, 0x83, + 0x9c, 0xfd, 0xce, 0xfc, 0x45, 0x30, 0xad, 0xf3, 0x17, 0xc2, 0x7e, 0xe6, 0x44, 0x82, 0xbf, 0xf6, 0xd4, 0xf6, 0x06, + 0x62, 0x39, 0x11, 0xa5, 0x94, 0xfa, 0xc6, 0xbc, 0x55, 0x83, 0x05, 0x50, 0xc2, 0xec, 0xa7, 0x51, 0x7f, 0x6d, 0xe3, + 0x4f, 0x1d, 0x5a, 0x17, 0x4c, 0xbd, 0x16, 0x30, 0x64, 0x86, 0x11, 0x2d, 0x58, 0x13, 0x69, 0x64, 0xe6, 0x8f, 0x33, + 0xb9, 0x90, 0xf3, 0x3e, 0xf3, 0xe7, 0x19, 0x9f, 0x73, 0xea, 0xb7, 0xd5, 0xe6, 0x4a, 0xce, 0xd0, 0x5f, 0xc5, 0xb3, + 0xb7, 0x97, 0x18, 0xdd, 0xf5, 0x0e, 0x62, 0x24, 0xf6, 0x95, 0x3e, 0x9c, 0xfe, 0x73, 0x77, 0x8e, 0x5c, 0x06, 0x3c, + 0x9e, 0x8b, 0x15, 0x67, 0x7e, 0x80, 0xf2, 0x34, 0xb7, 0x36, 0x4e, 0x10, 0xa9, 0xc9, 0xd2, 0xb8, 0xf2, 0x7c, 0x2f, + 0xe3, 0x60, 0x41, 0x76, 0x6c, 0x87, 0x3d, 0xd3, 0xf7, 0xb4, 0xd7, 0xd7, 0x1e, 0xc8, 0x88, 0x3f, 0x97, 0xbe, 0x9f, + 0x40, 0x94, 0xab, 0xad, 0xa7, 0x2a, 0xaa, 0xe6, 0x5e, 0x1e, 0xac, 0xf3, 0x2b, 0x2f, 0xfd, 0xc9, 0xd3, 0x4e, 0xf7, + 0xe5, 0x6e, 0xdd, 0x80, 0x4c, 0x71, 0xa2, 0xf6, 0xc9, 0xc7, 0x0c, 0x11, 0xc5, 0xc5, 0x6f, 0x5c, 0x4f, 0xef, 0x30, + 0xe1, 0x7b, 0x5f, 0x20, 0xcb, 0x47, 0x2a, 0x71, 0xc1, 0x7a, 0x4e, 0x53, 0x63, 0xf9, 0x2e, 0xbb, 0xfd, 0xa2, 0x6d, + 0x83, 0x31, 0xe9, 0xd1, 0x8c, 0xcf, 0xfa, 0x6f, 0xac, 0x03, 0x00, 0x16, 0x7f, 0x97, 0xf8, 0x4a, 0xac, 0xe6, 0xb9, + 0xc9, 0x12, 0x5b, 0x71, 0xd5, 0x78, 0x5e, 0x27, 0x72, 0x90, 0xdd, 0x64, 0x87, 0x08, 0x4d, 0x11, 0x31, 0xed, 0xa5, + 0x7a, 0xbd, 0x41, 0xdf, 0x41, 0x84, 0x85, 0x8a, 0xea, 0x4c, 0x3f, 0x69, 0x0a, 0x33, 0x46, 0xa7, 0x23, 0x40, 0xe5, + 0x80, 0xa4, 0x2f, 0x0f, 0xbe, 0x7d, 0x25, 0x64, 0x05, 0xee, 0x72, 0xe7, 0xb2, 0x90, 0x02, 0xfb, 0xf0, 0xa1, 0xe9, + 0x6f, 0xcf, 0xf3, 0x3c, 0x57, 0x59, 0x3c, 0x8f, 0x7f, 0x92, 0x1c, 0x26, 0x26, 0xda, 0x1a, 0x45, 0x3c, 0xd1, 0x02, + 0x6f, 0x7f, 0xd1, 0x14, 0x09, 0x57, 0x95, 0xc2, 0x8a, 0x02, 0xbd, 0x6a, 0x74, 0x49, 0x50, 0xbc, 0x2b, 0x93, 0x40, + 0x67, 0x70, 0x5d, 0x32, 0x58, 0xb0, 0x3b, 0x15, 0xd2, 0x73, 0x6a, 0x2e, 0xd8, 0xce, 0x04, 0x78, 0x7d, 0x48, 0xe5, + 0xc6, 0x2c, 0x55, 0x48, 0x59, 0x54, 0xe7, 0x05, 0x78, 0xdb, 0xe3, 0xa0, 0xd5, 0x89, 0xc2, 0xde, 0x6b, 0x01, 0x68, + 0xc0, 0xf2, 0xb9, 0xcd, 0x03, 0x5b, 0x72, 0x80, 0xa6, 0x41, 0xd3, 0x31, 0x80, 0xec, 0xef, 0x7c, 0x95, 0xf4, 0xbf, + 0xbd, 0xe3, 0x4f, 0xeb, 0x16, 0x0c, 0x21, 0x63, 0x36, 0x4f, 0x9f, 0xb5, 0x68, 0x08, 0x14, 0x6f, 0xa3, 0x48, 0xc4, + 0x9f, 0x59, 0xab, 0x2a, 0x0d, 0x0c, 0xb9, 0x0e, 0x83, 0x5a, 0xa5, 0x9d, 0xcc, 0x99, 0x96, 0x59, 0xa9, 0x53, 0xb5, + 0xb0, 0x7b, 0x80, 0x0a, 0x3c, 0xa8, 0xde, 0x4b, 0x0d, 0x2a, 0x07, 0x18, 0x8a, 0xe2, 0xa2, 0x0c, 0x9d, 0x8a, 0x7b, + 0xfa, 0x3e, 0x4f, 0xb1, 0x09, 0xff, 0xf9, 0xb2, 0xf2, 0x8d, 0xe7, 0x20, 0x5e, 0x4a, 0xff, 0xb9, 0x53, 0x7e, 0x8c, + 0xbc, 0x86, 0xfa, 0xea, 0x2a, 0x0a, 0x73, 0x3c, 0x62, 0xbc, 0xb3, 0x31, 0x34, 0x02, 0xb1, 0x94, 0xe2, 0x09, 0xe1, + 0xc5, 0x36, 0x0a, 0x3e, 0x87, 0x0a, 0xb4, 0x19, 0x01, 0x58, 0xb8, 0xbb, 0x16, 0xfc, 0xcb, 0xd7, 0x90, 0xbf, 0x57, + 0x3c, 0xa2, 0xa9, 0x4c, 0x6e, 0x57, 0xa7, 0xec, 0x6b, 0xb8, 0xc2, 0xe8, 0xed, 0xfb, 0xbe, 0x5e, 0xe6, 0xeb, 0x4e, + 0xff, 0xe1, 0x13, 0x3e, 0x76, 0xa7, 0xfd, 0x65, 0xe7, 0x61, 0x9d, 0x91, 0x7f, 0x3c, 0x36, 0x79, 0x1b, 0xd6, 0xb4, + 0x1b, 0xec, 0x65, 0x8f, 0x89, 0xc3, 0x4c, 0x3c, 0x14, 0xe3, 0xdf, 0x16, 0x15, 0x38, 0xe6, 0x85, 0x06, 0x52, 0x6b, + 0x7f, 0x4e, 0x1f, 0xff, 0xaa, 0xb0, 0x43, 0xcc, 0x0e, 0xac, 0x04, 0x81, 0xee, 0x7b, 0x05, 0x6e, 0x29, 0x5d, 0x0a, + 0xb4, 0xf5, 0x68, 0x51, 0x2e, 0xae, 0xef, 0x01, 0x21, 0xb5, 0xb6, 0x59, 0xd5, 0x65, 0xa2, 0x71, 0x29, 0x8e, 0x57, + 0xa7, 0x3e, 0xfa, 0x03, 0x2d, 0xf6, 0xd9, 0xc5, 0x16, 0x9a, 0x5a, 0x88, 0x33, 0x71, 0x36, 0x2e, 0xb8, 0x19, 0x37, + 0xeb, 0xce, 0x81, 0x0a, 0x7d, 0x35, 0x18, 0xca, 0x92, 0xd0, 0xda, 0xc0, 0x10, 0x4c, 0x2b, 0x37, 0x58, 0x14, 0x25, + 0xdd, 0x51, 0x2f, 0x8e, 0xb6, 0x0d, 0x2e, 0xdc, 0x46, 0x8c, 0x72, 0xbc, 0x65, 0x7f, 0x81, 0x2a, 0x61, 0xfe, 0x92, + 0x59, 0xe4, 0x5d, 0x93, 0xce, 0x9f, 0x23, 0x3b, 0x10, 0xb3, 0xd4, 0x96, 0xb5, 0x5a, 0x85, 0x9b, 0x09, 0xf9, 0xa6, + 0x7b, 0x8a, 0x42, 0x81, 0x6d, 0x53, 0xb9, 0x8b, 0xff, 0x3d, 0xe2, 0x6a, 0xe7, 0x1c, 0xf9, 0x37, 0x9b, 0xe6, 0xb9, + 0x9e, 0x89, 0x03, 0xa2, 0x9c, 0xae, 0x82, 0x99, 0xc3, 0x70, 0xc7, 0x3d, 0xf5, 0xf3, 0x22, 0x3d, 0xcf, 0xa8, 0x63, + 0x25, 0xb7, 0xef, 0xd4, 0x2f, 0xfc, 0x52, 0x3c, 0xf6, 0x0b, 0x7d, 0x61, 0xcf, 0x95, 0x78, 0x4b, 0xb7, 0x4f, 0x4e, + 0xa2, 0xd5, 0xa8, 0x9c, 0x9a, 0x76, 0xe1, 0x25, 0xd4, 0x6c, 0x6f, 0x68, 0xaf, 0xcc, 0x2d, 0x7a, 0xd9, 0x1d, 0xee, + 0x57, 0x76, 0x8a, 0x22, 0x76, 0xa5, 0x60, 0x9f, 0x56, 0xd2, 0xe3, 0x4a, 0x9c, 0x73, 0xa1, 0xb3, 0x4e, 0x2d, 0xa0, + 0x28, 0xcb, 0x00, 0x2b, 0x2f, 0x15, 0xfa, 0x6d, 0xc5, 0x13, 0x94, 0x1c, 0xa6, 0x36, 0x1b, 0x47, 0xcf, 0x7a, 0x49, + 0x25, 0xf7, 0xd5, 0x06, 0xf7, 0x92, 0x91, 0xd6, 0xde, 0xe1, 0xf2, 0xce, 0x56, 0x1f, 0xf1, 0xb4, 0x87, 0xfb, 0x8f, + 0x59, 0x81, 0x7f, 0xab, 0xca, 0xfb, 0x86, 0x31, 0x14, 0x02, 0xb5, 0xce, 0xa5, 0xd4, 0xb4, 0x49, 0xb8, 0x64, 0x10, + 0xee, 0x1d, 0xac, 0x11, 0x51, 0x27, 0xb0, 0xbf, 0x46, 0xc9, 0x6a, 0x67, 0xc0, 0xe7, 0x49, 0x88, 0x98, 0x13, 0xe5, + 0xb1, 0x7f, 0x7e, 0xb7, 0xb2, 0xc9, 0x9f, 0x98, 0x43, 0x9d, 0xa1, 0x4a, 0x58, 0x87, 0xf8, 0x83, 0xb8, 0x79, 0xd5, + 0xeb, 0xdb, 0x25, 0xca, 0xdf, 0x4c, 0x4f, 0x2c, 0xcc, 0x0a, 0x2b, 0xf8, 0x4b, 0x29, 0x5f, 0xdf, 0xf1, 0x01, 0xd4, + 0x67, 0x93, 0x1f, 0xff, 0xbc, 0xa1, 0x7b, 0xd9, 0x95, 0xff, 0x72, 0x87, 0x40, 0x31, 0xed, 0xe2, 0x70, 0x8e, 0x4b, + 0x87, 0xc2, 0xec, 0x02, 0xc3, 0xe2, 0x45, 0x55, 0x1d, 0xf0, 0xe1, 0x39, 0xe2, 0xe3, 0xf3, 0x24, 0x2e, 0x88, 0x84, + 0xd2, 0xfc, 0x79, 0x50, 0x37, 0xa3, 0xe3, 0xc2, 0x86, 0x3e, 0x38, 0x9c, 0xd1, 0x00, 0x84, 0xac, 0xb1, 0xc9, 0xf6, + 0x63, 0x95, 0x52, 0x99, 0x59, 0x3a, 0x72, 0xc7, 0xc6, 0x76, 0xb5, 0xd4, 0xe3, 0xbb, 0x7e, 0x1f, 0xee, 0x64, 0xd3, + 0xb2, 0x7e, 0xca, 0x10, 0xfb, 0xa8, 0x0b, 0xc3, 0x05, 0xc8, 0xda, 0x0f, 0xb9, 0xf6, 0x62, 0x19, 0xdb, 0x80, 0x3e, + 0xc4, 0xfe, 0xa7, 0x5d, 0x9c, 0xd1, 0xad, 0xf0, 0xb1, 0x58, 0xb4, 0x3a, 0xdb, 0x90, 0x24, 0x07, 0x46, 0x73, 0x48, + 0x30, 0x6e, 0x44, 0x68, 0x1b, 0x94, 0xe0, 0xb1, 0x62, 0x0a, 0x37, 0xe2, 0xfe, 0x38, 0x58, 0x68, 0x55, 0xd1, 0x8d, + 0xb9, 0x8d, 0x6d, 0x14, 0x67, 0xf0, 0xf5, 0x80, 0x7f, 0x15, 0x65, 0x7c, 0x80, 0xbb, 0x41, 0xe4, 0xce, 0x9e, 0x97, + 0x92, 0x48, 0x2d, 0xa7, 0xdb, 0x56, 0x6c, 0xe7, 0x97, 0xd8, 0xed, 0x82, 0x3d, 0x5f, 0x16, 0xe6, 0xcc, 0xd6, 0x20, + 0x33, 0x8c, 0xbd, 0xb7, 0xc0, 0xbb, 0x6d, 0x29, 0x4c, 0x76, 0xe9, 0x1b, 0xa8, 0x84, 0x56, 0xe7, 0xa3, 0xc3, 0x78, + 0x53, 0x07, 0xae, 0xe2, 0x78, 0x1a, 0xdb, 0x56, 0x62, 0x34, 0x46, 0x1e, 0xc9, 0x30, 0x95, 0xe1, 0x10, 0x7f, 0x24, + 0xbb, 0x29, 0x37, 0xd3, 0xa5, 0xce, 0x2e, 0x0d, 0x10, 0x74, 0x85, 0x55, 0x0f, 0x85, 0x24, 0x11, 0x70, 0xcb, 0x15, + 0xc8, 0xc3, 0x70, 0x3f, 0xaf, 0xc6, 0xc8, 0xe1, 0x6c, 0x81, 0xd0, 0xf0, 0xb2, 0x51, 0x90, 0x39, 0xf1, 0xed, 0x21, + 0xf1, 0x72, 0x6a, 0x7a, 0xc7, 0x11, 0x0f, 0x86, 0xea, 0x36, 0x0f, 0x5e, 0x8e, 0xaa, 0x4e, 0x67, 0xe9, 0x91, 0x70, + 0x4b, 0xe7, 0x59, 0x49, 0xca, 0x51, 0x35, 0x05, 0x7a, 0x8e, 0x34, 0x1a, 0xca, 0x5b, 0x5b, 0x29, 0x81, 0x55, 0xd0, + 0x32, 0x39, 0xfd, 0xbc, 0x23, 0x12, 0x91, 0x70, 0x21, 0xa0, 0xf8, 0xcb, 0x28, 0xa4, 0xd1, 0x5b, 0xcd, 0xc7, 0x30, + 0xc8, 0x70, 0x9d, 0x57, 0x5c, 0x0a, 0xfe, 0xf8, 0xc5, 0x01, 0xf4, 0x50, 0x94, 0x0d, 0xdc, 0x2f, 0x16, 0xfe, 0xcc, + 0x2e, 0x46, 0xf4, 0x36, 0x9b, 0xa1, 0xba, 0xcd, 0xe8, 0x27, 0xd6, 0xe7, 0x95, 0x22, 0x76, 0xcf, 0x0e, 0xed, 0xc1, + 0x8e, 0x19, 0x59, 0x5d, 0x25, 0x1d, 0xcd, 0x04, 0x99, 0xf4, 0x32, 0xf2, 0x0c, 0x41, 0x84, 0x0e, 0xd8, 0x27, 0x87, + 0x36, 0x4a, 0x87, 0xf9, 0x0a, 0xc2, 0x3f, 0xc7, 0x7c, 0x0e, 0x89, 0x6e, 0x76, 0x09, 0x8e, 0x7a, 0x8d, 0x48, 0x3a, + 0x89, 0x50, 0x04, 0x5e, 0xc8, 0x7a, 0x22, 0x98, 0x78, 0x41, 0xef, 0x26, 0xb8, 0xe2, 0xc5, 0xd1, 0x4d, 0x07, 0xcb, + 0x61, 0x46, 0xfc, 0x1b, 0x13, 0x46, 0xee, 0x12, 0xe2, 0xdb, 0x03, 0x84, 0x3b, 0xd8, 0x29, 0x88, 0xea, 0xe5, 0x56, + 0x9b, 0x5e, 0x9c, 0xa2, 0x9e, 0xc7, 0xf9, 0x78, 0x36, 0xd6, 0x91, 0x17, 0x72, 0x38, 0x5b, 0xc4, 0x30, 0x40, 0x16, + 0xc2, 0x93, 0x9b, 0x76, 0x97, 0x10, 0x90, 0x9c, 0x4c, 0x65, 0x59, 0xde, 0xc4, 0x4d, 0x27, 0x60, 0x91, 0xe7, 0x76, + 0x69, 0x4a, 0xf1, 0xf4, 0x9f, 0xaa, 0xb1, 0x0d, 0x67, 0x8b, 0x8c, 0x63, 0xd0, 0x62, 0x95, 0xce, 0x20, 0x10, 0x17, + 0xe5, 0x46, 0x4b, 0x2f, 0x0e, 0x73, 0xb8, 0xf9, 0xf7, 0xb8, 0xbc, 0x8d, 0x09, 0x20, 0x1f, 0xbc, 0x49, 0x3a, 0x40, + 0xcf, 0xf2, 0x7c, 0xce, 0xd0, 0x4b, 0x6f, 0x73, 0x91, 0x4c, 0x84, 0xff, 0xd3, 0xc9, 0x47, 0xa2, 0x1c, 0xe9, 0x15, + 0x72, 0x9c, 0x50, 0x51, 0xb2, 0x9d, 0x10, 0xd5, 0xcb, 0xc3, 0x7f, 0x61, 0xd5, 0x11, 0x02, 0xe7, 0xdb, 0x84, 0x2f, + 0x5f, 0x6e, 0xf9, 0xc1, 0xd7, 0x97, 0xec, 0x44, 0x28, 0xe5, 0x1f, 0x18, 0x87, 0x98, 0x56, 0x32, 0xb1, 0x63, 0x40, + 0x64, 0x7a, 0x58, 0xc0, 0x72, 0xe0, 0x66, 0xe4, 0xf1, 0xe3, 0xd6, 0x38, 0xd3, 0x14, 0x9f, 0xab, 0xff, 0x9f, 0xd8, + 0x7a, 0x10, 0xd7, 0x6e, 0x2f, 0x8d, 0x48, 0x62, 0x1a, 0xa3, 0x01, 0xf3, 0x8a, 0x06, 0x68, 0x9c, 0x94, 0x01, 0xc3, + 0x5e, 0x59, 0xfa, 0x85, 0x1e, 0x63, 0x93, 0x47, 0xaa, 0x99, 0x98, 0x1f, 0x21, 0x64, 0xbb, 0x46, 0xc1, 0x44, 0x12, + 0x8c, 0xf6, 0x2d, 0x50, 0xd8, 0x81, 0x14, 0x53, 0xdd, 0x01, 0xf9, 0x9c, 0xcb, 0xc8, 0x6b, 0x20, 0x1b, 0x7d, 0xbe, + 0xb9, 0xd7, 0xaf, 0x13, 0xfb, 0xd2, 0xa3, 0x39, 0x84, 0x48, 0x23, 0xf2, 0xfb, 0xf0, 0xfe, 0x58, 0xaf, 0x99, 0x77, + 0xbd, 0xca, 0x14, 0x2f, 0xc1, 0xc8, 0x07, 0x37, 0x96, 0x90, 0x0f, 0x1a, 0xac, 0x02, 0x73, 0xf2, 0x35, 0xaa, 0xb5, + 0x43, 0xcc, 0xce, 0xf3, 0x26, 0x47, 0xde, 0x76, 0x75, 0x54, 0x51, 0x58, 0xad, 0xc0, 0xf9, 0x55, 0x03, 0xad, 0xc4, + 0x07, 0xf2, 0x2f, 0x43, 0xa2, 0x8a, 0x09, 0x61, 0x80, 0x1e, 0x19, 0xe7, 0x1f, 0x84, 0x28, 0xe8, 0x32, 0xa9, 0x5a, + 0x36, 0xfb, 0x97, 0x9a, 0xc3, 0x55, 0x60, 0x04, 0xec, 0x36, 0xa6, 0x31, 0x8d, 0xe7, 0xe3, 0x28, 0x66, 0xd6, 0xbc, + 0x2b, 0x89, 0xaf, 0x70, 0x2e, 0x08, 0x2a, 0xac, 0xe1, 0xbe, 0xcb, 0xff, 0xfd, 0x7c, 0xfc, 0x90, 0x97, 0x62, 0xe7, + 0xd7, 0xe5, 0x1a, 0xfa, 0x61, 0xff, 0x75, 0x29, 0x56, 0xbd, 0x49, 0x2d, 0x7a, 0x37, 0x9a, 0x36, 0x8e, 0xff, 0x7c, + 0x76, 0xb1, 0x91, 0x4e, 0xef, 0x78, 0xcb, 0x7b, 0xd0, 0x37, 0xa7, 0xe9, 0x69, 0x5c, 0xe0, 0xe7, 0x2c, 0x2f, 0x67, + 0xff, 0x95, 0xbb, 0x94, 0xc7, 0xf5, 0x7b, 0x76, 0xdd, 0xa1, 0x39, 0xad, 0xbd, 0xb1, 0xec, 0xdd, 0xb3, 0x2b, 0xfe, + 0x1e, 0x81, 0x2c, 0xbe, 0x08, 0xc9, 0xa4, 0x52, 0x09, 0x20, 0xd0, 0x5c, 0x0f, 0x7e, 0xf7, 0xc4, 0x28, 0xa5, 0x1e, + 0xef, 0x3f, 0x26, 0x5f, 0x95, 0x75, 0xb8, 0x3b, 0xb7, 0x40, 0xd6, 0x23, 0xfd, 0x3b, 0x4f, 0x37, 0xba, 0x5f, 0xd0, + 0xa8, 0x3a, 0x75, 0x90, 0x19, 0x8d, 0x33, 0x2d, 0x0d, 0xf9, 0xb7, 0x8d, 0xe6, 0x8c, 0xc2, 0xb7, 0x82, 0x46, 0x74, + 0x13, 0xe1, 0x1f, 0x57, 0x8d, 0x03, 0x4a, 0x0a, 0xf8, 0x61, 0x9b, 0xf6, 0x6d, 0xf7, 0x72, 0x2f, 0xa4, 0xa9, 0xf2, + 0xcb, 0x33, 0x16, 0x18, 0xb4, 0x0f, 0x74, 0x66, 0x47, 0xff, 0x7f, 0x0a, 0x68, 0xbd, 0x88, 0x51, 0xb2, 0x95, 0x3a, + 0x40, 0x5c, 0x6c, 0xe3, 0xe6, 0x0b, 0xbd, 0x71, 0x9a, 0x0b, 0x67, 0x1e, 0xf5, 0xe8, 0x24, 0xdd, 0x02, 0x18, 0xd5, + 0xfc, 0x7e, 0xc4, 0xab, 0x53, 0x57, 0x46, 0x7c, 0x54, 0xbc, 0xa3, 0xbb, 0x0b, 0xcc, 0xf6, 0xbf, 0xf2, 0x2e, 0x46, + 0x34, 0x7f, 0xf7, 0x11, 0xe8, 0x86, 0x1f, 0xb3, 0xd3, 0x37, 0x9f, 0xf9, 0xe3, 0x03, 0x3e, 0x0c, 0xed, 0x1e, 0xa3, + 0x79, 0x67, 0xdc, 0x9a, 0x27, 0x3c, 0x31, 0xc8, 0x0c, 0xe0, 0xb2, 0xcf, 0xde, 0x7b, 0x2c, 0xe3, 0xc0, 0x77, 0x20, + 0x56, 0x26, 0xf3, 0x16, 0x30, 0x29, 0x17, 0x23, 0xa4, 0x35, 0x32, 0xfa, 0x37, 0xe0, 0x45, 0xc9, 0xe8, 0x9f, 0xce, + 0x3d, 0x8a, 0x6e, 0x48, 0xf4, 0xc9, 0x93, 0x01, 0xcb, 0x3a, 0x28, 0x5a, 0x62, 0x52, 0x21, 0x3a, 0x84, 0x2c, 0x13, + 0xa0, 0xf4, 0x49, 0xa0, 0xa1, 0xf0, 0x77, 0x2d, 0x27, 0xbd, 0x9f, 0x7b, 0x66, 0x82, 0xa4, 0xc7, 0xe4, 0x28, 0x8d, + 0x4c, 0x18, 0xf9, 0x73, 0xcd, 0xcb, 0xeb, 0xeb, 0xa7, 0x76, 0x7b, 0xd0, 0x7c, 0x64, 0xbf, 0x95, 0xe6, 0xc4, 0xe4, + 0x6b, 0xad, 0x06, 0x2b, 0x79, 0x03, 0x28, 0x9b, 0x7d, 0x41, 0x2b, 0x60, 0xf1, 0x5b, 0x0d, 0x61, 0xe9, 0x99, 0x0c, + 0xb4, 0x06, 0x4e, 0xd2, 0x73, 0x36, 0xb8, 0x6e, 0x98, 0x1f, 0x91, 0x5e, 0xaf, 0x98, 0xa8, 0x32, 0xa7, 0x27, 0x7d, + 0xba, 0xb9, 0x1e, 0x7b, 0xb1, 0xd0, 0x87, 0xd4, 0x13, 0xfa, 0x93, 0x17, 0xe1, 0x6c, 0xf9, 0xb9, 0xec, 0x3f, 0x4d, + 0x20, 0x75, 0xd5, 0x18, 0x2d, 0x74, 0x7e, 0x3d, 0xbe, 0x9b, 0x35, 0x3e, 0x1a, 0xd9, 0xea, 0x6d, 0xbb, 0x73, 0x64, + 0xb9, 0x77, 0x8b, 0x59, 0x5f, 0x42, 0x3e, 0xa3, 0x58, 0x33, 0x99, 0x83, 0x9c, 0x23, 0xb4, 0xbf, 0xd6, 0x95, 0xe4, + 0xb8, 0xf6, 0x61, 0x4e, 0x41, 0x7a, 0x6c, 0x0d, 0xeb, 0x20, 0x6a, 0xbe, 0xad, 0x7d, 0x06, 0x2d, 0xbf, 0x9e, 0x7a, + 0x9d, 0x16, 0x4c, 0xf2, 0xa4, 0x73, 0x5f, 0xf7, 0x8f, 0x34, 0xe2, 0x5e, 0x7a, 0x59, 0x13, 0x45, 0xb7, 0x48, 0x40, + 0xd7, 0x2a, 0x2d, 0xf4, 0xb2, 0xe2, 0x3c, 0xad, 0xe8, 0x4f, 0x33, 0xe6, 0x51, 0xc9, 0xaa, 0x51, 0xa9, 0x9e, 0x5c, + 0x63, 0x9c, 0x29, 0xeb, 0x09, 0x20, 0x17, 0x45, 0x02, 0xc7, 0x59, 0x6f, 0xd7, 0xa7, 0x4b, 0x43, 0x07, 0xf1, 0xd1, + 0xdb, 0xb8, 0xe9, 0xbc, 0x83, 0x69, 0x2c, 0xdd, 0x9f, 0x48, 0x67, 0x19, 0xc3, 0x89, 0x2a, 0x4b, 0xf2, 0xb4, 0x1c, + 0x85, 0xba, 0xa3, 0xbb, 0x20, 0x29, 0x4b, 0xf6, 0x46, 0x3b, 0xfb, 0xe3, 0x7a, 0xf2, 0x28, 0xfb, 0x30, 0xec, 0xa1, + 0x0a, 0xdc, 0x43, 0xaa, 0xef, 0x72, 0xff, 0xba, 0xcc, 0x94, 0xa6, 0xc1, 0xfe, 0xc7, 0xd7, 0xa1, 0x03, 0x3f, 0x0e, + 0x6e, 0xc7, 0x11, 0x12, 0x28, 0xb7, 0x98, 0xa6, 0x0c, 0x5b, 0x4e, 0x30, 0xd9, 0xee, 0x0d, 0x37, 0xc5, 0xd5, 0x9e, + 0x4b, 0x14, 0x83, 0x25, 0xf7, 0xc0, 0xcb, 0x67, 0xb4, 0x7f, 0x62, 0xeb, 0x26, 0xe6, 0xa9, 0x6b, 0xe1, 0xa3, 0xd4, + 0xc2, 0x34, 0x74, 0x60, 0xb0, 0xc8, 0x59, 0x92, 0x8c, 0x04, 0xbb, 0xfc, 0xd2, 0x6a, 0x27, 0xcf, 0x72, 0x25, 0xd3, + 0xd7, 0x5e, 0x4f, 0x4c, 0x31, 0xd2, 0xb0, 0xa5, 0xed, 0x70, 0xd8, 0xc9, 0x79, 0x02, 0x22, 0x44, 0x54, 0xcf, 0x97, + 0xb8, 0xa6, 0xbe, 0x10, 0x67, 0xdd, 0xf9, 0x32, 0x56, 0xb4, 0xe7, 0x41, 0x01, 0x08, 0xad, 0x36, 0xad, 0x54, 0x17, + 0xdc, 0xd0, 0x23, 0x48, 0x77, 0xeb, 0xe5, 0x1d, 0x14, 0x55, 0xcd, 0xf4, 0x60, 0xd2, 0x8b, 0x1f, 0xe7, 0x5d, 0xe1, + 0x61, 0x16, 0x19, 0x2a, 0x80, 0x1b, 0xa3, 0xef, 0xe0, 0x72, 0x7d, 0xcf, 0x43, 0xb8, 0xb5, 0xe6, 0x4c, 0xcf, 0x4f, + 0x5b, 0x8f, 0x78, 0xf1, 0xe6, 0x61, 0x1c, 0xc2, 0x5d, 0x6c, 0x7d, 0xfa, 0x24, 0x5f, 0x3b, 0x6c, 0xe7, 0xd1, 0xa2, + 0xd0, 0xd6, 0xe5, 0xd4, 0xf6, 0xe2, 0xce, 0xe7, 0xf9, 0x27, 0xb5, 0x05, 0xca, 0x0a, 0xbc, 0xf6, 0xea, 0x3e, 0x22, + 0x14, 0x73, 0x07, 0xdf, 0xfe, 0x2f, 0x1d, 0xb5, 0xdd, 0x7c, 0xde, 0x0e, 0x67, 0x46, 0x0f, 0xcf, 0x48, 0x88, 0xba, + 0x3c, 0xd8, 0x24, 0xd7, 0xaf, 0xfe, 0xe9, 0x29, 0x7e, 0xa5, 0x9d, 0xe6, 0x5f, 0x73, 0xce, 0x0b, 0x63, 0x53, 0x3e, + 0xdb, 0x47, 0x9a, 0x30, 0xba, 0x46, 0x84, 0xcb, 0xef, 0xdb, 0xd0, 0x4a, 0x83, 0x8c, 0x48, 0x08, 0x79, 0xbd, 0x75, + 0x05, 0xb8, 0xef, 0x2f, 0xdb, 0x1d, 0xbc, 0xa5, 0x44, 0xe2, 0x8d, 0xea, 0x38, 0x6e, 0xcf, 0xc8, 0xc2, 0xf5, 0xfd, + 0x5b, 0x07, 0x82, 0x7d, 0xad, 0x7d, 0x25, 0xbf, 0xdc, 0x39, 0x7a, 0x01, 0x06, 0x94, 0x30, 0x84, 0x27, 0x51, 0xff, + 0x97, 0xd8, 0x88, 0xd4, 0x6d, 0xc6, 0x74, 0xc2, 0x84, 0xfd, 0x59, 0xd1, 0xaa, 0xad, 0xf4, 0x00, 0x28, 0xa6, 0x4e, + 0xae, 0x06, 0x51, 0x74, 0x87, 0x26, 0xe2, 0x8e, 0x39, 0x5a, 0xde, 0x13, 0x9a, 0xb5, 0x40, 0x15, 0x4e, 0x61, 0xcf, + 0xa3, 0x50, 0x9a, 0xe1, 0x19, 0xf4, 0x01, 0xf6, 0x52, 0x84, 0x9c, 0xb9, 0x24, 0x79, 0xe6, 0xc0, 0x6b, 0x13, 0x04, + 0x69, 0x74, 0xb7, 0x7a, 0x4a, 0xb1, 0x8b, 0x6c, 0xb7, 0x20, 0xe9, 0xcd, 0x22, 0x74, 0x2b, 0x56, 0x49, 0x8a, 0xbb, + 0x99, 0x8a, 0xad, 0x0e, 0x1e, 0x61, 0x8f, 0x48, 0xdf, 0x96, 0xfd, 0xbd, 0x75, 0xc0, 0x42, 0x17, 0x45, 0x4d, 0x4a, + 0xed, 0xbf, 0x29, 0x1d, 0x38, 0xa1, 0x86, 0x09, 0x05, 0x05, 0xfb, 0x6c, 0xdc, 0x62, 0xbc, 0x7b, 0x6b, 0x6d, 0x6f, + 0x21, 0xf0, 0x2a, 0x34, 0x37, 0xd5, 0x82, 0x5c, 0xe1, 0x0b, 0x64, 0xc9, 0xb5, 0x15, 0x42, 0xd7, 0x37, 0x2d, 0xbb, + 0xf0, 0xfc, 0xc2, 0xf4, 0xc7, 0x56, 0x29, 0xea, 0x52, 0x90, 0x4b, 0x38, 0xb5, 0xb2, 0x46, 0x57, 0x1f, 0xd8, 0x9a, + 0x8e, 0x51, 0xbb, 0x33, 0xce, 0x5e, 0x21, 0x90, 0xfc, 0x89, 0x4a, 0x9d, 0x53, 0x9a, 0x11, 0x18, 0x5e, 0x0f, 0x8a, + 0xd5, 0x2f, 0xb9, 0x16, 0x30, 0x0e, 0x0f, 0xf4, 0xc7, 0xa0, 0x48, 0x9e, 0x64, 0x62, 0x0e, 0x03, 0x4f, 0xe5, 0xb0, + 0x73, 0xcf, 0xe9, 0x4e, 0xe6, 0xf7, 0xbe, 0xb1, 0xb7, 0xc7, 0xae, 0xe3, 0x96, 0x31, 0x3f, 0x8c, 0x20, 0x6a, 0x25, + 0xc2, 0x48, 0x45, 0x1e, 0x31, 0x80, 0x12, 0x4e, 0xae, 0x1b, 0x70, 0xa8, 0xa9, 0x36, 0xdc, 0xa7, 0xe8, 0x08, 0xcc, + 0xa9, 0xcb, 0x34, 0xaa, 0x39, 0x55, 0x99, 0x20, 0x84, 0xcf, 0x8d, 0x5b, 0xe7, 0x78, 0x02, 0x33, 0xed, 0x80, 0xd5, + 0x26, 0xaf, 0x53, 0x1c, 0x84, 0xcc, 0xd4, 0x9d, 0x2d, 0x1a, 0x13, 0x49, 0x4d, 0xb5, 0x4b, 0xad, 0x05, 0xe3, 0x64, + 0xb3, 0x6b, 0xd4, 0x6e, 0x2b, 0x32, 0xb8, 0x88, 0x15, 0x0f, 0x64, 0x04, 0x38, 0xba, 0x96, 0x6b, 0x94, 0x27, 0x47, + 0x5a, 0x10, 0xe6, 0x26, 0x39, 0x8e, 0x98, 0xb6, 0x7f, 0xdc, 0x8d, 0xe8, 0x66, 0x9e, 0x99, 0x8a, 0xc3, 0x5f, 0xbd, + 0xe7, 0xb6, 0x5e, 0x59, 0x2a, 0xd6, 0xf3, 0x2c, 0x25, 0xeb, 0x95, 0xcf, 0x2c, 0xa5, 0x21, 0xb9, 0xb0, 0x16, 0xd8, + 0x6c, 0x9a, 0xa5, 0xd9, 0x72, 0x7a, 0xde, 0xb9, 0x45, 0x66, 0x5e, 0xf0, 0x08, 0x53, 0xde, 0xae, 0xbc, 0x44, 0x67, + 0x03, 0xf6, 0x3f, 0xfb, 0x7c, 0x09, 0x9a, 0x19, 0x2b, 0x34, 0xc7, 0xbb, 0xc2, 0x1c, 0x12, 0x59, 0x61, 0xd4, 0x8f, + 0x4b, 0xf9, 0xec, 0x5d, 0x70, 0xda, 0x6a, 0xe7, 0x46, 0x05, 0x85, 0xef, 0x4d, 0x52, 0x60, 0x22, 0x09, 0x6c, 0x72, + 0x34, 0xee, 0x83, 0xf3, 0xac, 0x9c, 0xe9, 0x97, 0x03, 0x04, 0xff, 0x89, 0x6d, 0xc6, 0x35, 0x27, 0x30, 0x77, 0x06, + 0x77, 0x4a, 0xa8, 0x6e, 0x88, 0xe1, 0xf5, 0xd9, 0x75, 0x4e, 0x56, 0x1c, 0x73, 0x4b, 0xb2, 0x10, 0xe0, 0xb5, 0x07, + 0xb7, 0xcf, 0x33, 0x6b, 0x71, 0xa7, 0xe2, 0x34, 0xd4, 0x66, 0x5f, 0xfa, 0xcc, 0xd7, 0x83, 0x5f, 0x8d, 0x1c, 0x65, + 0x5c, 0xe0, 0x66, 0xd7, 0x8b, 0x81, 0x21, 0x34, 0x9e, 0x05, 0xe8, 0x11, 0x4f, 0xe9, 0xbf, 0x80, 0x10, 0xbf, 0x1b, + 0xfc, 0x2a, 0x33, 0x83, 0xd5, 0xd7, 0x2a, 0x06, 0x89, 0x9e, 0x64, 0x42, 0x81, 0x91, 0x61, 0xe8, 0xba, 0x2a, 0x8b, + 0x84, 0x37, 0xbc, 0xd8, 0xcd, 0xee, 0xcd, 0x98, 0x3f, 0x60, 0xa8, 0x43, 0xf8, 0x25, 0xb1, 0x27, 0xe6, 0x39, 0x9c, + 0x6a, 0xe6, 0x65, 0x76, 0x56, 0x45, 0x63, 0xbd, 0x59, 0xe3, 0x89, 0x09, 0xd5, 0x87, 0x68, 0xdb, 0x37, 0xc5, 0xdc, + 0x6e, 0xf7, 0xd6, 0x87, 0xd3, 0x44, 0x8d, 0x98, 0x99, 0x9a, 0x8f, 0xfb, 0xc6, 0x0a, 0x69, 0x33, 0x52, 0x64, 0x12, + 0xaa, 0x0c, 0x56, 0xc2, 0xc8, 0x3d, 0xbd, 0x6d, 0x75, 0x74, 0x5a, 0x00, 0x4e, 0x34, 0xcb, 0xdb, 0x4a, 0x64, 0xa3, + 0xbd, 0xb6, 0x1b, 0x85, 0xa8, 0x17, 0x3d, 0x9e, 0x51, 0x28, 0x15, 0x37, 0x34, 0x70, 0x6e, 0x06, 0x02, 0x4b, 0x3f, + 0xc5, 0x4b, 0xd8, 0x8b, 0xae, 0x3d, 0x6b, 0xc2, 0xb5, 0x51, 0x7b, 0x87, 0xb4, 0xac, 0x54, 0x4b, 0xd9, 0x77, 0x8e, + 0x74, 0xe3, 0x85, 0xaa, 0x97, 0xb9, 0xd0, 0xb9, 0xda, 0x4f, 0x7c, 0x6c, 0x1b, 0x23, 0x4d, 0xed, 0x9a, 0xfe, 0x66, + 0xce, 0x36, 0xd7, 0x99, 0xac, 0x90, 0x1f, 0x2c, 0x43, 0xfe, 0x04, 0xe9, 0xb6, 0x91, 0x4d, 0xac, 0xc4, 0xfa, 0x85, + 0x1f, 0xf0, 0x0e, 0x3a, 0x67, 0x2d, 0x3b, 0xb0, 0x36, 0xdb, 0x2e, 0x58, 0x26, 0x3f, 0x58, 0xae, 0x5d, 0xe3, 0x37, + 0x7c, 0x08, 0x57, 0xb2, 0x3a, 0x97, 0x9d, 0xec, 0x3d, 0xfe, 0x45, 0xfd, 0xf2, 0xfb, 0x19, 0x3d, 0x8b, 0x0f, 0x96, + 0x35, 0xde, 0x4c, 0x9f, 0xb2, 0x32, 0xfb, 0xc5, 0xed, 0x5b, 0x8b, 0x8f, 0x37, 0x97, 0x36, 0x38, 0x8f, 0x61, 0x68, + 0xef, 0xc5, 0xdd, 0x83, 0xfa, 0xc3, 0x70, 0x56, 0x4e, 0xd0, 0x6a, 0x18, 0x19, 0xe0, 0xce, 0xd6, 0xf3, 0x05, 0xbd, + 0xc7, 0xc6, 0x4c, 0x1f, 0xee, 0xf9, 0xd0, 0xbb, 0xfc, 0xc7, 0xcb, 0x7e, 0x24, 0x9c, 0x3d, 0x3a, 0xbb, 0x40, 0xd0, + 0x5a, 0xd7, 0x56, 0x4a, 0xf5, 0x98, 0xd7, 0x2e, 0x8e, 0xd0, 0x92, 0x3d, 0x2f, 0x75, 0x34, 0xff, 0xd0, 0x2a, 0x87, + 0x0d, 0x1a, 0x63, 0xf5, 0xbe, 0xd5, 0x96, 0x46, 0x6f, 0x3f, 0x10, 0x16, 0xa6, 0xa1, 0x52, 0x81, 0x80, 0x4a, 0xff, + 0xcc, 0x26, 0x5c, 0x7b, 0x9b, 0xc9, 0x28, 0x7d, 0x8a, 0x30, 0x7b, 0xd4, 0x93, 0xc5, 0xfb, 0x8e, 0x9d, 0xac, 0xd5, + 0x1b, 0xca, 0x74, 0x58, 0x69, 0x33, 0x59, 0xa9, 0x11, 0x46, 0x0c, 0x33, 0x9b, 0xe1, 0x85, 0x2a, 0xa7, 0xc3, 0xae, + 0x04, 0x81, 0xa9, 0xda, 0x78, 0xe2, 0xdc, 0xc1, 0xf3, 0xdf, 0xb2, 0x3e, 0x40, 0x8c, 0xe9, 0xe1, 0xc4, 0xde, 0x81, + 0x2e, 0xb5, 0x8b, 0x27, 0xfc, 0xcb, 0xdf, 0x92, 0x43, 0xb0, 0x42, 0xb5, 0xf1, 0xfd, 0x00, 0xd6, 0xd7, 0x20, 0xe6, + 0x6d, 0x77, 0xe2, 0xe0, 0x8e, 0x5d, 0x59, 0x3e, 0xcd, 0xcb, 0x03, 0x88, 0x52, 0x36, 0x72, 0xb3, 0x61, 0xcd, 0xaf, + 0x66, 0x16, 0xbb, 0x36, 0x9e, 0x1f, 0xc8, 0xfa, 0xb0, 0xcd, 0x9f, 0x0b, 0x90, 0xa2, 0x28, 0xd0, 0x96, 0xbb, 0xda, + 0xa2, 0x8d, 0x80, 0x69, 0xd7, 0x3a, 0x6f, 0x6d, 0x21, 0xeb, 0xa4, 0x76, 0xca, 0xa0, 0x2b, 0x65, 0x8a, 0x9c, 0x9a, + 0x51, 0x23, 0x44, 0xc7, 0xf8, 0x41, 0x0e, 0xfd, 0x62, 0xf5, 0xdd, 0xf5, 0x3b, 0x5d, 0x80, 0xb8, 0xe2, 0x54, 0xe6, + 0x59, 0x49, 0xac, 0x0f, 0x37, 0x79, 0xcf, 0x1b, 0xf4, 0xbf, 0xd4, 0x95, 0xef, 0xcb, 0xda, 0x13, 0x24, 0x03, 0x41, + 0x3a, 0x0e, 0xfe, 0x18, 0xc0, 0xf0, 0xc7, 0x06, 0x46, 0x2f, 0x7a, 0x78, 0x1e, 0x54, 0xbf, 0x76, 0xc2, 0x77, 0x96, + 0x5f, 0xaa, 0xd0, 0xfb, 0x49, 0xf5, 0x0b, 0x58, 0x5f, 0x83, 0xa0, 0x8e, 0x44, 0xcd, 0xef, 0x69, 0x5b, 0xf7, 0x2b, + 0x8c, 0x78, 0x91, 0x0f, 0x15, 0xf9, 0xeb, 0xba, 0xfa, 0x3c, 0x87, 0x01, 0x39, 0xf6, 0x09, 0x06, 0x36, 0xfd, 0xb2, + 0x0f, 0x21, 0x78, 0x5f, 0x5f, 0xd5, 0x42, 0xe3, 0x97, 0x22, 0x4e, 0x50, 0xe1, 0x81, 0x2c, 0x74, 0x3c, 0xb5, 0x72, + 0x6b, 0x1d, 0x99, 0x68, 0x6c, 0x62, 0x14, 0x3a, 0x8b, 0x15, 0x6c, 0xcc, 0x27, 0xa3, 0xba, 0xf2, 0x86, 0x09, 0x86, + 0x5f, 0xad, 0x3f, 0x9d, 0xa5, 0x57, 0x5b, 0x85, 0xbd, 0xaa, 0xf0, 0x5f, 0x75, 0x13, 0xbe, 0xc9, 0x70, 0x58, 0x05, + 0x2f, 0x08, 0x15, 0xfc, 0x40, 0x27, 0x55, 0xa8, 0xa3, 0xd3, 0x10, 0xa1, 0x55, 0xb3, 0x82, 0x1c, 0x15, 0xda, 0xef, + 0xdb, 0xd4, 0xd6, 0x9b, 0xea, 0xec, 0xed, 0x58, 0xd5, 0x54, 0x98, 0x1f, 0x8f, 0x59, 0x4d, 0x33, 0x12, 0x95, 0x2c, + 0xbf, 0x83, 0xdd, 0x69, 0x0b, 0x6f, 0x9f, 0xc0, 0xfb, 0x9b, 0xfa, 0x31, 0xe3, 0xb3, 0x6c, 0xd2, 0x04, 0xba, 0x33, + 0xd7, 0x02, 0xb5, 0x4f, 0x4d, 0xdd, 0x91, 0xb9, 0x0e, 0xec, 0x5d, 0xcd, 0x97, 0xf8, 0x4c, 0x84, 0xbb, 0x5f, 0x93, + 0xa8, 0xcc, 0x69, 0x06, 0x6d, 0x2c, 0xa5, 0x89, 0xaa, 0xdb, 0x70, 0xca, 0xb0, 0xf7, 0x0c, 0xed, 0x02, 0x6a, 0xf4, + 0x44, 0x77, 0x62, 0x8c, 0x90, 0xc6, 0xfd, 0x22, 0xb4, 0x1f, 0xe9, 0x79, 0x2b, 0x90, 0x8e, 0xed, 0x18, 0xa6, 0x9b, + 0x06, 0xc8, 0x5a, 0xe8, 0xe3, 0x5f, 0x5f, 0xed, 0xc3, 0xd8, 0xe6, 0xfd, 0x06, 0x61, 0xa9, 0xde, 0x1e, 0x1d, 0x20, + 0xf9, 0x9e, 0x52, 0x58, 0x5c, 0xd1, 0x1a, 0xad, 0x86, 0x8d, 0x83, 0x5c, 0x61, 0x30, 0xca, 0x54, 0xe9, 0x3c, 0x62, + 0x38, 0x1a, 0xc2, 0x08, 0x85, 0x42, 0x5e, 0x7d, 0xc4, 0x9a, 0x79, 0xdc, 0x9e, 0x3d, 0x94, 0x56, 0x07, 0xbf, 0x7a, + 0xb2, 0x46, 0x7d, 0xe9, 0x5d, 0x6e, 0xc6, 0x52, 0x8b, 0x8f, 0x57, 0xbc, 0xd1, 0xeb, 0xcb, 0x84, 0x66, 0x6e, 0xd1, + 0xa0, 0x14, 0x1b, 0x12, 0xbb, 0x95, 0xdf, 0x13, 0xeb, 0xb1, 0x59, 0x21, 0x09, 0x99, 0x5f, 0x5e, 0x99, 0xca, 0x53, + 0x79, 0x7f, 0x65, 0x39, 0xc3, 0x51, 0x3c, 0x78, 0x07, 0x7e, 0xd1, 0xcb, 0x9f, 0xa4, 0xde, 0xaa, 0x6e, 0x4b, 0x1b, + 0x14, 0xb5, 0x73, 0xcb, 0x86, 0x73, 0xe1, 0x3a, 0x29, 0x54, 0xc1, 0x0d, 0x16, 0x49, 0x23, 0x6f, 0x1d, 0x2f, 0x3e, + 0xc5, 0x60, 0xca, 0xc2, 0x19, 0x94, 0xb5, 0xcc, 0x05, 0xd6, 0x68, 0x1f, 0x86, 0x67, 0x8b, 0xcc, 0x18, 0x33, 0x18, + 0xdb, 0x70, 0x6e, 0xf9, 0xac, 0xfb, 0xfa, 0x85, 0xe0, 0xfd, 0xc6, 0x48, 0x44, 0x2c, 0x1f, 0xa0, 0x0f, 0x06, 0xa4, + 0x7f, 0x59, 0x62, 0xe4, 0xc3, 0x73, 0x05, 0x7e, 0xd2, 0xb2, 0x70, 0x00, 0x36, 0x6b, 0xef, 0x30, 0x2e, 0x92, 0x79, + 0xab, 0xdb, 0x31, 0x3b, 0x04, 0x37, 0x6c, 0x8d, 0x22, 0x18, 0x15, 0xa3, 0x25, 0x18, 0xac, 0xa0, 0x21, 0xb8, 0x80, + 0xf3, 0x75, 0xc4, 0xaa, 0xc7, 0x29, 0x2e, 0x33, 0x75, 0x86, 0x7f, 0x76, 0x37, 0xcd, 0xb2, 0x1a, 0xc4, 0x07, 0xa1, + 0xc8, 0x16, 0xec, 0xc1, 0xc5, 0x63, 0xe1, 0xcf, 0x21, 0xdf, 0x45, 0x61, 0xe9, 0x1a, 0xff, 0xaf, 0x43, 0xaa, 0xf7, + 0x3d, 0xec, 0x9e, 0x60, 0x0f, 0x3a, 0xa9, 0x2d, 0x34, 0x7f, 0x85, 0x55, 0x15, 0x55, 0xf3, 0xcd, 0x08, 0x8f, 0x16, + 0x5c, 0xab, 0x23, 0xd0, 0x41, 0x20, 0xd4, 0x6a, 0x06, 0x03, 0xb4, 0xe3, 0x07, 0xf8, 0xd2, 0xf1, 0xf8, 0x25, 0x89, + 0x09, 0xcf, 0xef, 0x9b, 0x10, 0xc4, 0xe3, 0xe8, 0x71, 0xe7, 0xfa, 0x43, 0x95, 0x21, 0xb2, 0x48, 0xea, 0x7e, 0x84, + 0xb9, 0xfd, 0x34, 0x17, 0x2e, 0x16, 0x27, 0xe8, 0xb1, 0x5c, 0x71, 0xc7, 0x3d, 0xea, 0x6e, 0xda, 0x3d, 0x9f, 0xb2, + 0x27, 0x31, 0x96, 0x52, 0xc4, 0x1d, 0xad, 0xcd, 0xb8, 0x22, 0x45, 0xae, 0x36, 0x81, 0x5e, 0x8e, 0xf4, 0x1c, 0x8f, + 0x64, 0x29, 0x51, 0xc7, 0x12, 0x44, 0xad, 0xe2, 0x3b, 0x23, 0x05, 0xd5, 0x28, 0xef, 0x72, 0xf7, 0xad, 0xd3, 0xd4, + 0xdd, 0xcf, 0xee, 0xa7, 0xc1, 0xcb, 0x54, 0xe7, 0x8c, 0x77, 0x5e, 0xb4, 0x5a, 0xfb, 0x22, 0x46, 0xaf, 0x1f, 0x0b, + 0x32, 0x9c, 0xf6, 0x5d, 0x67, 0x01, 0x6a, 0x95, 0xe5, 0xbf, 0x41, 0x20, 0x53, 0x74, 0x97, 0x9e, 0x8e, 0x68, 0xae, + 0x74, 0xf9, 0x8e, 0x0e, 0x54, 0x26, 0x0a, 0x31, 0xd3, 0x68, 0xf6, 0x80, 0xce, 0x2d, 0xcf, 0x75, 0x19, 0xf5, 0x2e, + 0xa2, 0x0d, 0x0a, 0xb5, 0xcf, 0xd1, 0x5d, 0x2f, 0x3a, 0x87, 0xeb, 0x94, 0xdb, 0x47, 0xcb, 0x45, 0xe5, 0xb3, 0xf1, + 0x70, 0x61, 0x97, 0x48, 0x22, 0x1f, 0x78, 0x09, 0x31, 0x74, 0xdf, 0xce, 0x30, 0x83, 0xb3, 0xda, 0xbb, 0x5d, 0xaa, + 0x1b, 0x3e, 0x84, 0x1e, 0xc5, 0xc2, 0xb5, 0x59, 0xce, 0xff, 0x97, 0xde, 0x45, 0xf5, 0xb7, 0x3a, 0x25, 0xee, 0x17, + 0xfe, 0x5d, 0x24, 0x8a, 0x84, 0x1e, 0xd2, 0x90, 0xde, 0x9f, 0x95, 0x1d, 0x98, 0x0f, 0xed, 0xa1, 0x32, 0x35, 0x79, + 0x9e, 0x05, 0xa0, 0xf5, 0xaa, 0x50, 0x46, 0x0e, 0x46, 0x4f, 0xce, 0x3b, 0xa4, 0x10, 0x86, 0x90, 0xc3, 0x20, 0x11, + 0x73, 0x1d, 0x70, 0x73, 0xd5, 0xed, 0x2c, 0x45, 0x85, 0xee, 0x1a, 0x96, 0x12, 0xd0, 0x11, 0x1d, 0x92, 0xcc, 0x9c, + 0xd0, 0x10, 0x14, 0x28, 0xf2, 0x1e, 0x31, 0x18, 0x4d, 0xe0, 0x3f, 0x98, 0x7d, 0x14, 0xd2, 0x08, 0x08, 0xe3, 0x14, + 0xc5, 0x7b, 0x20, 0x0e, 0x94, 0xd6, 0x3d, 0x98, 0x56, 0xe1, 0xaa, 0x57, 0xda, 0xca, 0x18, 0xbe, 0xc6, 0xb9, 0x33, + 0xc8, 0x05, 0x9e, 0xea, 0x5e, 0xcc, 0x80, 0x28, 0x40, 0x29, 0x68, 0xc1, 0x49, 0x90, 0x7c, 0xa8, 0x15, 0x48, 0xc0, + 0x21, 0xae, 0x41, 0xa9, 0xb1, 0xe0, 0xd5, 0x78, 0xa3, 0x10, 0x96, 0x62, 0x24, 0x02, 0x21, 0xd9, 0x30, 0xac, 0x98, + 0x0a, 0xb4, 0xfb, 0xc5, 0xbe, 0xf7, 0xc2, 0xe3, 0x43, 0x7d, 0x23, 0xe6, 0x02, 0x09, 0xa3, 0xb3, 0x93, 0x7b, 0x81, + 0x24, 0x7f, 0xb5, 0xa7, 0x2b, 0xb3, 0xbc, 0xf0, 0x8d, 0x85, 0x73, 0xb5, 0x12, 0x10, 0xf6, 0x6f, 0x8c, 0x03, 0x01, + 0x30, 0x97, 0xce, 0x6a, 0x2d, 0x91, 0x95, 0x0b, 0x69, 0xd6, 0x63, 0x29, 0xd6, 0xdd, 0x3c, 0x54, 0x80, 0x29, 0xb5, + 0xb8, 0x20, 0x95, 0x15, 0xde, 0x68, 0x0e, 0xa6, 0xf0, 0xa6, 0x83, 0xae, 0xcd, 0x67, 0xff, 0x43, 0xee, 0x1e, 0x1e, + 0x87, 0x57, 0xaa, 0x5b, 0x82, 0x51, 0x67, 0x92, 0xc1, 0x89, 0x4c, 0xa5, 0x9e, 0x06, 0xb1, 0x93, 0xbe, 0x13, 0x20, + 0x90, 0xd0, 0x38, 0x25, 0x9d, 0x8d, 0x74, 0xe8, 0x03, 0xf7, 0x43, 0x59, 0x50, 0x7c, 0x1d, 0x75, 0x7c, 0x11, 0x45, + 0x58, 0x64, 0xa5, 0x67, 0x97, 0x57, 0x37, 0x8d, 0xce, 0xcc, 0x4b, 0xcb, 0x9c, 0xc6, 0x4f, 0x60, 0xc9, 0x0a, 0x51, + 0xf2, 0x92, 0xb4, 0xb0, 0x9c, 0xe0, 0x7a, 0xa0, 0xe9, 0xb0, 0x20, 0x73, 0xe3, 0xb8, 0xfe, 0x51, 0x31, 0x8e, 0xa9, + 0xc3, 0x9e, 0xd2, 0x9b, 0x0a, 0x3c, 0x75, 0x64, 0x15, 0x3a, 0x10, 0x9e, 0x61, 0xbc, 0xa6, 0x81, 0x37, 0xfb, 0xf5, + 0xfc, 0xdf, 0x01, 0x8d, 0xe3, 0xc3, 0x25, 0x6d, 0xb8, 0x0e, 0xab, 0x70, 0x21, 0x8e, 0xc9, 0x0f, 0x26, 0x93, 0xb8, + 0x26, 0x71, 0xe0, 0xf7, 0x61, 0x89, 0x54, 0x88, 0x0c, 0xea, 0x58, 0xb9, 0x1d, 0xfb, 0x0b, 0x40, 0x8f, 0x87, 0x4c, + 0xe7, 0x81, 0x2f, 0x58, 0xe0, 0x38, 0xa8, 0x66, 0x37, 0x87, 0x8a, 0x05, 0xc0, 0x85, 0x59, 0x29, 0x5c, 0x8c, 0xd6, + 0x28, 0xc5, 0xec, 0x19, 0x1f, 0xda, 0x55, 0x03, 0x96, 0x99, 0x77, 0x66, 0xe5, 0xdc, 0x5a, 0x48, 0xc1, 0xed, 0xfa, + 0x46, 0x4c, 0x70, 0xbb, 0x46, 0xb2, 0x2d, 0xea, 0xd7, 0xe0, 0x58, 0x5c, 0x5c, 0xd7, 0xf8, 0xac, 0xcc, 0xdc, 0x49, + 0xfb, 0xc4, 0x75, 0x94, 0x56, 0x20, 0x89, 0xe7, 0x79, 0x18, 0x89, 0x05, 0xd3, 0xe7, 0x84, 0xa8, 0xc4, 0xb0, 0xf4, + 0xb1, 0xec, 0x0c, 0x83, 0xc7, 0x1c, 0x1d, 0x79, 0x66, 0xe7, 0x1c, 0xfe, 0xc7, 0x05, 0x60, 0x59, 0x7c, 0x2a, 0xe3, + 0x5f, 0x1c, 0x8f, 0xb2, 0x27, 0xf2, 0xfe, 0x4a, 0xe2, 0x4e, 0xc5, 0x1c, 0x48, 0x23, 0x5b, 0xc6, 0xd2, 0x16, 0xc8, + 0x45, 0xc6, 0x33, 0xec, 0xfc, 0xd4, 0xfa, 0x98, 0xfd, 0xd8, 0xc7, 0xaa, 0xe1, 0xd7, 0x81, 0x6e, 0x93, 0x12, 0xf4, + 0xad, 0x94, 0xe9, 0xec, 0xbd, 0x99, 0xd2, 0xdc, 0x89, 0xab, 0x7a, 0x65, 0x6b, 0x1b, 0x6a, 0x9b, 0xc4, 0xf5, 0x5b, + 0xf3, 0x18, 0x98, 0xb6, 0x4e, 0x5c, 0x19, 0x0a, 0x6d, 0xb2, 0x3c, 0xd3, 0x20, 0x55, 0x31, 0x74, 0xf7, 0x8a, 0x0f, + 0x9d, 0xee, 0x70, 0x36, 0x5f, 0x9a, 0xf4, 0x30, 0x9e, 0xc5, 0xb5, 0x5c, 0x92, 0xc1, 0x07, 0x85, 0xc3, 0x21, 0x49, + 0xd1, 0x22, 0x97, 0x21, 0x80, 0xdc, 0xed, 0xe0, 0x6e, 0xb2, 0xdd, 0x94, 0x77, 0xcc, 0x5e, 0x9a, 0xa3, 0xcf, 0xdb, + 0x72, 0x31, 0xa1, 0x46, 0x4c, 0xd5, 0x79, 0x6b, 0xbb, 0x6e, 0x0a, 0x4a, 0x39, 0x0a, 0xa4, 0x53, 0x16, 0xa2, 0x82, + 0x9f, 0x98, 0xef, 0xff, 0xa0, 0x28, 0x37, 0x04, 0xdc, 0xf2, 0x3a, 0x7e, 0xdc, 0x69, 0x2d, 0x63, 0x58, 0x8e, 0x8c, + 0x0b, 0xd3, 0xbf, 0xa4, 0x59, 0xcd, 0x96, 0x65, 0xe2, 0x75, 0x9d, 0x3d, 0x28, 0x2e, 0xe1, 0x5c, 0xad, 0x65, 0xe1, + 0x3a, 0xd2, 0xd0, 0x84, 0xfe, 0x10, 0x0a, 0xdb, 0xa6, 0x32, 0x70, 0xa2, 0x94, 0x21, 0x3f, 0x97, 0x86, 0x29, 0x18, + 0x7e, 0x13, 0x60, 0x9d, 0x66, 0x18, 0x85, 0xb4, 0x00, 0xaa, 0x0f, 0x47, 0x93, 0x6e, 0x08, 0x3b, 0x07, 0x1d, 0x47, + 0xe9, 0xec, 0xc0, 0x7a, 0x40, 0xce, 0xf3, 0xd9, 0x5e, 0xef, 0xcd, 0xfa, 0xda, 0xf8, 0x07, 0x04, 0x3e, 0xf3, 0x2d, + 0xfa, 0xda, 0x06, 0xe2, 0x7c, 0x39, 0x23, 0xc6, 0xb6, 0x0c, 0xd8, 0x52, 0xe5, 0x10, 0xb6, 0x54, 0x0c, 0x13, 0x33, + 0x75, 0x62, 0x8a, 0x17, 0x65, 0xdd, 0x79, 0x3e, 0x04, 0x2c, 0x50, 0x7e, 0xc0, 0x91, 0x25, 0xc7, 0x74, 0x14, 0x29, + 0x3a, 0x0d, 0x14, 0x2c, 0x50, 0x7e, 0x7d, 0x5b, 0xfe, 0x61, 0x08, 0xb0, 0x1c, 0x69, 0x95, 0x81, 0x64, 0x6a, 0x63, + 0x39, 0xa9, 0xc5, 0xa9, 0x38, 0x8b, 0xca, 0x30, 0xfa, 0xdd, 0xb8, 0x78, 0xe9, 0xbd, 0x56, 0x6f, 0xe1, 0x29, 0x57, + 0xb0, 0x46, 0x13, 0xd3, 0x13, 0xb1, 0xbf, 0xe0, 0x7c, 0x30, 0xc8, 0x6f, 0x78, 0x77, 0x08, 0xa9, 0x8d, 0x62, 0x8f, + 0xda, 0x0f, 0x4c, 0x46, 0xa5, 0xd5, 0x25, 0x2f, 0xea, 0x45, 0xb6, 0x65, 0x17, 0xbb, 0x72, 0x8f, 0x81, 0xcb, 0x8b, + 0x11, 0xe8, 0xf1, 0xf6, 0x1a, 0x1c, 0x00, 0x1f, 0x2d, 0x8a, 0xab, 0x61, 0x5b, 0xa4, 0x40, 0xd8, 0xd6, 0x7b, 0xde, + 0xea, 0x53, 0x2b, 0xc8, 0x63, 0x10, 0x5a, 0x97, 0x13, 0xde, 0xb9, 0x75, 0xca, 0x90, 0x16, 0x31, 0xce, 0xb3, 0xa8, + 0xd0, 0x87, 0x49, 0x55, 0xc9, 0x86, 0x7f, 0xc0, 0x60, 0xe4, 0x16, 0x53, 0xe1, 0xdf, 0xe2, 0xaf, 0xcc, 0x0d, 0xf7, + 0x6a, 0x98, 0xce, 0xa9, 0x36, 0xef, 0xba, 0xed, 0xf0, 0xc3, 0xf0, 0xdd, 0x12, 0x7a, 0x54, 0x60, 0x9c, 0xe6, 0x89, + 0xd9, 0x1a, 0x7e, 0xa5, 0x80, 0x6f, 0x1f, 0xca, 0xb4, 0x0d, 0x37, 0xd3, 0xaa, 0xbd, 0xe9, 0xb6, 0x1b, 0x40, 0xe6, + 0xac, 0x66, 0xf9, 0xe6, 0x83, 0x3b, 0x09, 0x69, 0x11, 0xfe, 0x58, 0x26, 0xea, 0x11, 0xb6, 0x74, 0xe8, 0x04, 0x3c, + 0xd3, 0xd3, 0xaa, 0xc6, 0xf3, 0x75, 0x56, 0x22, 0x7f, 0xb4, 0x37, 0xfe, 0xe4, 0x83, 0xb7, 0xbe, 0x83, 0x1a, 0x79, + 0xa2, 0x47, 0x84, 0x0b, 0xd5, 0x25, 0xb4, 0xad, 0x1a, 0xb2, 0x28, 0x96, 0xdc, 0x06, 0xde, 0x13, 0x53, 0x84, 0xc3, + 0x4f, 0xed, 0xe9, 0x52, 0xd4, 0xfe, 0x98, 0x19, 0xfc, 0x07, 0x80, 0x44, 0xe5, 0xf2, 0xbf, 0xc3, 0xe3, 0x1d, 0x85, + 0x88, 0x78, 0x0b, 0xc9, 0x82, 0x05, 0x18, 0x79, 0xa8, 0xcc, 0x48, 0x4a, 0xca, 0xb5, 0x12, 0x80, 0xef, 0xc3, 0xd0, + 0x56, 0x5d, 0x83, 0x1c, 0x6c, 0xf0, 0xb7, 0x0c, 0xe2, 0x61, 0xd7, 0x23, 0xad, 0xf1, 0xf2, 0xf8, 0xd2, 0xa7, 0x9a, + 0xd0, 0xe2, 0xdb, 0x48, 0x59, 0xbc, 0x5c, 0x3d, 0x10, 0x1d, 0x49, 0x0c, 0x71, 0x23, 0x27, 0xc9, 0x9b, 0xc4, 0xfb, + 0x69, 0x63, 0x44, 0x72, 0x62, 0x9d, 0xbd, 0x20, 0xe5, 0x17, 0x62, 0xf3, 0xdd, 0xb8, 0x73, 0xb8, 0x73, 0xbd, 0xaf, + 0x94, 0x45, 0x5d, 0x8b, 0x7a, 0x68, 0x76, 0x1d, 0xfd, 0xd9, 0x94, 0x30, 0xa4, 0x43, 0xa2, 0x41, 0x21, 0x2d, 0x2a, + 0x0b, 0xa4, 0x81, 0x9e, 0x44, 0xf6, 0x71, 0x58, 0xcc, 0xde, 0xbd, 0x4a, 0x7d, 0x92, 0x48, 0x49, 0x6c, 0x0f, 0x58, + 0x9a, 0x4c, 0xbc, 0xb9, 0x30, 0xfb, 0xbf, 0xb2, 0xf3, 0xf2, 0x21, 0xd2, 0x98, 0xaa, 0x63, 0x64, 0xa1, 0x06, 0x4a, + 0x59, 0x0b, 0xa7, 0x2d, 0xbe, 0x14, 0x45, 0x5b, 0x85, 0x9e, 0x6a, 0x1e, 0x78, 0x5a, 0x58, 0x13, 0xc5, 0x16, 0xf4, + 0x74, 0x98, 0x96, 0x25, 0xb5, 0x09, 0x4f, 0x5f, 0x7a, 0x9e, 0xe5, 0x39, 0xdb, 0x5d, 0x9a, 0x7d, 0xeb, 0xa0, 0x5e, + 0x53, 0xcb, 0xf6, 0x53, 0x95, 0x69, 0xd0, 0x12, 0x04, 0xf5, 0x10, 0xe4, 0x56, 0x61, 0xe2, 0xc6, 0x38, 0x4f, 0x77, + 0xed, 0xd6, 0x9d, 0xf9, 0xa7, 0x5d, 0x10, 0x17, 0x12, 0x18, 0x34, 0x12, 0xad, 0x26, 0xf4, 0x63, 0xc3, 0x52, 0x18, + 0x72, 0xb6, 0x64, 0x96, 0xf3, 0x6a, 0x20, 0x3f, 0xd3, 0x56, 0x70, 0x40, 0xc2, 0xe8, 0x1c, 0x63, 0x66, 0xf0, 0x39, + 0x12, 0xc3, 0x57, 0x6d, 0xd2, 0x73, 0x24, 0xf7, 0x34, 0xc1, 0x54, 0x00, 0xf3, 0x4a, 0xc1, 0x74, 0xd6, 0x37, 0x8b, + 0x0a, 0x56, 0xfc, 0xf0, 0xe3, 0x2f, 0xa8, 0xde, 0x07, 0x05, 0x9d, 0x04, 0x57, 0xea, 0xb6, 0x9d, 0xf1, 0x6d, 0xf7, + 0x41, 0x01, 0x5e, 0xa8, 0x21, 0xf3, 0x12, 0xff, 0x57, 0x2f, 0xd6, 0xd4, 0x2f, 0xf2, 0xd9, 0x61, 0xa2, 0xef, 0x32, + 0x69, 0xe6, 0xf7, 0xa5, 0x01, 0x65, 0x7e, 0xc9, 0xe3, 0x8a, 0x69, 0xde, 0x23, 0xfe, 0xd3, 0x98, 0xdb, 0xc2, 0x84, + 0x76, 0x98, 0x3e, 0x4a, 0xd4, 0xdc, 0x3e, 0x13, 0x54, 0xfb, 0x86, 0x97, 0xea, 0x31, 0x17, 0xac, 0x63, 0x72, 0x4b, + 0x89, 0xf5, 0x95, 0xc0, 0x83, 0x2c, 0x92, 0x89, 0x7b, 0xa9, 0xf6, 0x8e, 0xf2, 0x7c, 0xa7, 0xf6, 0x34, 0x39, 0x61, + 0x5d, 0x5c, 0x5d, 0xc9, 0xd7, 0x31, 0xc2, 0x6e, 0xbd, 0x59, 0x5e, 0xab, 0x62, 0xcc, 0x28, 0xd9, 0xd4, 0x6e, 0xef, + 0x62, 0x31, 0xe3, 0x26, 0x0c, 0x45, 0xb6, 0x28, 0x97, 0x8f, 0x5c, 0x3c, 0xe4, 0xfb, 0x94, 0x5f, 0xfd, 0x67, 0x0b, + 0x71, 0xf3, 0xf9, 0xf9, 0x1b, 0x23, 0x2c, 0x08, 0x03, 0xdb, 0xad, 0x22, 0x3e, 0x9d, 0x09, 0x14, 0xc6, 0xc6, 0x04, + 0x9b, 0xd7, 0xba, 0x09, 0xbc, 0x48, 0x94, 0x91, 0x34, 0xcc, 0xcf, 0xf2, 0x10, 0xa8, 0x62, 0xe8, 0x49, 0x6b, 0x25, + 0x8a, 0xd6, 0xf7, 0x63, 0x9f, 0x01, 0x21, 0x55, 0xb2, 0xac, 0x88, 0x2b, 0x57, 0x28, 0x04, 0x22, 0x09, 0x07, 0x47, + 0x60, 0x9b, 0x26, 0x84, 0x4f, 0x0f, 0xe9, 0xa5, 0x2e, 0x73, 0xc9, 0xc5, 0x35, 0x38, 0x0a, 0x60, 0x69, 0x32, 0xe2, + 0xd7, 0xbb, 0x55, 0x5e, 0xfa, 0xa5, 0x9d, 0x6e, 0xfe, 0x9e, 0x03, 0x8e, 0x0b, 0xdd, 0x17, 0x05, 0x68, 0x0d, 0x58, + 0x56, 0x28, 0x6f, 0x1f, 0x83, 0x8b, 0xd2, 0x61, 0xf4, 0x72, 0x5c, 0x2d, 0xa2, 0xba, 0x42, 0x59, 0xbb, 0x5d, 0x11, + 0x95, 0xb7, 0xf3, 0xd7, 0x34, 0xa9, 0x45, 0x04, 0x71, 0xde, 0x47, 0x34, 0xcb, 0x44, 0x98, 0x5d, 0xdc, 0x75, 0xa8, + 0xc7, 0x90, 0xf4, 0xa1, 0x15, 0x17, 0x11, 0xf8, 0xb4, 0x02, 0x69, 0x63, 0x6e, 0x0f, 0xe9, 0xb7, 0xb6, 0xa3, 0x00, + 0xe8, 0x85, 0xb0, 0x90, 0xb9, 0x91, 0x14, 0x3c, 0x7b, 0x0f, 0x54, 0x92, 0xf4, 0xb9, 0x1a, 0xb3, 0xae, 0xc7, 0x17, + 0xaf, 0x95, 0xbe, 0x05, 0x79, 0x6f, 0x8a, 0xe0, 0xe9, 0xaa, 0xbd, 0x74, 0x21, 0xa0, 0xbd, 0xce, 0x74, 0xb6, 0x34, + 0x7b, 0x1f, 0xbc, 0x17, 0x1d, 0x78, 0x0d, 0xf5, 0x66, 0x89, 0x3c, 0x66, 0xf0, 0x65, 0x93, 0x90, 0xe4, 0xb5, 0x91, + 0x0a, 0xa2, 0xa0, 0x07, 0xae, 0x51, 0x91, 0x8c, 0x92, 0x8b, 0x6e, 0xfb, 0xb3, 0x19, 0xa4, 0x5c, 0x5e, 0x7d, 0xcd, + 0xdb, 0x9d, 0x83, 0x28, 0xa5, 0xf9, 0xeb, 0x85, 0x4f, 0xbb, 0x67, 0x74, 0xe5, 0x35, 0x81, 0x56, 0x33, 0x7a, 0x4b, + 0x8d, 0x6a, 0xa4, 0xa9, 0x48, 0x05, 0xb1, 0x77, 0x59, 0x83, 0xb5, 0xf1, 0x78, 0x30, 0x95, 0x1a, 0xbc, 0xcf, 0xf4, + 0xa4, 0x75, 0xfa, 0xf6, 0x69, 0x39, 0x84, 0x57, 0xdf, 0x6d, 0x37, 0x2a, 0xf5, 0x5c, 0x7c, 0x2f, 0xdb, 0x45, 0xa6, + 0x75, 0x9c, 0xe8, 0x64, 0xd2, 0x57, 0x69, 0xb7, 0x27, 0x55, 0x3d, 0x73, 0xe1, 0x00, 0xfd, 0xbb, 0x51, 0xa6, 0x94, + 0xa5, 0xf1, 0xda, 0x25, 0xac, 0xa7, 0xde, 0x08, 0xbb, 0x2e, 0x0a, 0xc0, 0x3f, 0x67, 0x1c, 0x68, 0xa2, 0x63, 0xc5, + 0x3a, 0xbe, 0x2e, 0x75, 0x3c, 0x94, 0xfc, 0x80, 0x6d, 0x66, 0x92, 0x77, 0x28, 0x6e, 0xde, 0x10, 0xd8, 0x9f, 0x29, + 0xd6, 0x76, 0xab, 0xc4, 0x19, 0xab, 0xd8, 0x2b, 0xb1, 0xd7, 0x1e, 0x6f, 0x98, 0x40, 0x99, 0xad, 0x2b, 0x4c, 0x99, + 0x33, 0xbf, 0xc9, 0x67, 0x2f, 0xaf, 0x6f, 0x9e, 0xfd, 0x65, 0xe7, 0x35, 0xc3, 0xb0, 0x92, 0x3d, 0x27, 0xcb, 0x21, + 0xac, 0xca, 0xf8, 0x99, 0x9e, 0x6a, 0x1e, 0xdf, 0x51, 0x6f, 0xdc, 0x9b, 0xa4, 0x07, 0xe3, 0xdb, 0x13, 0x92, 0x87, + 0x82, 0x09, 0x18, 0xe9, 0xcf, 0x47, 0xa3, 0x00, 0x9d, 0x5c, 0x76, 0x0d, 0x2e, 0x12, 0x93, 0x3f, 0xa6, 0x5e, 0xc6, + 0x22, 0x05, 0xa9, 0x3a, 0xc2, 0x5d, 0x83, 0x48, 0x8a, 0x2a, 0xe8, 0xc8, 0x8b, 0x02, 0x4c, 0x5a, 0x70, 0x60, 0x01, + 0x0a, 0x3a, 0x5f, 0x79, 0x89, 0x25, 0x7e, 0x88, 0xfe, 0xf1, 0x1f, 0x56, 0x27, 0xbd, 0x68, 0xfe, 0x31, 0xbd, 0xf0, + 0xff, 0x4f, 0xe7, 0xb3, 0x75, 0x8e, 0x0d, 0x08, 0xd6, 0xff, 0x05, 0x62, 0x17, 0x9a, 0xa0, 0x04, 0xe9, 0x01, 0x84, + 0x43, 0xfe, 0xf4, 0x4e, 0x33, 0x9c, 0xb0, 0x7c, 0xaa, 0x26, 0xc3, 0x78, 0x2a, 0xce, 0xc9, 0x84, 0xc6, 0x71, 0x1a, + 0x4d, 0x29, 0xc0, 0x33, 0x6e, 0xcc, 0x5e, 0xc3, 0xd0, 0x7b, 0xdd, 0xa3, 0x40, 0xe8, 0x24, 0xdd, 0xcc, 0x58, 0x8a, + 0x49, 0xb4, 0xac, 0x57, 0x6d, 0x9c, 0x1c, 0xf6, 0xe0, 0x0c, 0xb4, 0xcc, 0x32, 0x27, 0x5a, 0x0f, 0x16, 0x42, 0x73, + 0x6e, 0x6c, 0x30, 0xdd, 0x5b, 0x28, 0xdd, 0x11, 0x41, 0x63, 0xf7, 0x98, 0xca, 0x56, 0x44, 0x25, 0xa5, 0x88, 0x78, + 0x63, 0x20, 0x4c, 0x2f, 0x7e, 0x9d, 0x9a, 0x53, 0xdf, 0xf6, 0x51, 0xbe, 0x32, 0xa9, 0x94, 0xb5, 0x5c, 0x48, 0x83, + 0xf1, 0xea, 0xcb, 0x48, 0x08, 0xcd, 0x44, 0x48, 0x91, 0x17, 0xb2, 0x27, 0x60, 0x13, 0x23, 0xbe, 0xc7, 0xa5, 0x84, + 0x32, 0x39, 0x28, 0x55, 0x50, 0x3c, 0x3e, 0xd4, 0x8f, 0x18, 0x10, 0xba, 0x1a, 0x23, 0x89, 0xe5, 0x58, 0xe8, 0x27, + 0xd2, 0x17, 0x34, 0x71, 0x08, 0x5c, 0xe3, 0x07, 0xd1, 0x67, 0x4f, 0xc6, 0xcb, 0x5e, 0x81, 0xcd, 0x39, 0xb0, 0x89, + 0xdc, 0x1c, 0xb4, 0xee, 0x3f, 0x65, 0x02, 0x4d, 0x14, 0x59, 0xeb, 0x39, 0x4b, 0xf6, 0x2c, 0xc5, 0x3c, 0x54, 0x4b, + 0x96, 0x40, 0xa3, 0xa8, 0x88, 0xd0, 0x5f, 0x0e, 0xf6, 0x50, 0xcf, 0x2a, 0x23, 0xb6, 0x30, 0xaf, 0x4e, 0xa8, 0xb2, + 0xd5, 0x51, 0xd2, 0x2b, 0x0b, 0xf5, 0x70, 0x37, 0x80, 0xf1, 0xb5, 0x9f, 0x7b, 0xe3, 0xce, 0xfa, 0x84, 0x6d, 0xcf, + 0x37, 0xb9, 0x38, 0xfe, 0x7a, 0xe6, 0xe5, 0xe1, 0xe0, 0xb9, 0x67, 0xb1, 0xd9, 0x50, 0x09, 0x81, 0x48, 0x26, 0x82, + 0x4a, 0xf5, 0xdb, 0x6c, 0x38, 0x20, 0xb0, 0x83, 0x62, 0x2b, 0xe1, 0xa5, 0x52, 0x51, 0xee, 0xad, 0xd5, 0x58, 0x0e, + 0x1c, 0x8e, 0x21, 0x8d, 0x63, 0x97, 0x98, 0x82, 0x38, 0xd6, 0x67, 0xe9, 0x21, 0xf0, 0x6b, 0x6b, 0x34, 0x4f, 0xec, + 0x90, 0x21, 0xd3, 0xa4, 0x4a, 0x45, 0xfa, 0x39, 0x00, 0x6e, 0x23, 0xa7, 0x17, 0xe9, 0x1f, 0x50, 0x02, 0xfc, 0x2a, + 0x16, 0x7b, 0xa3, 0x32, 0x16, 0xc9, 0xaa, 0xac, 0x56, 0x0a, 0xa7, 0xe3, 0x03, 0xf0, 0xd2, 0xbf, 0x2c, 0x58, 0xa0, + 0x6a, 0xa4, 0x67, 0x0b, 0x77, 0x08, 0xd4, 0x4a, 0xdf, 0x8d, 0x10, 0xb5, 0xee, 0xd7, 0xf5, 0xa8, 0xba, 0xb9, 0x58, + 0x8c, 0x31, 0xb6, 0xdc, 0x9b, 0x4f, 0x2a, 0xb7, 0x6d, 0x31, 0xfa, 0x36, 0x77, 0x60, 0x6b, 0xd2, 0x3d, 0xfd, 0xb0, + 0x3d, 0xe5, 0xe1, 0x25, 0x7e, 0xf3, 0x6a, 0x22, 0x33, 0x79, 0x7c, 0x26, 0x74, 0xef, 0xa9, 0x42, 0xcd, 0x8d, 0x85, + 0x3c, 0xf5, 0xbb, 0xf7, 0x34, 0xe1, 0x87, 0xfa, 0xaf, 0xd4, 0x78, 0x52, 0x93, 0x93, 0xbf, 0x99, 0xc5, 0x64, 0x13, + 0x86, 0xfd, 0xb0, 0x81, 0x20, 0x10, 0x50, 0x25, 0x80, 0xed, 0x51, 0x94, 0xeb, 0x6f, 0x4a, 0xaa, 0x5b, 0xc0, 0x85, + 0xa2, 0x53, 0x51, 0xf7, 0x29, 0xe1, 0x59, 0xcf, 0xb6, 0x5b, 0x28, 0x22, 0x2e, 0xaa, 0x33, 0x71, 0xa2, 0xcd, 0xcf, + 0xd9, 0xfe, 0x51, 0x75, 0x40, 0x87, 0x2c, 0x3e, 0xea, 0x9a, 0x50, 0x10, 0x37, 0xbc, 0x50, 0x86, 0x09, 0x92, 0x08, + 0x65, 0xd3, 0x6c, 0xf7, 0x65, 0xba, 0xc6, 0xad, 0xfd, 0xb9, 0xd0, 0xf7, 0x76, 0x20, 0x12, 0x17, 0x33, 0xd1, 0xac, + 0x08, 0x57, 0xf2, 0xe0, 0x54, 0xd1, 0xe6, 0x2c, 0xc8, 0xd6, 0xec, 0xad, 0x9e, 0x93, 0x39, 0xe0, 0x28, 0xd2, 0x72, + 0xcc, 0x8f, 0x3c, 0x17, 0xcf, 0xd5, 0x67, 0x8b, 0xe4, 0x7e, 0xc9, 0xc6, 0xb6, 0xdd, 0x4b, 0xd2, 0x93, 0x1c, 0x60, + 0x22, 0x04, 0xdf, 0x94, 0x90, 0x0e, 0xc0, 0xfa, 0xda, 0xb2, 0x04, 0x99, 0xa9, 0x02, 0x22, 0x93, 0xe9, 0xfc, 0xda, + 0x93, 0x83, 0xa0, 0x33, 0x15, 0x70, 0xc0, 0x10, 0x7e, 0x06, 0x06, 0x1a, 0xdf, 0x73, 0x90, 0xa4, 0x44, 0x43, 0x32, + 0xc5, 0x3d, 0x20, 0x52, 0xc0, 0xcd, 0x37, 0x4f, 0xb6, 0x68, 0x5d, 0x19, 0xa7, 0x90, 0x5e, 0xd2, 0x7a, 0x30, 0x25, + 0x31, 0x9e, 0x49, 0xb8, 0xc5, 0xf1, 0xcf, 0x96, 0xc0, 0xeb, 0x44, 0x5e, 0xa5, 0x64, 0x4b, 0xce, 0x09, 0x0c, 0x2e, + 0x47, 0x2b, 0x36, 0xb0, 0xb8, 0x7e, 0x0a, 0x6b, 0x8c, 0x27, 0x59, 0x1e, 0x8b, 0x9b, 0xbf, 0xef, 0xbf, 0xf5, 0xc8, + 0xee, 0xb3, 0xdd, 0x9d, 0x4f, 0x4d, 0x6b, 0x05, 0x83, 0x18, 0x53, 0xb2, 0x01, 0x6a, 0x05, 0xcf, 0x6c, 0xc8, 0xd8, + 0x95, 0xe6, 0x59, 0xe0, 0xa8, 0x67, 0xbb, 0x6f, 0xcd, 0xee, 0x58, 0xbe, 0x41, 0xfc, 0x44, 0x43, 0xd8, 0x78, 0xa7, + 0x02, 0x9b, 0xa8, 0xe5, 0xf7, 0xe8, 0xb7, 0xbb, 0xb3, 0x1d, 0xda, 0xab, 0x78, 0x37, 0x8d, 0x67, 0x19, 0x53, 0x59, + 0x96, 0xbf, 0x8f, 0x9e, 0xf9, 0xd1, 0xd1, 0x12, 0x86, 0xfa, 0xc8, 0x39, 0x4e, 0xd3, 0x38, 0xe8, 0x08, 0xf5, 0x84, + 0x1d, 0x7f, 0x87, 0x88, 0xff, 0xef, 0x0c, 0xff, 0x77, 0xf0, 0xbf, 0xe7, 0xf8, 0x17, 0xa1, 0xae, 0xf3, 0xd5, 0xe6, + 0xaf, 0x28, 0xff, 0x8d, 0x8f, 0x64, 0x9d, 0xbf, 0xda, 0xdb, 0x7d, 0x61, 0x5a, 0x6f, 0x92, 0x47, 0x6b, 0x13, 0x05, + 0xf4, 0xd5, 0xe3, 0xce, 0xe9, 0x04, 0x51, 0xe6, 0xd4, 0xf9, 0x88, 0xf2, 0xfb, 0x6e, 0x70, 0xe1, 0x17, 0xe3, 0x06, + 0x96, 0xad, 0x2f, 0xff, 0xf4, 0xaf, 0x90, 0xcb, 0x7d, 0x7e, 0xce, 0xb5, 0xad, 0x68, 0x94, 0xfd, 0x7d, 0x68, 0xdc, + 0x11, 0x41, 0x0d, 0xe7, 0xfe, 0xf3, 0x44, 0xf3, 0x4f, 0x06, 0x97, 0x22, 0x47, 0x6b, 0x85, 0xc5, 0x67, 0xfe, 0x4c, + 0x2b, 0x7b, 0xdf, 0xb8, 0x75, 0x65, 0x1b, 0x5a, 0x8c, 0x36, 0xdd, 0x00, 0x83, 0x49, 0x05, 0xad, 0x95, 0xb5, 0xfb, + 0xfc, 0x97, 0x89, 0x31, 0x24, 0x3c, 0xfa, 0xd9, 0xaf, 0x81, 0xfa, 0x8d, 0x6b, 0xb3, 0x06, 0x12, 0x5b, 0xa6, 0xe7, + 0x2f, 0x05, 0xb3, 0x09, 0x0d, 0x0f, 0xf5, 0x74, 0xa9, 0xf7, 0xea, 0xb3, 0x48, 0x34, 0xc6, 0xed, 0x50, 0x5c, 0x5f, + 0x01, 0x0a, 0xa4, 0x38, 0x4f, 0x97, 0xff, 0xc7, 0x5f, 0x88, 0xa2, 0xeb, 0x12, 0x3a, 0x2a, 0xe7, 0xa4, 0xe6, 0xa7, + 0xd0, 0x4b, 0x54, 0x7a, 0xa9, 0x12, 0x75, 0x54, 0x4c, 0x3c, 0x0b, 0x82, 0xeb, 0x8a, 0x9c, 0x9b, 0x5c, 0x0d, 0xe7, + 0x64, 0xdc, 0xe7, 0xff, 0x38, 0xcb, 0x49, 0x82, 0x87, 0xc0, 0x8c, 0xef, 0xec, 0x0a, 0x61, 0x49, 0x5e, 0xc3, 0xc1, + 0xec, 0xc1, 0xb0, 0x7c, 0x52, 0x03, 0x0d, 0x86, 0xf9, 0xf3, 0x05, 0x24, 0xe0, 0x1b, 0xdf, 0x0c, 0x52, 0xa3, 0xe2, + 0xa9, 0x3d, 0xad, 0x43, 0x95, 0x36, 0x06, 0x86, 0x21, 0x11, 0xc1, 0x21, 0xe7, 0x00, 0xf1, 0x41, 0xa7, 0xb3, 0xa3, + 0x8d, 0x63, 0x26, 0xa7, 0xe4, 0xa0, 0x1f, 0xe7, 0x52, 0x15, 0xca, 0xa1, 0x96, 0xd4, 0x51, 0xd1, 0x90, 0xe3, 0x32, + 0x3a, 0xd0, 0xa2, 0xdb, 0x2b, 0xc8, 0xfa, 0x32, 0x17, 0xef, 0xb2, 0xd9, 0xea, 0x91, 0x08, 0x33, 0x89, 0x18, 0xa9, + 0xe0, 0xb4, 0x64, 0x55, 0x0c, 0xfd, 0xa2, 0x25, 0xa6, 0x36, 0xe2, 0xe9, 0xde, 0x5a, 0x3a, 0x75, 0x1e, 0x79, 0x70, + 0x65, 0x90, 0xbd, 0xe2, 0x35, 0xe0, 0xde, 0x24, 0x1b, 0x66, 0x19, 0x2b, 0xb8, 0xb0, 0xbe, 0xf6, 0x69, 0xf5, 0xc1, + 0xbe, 0x43, 0xc1, 0x79, 0xf6, 0x5e, 0x0c, 0xba, 0x19, 0xec, 0x82, 0xea, 0x8d, 0xfd, 0x3c, 0x0d, 0xf8, 0xdd, 0x03, + 0xa5, 0xa0, 0xc0, 0x31, 0xa8, 0xa7, 0x3b, 0x7f, 0x43, 0x04, 0x7e, 0x60, 0x37, 0x60, 0x9d, 0xb3, 0x6d, 0xc1, 0x95, + 0x84, 0x6a, 0x20, 0xd5, 0x65, 0x2c, 0x0b, 0x6c, 0xa7, 0x87, 0x2d, 0xae, 0x7b, 0x8e, 0x06, 0xa5, 0xba, 0x37, 0x13, + 0x94, 0x89, 0xa3, 0x65, 0xae, 0x03, 0x5b, 0x04, 0x30, 0xb2, 0x15, 0xe1, 0x36, 0x20, 0xaf, 0x2e, 0x66, 0x34, 0xf4, + 0x87, 0xb8, 0x9a, 0x04, 0x34, 0xa0, 0x91, 0x8b, 0x8e, 0x17, 0x5d, 0x2f, 0x9d, 0xd2, 0xdb, 0xe3, 0x09, 0x72, 0xb0, + 0x73, 0x16, 0x86, 0xf2, 0x7a, 0x92, 0xf3, 0xa5, 0xe7, 0xbf, 0x9e, 0x10, 0xed, 0xf5, 0x31, 0x86, 0xb3, 0x85, 0x94, + 0xc4, 0x06, 0x32, 0xb4, 0x3a, 0x8e, 0xe8, 0xe0, 0x21, 0x0f, 0xe8, 0x49, 0x69, 0x96, 0xd7, 0x4d, 0x88, 0x8a, 0xb9, + 0x08, 0x95, 0xc8, 0xcb, 0xe2, 0x9a, 0xad, 0x2b, 0x10, 0x98, 0xd9, 0x2e, 0xf8, 0x82, 0xa3, 0xd1, 0xca, 0xf0, 0x82, + 0x30, 0x27, 0x8c, 0x90, 0xc5, 0xfa, 0x27, 0xc0, 0x1b, 0xbe, 0x06, 0x45, 0xfe, 0x64, 0x5c, 0x12, 0x9e, 0x59, 0xe6, + 0x0e, 0xe1, 0x84, 0x9f, 0x2a, 0x61, 0x75, 0x17, 0x15, 0xfa, 0xc7, 0xf3, 0x52, 0x50, 0x1c, 0x35, 0x06, 0x0c, 0xfb, + 0x22, 0x04, 0x91, 0x8a, 0xd2, 0xec, 0x3b, 0x88, 0x54, 0xf7, 0x03, 0xb1, 0x1e, 0x8e, 0x68, 0xc3, 0x23, 0x11, 0xc8, + 0x1f, 0x49, 0xb5, 0xd0, 0xc6, 0x47, 0x22, 0x24, 0xbd, 0x7d, 0xf3, 0xc1, 0x79, 0xb9, 0x29, 0xaa, 0x2c, 0xa3, 0xd0, + 0x27, 0x3a, 0xa8, 0x9c, 0xea, 0xf1, 0x7a, 0x96, 0xda, 0x0a, 0xae, 0x5b, 0x2b, 0x81, 0x8a, 0x8d, 0xe9, 0x32, 0xf7, + 0x91, 0xed, 0xb5, 0x4b, 0x29, 0xf1, 0xe7, 0x79, 0xc6, 0x1a, 0x8e, 0x04, 0xec, 0xa6, 0x17, 0x86, 0xb1, 0x93, 0x1d, + 0x43, 0x47, 0x92, 0xf4, 0x82, 0xd2, 0x74, 0x8c, 0x91, 0x9b, 0xd7, 0xdd, 0x60, 0xbb, 0x8a, 0xc1, 0x57, 0x03, 0xc3, + 0x39, 0x34, 0x69, 0x38, 0x29, 0x38, 0xd7, 0xad, 0xfb, 0xeb, 0x78, 0xe9, 0x3c, 0x2f, 0xfb, 0x18, 0x96, 0x47, 0x3c, + 0xd7, 0xf6, 0x5f, 0x2f, 0xe4, 0xf9, 0x7a, 0x09, 0xcb, 0xd6, 0x5f, 0x93, 0xe4, 0x08, 0xd9, 0xcf, 0x2a, 0xa2, 0xe6, + 0x17, 0x8c, 0x43, 0xe4, 0x4f, 0x71, 0x4c, 0x15, 0xcd, 0x5c, 0x75, 0x7a, 0x54, 0x7a, 0x83, 0x5e, 0x3c, 0x3c, 0x20, + 0x38, 0x09, 0x90, 0x27, 0x4e, 0x42, 0x18, 0x30, 0xe4, 0x14, 0xd6, 0x7c, 0x05, 0x3c, 0xe6, 0x71, 0xc3, 0x53, 0x9a, + 0xab, 0x3f, 0x01, 0x34, 0xa0, 0x02, 0xea, 0xc3, 0x0e, 0x5f, 0x2c, 0xc1, 0x86, 0x46, 0x08, 0x21, 0x9f, 0x10, 0xfc, + 0x2e, 0xff, 0x4a, 0x50, 0x16, 0x3b, 0x05, 0x32, 0x5a, 0xa7, 0xda, 0x15, 0x93, 0x95, 0x36, 0xa8, 0xff, 0x96, 0x76, + 0x53, 0x5e, 0x10, 0x39, 0x88, 0x50, 0x01, 0x06, 0xb2, 0xda, 0x50, 0xe0, 0xd5, 0x65, 0x9b, 0x69, 0x06, 0x63, 0xf5, + 0x51, 0xd3, 0x66, 0xef, 0xf6, 0x4a, 0x4f, 0x65, 0x40, 0xd4, 0x06, 0x86, 0x77, 0xfd, 0xcf, 0xe1, 0x69, 0x19, 0xcf, + 0x8b, 0x41, 0x35, 0x4e, 0x87, 0xc8, 0x70, 0x9d, 0x3a, 0x56, 0xad, 0xfc, 0xcf, 0x94, 0x12, 0xe3, 0x53, 0x51, 0xe1, + 0xc0, 0xba, 0x6a, 0xa8, 0x5a, 0x10, 0xa6, 0x8d, 0xd2, 0x3f, 0x28, 0xca, 0xa0, 0x05, 0x58, 0x1a, 0xb5, 0xc6, 0x01, + 0x9b, 0x9a, 0x5e, 0x9e, 0x1b, 0xd3, 0x89, 0x57, 0xfb, 0x9b, 0xc0, 0x84, 0xbb, 0xfa, 0x31, 0x87, 0xba, 0xb6, 0x78, + 0xae, 0xef, 0xbb, 0x8f, 0xa7, 0x3c, 0x24, 0x7a, 0x26, 0x16, 0x9a, 0xb2, 0xb9, 0xce, 0xe7, 0x1d, 0x14, 0x1b, 0x5e, + 0xd8, 0xe7, 0x2b, 0x24, 0xc8, 0x01, 0x87, 0x4d, 0x71, 0x35, 0x0e, 0xcf, 0x38, 0x27, 0x2a, 0x7d, 0x2e, 0xf6, 0x83, + 0x03, 0x38, 0x3c, 0xe7, 0x4a, 0x59, 0xcb, 0xe7, 0x6f, 0xd3, 0x19, 0x4f, 0xfa, 0x12, 0x9c, 0x94, 0x32, 0xf1, 0x8a, + 0xc5, 0x9f, 0xf1, 0xf1, 0x11, 0x67, 0x78, 0x7f, 0xa3, 0xf3, 0x69, 0x7f, 0x9b, 0xae, 0x8d, 0x79, 0x0f, 0xf2, 0x46, + 0x05, 0xcf, 0x5d, 0x58, 0xc6, 0x36, 0x51, 0x40, 0xf0, 0x37, 0x3a, 0xa7, 0x69, 0x1e, 0x42, 0x3c, 0xac, 0xa2, 0x22, + 0xbf, 0xa3, 0x48, 0x96, 0x68, 0xe1, 0x6d, 0x4e, 0x8b, 0xf4, 0x6a, 0x7a, 0xde, 0xe6, 0x4b, 0x99, 0x0e, 0xed, 0xc6, + 0xd4, 0x49, 0xb4, 0xaa, 0x30, 0x21, 0x88, 0xc0, 0x9d, 0x2e, 0x91, 0x60, 0xb7, 0x26, 0xb3, 0x27, 0xfc, 0x6b, 0x4c, + 0xe3, 0x20, 0x6e, 0xd9, 0x73, 0x8b, 0x7d, 0x4f, 0xcd, 0x1c, 0xe8, 0x85, 0x9c, 0xa1, 0x38, 0x64, 0x0c, 0xfa, 0x42, + 0xae, 0x1f, 0x8f, 0xd0, 0x49, 0x05, 0xf8, 0x55, 0xc9, 0xaf, 0x7a, 0xbc, 0x96, 0x0a, 0x95, 0xea, 0x6c, 0x22, 0x47, + 0xfb, 0xb3, 0xb4, 0xf1, 0x18, 0xfb, 0xb1, 0x3c, 0x7d, 0xed, 0xe9, 0x04, 0x50, 0xb1, 0x25, 0xb7, 0xb2, 0x6f, 0xc8, + 0x43, 0x4b, 0xbf, 0x79, 0x9c, 0x61, 0xe6, 0x08, 0xd0, 0xd9, 0xaa, 0xe0, 0xa9, 0x8b, 0xd9, 0xdb, 0x5b, 0x6e, 0x2a, + 0xb6, 0xdf, 0xf0, 0x19, 0x60, 0x27, 0x89, 0x40, 0x34, 0xaa, 0xf6, 0x59, 0xa7, 0x31, 0x79, 0x2a, 0x9e, 0x42, 0x23, + 0x60, 0x29, 0x22, 0xd7, 0x94, 0x68, 0x5d, 0xcb, 0xd0, 0x45, 0x72, 0x7c, 0xbb, 0x1c, 0x82, 0x04, 0x77, 0x68, 0xd6, + 0x48, 0xe8, 0xa9, 0xba, 0x62, 0xae, 0x54, 0x95, 0x50, 0x77, 0x3a, 0x4c, 0x79, 0x6e, 0xac, 0xf0, 0x28, 0x73, 0xd1, + 0xb9, 0x8e, 0x55, 0x25, 0xc2, 0x22, 0x2e, 0xce, 0x02, 0x2f, 0x02, 0xfc, 0x08, 0x0c, 0xe2, 0x52, 0x95, 0x65, 0xa6, + 0x08, 0x49, 0x93, 0x6d, 0x81, 0xb1, 0xe2, 0xd0, 0x6f, 0xa2, 0x9a, 0x07, 0x5f, 0xff, 0x34, 0xa1, 0x28, 0xec, 0x04, + 0x44, 0xa3, 0x41, 0xb7, 0x16, 0xd0, 0xcc, 0xfc, 0x9b, 0x72, 0x78, 0x0c, 0x9d, 0x27, 0xf1, 0x86, 0x25, 0xc9, 0xa2, + 0x3f, 0xff, 0x97, 0x06, 0x61, 0xd2, 0x3b, 0x90, 0x52, 0x95, 0x40, 0xbb, 0x61, 0x6e, 0x3e, 0x89, 0x0c, 0xd9, 0xa2, + 0x5c, 0xec, 0x71, 0x94, 0x73, 0x1b, 0xd2, 0x0a, 0x66, 0x5e, 0x42, 0xc9, 0x3b, 0x5a, 0xaf, 0xbc, 0x0e, 0xab, 0x6e, + 0x79, 0x8d, 0x97, 0x38, 0xd1, 0x2f, 0x58, 0x74, 0x4d, 0xae, 0xc1, 0x6d, 0x42, 0x03, 0x32, 0x2b, 0x13, 0xd5, 0x1b, + 0x82, 0xa7, 0x90, 0xb2, 0x31, 0xe0, 0xe2, 0xdc, 0xe0, 0xd1, 0xf9, 0x9c, 0x90, 0xb2, 0x90, 0x0b, 0xcd, 0x00, 0x27, + 0x31, 0x92, 0xd1, 0xa6, 0x2e, 0xe5, 0x42, 0x9d, 0x7e, 0xe0, 0xad, 0x83, 0x1e, 0xd6, 0x66, 0x67, 0x2b, 0xf6, 0xb2, + 0x5e, 0x31, 0xb2, 0xa6, 0xea, 0x72, 0x52, 0x6e, 0x4a, 0x68, 0x68, 0x3f, 0xc6, 0x7b, 0x19, 0x87, 0x1c, 0x66, 0xd5, + 0x18, 0x89, 0x3d, 0x23, 0x39, 0xb3, 0x49, 0x92, 0x65, 0xd6, 0x65, 0x2d, 0xfd, 0x6c, 0xae, 0xfd, 0x8f, 0x6c, 0xe1, + 0x11, 0xcb, 0x38, 0xbf, 0xd2, 0x43, 0x49, 0xb8, 0xb2, 0x4e, 0x93, 0xe7, 0xe9, 0x07, 0xe1, 0xba, 0xdb, 0xd4, 0x8c, + 0x86, 0xf7, 0x80, 0x84, 0x2c, 0x6d, 0xd9, 0xaa, 0x36, 0x36, 0x36, 0xf8, 0xb9, 0xcf, 0x03, 0x0b, 0xf1, 0xa0, 0x21, + 0x19, 0xfd, 0x7c, 0x9b, 0xc7, 0x86, 0x85, 0xce, 0xe8, 0xa0, 0x94, 0x5e, 0x4b, 0x71, 0xee, 0x05, 0x4a, 0x2c, 0xf8, + 0x72, 0xaf, 0xa4, 0xc9, 0x99, 0x07, 0x7e, 0x10, 0x67, 0x46, 0x17, 0x99, 0x77, 0xbe, 0x46, 0xdd, 0x4a, 0xef, 0x95, + 0x96, 0xf4, 0x83, 0x5f, 0xfd, 0x6c, 0x95, 0x5e, 0xa7, 0x5f, 0x22, 0x73, 0xe6, 0x2b, 0x5c, 0xa2, 0x5d, 0x81, 0x1d, + 0xc6, 0x49, 0x5d, 0xf3, 0xeb, 0x0e, 0xd0, 0xfb, 0xc2, 0x9b, 0x81, 0x46, 0x89, 0xc0, 0x0b, 0x4d, 0x2e, 0x71, 0x25, + 0xbe, 0x10, 0xde, 0x14, 0x7b, 0x98, 0xc1, 0x44, 0x6c, 0x14, 0x41, 0x3c, 0x00, 0xff, 0x1c, 0xe2, 0x97, 0x31, 0xbf, + 0xd7, 0x41, 0x6d, 0xb4, 0x20, 0x34, 0x45, 0xe6, 0x26, 0x7b, 0x11, 0x5b, 0x90, 0xc3, 0x2b, 0x81, 0x04, 0xb9, 0x66, + 0x8d, 0x1d, 0xb0, 0x65, 0x5b, 0xa1, 0x6c, 0xde, 0x70, 0x6c, 0xb0, 0x3b, 0x7b, 0x69, 0x6d, 0x12, 0x84, 0xb8, 0x44, + 0xd4, 0x9a, 0x1e, 0x19, 0xe5, 0xab, 0x96, 0x82, 0x4a, 0xf4, 0xf3, 0xf4, 0xbf, 0xb1, 0x49, 0x6f, 0x17, 0x04, 0xfd, + 0x52, 0x64, 0xda, 0x2f, 0x34, 0x0c, 0x4a, 0x8b, 0xbc, 0xff, 0x43, 0x09, 0x7c, 0x1d, 0xac, 0xe9, 0x3a, 0xd5, 0x6e, + 0x7d, 0xf1, 0x4f, 0x50, 0x3d, 0x4f, 0xc6, 0x1d, 0x46, 0x2e, 0x0c, 0x68, 0x7e, 0x6f, 0x24, 0x9d, 0xb7, 0xfe, 0xcf, + 0x12, 0x02, 0x67, 0x90, 0x25, 0x14, 0xae, 0x11, 0xf9, 0x26, 0xff, 0xc0, 0x30, 0xed, 0x15, 0xc1, 0xce, 0x8e, 0xbc, + 0x37, 0xb6, 0x93, 0x75, 0x88, 0x36, 0xf4, 0x0d, 0x98, 0xb2, 0x7e, 0xa3, 0x59, 0x36, 0x40, 0x6d, 0x3b, 0xe3, 0xb6, + 0x2b, 0x69, 0x16, 0x92, 0xe1, 0x99, 0xc5, 0x20, 0xd5, 0x46, 0x7e, 0xe6, 0x95, 0x85, 0x33, 0x73, 0x77, 0xa1, 0x91, + 0x9f, 0x3d, 0x9d, 0xe1, 0xf0, 0xc6, 0x46, 0x7a, 0xe1, 0x09, 0x4b, 0x73, 0x43, 0x07, 0x00, 0x20, 0x5c, 0x2a, 0x3a, + 0xde, 0xb1, 0x54, 0xd9, 0x4a, 0xd4, 0x57, 0x82, 0xe6, 0xe0, 0x38, 0x1b, 0x5d, 0xc8, 0x27, 0x4c, 0x18, 0xe9, 0x79, + 0x0e, 0x11, 0x8f, 0x63, 0xe5, 0x32, 0x18, 0x32, 0x07, 0x3f, 0x91, 0x60, 0x47, 0xb3, 0xc3, 0x59, 0x0e, 0x57, 0xa9, + 0x5f, 0x27, 0xa8, 0x86, 0xd4, 0x58, 0x65, 0x6c, 0xc3, 0xfa, 0xff, 0x0a, 0x27, 0x2e, 0xeb, 0x79, 0x42, 0x2f, 0x3a, + 0x2c, 0xdc, 0xc2, 0x47, 0x2e, 0xf9, 0x87, 0x38, 0x38, 0xc4, 0xd9, 0x18, 0xe7, 0x19, 0x48, 0x9e, 0x36, 0x50, 0x18, + 0x79, 0x39, 0xbe, 0x54, 0x72, 0xb6, 0x1b, 0xaa, 0x4a, 0x4a, 0x97, 0x74, 0xab, 0xbd, 0x67, 0xb2, 0x1f, 0x91, 0x13, + 0x27, 0x45, 0x24, 0x99, 0x4c, 0x2a, 0xa8, 0x53, 0x1a, 0x6c, 0xfa, 0x15, 0xc0, 0x08, 0xe6, 0x5a, 0xd7, 0x34, 0x45, + 0x69, 0x99, 0x70, 0xf8, 0x1c, 0x2d, 0xd6, 0x99, 0x43, 0xd1, 0xc1, 0x60, 0x50, 0x48, 0x33, 0xb4, 0x0b, 0x3c, 0xc8, + 0xa8, 0xcc, 0x1e, 0x7f, 0x9e, 0x9f, 0x14, 0x4d, 0xd8, 0x64, 0xc5, 0x2a, 0x51, 0xa4, 0x96, 0xb1, 0xa4, 0x54, 0x75, + 0xfe, 0xed, 0x3e, 0x1a, 0x1a, 0x1d, 0x58, 0x03, 0x3a, 0x0a, 0x25, 0x9c, 0xf7, 0x50, 0x3c, 0xe9, 0x4e, 0xcd, 0xde, + 0x7e, 0xeb, 0x77, 0x57, 0x1f, 0xe2, 0x54, 0xdf, 0x25, 0x9d, 0xdc, 0x37, 0x9e, 0xb0, 0x9d, 0xb1, 0xd7, 0xba, 0xbe, + 0x63, 0x0b, 0x60, 0x97, 0x19, 0xcf, 0xc6, 0xf0, 0xbe, 0x8e, 0xbd, 0xf8, 0x82, 0x5c, 0x3d, 0x7f, 0xd6, 0x6a, 0xf9, + 0x8c, 0x5b, 0x0a, 0xa7, 0x1c, 0xa1, 0x85, 0x45, 0x40, 0x76, 0xc0, 0x28, 0x20, 0x2f, 0xa1, 0xcb, 0x18, 0xc2, 0x83, + 0x5f, 0x1b, 0x14, 0xad, 0x0e, 0x38, 0x13, 0xe8, 0x51, 0x3f, 0xe8, 0xc1, 0xdf, 0x74, 0x12, 0x6f, 0x4c, 0xbc, 0xb7, + 0x68, 0x4a, 0xbf, 0x5a, 0x69, 0x53, 0xa6, 0x3c, 0xbb, 0xe4, 0xc3, 0xd8, 0x0e, 0x55, 0x5b, 0xbb, 0x0d, 0x35, 0xc4, + 0x67, 0xb8, 0x9f, 0xb8, 0xa2, 0x78, 0x6c, 0x06, 0xea, 0x6f, 0xfc, 0xc8, 0x3b, 0x6d, 0x3f, 0x92, 0xfb, 0x10, 0x87, + 0x1e, 0xcb, 0xbe, 0x2a, 0xfb, 0x00, 0xae, 0xd9, 0xd9, 0x58, 0x89, 0x9f, 0x15, 0x61, 0xb8, 0x1c, 0x82, 0xf1, 0x67, + 0xb5, 0x0d, 0x3d, 0xa9, 0xa7, 0xac, 0x79, 0xfc, 0x3a, 0x32, 0xbf, 0xc9, 0xc2, 0xa4, 0x5e, 0xc8, 0x9a, 0x1e, 0xa7, + 0x33, 0x43, 0xe7, 0x4c, 0xe4, 0xee, 0xd9, 0xaf, 0xd1, 0xda, 0xc8, 0x48, 0xed, 0x6c, 0xd1, 0xc7, 0xdf, 0x35, 0x55, + 0x36, 0x9a, 0x34, 0xe3, 0xb1, 0x73, 0xad, 0x4b, 0x97, 0xba, 0x8c, 0x4c, 0xf7, 0x67, 0xbd, 0x58, 0x50, 0xa5, 0x55, + 0x66, 0xde, 0x27, 0x52, 0xc3, 0xb8, 0xd6, 0x6a, 0xe2, 0x9d, 0x5e, 0x7c, 0xcb, 0x8e, 0xdd, 0x74, 0x96, 0x64, 0x30, + 0x98, 0x8e, 0xdc, 0x0c, 0x1d, 0xc9, 0x08, 0x53, 0xbf, 0x7c, 0x32, 0x30, 0xeb, 0x3a, 0x7f, 0x6f, 0x5f, 0x33, 0x8e, + 0xe2, 0x5b, 0x8f, 0x15, 0xfd, 0xb6, 0x4e, 0xb7, 0xef, 0x09, 0x70, 0x6d, 0x53, 0xfb, 0xd4, 0x5b, 0xa5, 0x9c, 0x8d, + 0xe4, 0x58, 0x4e, 0xe2, 0x09, 0x14, 0x3c, 0x00, 0x49, 0x04, 0x4c, 0x9a, 0xf9, 0xc5, 0x52, 0xc9, 0x84, 0x8d, 0xba, + 0xdb, 0xef, 0x15, 0x17, 0x18, 0xb5, 0x7f, 0x94, 0xb9, 0x9a, 0x5e, 0x8e, 0x14, 0x25, 0xd3, 0x81, 0x81, 0xa6, 0x30, + 0x96, 0x3e, 0xff, 0x62, 0x5b, 0x89, 0x05, 0x95, 0x45, 0xb1, 0xb0, 0x66, 0x74, 0x00, 0x22, 0xe8, 0x63, 0x2a, 0xa8, + 0x1a, 0x7e, 0x43, 0xca, 0xe7, 0xf4, 0xeb, 0xd9, 0xf9, 0x88, 0x6d, 0xba, 0x29, 0x7d, 0x8c, 0x07, 0xab, 0xec, 0x75, + 0x7e, 0x1f, 0x9f, 0xfa, 0x42, 0x2f, 0x5e, 0x49, 0xde, 0x59, 0xf7, 0x4f, 0xf3, 0xcf, 0xe7, 0x6c, 0xfe, 0xf9, 0xac, + 0x19, 0x60, 0x98, 0xd9, 0x18, 0xf7, 0x2a, 0x7a, 0xb5, 0xc0, 0x35, 0x4e, 0x65, 0x78, 0x1a, 0xcb, 0x7f, 0x74, 0x16, + 0xae, 0x32, 0xc0, 0x99, 0x6c, 0xa0, 0x4d, 0x65, 0x10, 0x7c, 0x73, 0xc3, 0x82, 0xa6, 0x2b, 0x27, 0x26, 0x9a, 0x99, + 0x48, 0x5c, 0x82, 0x9c, 0xc4, 0x1c, 0xec, 0xc9, 0xa5, 0xea, 0x3a, 0x25, 0x33, 0x7e, 0x66, 0xda, 0x82, 0x9e, 0xa0, + 0xf0, 0x53, 0x90, 0xa9, 0x00, 0x41, 0x74, 0xbe, 0x4f, 0xf3, 0x38, 0x4a, 0x7f, 0xe7, 0x98, 0xa7, 0xfc, 0xff, 0x89, + 0x2c, 0x11, 0xa9, 0x6b, 0x19, 0xc2, 0x01, 0xbf, 0x46, 0x34, 0x75, 0xc6, 0xef, 0xc5, 0xa0, 0x7e, 0x91, 0xde, 0xaa, + 0x11, 0x38, 0x17, 0xa0, 0xae, 0x46, 0x98, 0x06, 0xf2, 0x8e, 0xf6, 0x1a, 0xf9, 0xc5, 0xa9, 0xd2, 0x2d, 0x7b, 0x5a, + 0xf2, 0x8a, 0x2c, 0x54, 0xfe, 0xc9, 0x98, 0x62, 0x56, 0x15, 0x1d, 0x47, 0x9a, 0xf7, 0x0d, 0x36, 0xc9, 0x17, 0x4e, + 0x12, 0x7a, 0xca, 0xa9, 0xb1, 0x30, 0x6e, 0x15, 0x5e, 0xda, 0x85, 0xd1, 0x07, 0x64, 0x0e, 0x34, 0x17, 0x65, 0x3f, + 0x0e, 0x71, 0x44, 0x7c, 0xa0, 0x9f, 0xf1, 0x2d, 0x64, 0xdc, 0xf6, 0x1c, 0x27, 0xc4, 0xa9, 0xfa, 0x62, 0x28, 0x5e, + 0xc6, 0x26, 0x15, 0x92, 0x1b, 0xb2, 0x18, 0xca, 0xaa, 0x85, 0xe7, 0x67, 0xf0, 0x7c, 0xe1, 0xf9, 0x69, 0x9e, 0x1b, + 0xbc, 0x25, 0x53, 0x51, 0x46, 0x62, 0xed, 0x0a, 0x3b, 0x6a, 0xf9, 0x4d, 0x5e, 0x61, 0xef, 0x73, 0x00, 0x12, 0xd5, + 0x5e, 0x15, 0x0b, 0x72, 0x01, 0x1b, 0xd0, 0xa0, 0x3a, 0x0c, 0xf2, 0x12, 0x5f, 0x0a, 0x20, 0x00, 0xa3, 0x07, 0xef, + 0xd9, 0x71, 0x3a, 0x6d, 0x0c, 0xe3, 0x2e, 0xc7, 0x04, 0x4a, 0xae, 0xd9, 0x54, 0x12, 0xf2, 0x26, 0x27, 0x9c, 0x7d, + 0x01, 0x2a, 0x84, 0x31, 0x80, 0x3c, 0x35, 0xb6, 0x58, 0xbd, 0x7e, 0x2b, 0xd5, 0xe7, 0x04, 0xa3, 0xb0, 0x97, 0x98, + 0x7d, 0xa1, 0x1b, 0x11, 0x11, 0x39, 0x8b, 0x39, 0xb9, 0xf4, 0xda, 0x55, 0xea, 0xfb, 0xd1, 0xd9, 0x27, 0x04, 0xf4, + 0xed, 0xe4, 0x5b, 0x73, 0x13, 0x0e, 0xc7, 0x97, 0x2c, 0x33, 0xd1, 0x7f, 0xae, 0x16, 0xc8, 0xb3, 0x32, 0xe7, 0x3d, + 0x57, 0xc7, 0xae, 0x89, 0x34, 0xc5, 0xa6, 0xa0, 0x53, 0x70, 0x4a, 0x10, 0x41, 0x5b, 0x70, 0x92, 0xe4, 0x90, 0x88, + 0xca, 0x40, 0x7f, 0x36, 0x15, 0x4b, 0x30, 0x5b, 0xc3, 0x52, 0x9d, 0xd7, 0x5a, 0xec, 0x9a, 0x1f, 0xb2, 0x27, 0x99, + 0x65, 0xc3, 0x45, 0xea, 0x4c, 0x27, 0xc5, 0x75, 0x64, 0x8d, 0xc2, 0xd4, 0xb8, 0x8b, 0xc5, 0xab, 0x84, 0x2b, 0xa9, + 0xdc, 0xa4, 0x4a, 0xbc, 0xd9, 0xa8, 0x19, 0x8f, 0xc6, 0xe5, 0x8f, 0x6d, 0x76, 0xf4, 0x46, 0x4a, 0xc0, 0xe3, 0x21, + 0xd5, 0xde, 0xa6, 0x33, 0x6e, 0x43, 0xfe, 0xed, 0xea, 0x69, 0x0b, 0x58, 0xfc, 0x4f, 0x7a, 0x18, 0xe2, 0xff, 0x8d, + 0x0a, 0x8a, 0xc8, 0x36, 0x42, 0x78, 0x14, 0x02, 0x84, 0xfb, 0xc2, 0xc9, 0x0b, 0x62, 0x51, 0xc4, 0xa3, 0xb0, 0xf7, + 0x3a, 0x6b, 0xe5, 0x12, 0x16, 0x7f, 0x1f, 0x74, 0xff, 0x8e, 0x42, 0xe7, 0x5c, 0xaf, 0xf1, 0xa7, 0xe2, 0xc7, 0x1f, + 0x39, 0x76, 0x74, 0x28, 0xc2, 0x4d, 0x0c, 0xad, 0x04, 0xcf, 0xb6, 0x44, 0x75, 0xac, 0x0a, 0x46, 0x84, 0x30, 0x07, + 0xa9, 0x6a, 0xc1, 0x04, 0xbb, 0xf0, 0x13, 0x2c, 0x3e, 0x10, 0x4e, 0xff, 0x1b, 0x1a, 0xb7, 0x09, 0x61, 0x36, 0x58, + 0xe5, 0xa8, 0x93, 0x27, 0xf1, 0x6a, 0x9f, 0xf9, 0x23, 0xe3, 0xd6, 0x57, 0x5f, 0xa4, 0xd5, 0x48, 0xf6, 0x14, 0x33, + 0x38, 0xbc, 0x76, 0x4b, 0x99, 0x19, 0x9f, 0x23, 0x26, 0x55, 0xf3, 0xe4, 0x45, 0x41, 0xa9, 0xa1, 0xae, 0x59, 0x9e, + 0x0b, 0xf3, 0x9c, 0xe1, 0xb9, 0xc0, 0x60, 0x2c, 0xeb, 0xb2, 0xc6, 0x19, 0x9f, 0xc6, 0x56, 0x0c, 0x8c, 0x3b, 0x1e, + 0x0e, 0x7a, 0x8e, 0x39, 0x54, 0x80, 0x5e, 0x04, 0x6e, 0x22, 0x3e, 0xe9, 0x25, 0x70, 0xea, 0xd4, 0x86, 0xf3, 0xcc, + 0x5c, 0x8e, 0x98, 0xf2, 0x0c, 0xa7, 0x98, 0xd2, 0xef, 0xdc, 0x26, 0xd2, 0x6e, 0x7d, 0xad, 0xc9, 0x47, 0xc1, 0xad, + 0x7a, 0x3a, 0xac, 0x23, 0x1a, 0x97, 0x16, 0xc1, 0x93, 0x45, 0x79, 0xd8, 0xbf, 0xf1, 0x9c, 0x5f, 0x89, 0xb4, 0x93, + 0x52, 0x25, 0x3a, 0x9f, 0x79, 0x0e, 0x42, 0x48, 0x93, 0x96, 0xd0, 0xf6, 0xb9, 0x20, 0x25, 0xc6, 0xb5, 0x53, 0x8a, + 0xd8, 0xcd, 0xc1, 0xfe, 0xd7, 0x53, 0xb9, 0x48, 0xab, 0xeb, 0x87, 0x77, 0x8b, 0x85, 0xd8, 0x4e, 0xc8, 0x79, 0x2e, + 0x64, 0xef, 0xfd, 0x6b, 0x12, 0x5e, 0x27, 0xe5, 0x97, 0xfd, 0x20, 0x71, 0xef, 0x5c, 0x6a, 0xfe, 0x5f, 0x38, 0x6c, + 0x7a, 0xf4, 0xca, 0xc1, 0x6c, 0x33, 0x01, 0x93, 0xa3, 0x52, 0x55, 0xdf, 0x8f, 0x80, 0xd4, 0xf0, 0x30, 0x40, 0x59, + 0xc5, 0xfe, 0x41, 0xbd, 0x25, 0x00, 0xb8, 0xd4, 0xb3, 0xfd, 0xb0, 0xb4, 0xbf, 0xfb, 0x61, 0xab, 0x13, 0x2e, 0x57, + 0x4a, 0xfe, 0x1b, 0x18, 0x42, 0x97, 0x86, 0xe4, 0xb0, 0x75, 0xd6, 0x03, 0x21, 0x77, 0x9e, 0x73, 0x8a, 0x51, 0x5c, + 0x4b, 0xbe, 0x60, 0xfb, 0x91, 0x94, 0xd7, 0xc9, 0x7c, 0x7e, 0xb1, 0xb5, 0x82, 0xcf, 0x3a, 0xa9, 0xbf, 0xa8, 0x44, + 0xff, 0x05, 0xec, 0x3b, 0x88, 0x0f, 0xcf, 0x13, 0xab, 0xca, 0x0f, 0x1d, 0x87, 0x23, 0xac, 0xb0, 0x95, 0x2a, 0x0d, + 0xcd, 0x23, 0xd3, 0xc7, 0x94, 0x9c, 0x9a, 0x80, 0x71, 0x48, 0x72, 0xb6, 0x56, 0x91, 0x4b, 0x92, 0x6a, 0x16, 0xee, + 0xeb, 0x06, 0x9f, 0x84, 0xe9, 0x92, 0x56, 0xe1, 0x1e, 0xa0, 0x7f, 0x0c, 0x7a, 0xfe, 0xcf, 0x4a, 0x71, 0xc0, 0xb0, + 0xea, 0x85, 0x4c, 0xfc, 0x64, 0x00, 0xd7, 0x66, 0x9e, 0xcc, 0x71, 0xe3, 0x6d, 0xd4, 0x47, 0x6d, 0x4b, 0xc0, 0x4f, + 0xa2, 0x27, 0x8f, 0x61, 0x4c, 0x16, 0x86, 0x02, 0x77, 0x2e, 0xf5, 0x73, 0xb0, 0x70, 0xc3, 0x31, 0xfa, 0x86, 0xe0, + 0x5b, 0xfe, 0xfc, 0x0e, 0xd4, 0x71, 0x8c, 0x86, 0x2e, 0x8d, 0xa4, 0xf4, 0x3b, 0x14, 0x44, 0x1a, 0x48, 0xa0, 0x79, + 0xee, 0x6b, 0x28, 0x9c, 0x4e, 0x22, 0x3b, 0xc9, 0x11, 0xd6, 0xce, 0x82, 0x59, 0xab, 0x9c, 0xbe, 0x9e, 0xed, 0x3b, + 0x0c, 0xd0, 0xb5, 0xcb, 0xe9, 0x7d, 0xae, 0xbf, 0x95, 0xa9, 0x9f, 0x2b, 0x84, 0x96, 0x65, 0x9b, 0x9d, 0x43, 0x20, + 0x94, 0x6b, 0xf5, 0x34, 0x44, 0xc1, 0x10, 0xef, 0x74, 0xac, 0x6e, 0xd0, 0x47, 0x2a, 0x5a, 0xe7, 0xab, 0x0d, 0xda, + 0x7c, 0xab, 0xf0, 0xd2, 0xf5, 0xca, 0xf6, 0x34, 0x24, 0xc5, 0x34, 0x62, 0x38, 0xf8, 0x66, 0x42, 0x67, 0xf2, 0x3e, + 0x6e, 0x90, 0x4d, 0x9c, 0x21, 0x79, 0xb8, 0x8e, 0x58, 0xba, 0x4a, 0x58, 0x54, 0xb6, 0xf0, 0x74, 0x4a, 0x3b, 0x5c, + 0xdd, 0x15, 0xe1, 0xe6, 0x4c, 0x06, 0x6a, 0xb9, 0x8e, 0xbe, 0x89, 0xc4, 0x20, 0x07, 0x6c, 0xb9, 0x4e, 0x1f, 0x1d, + 0x55, 0xaa, 0x90, 0xe9, 0x76, 0xde, 0xbc, 0x86, 0x05, 0x87, 0x2d, 0x63, 0x29, 0x0d, 0x30, 0xf0, 0x5d, 0x7b, 0x79, + 0x5f, 0x6d, 0x76, 0x2a, 0x3c, 0xe6, 0xbc, 0x4b, 0x69, 0x91, 0x57, 0xa9, 0x7f, 0x6e, 0xe3, 0xb5, 0xf9, 0xd5, 0xdc, + 0x46, 0x17, 0xbc, 0x39, 0x27, 0x3a, 0xad, 0x6f, 0x31, 0x5e, 0x76, 0x88, 0x68, 0xe7, 0x3e, 0x97, 0x79, 0xba, 0x85, + 0xd4, 0x62, 0xcc, 0x3a, 0xc7, 0x4c, 0x4f, 0x4b, 0x8b, 0xf2, 0x11, 0x63, 0x50, 0xc9, 0x9d, 0xf2, 0xcd, 0xc8, 0x53, + 0x8d, 0xc2, 0x10, 0xab, 0x6d, 0xe8, 0x19, 0xb4, 0xd2, 0x4f, 0x34, 0xfb, 0xf6, 0x76, 0xb3, 0x06, 0xa8, 0xc4, 0x62, + 0x1f, 0x99, 0x65, 0xe1, 0xe3, 0x72, 0x07, 0xa1, 0x83, 0x34, 0xb7, 0x93, 0xc3, 0x38, 0x3d, 0x46, 0x2b, 0xf4, 0xbb, + 0xf6, 0x14, 0xb3, 0x80, 0x48, 0x52, 0x2f, 0x90, 0xce, 0x01, 0x77, 0x49, 0xfd, 0x59, 0x9c, 0x19, 0x61, 0x3e, 0x7a, + 0x29, 0xa1, 0xdc, 0x25, 0x94, 0x1b, 0xaf, 0xf3, 0x24, 0x00, 0x3e, 0x89, 0xb6, 0xd7, 0x56, 0x9c, 0xe6, 0x9c, 0xd7, + 0xb6, 0x08, 0xc3, 0xae, 0xeb, 0xc5, 0x48, 0x72, 0x33, 0x7b, 0x61, 0xcc, 0x18, 0x04, 0x22, 0xa8, 0xe8, 0xe6, 0x80, + 0xc9, 0x98, 0x3a, 0xc2, 0x41, 0xe7, 0x4f, 0xb2, 0xa9, 0xa6, 0xb4, 0x98, 0xda, 0xd1, 0xff, 0xce, 0x1e, 0xfc, 0xf0, + 0x68, 0x7a, 0xfe, 0xeb, 0xdb, 0xfc, 0x1a, 0x34, 0x41, 0x0f, 0xe1, 0x7e, 0x77, 0x35, 0x53, 0xa1, 0xa0, 0xb0, 0xb2, + 0x7d, 0x69, 0x01, 0x50, 0x63, 0x2a, 0x4a, 0x57, 0xd7, 0xd6, 0xfd, 0x49, 0xc6, 0xa7, 0x35, 0x6d, 0x7c, 0x1d, 0xa8, + 0xf2, 0xb5, 0xa1, 0x6c, 0xdd, 0xe1, 0xf3, 0x50, 0xcd, 0x78, 0x0a, 0xda, 0x5c, 0x18, 0xfc, 0x0a, 0x39, 0x6e, 0x42, + 0x27, 0x43, 0x95, 0x4d, 0xb3, 0x13, 0x6f, 0x59, 0x25, 0x6b, 0x29, 0x39, 0x91, 0x12, 0xf6, 0xaa, 0xf2, 0x47, 0xdd, + 0x93, 0xd4, 0x96, 0x6a, 0x70, 0x83, 0x13, 0x94, 0x36, 0x3a, 0xfa, 0x2a, 0x6e, 0xe4, 0xa7, 0x1b, 0x40, 0x44, 0x4d, + 0x4f, 0x87, 0xf6, 0x76, 0xfe, 0x79, 0x6f, 0x8c, 0xb0, 0x49, 0x05, 0x3f, 0xca, 0xa8, 0xa9, 0x1a, 0x9e, 0xfb, 0x53, + 0xaf, 0x6c, 0x87, 0x67, 0xba, 0x9b, 0x2c, 0x6a, 0x8b, 0x3e, 0xe3, 0x02, 0xac, 0x9a, 0x68, 0xaa, 0x71, 0xa1, 0x08, + 0x63, 0x1a, 0x6f, 0xce, 0xda, 0x89, 0xb5, 0xea, 0xd5, 0xed, 0xac, 0x97, 0x1e, 0x6d, 0xb3, 0x05, 0x6a, 0xbc, 0x68, + 0xda, 0xf2, 0x2a, 0x7c, 0xb7, 0xa4, 0x9b, 0x95, 0x23, 0xc8, 0xdc, 0x04, 0xe8, 0x67, 0x3f, 0x64, 0x26, 0x7a, 0x07, + 0xe1, 0x4e, 0x2a, 0xd9, 0x93, 0x8a, 0xcd, 0x78, 0xbe, 0xbd, 0x72, 0x7e, 0x5a, 0x92, 0x6d, 0xc4, 0xda, 0x8d, 0x2a, + 0xe3, 0xb1, 0x35, 0xc8, 0xdb, 0x35, 0xd3, 0x59, 0xa2, 0xbf, 0x86, 0x7b, 0xfd, 0xf9, 0x76, 0x0d, 0x4d, 0xb7, 0xd5, + 0xdc, 0x79, 0xe9, 0xe4, 0x28, 0x29, 0x79, 0xf1, 0x03, 0x7b, 0x6c, 0xe1, 0x7c, 0xb0, 0xf1, 0x90, 0x60, 0x99, 0x8a, + 0x55, 0x9f, 0x55, 0xac, 0xf2, 0x2c, 0x11, 0x85, 0xd9, 0xd3, 0x2e, 0x80, 0xe3, 0x0f, 0x2b, 0xb9, 0x8a, 0x1f, 0x66, + 0x5a, 0xb5, 0x7c, 0x58, 0x08, 0xd5, 0xf4, 0x24, 0x10, 0x19, 0x98, 0x35, 0xf2, 0x70, 0x61, 0xea, 0x98, 0x4c, 0x4a, + 0x49, 0x20, 0x57, 0x58, 0x4d, 0xab, 0x6a, 0xd5, 0x87, 0x1f, 0x6e, 0xb8, 0x41, 0x5d, 0xf9, 0x67, 0x33, 0xae, 0xff, + 0xaf, 0x99, 0x89, 0xe5, 0xa0, 0xb1, 0xd0, 0xfa, 0x27, 0x2d, 0xcc, 0xfd, 0x08, 0xdd, 0x35, 0xc3, 0x95, 0xe9, 0x4b, + 0x14, 0xf3, 0x2b, 0x46, 0x66, 0x5b, 0xe4, 0xb5, 0x32, 0xd8, 0xc5, 0xde, 0x98, 0x49, 0x5b, 0x27, 0xc6, 0x34, 0x36, + 0x24, 0x56, 0x67, 0x51, 0x23, 0x43, 0x6a, 0x6b, 0xf3, 0x9d, 0xbe, 0xa2, 0xf9, 0x25, 0x2f, 0xf1, 0x97, 0x69, 0xc8, + 0xc2, 0x7c, 0xab, 0xe8, 0x8d, 0xf4, 0x1a, 0x7a, 0x09, 0xfe, 0x62, 0xe1, 0xde, 0x04, 0x78, 0x8e, 0x22, 0x2a, 0x46, + 0x63, 0xf0, 0x4d, 0x16, 0x07, 0x7a, 0xbf, 0xc2, 0xad, 0x20, 0xc3, 0x94, 0x15, 0xff, 0x5f, 0x03, 0xad, 0xf4, 0x2f, + 0x20, 0xf6, 0x0d, 0xbe, 0xc0, 0xca, 0x5b, 0x41, 0xb9, 0x87, 0xe9, 0xfe, 0x02, 0xdf, 0x0a, 0x71, 0x30, 0x68, 0x44, + 0x4d, 0x58, 0xeb, 0x3d, 0xc5, 0x38, 0x31, 0x3d, 0xf8, 0x67, 0x7a, 0x68, 0x25, 0x08, 0x87, 0x31, 0x86, 0x0e, 0x52, + 0x80, 0x60, 0xa5, 0x7c, 0xf5, 0xf5, 0xa1, 0x55, 0xa1, 0x64, 0xe4, 0xab, 0x66, 0x83, 0x4f, 0xb1, 0x9d, 0xa7, 0xd8, + 0x3e, 0x2b, 0x5f, 0x5a, 0x6b, 0x4d, 0x67, 0xab, 0x46, 0x7a, 0xbe, 0x0a, 0x2e, 0x30, 0xec, 0x60, 0xe0, 0xbc, 0x0a, + 0xbe, 0x22, 0x41, 0x93, 0x40, 0x58, 0x40, 0x83, 0xe7, 0x36, 0x94, 0x93, 0x0f, 0x09, 0x94, 0xba, 0x04, 0xbb, 0xcd, + 0x8f, 0x48, 0x6e, 0x03, 0x21, 0xb5, 0x18, 0x67, 0x0b, 0x7b, 0x70, 0x26, 0xcd, 0xfa, 0xb9, 0xb1, 0x1e, 0x5a, 0x89, + 0x92, 0x36, 0x5d, 0x9e, 0x0d, 0xae, 0x19, 0x29, 0xac, 0x93, 0xff, 0x2f, 0x39, 0x6e, 0xf3, 0xbd, 0x2a, 0x08, 0xae, + 0xc4, 0x49, 0x5f, 0xeb, 0x29, 0x28, 0x77, 0x3a, 0x74, 0xe0, 0x8e, 0x20, 0x4b, 0xd9, 0xf3, 0xcc, 0xd3, 0xe7, 0x76, + 0x81, 0x9d, 0x98, 0xed, 0x9e, 0x95, 0xce, 0x70, 0xc2, 0xf1, 0xfb, 0x05, 0xef, 0x2e, 0xfd, 0x39, 0xa4, 0xdc, 0xdf, + 0x57, 0x96, 0x8c, 0x77, 0xc7, 0xdd, 0xb3, 0x3f, 0xc7, 0x1b, 0xb7, 0xfc, 0x29, 0xf7, 0xc3, 0x6d, 0x24, 0xdd, 0xf0, + 0xaa, 0xf9, 0x17, 0x8f, 0x6c, 0xe5, 0x56, 0x42, 0xd0, 0x3a, 0x9d, 0xe3, 0x9b, 0xaf, 0x41, 0xa7, 0x12, 0xb6, 0xf8, + 0xf1, 0x5d, 0xf2, 0x5b, 0x63, 0x2f, 0x46, 0x61, 0x70, 0x68, 0xa6, 0x76, 0x95, 0x9c, 0xb7, 0xe0, 0xb6, 0x4c, 0xed, + 0x56, 0x81, 0x34, 0x7a, 0xe7, 0xb9, 0xfa, 0x3a, 0xfd, 0x54, 0x35, 0xff, 0x31, 0x73, 0x13, 0xf6, 0xb1, 0x42, 0x91, + 0xf8, 0x67, 0x6a, 0x74, 0x83, 0x91, 0xca, 0x03, 0x75, 0x81, 0x55, 0x4b, 0x82, 0xca, 0xdb, 0x11, 0x3f, 0xe5, 0xd7, + 0xdc, 0xe5, 0x48, 0x6c, 0x84, 0xfd, 0x69, 0x6c, 0x3e, 0x53, 0x9f, 0xed, 0x6c, 0x99, 0x65, 0xb7, 0x8f, 0x99, 0x87, + 0x47, 0xf9, 0x5b, 0xaa, 0x5b, 0xe4, 0x7b, 0xb3, 0x2c, 0x2f, 0xca, 0xec, 0xd4, 0x3e, 0x87, 0x4f, 0xb4, 0xd1, 0x24, + 0x7f, 0xe3, 0x71, 0x2f, 0xb1, 0x21, 0xd9, 0x34, 0x2f, 0x33, 0x87, 0xd8, 0xc3, 0xf3, 0x1b, 0x3f, 0x65, 0x53, 0x99, + 0x28, 0x38, 0x6d, 0x87, 0xa6, 0x03, 0xf7, 0x0d, 0xd4, 0x41, 0x15, 0x20, 0xa7, 0x2b, 0xb0, 0xd1, 0x80, 0x59, 0x6d, + 0xd4, 0x97, 0xa5, 0xb2, 0x8a, 0xb3, 0xae, 0xdb, 0x75, 0xfa, 0xc5, 0x46, 0x12, 0xb8, 0xb1, 0x47, 0x91, 0x3c, 0x9d, + 0x95, 0xae, 0xf1, 0x57, 0x79, 0xc9, 0x1c, 0xa5, 0x0f, 0xf8, 0xfc, 0x5b, 0xf0, 0x48, 0xfe, 0x52, 0x74, 0x8d, 0xab, + 0xdc, 0x66, 0x34, 0x6a, 0xcc, 0xc9, 0x78, 0x43, 0xd8, 0x58, 0x14, 0x55, 0x31, 0x35, 0xa7, 0xfc, 0x9c, 0x19, 0x8d, + 0xe4, 0x05, 0x09, 0xf2, 0xb9, 0xb7, 0xfb, 0x99, 0x5c, 0xf0, 0x2b, 0xd7, 0xa1, 0x57, 0x56, 0xa3, 0xe2, 0x4b, 0xe7, + 0xb8, 0xb7, 0xee, 0x50, 0x80, 0x0c, 0xd8, 0x43, 0x86, 0x9c, 0xf2, 0x56, 0xd3, 0xbe, 0x5e, 0x7e, 0xff, 0x82, 0x91, + 0x14, 0xab, 0xb2, 0xef, 0xc6, 0x26, 0x4c, 0x22, 0x3b, 0xc2, 0x5e, 0x35, 0xb2, 0x9b, 0x63, 0x11, 0x21, 0x21, 0x19, + 0xf7, 0x4c, 0xc8, 0xe8, 0xad, 0x5e, 0x26, 0x80, 0x73, 0xe2, 0x49, 0xe1, 0x4c, 0x4e, 0x9c, 0xc8, 0xb2, 0xb4, 0x15, + 0x5b, 0x62, 0xe1, 0x08, 0x5f, 0x6b, 0xca, 0x6c, 0xdb, 0x18, 0x2b, 0x68, 0x70, 0xcc, 0x01, 0xa2, 0x33, 0x9f, 0xf1, + 0x86, 0x09, 0xe7, 0xfc, 0xa9, 0xf2, 0xbe, 0x59, 0xa6, 0x41, 0x5f, 0x15, 0x2c, 0x6c, 0xb7, 0x9e, 0x20, 0xca, 0x34, + 0x03, 0x03, 0xf2, 0x6d, 0x85, 0x6a, 0xfe, 0x95, 0x97, 0x28, 0xf8, 0xee, 0x32, 0xf2, 0xf3, 0xe7, 0x70, 0x1b, 0x21, + 0x1a, 0x04, 0x8d, 0xed, 0x85, 0x51, 0xb2, 0xb3, 0xac, 0x86, 0x5c, 0x84, 0x93, 0xe0, 0x83, 0xdd, 0x53, 0xb8, 0x3e, + 0x87, 0xa4, 0x97, 0xe0, 0x29, 0x30, 0x4f, 0x68, 0xa4, 0xb4, 0xdf, 0xf7, 0x2e, 0x08, 0x84, 0xe6, 0x2d, 0x8f, 0x70, + 0x40, 0x32, 0x62, 0x36, 0x16, 0x1e, 0x37, 0x0b, 0x97, 0x56, 0xc1, 0xa9, 0xcb, 0x6a, 0xd4, 0x15, 0xc9, 0x25, 0x85, + 0x34, 0x63, 0xf0, 0x60, 0x74, 0x24, 0x24, 0x96, 0x85, 0x60, 0xcc, 0x86, 0xbf, 0xf5, 0x02, 0xeb, 0xa7, 0x39, 0x00, + 0xf6, 0x04, 0xf1, 0xd8, 0x84, 0xe9, 0x0d, 0x44, 0x78, 0xb6, 0x2d, 0x0f, 0x98, 0xf7, 0x05, 0x1a, 0xcf, 0xf9, 0x98, + 0x52, 0xe6, 0x7c, 0x01, 0x2e, 0x33, 0xf1, 0x62, 0x20, 0x87, 0x80, 0x7b, 0x88, 0x87, 0xfc, 0xe0, 0xb1, 0x97, 0x60, + 0x67, 0x64, 0xa5, 0xbb, 0x5d, 0xbb, 0x71, 0xcc, 0x2d, 0x45, 0x0e, 0xbc, 0xd8, 0x3f, 0x38, 0xac, 0x6b, 0xb1, 0x4a, + 0xbf, 0x69, 0xa0, 0xd2, 0xbf, 0xfa, 0xf7, 0xcd, 0xe7, 0xc8, 0xb7, 0xcf, 0xb5, 0xd4, 0xa2, 0xf8, 0x81, 0x77, 0x56, + 0x93, 0x82, 0x76, 0xff, 0xab, 0x26, 0x23, 0x5f, 0x52, 0x5a, 0x2d, 0x8b, 0x4f, 0xb5, 0x8b, 0x9e, 0xa2, 0x5a, 0x36, + 0x79, 0x6e, 0x0f, 0x49, 0x8e, 0x5e, 0x6b, 0x19, 0x56, 0xb5, 0x3d, 0xea, 0x83, 0xbd, 0xd7, 0x7b, 0x41, 0x1e, 0x52, + 0x0f, 0x89, 0xaa, 0x37, 0x5e, 0x40, 0xc5, 0x7f, 0xf5, 0x95, 0xea, 0x99, 0x5f, 0xd0, 0x90, 0x37, 0xca, 0x31, 0xbe, + 0x6c, 0x9c, 0xb6, 0xae, 0x1e, 0xc5, 0x14, 0xac, 0x14, 0x65, 0x65, 0x88, 0x1b, 0x59, 0xa1, 0x6b, 0x44, 0x5a, 0x03, + 0x7f, 0x63, 0xa3, 0x14, 0x5b, 0x4d, 0xb5, 0x91, 0xf4, 0xdf, 0x99, 0xfe, 0x0f, 0x89, 0xec, 0xff, 0x14, 0xc7, 0xfe, + 0x8f, 0x73, 0x84, 0x16, 0xf6, 0x8d, 0x65, 0x44, 0xc0, 0x15, 0x4d, 0x8a, 0xe3, 0x2b, 0x83, 0xb3, 0x44, 0x50, 0xa3, + 0x89, 0xc8, 0x6e, 0x3c, 0x51, 0x99, 0x11, 0x79, 0x6e, 0x15, 0x3c, 0x4b, 0x7b, 0x74, 0x4f, 0xee, 0xef, 0x9c, 0x40, + 0x94, 0x63, 0x52, 0x6d, 0x6f, 0xc6, 0x9c, 0xc3, 0x10, 0x17, 0x93, 0x8b, 0x0a, 0x60, 0x04, 0x37, 0x84, 0x6c, 0x2c, + 0x81, 0x8e, 0x92, 0xec, 0x47, 0x23, 0xe6, 0x00, 0x34, 0xc0, 0x7e, 0xd9, 0x20, 0xb0, 0x1c, 0xcc, 0x30, 0x43, 0x30, + 0x3a, 0xaf, 0x0e, 0xf0, 0x39, 0x66, 0x7b, 0xc7, 0x26, 0xf8, 0xb0, 0x32, 0x07, 0x3b, 0xd0, 0x20, 0x1c, 0x30, 0x8f, + 0x66, 0x79, 0x26, 0x28, 0x9a, 0xe0, 0x23, 0xea, 0x2c, 0xfb, 0x5c, 0xfc, 0x92, 0xa5, 0xad, 0xd7, 0x25, 0x87, 0x2e, + 0x16, 0x9f, 0x66, 0xd6, 0x50, 0xfa, 0x13, 0xf8, 0x5f, 0x83, 0x3b, 0xb0, 0xc7, 0x1d, 0x24, 0xc2, 0x56, 0x14, 0x4e, + 0xa5, 0xea, 0x9f, 0x5d, 0x06, 0x84, 0x92, 0x9e, 0x66, 0x48, 0xb9, 0x40, 0xeb, 0x1a, 0xe2, 0x1a, 0x6c, 0x0c, 0x86, + 0xed, 0x8c, 0x67, 0x9a, 0xdb, 0xd6, 0x33, 0xe3, 0xa4, 0x5e, 0xa3, 0x4d, 0x46, 0x87, 0x64, 0x5e, 0x44, 0x99, 0xbb, + 0xc8, 0x2f, 0x47, 0x4a, 0xad, 0xb9, 0x51, 0xac, 0x31, 0xe5, 0xa5, 0xd3, 0xec, 0xc3, 0x39, 0x82, 0xd3, 0x45, 0x50, + 0xf5, 0xc3, 0x1e, 0x9c, 0xb5, 0x31, 0xf8, 0x91, 0x62, 0x51, 0x20, 0xbd, 0x5d, 0x11, 0x52, 0xf3, 0x93, 0x1d, 0xab, + 0x59, 0x4c, 0x4b, 0x6f, 0x17, 0x1e, 0xce, 0xb7, 0xc5, 0x14, 0x64, 0x1c, 0x08, 0xf9, 0x23, 0x18, 0xd8, 0x14, 0x77, + 0x66, 0xa2, 0x2d, 0x82, 0xe0, 0x04, 0xa1, 0x8c, 0x48, 0x87, 0x6f, 0x45, 0x29, 0x2a, 0x02, 0x91, 0xfb, 0xd1, 0x7b, + 0x4c, 0xcb, 0x6a, 0x28, 0xad, 0x81, 0x3d, 0x2a, 0x2a, 0x06, 0xca, 0xeb, 0x4c, 0x68, 0x38, 0x45, 0x8b, 0x90, 0xa9, + 0x78, 0xb3, 0x00, 0x2b, 0x37, 0xf8, 0xa4, 0xc5, 0x4a, 0xcf, 0x58, 0xf6, 0x07, 0xf9, 0x4a, 0x59, 0xd1, 0x30, 0x81, + 0xee, 0x31, 0x55, 0xdb, 0x7f, 0xe3, 0xa2, 0x8d, 0xb9, 0x01, 0x7e, 0x06, 0x8c, 0xe2, 0x7a, 0x85, 0x09, 0xab, 0x15, + 0xdc, 0x01, 0x2c, 0xbd, 0x71, 0x6f, 0xd5, 0x4c, 0xc9, 0xa7, 0x5c, 0x49, 0x29, 0x13, 0xac, 0x77, 0x2a, 0x4b, 0x70, + 0xf2, 0x21, 0x04, 0x43, 0x7c, 0xf7, 0x65, 0xe6, 0xd7, 0x6b, 0x9e, 0xda, 0x84, 0x27, 0xd1, 0x9e, 0xf6, 0xd1, 0x37, + 0x6e, 0xc3, 0xab, 0x16, 0x43, 0x5c, 0x9d, 0x65, 0xe7, 0xe4, 0x4b, 0x64, 0x4d, 0xe5, 0x80, 0x1f, 0xb1, 0x1a, 0xea, + 0x12, 0xf8, 0x8c, 0x98, 0x37, 0x0c, 0xe6, 0x6f, 0x74, 0x8a, 0xc5, 0xbc, 0xa9, 0x22, 0xc5, 0xe1, 0xfe, 0x08, 0x8b, + 0x8c, 0x4b, 0x94, 0x23, 0x7d, 0xc8, 0xbf, 0x83, 0xc1, 0xa8, 0xd2, 0xcd, 0x8a, 0xfa, 0x9a, 0xf1, 0xbc, 0xed, 0x63, + 0x6b, 0x12, 0xb6, 0x00, 0x6b, 0x91, 0xf1, 0x04, 0xe8, 0xbe, 0x56, 0x6f, 0x0b, 0xd9, 0x9a, 0x68, 0x89, 0x86, 0xa6, + 0x50, 0xd4, 0x0d, 0x04, 0x13, 0x93, 0xd2, 0xee, 0xc0, 0x48, 0x82, 0xf1, 0xac, 0xa9, 0xfc, 0x82, 0xfc, 0xb8, 0x5e, + 0xc4, 0xd6, 0x98, 0x0b, 0x1d, 0x33, 0xa2, 0x49, 0x4d, 0x9a, 0x09, 0x88, 0x05, 0xf0, 0x72, 0x16, 0x3d, 0xac, 0xf3, + 0x84, 0x53, 0x73, 0xef, 0xd4, 0x11, 0x33, 0x30, 0x40, 0xa7, 0x10, 0xa9, 0x14, 0x3f, 0xbc, 0x0f, 0x52, 0x0a, 0x04, + 0xa0, 0xec, 0x98, 0x0d, 0x2d, 0x29, 0xa8, 0x4f, 0x6b, 0x97, 0x99, 0x5a, 0xdb, 0x1a, 0xfe, 0x94, 0xd9, 0xac, 0x45, + 0x55, 0xcc, 0x1f, 0x2e, 0xf3, 0x8b, 0x98, 0x71, 0xd1, 0xf0, 0x09, 0x43, 0xd5, 0x61, 0x05, 0x7a, 0x8f, 0x9b, 0xbc, + 0x5e, 0x99, 0xf0, 0x7e, 0x5e, 0xef, 0x9b, 0xfb, 0x22, 0x16, 0x5e, 0x14, 0x38, 0xf7, 0xa5, 0x82, 0x97, 0x86, 0x13, + 0xb8, 0xc4, 0x43, 0x99, 0xf9, 0x54, 0xb6, 0x95, 0x99, 0xa2, 0x04, 0x25, 0xb5, 0x88, 0x5c, 0x92, 0x0b, 0x82, 0x94, + 0x8a, 0x97, 0x81, 0x50, 0xdb, 0xb7, 0x0b, 0x90, 0xbd, 0xaf, 0x2d, 0xe3, 0xb5, 0x64, 0xe7, 0x22, 0x94, 0xcd, 0x66, + 0x5c, 0xdb, 0xcb, 0x69, 0xb7, 0xbf, 0x95, 0x41, 0x35, 0x00, 0x25, 0xb3, 0xe1, 0x32, 0xe2, 0x9b, 0x9b, 0x1d, 0x0a, + 0x08, 0xed, 0xfc, 0x6d, 0x57, 0xca, 0x2c, 0xcc, 0x41, 0xd7, 0xec, 0xa8, 0xf8, 0x17, 0xe5, 0xdd, 0x59, 0xed, 0x66, + 0x7c, 0xc9, 0x3b, 0x18, 0xdf, 0x14, 0xca, 0x01, 0x2e, 0x79, 0x21, 0xeb, 0x07, 0x6e, 0x94, 0xc8, 0x12, 0xc2, 0x32, + 0xbb, 0xa8, 0x15, 0x95, 0xc9, 0x98, 0x36, 0xbd, 0xad, 0xc8, 0x66, 0x23, 0x63, 0x5d, 0x96, 0x68, 0x79, 0x6e, 0xc5, + 0xc9, 0xab, 0x87, 0x3f, 0x52, 0xe5, 0xf4, 0x7d, 0x89, 0xfa, 0xd4, 0x07, 0x77, 0x6f, 0x2b, 0xb3, 0x84, 0x4f, 0x4d, + 0x13, 0x45, 0x70, 0x07, 0xcc, 0x55, 0xb6, 0x22, 0x6a, 0x61, 0x48, 0xfd, 0x17, 0x5e, 0xfa, 0xc6, 0x13, 0xaa, 0xb6, + 0x73, 0xd5, 0xab, 0x39, 0x24, 0x66, 0x8c, 0xe7, 0x68, 0xf5, 0x01, 0xb2, 0x81, 0xae, 0xcd, 0xbe, 0x02, 0x02, 0xaf, + 0x99, 0xfc, 0xf2, 0xdb, 0x61, 0xcc, 0xd9, 0x6d, 0x5e, 0x68, 0x64, 0x2b, 0x67, 0xd9, 0xc1, 0x8d, 0xb4, 0x55, 0x7b, + 0x3c, 0xd3, 0x4d, 0x5c, 0x69, 0x99, 0x8c, 0x31, 0xbf, 0xad, 0xea, 0xab, 0x05, 0xdf, 0x99, 0xdb, 0xce, 0x4a, 0xa6, + 0x91, 0xab, 0xd5, 0x57, 0x78, 0x5e, 0x34, 0x41, 0x27, 0x2e, 0x31, 0x53, 0xab, 0xea, 0xb7, 0xaa, 0x1c, 0x15, 0x15, + 0xcb, 0xb9, 0xd5, 0xa6, 0xca, 0x6b, 0xb7, 0xa8, 0xdf, 0x9f, 0x8d, 0x09, 0x41, 0x65, 0x8a, 0xcc, 0x58, 0xa2, 0x8b, + 0x78, 0xa1, 0x9f, 0xd3, 0xba, 0xa8, 0xe8, 0xf1, 0xca, 0xaa, 0x86, 0x18, 0x02, 0xb7, 0xda, 0xc9, 0x20, 0x65, 0x16, + 0x89, 0x79, 0x16, 0x31, 0xdb, 0xeb, 0xd1, 0xb6, 0x39, 0x1b, 0x06, 0x6e, 0xd2, 0x39, 0x81, 0x73, 0x12, 0xfe, 0xa6, + 0x32, 0xdb, 0xb0, 0xa2, 0x6e, 0x79, 0x8d, 0xae, 0xa2, 0x6e, 0xcd, 0x79, 0x3d, 0x55, 0xc5, 0x0f, 0xd4, 0x71, 0xb5, + 0x5e, 0xdd, 0x88, 0x0e, 0x41, 0x81, 0x0b, 0xeb, 0xe7, 0x00, 0xfe, 0x6f, 0xab, 0x18, 0x1e, 0xec, 0xaa, 0x0f, 0xd5, + 0xaa, 0x69, 0x63, 0xdf, 0x38, 0x20, 0xb0, 0x58, 0x15, 0x5c, 0xa0, 0x33, 0xac, 0x50, 0x2f, 0x33, 0x6d, 0x30, 0x4c, + 0x8c, 0x53, 0x4b, 0xcf, 0xa5, 0x13, 0x11, 0x7d, 0x1a, 0x5e, 0xcc, 0x34, 0x77, 0x68, 0xb3, 0x55, 0x6e, 0x11, 0x6a, + 0x47, 0xb0, 0x08, 0x26, 0xd3, 0x65, 0x1c, 0x8c, 0xfc, 0x36, 0xb4, 0xd1, 0x00, 0x13, 0x97, 0x36, 0x65, 0x21, 0xc4, + 0xca, 0x02, 0xaa, 0xf7, 0x5d, 0xb0, 0x40, 0x30, 0xb3, 0x27, 0xa4, 0x5f, 0x41, 0x54, 0xf9, 0x69, 0x7c, 0x3e, 0xa9, + 0xa4, 0xb6, 0x9d, 0xaf, 0x73, 0x0d, 0x1c, 0xaa, 0xa6, 0xa2, 0x2a, 0x57, 0xb6, 0x21, 0x72, 0x18, 0xe4, 0x3a, 0x52, + 0x42, 0x2d, 0x91, 0x2f, 0x33, 0x4a, 0x27, 0x13, 0x46, 0xcb, 0xb5, 0x29, 0x56, 0x81, 0xb4, 0xd6, 0x85, 0x77, 0xf9, + 0x1f, 0x86, 0xb2, 0x0d, 0x7a, 0x21, 0x4a, 0xe4, 0xa2, 0x67, 0xf1, 0xe7, 0x6b, 0x9d, 0x03, 0xa4, 0xfa, 0x5f, 0xad, + 0x5c, 0x2a, 0x63, 0x03, 0xa7, 0xd8, 0x95, 0x91, 0xf8, 0x20, 0xad, 0x25, 0xfa, 0x3e, 0xd7, 0x31, 0xea, 0xba, 0x07, + 0x8d, 0xeb, 0x55, 0xb1, 0x18, 0xfa, 0xc5, 0x7b, 0x12, 0xdd, 0xc2, 0x97, 0x05, 0x25, 0x1d, 0xf4, 0xd3, 0x5d, 0xdd, + 0x5f, 0xa7, 0x84, 0x44, 0x65, 0x31, 0x31, 0x84, 0x55, 0x7e, 0x66, 0x38, 0x6c, 0x15, 0xd1, 0xac, 0xb6, 0xeb, 0xec, + 0xfb, 0x03, 0x05, 0x13, 0x88, 0xa0, 0xb0, 0xf8, 0xff, 0x73, 0x1f, 0x14, 0x68, 0xe0, 0xa4, 0xce, 0x8d, 0x4a, 0x4e, + 0xfb, 0xb9, 0x76, 0x7d, 0xa8, 0xbc, 0x04, 0x40, 0x56, 0x8f, 0x37, 0xb2, 0xbe, 0xe5, 0x77, 0x2b, 0x8b, 0x17, 0x30, + 0x96, 0x3f, 0x85, 0x4d, 0x56, 0x23, 0xda, 0x8c, 0xae, 0xd5, 0x6c, 0x94, 0x4f, 0x99, 0x18, 0x8e, 0x36, 0xd0, 0xf5, + 0xbb, 0x6d, 0x6f, 0x50, 0x5d, 0x58, 0x4b, 0xec, 0x3e, 0x60, 0x5d, 0xa9, 0x80, 0xfd, 0xac, 0xa9, 0xa5, 0x3b, 0x15, + 0xfc, 0xfc, 0x56, 0x4f, 0xa5, 0x59, 0xd8, 0x3a, 0x02, 0x7a, 0xf6, 0xd5, 0x15, 0x07, 0xc0, 0x07, 0x74, 0x0d, 0x0b, + 0x3d, 0x76, 0x2c, 0xf5, 0x99, 0x45, 0x94, 0x7d, 0xe6, 0x1e, 0x5f, 0xdf, 0x0c, 0x85, 0x87, 0x9d, 0xdb, 0x6f, 0xbd, + 0x2a, 0xc6, 0xf1, 0xc2, 0xba, 0xba, 0xc8, 0x05, 0xc5, 0x13, 0x92, 0x9c, 0x5f, 0xce, 0x61, 0xa8, 0x5a, 0x49, 0xc4, + 0x5c, 0x85, 0x04, 0x41, 0xed, 0xbb, 0x22, 0xe0, 0x31, 0x39, 0x5e, 0x81, 0xbb, 0x07, 0xa6, 0xa8, 0x9b, 0x1d, 0x42, + 0xe8, 0x96, 0xb4, 0xba, 0x5b, 0x91, 0x00, 0xd0, 0x2e, 0xd8, 0xfe, 0xc6, 0x79, 0x89, 0x2d, 0x68, 0x19, 0xad, 0x17, + 0x21, 0x0c, 0x44, 0x22, 0x85, 0x31, 0x72, 0x7a, 0x38, 0x5b, 0xd7, 0xa0, 0x18, 0xfa, 0x53, 0x17, 0x38, 0xf5, 0xe3, + 0xd9, 0xb6, 0x4b, 0x85, 0x64, 0x22, 0xe8, 0xd0, 0x14, 0x58, 0x96, 0x9d, 0x38, 0xed, 0x91, 0xe4, 0xfd, 0x7d, 0x9e, + 0x92, 0x15, 0x15, 0x3f, 0x94, 0xc3, 0xcf, 0x27, 0x24, 0x38, 0xd4, 0x13, 0x30, 0x83, 0x0e, 0x78, 0xa6, 0xf7, 0xa9, + 0x13, 0x23, 0x95, 0xf5, 0x0e, 0x38, 0x8a, 0x88, 0x32, 0xd3, 0x25, 0xbb, 0xc7, 0xed, 0xf1, 0x14, 0x70, 0x23, 0x63, + 0xda, 0x65, 0x8e, 0x61, 0x26, 0x30, 0x8e, 0xf9, 0x6a, 0x7c, 0x3e, 0xa2, 0x1f, 0xc7, 0x7d, 0x44, 0xc9, 0x45, 0xa5, + 0x86, 0xc2, 0x36, 0x66, 0x8b, 0x5a, 0xf4, 0xd4, 0xbe, 0x91, 0x48, 0x47, 0xaf, 0x60, 0x2c, 0x17, 0x98, 0x06, 0x2b, + 0x9d, 0xf3, 0x8a, 0x82, 0x15, 0x4d, 0x80, 0xb8, 0x0a, 0xe3, 0x94, 0x51, 0x6b, 0xcc, 0x52, 0x18, 0x5c, 0x51, 0x93, + 0x11, 0xc1, 0xaf, 0x26, 0xf4, 0xec, 0x63, 0x31, 0xdc, 0x93, 0x97, 0xc3, 0xa1, 0x1e, 0xad, 0xa7, 0x75, 0xf7, 0x06, + 0x16, 0x63, 0xc1, 0xb2, 0xc0, 0x9c, 0x9d, 0x0e, 0x3a, 0xaf, 0xd8, 0xd6, 0xd4, 0x3a, 0x5d, 0xad, 0x1f, 0x5a, 0xd9, + 0x28, 0x96, 0xd3, 0x24, 0x92, 0x38, 0x6f, 0xa6, 0x51, 0x8c, 0x3f, 0x34, 0x5c, 0xea, 0x86, 0xfa, 0xc4, 0x6f, 0xcd, + 0x5f, 0x4b, 0xa5, 0xbf, 0xce, 0x3f, 0x8a, 0x85, 0x1d, 0x9b, 0xd8, 0x6f, 0xb4, 0x60, 0x65, 0xd1, 0xd8, 0x40, 0xa8, + 0xea, 0x0b, 0x9e, 0x25, 0x2b, 0x95, 0x27, 0xdf, 0x8d, 0xd8, 0xd2, 0x02, 0x3f, 0x8f, 0xf2, 0x6a, 0xea, 0xcd, 0x88, + 0x41, 0xb5, 0x7c, 0x8a, 0x6a, 0x77, 0x72, 0x20, 0x5c, 0x26, 0x37, 0x56, 0x95, 0x07, 0x88, 0x9e, 0x5f, 0x96, 0x1e, + 0x09, 0x73, 0xa9, 0x98, 0x92, 0x06, 0xcf, 0x89, 0xa0, 0xb7, 0x30, 0x85, 0x18, 0x1e, 0x49, 0xdf, 0xa0, 0xf2, 0xee, + 0x8f, 0xeb, 0x7d, 0xaf, 0x0b, 0xdc, 0x19, 0x3b, 0xdf, 0x74, 0x2f, 0x9d, 0x19, 0x34, 0x7a, 0xfd, 0x73, 0xa8, 0x5a, + 0x84, 0xc1, 0x4e, 0xd3, 0x85, 0xa0, 0x09, 0xea, 0x5f, 0x8c, 0x06, 0xd6, 0x32, 0x5d, 0xeb, 0xed, 0x20, 0x53, 0x21, + 0x31, 0xfe, 0x5f, 0x64, 0xbc, 0x0c, 0x98, 0x9c, 0x8c, 0xe2, 0x16, 0x3c, 0x00, 0x37, 0xd3, 0x90, 0x0b, 0x94, 0xd9, + 0xc3, 0x13, 0xa8, 0xc9, 0x58, 0x84, 0x67, 0x39, 0xe6, 0x3a, 0x75, 0x60, 0x3d, 0xb2, 0x79, 0x58, 0xa3, 0x70, 0xb5, + 0x9c, 0x4d, 0x4e, 0xc9, 0x8e, 0x59, 0x5d, 0xed, 0x63, 0x77, 0xc6, 0x25, 0x9e, 0x39, 0x1f, 0xf2, 0x6d, 0xec, 0x71, + 0xc0, 0x1c, 0x87, 0x07, 0x0e, 0x33, 0xe7, 0x21, 0x82, 0x5c, 0x37, 0xc0, 0x16, 0x60, 0xb2, 0x93, 0xb5, 0x6b, 0x94, + 0xb0, 0x78, 0x73, 0x03, 0x20, 0x8f, 0x64, 0x12, 0x42, 0x2e, 0x1b, 0x7e, 0x96, 0x5a, 0xaa, 0x8f, 0x80, 0x8f, 0xd5, + 0x87, 0x9a, 0x06, 0x42, 0xdc, 0x36, 0x42, 0x1e, 0x30, 0x26, 0xae, 0xcc, 0x2f, 0xaa, 0x09, 0x2e, 0xf8, 0x2f, 0x7b, + 0x1d, 0x7f, 0xdd, 0xac, 0xfb, 0x9e, 0x21, 0x22, 0x1d, 0xac, 0x45, 0x64, 0xbd, 0x22, 0x85, 0xff, 0x86, 0xdf, 0x03, + 0x45, 0x09, 0xc5, 0x52, 0xb1, 0xfc, 0x88, 0xea, 0x1e, 0xe3, 0x1e, 0xf2, 0xde, 0x4e, 0x7e, 0x1f, 0x09, 0x83, 0x1e, + 0x50, 0x63, 0x94, 0xa4, 0x38, 0x52, 0xab, 0x9e, 0x7b, 0x14, 0x2c, 0x85, 0xa5, 0x86, 0xe7, 0x88, 0xd2, 0xd5, 0xf7, + 0x0a, 0xb5, 0xf0, 0x1f, 0x2d, 0x6d, 0x9e, 0x86, 0x7d, 0x44, 0xf2, 0x4d, 0x46, 0xd7, 0xc8, 0x42, 0x25, 0x51, 0x78, + 0x23, 0x04, 0x9e, 0x73, 0xc6, 0x53, 0x7d, 0x84, 0x98, 0x07, 0xa2, 0xc9, 0xc8, 0xf5, 0x80, 0xde, 0xd1, 0xe6, 0xe8, + 0x45, 0x72, 0x4c, 0xdf, 0xb4, 0x0f, 0x83, 0xb0, 0x1d, 0x5b, 0x5c, 0x6a, 0x4c, 0x44, 0x6b, 0x5a, 0x75, 0xd9, 0x23, + 0x52, 0xef, 0x3c, 0x15, 0xa3, 0x04, 0x25, 0x72, 0x13, 0x15, 0xdd, 0x39, 0x4e, 0xed, 0xa2, 0xe8, 0xf6, 0x19, 0x4b, + 0xb8, 0x18, 0x55, 0x7c, 0x5f, 0x06, 0x9f, 0x44, 0x52, 0x34, 0x60, 0xd8, 0x80, 0xaf, 0xf7, 0xff, 0x60, 0xe8, 0x66, + 0x20, 0x97, 0xda, 0x30, 0x65, 0xf3, 0xe9, 0x9c, 0x66, 0x5b, 0xc3, 0x7d, 0x83, 0xd6, 0x97, 0x50, 0xcf, 0xb9, 0xcf, + 0x88, 0xdf, 0x4a, 0xcd, 0x2d, 0x26, 0xab, 0x36, 0x1b, 0x59, 0x4c, 0xd6, 0x61, 0xd5, 0x3d, 0x46, 0xa6, 0x10, 0xff, + 0x42, 0x93, 0x5c, 0x10, 0x1e, 0x55, 0xc9, 0x82, 0x7f, 0xd0, 0xcc, 0x60, 0xc3, 0x79, 0x9d, 0xfe, 0x8d, 0x32, 0x80, + 0xf7, 0x3b, 0xcb, 0x5a, 0x41, 0x35, 0x25, 0xb5, 0xe3, 0xba, 0x4b, 0xc7, 0x2f, 0x5d, 0xa0, 0x07, 0x99, 0x89, 0x67, + 0x47, 0x25, 0x21, 0x66, 0x81, 0x75, 0x2f, 0x91, 0xfa, 0xe6, 0x27, 0xe9, 0x91, 0x36, 0x78, 0xce, 0x42, 0xb4, 0xa0, + 0x17, 0x15, 0xfb, 0x7b, 0xa5, 0x34, 0xbd, 0x57, 0x76, 0x06, 0xaf, 0x8d, 0x99, 0xca, 0x41, 0xb0, 0xee, 0x12, 0x7a, + 0xb9, 0x3c, 0xc9, 0x8f, 0x6d, 0xe6, 0x92, 0xe6, 0x23, 0xe9, 0xef, 0xaa, 0x14, 0xeb, 0xc7, 0x80, 0xd6, 0xbf, 0xa6, + 0xcc, 0x92, 0x14, 0x68, 0x30, 0x58, 0x8d, 0x15, 0xdb, 0x04, 0x1c, 0x52, 0x68, 0x22, 0xa2, 0x89, 0x76, 0xdc, 0xee, + 0x68, 0x7c, 0x86, 0xd4, 0x47, 0xb8, 0x40, 0x32, 0xe0, 0x91, 0x43, 0x4c, 0x56, 0xc5, 0x2e, 0xc0, 0x17, 0xb8, 0x7d, + 0x3c, 0x83, 0x7e, 0xd8, 0x6e, 0xdd, 0x20, 0xe5, 0xa6, 0x1c, 0x17, 0x01, 0x6b, 0x08, 0x80, 0xa7, 0x5c, 0x13, 0xad, + 0x06, 0x52, 0x7d, 0x69, 0x04, 0xec, 0xbb, 0x83, 0xfa, 0x38, 0x9a, 0xa6, 0x8c, 0x65, 0xd3, 0xc4, 0x4b, 0xc9, 0x23, + 0xc4, 0x88, 0x7d, 0x85, 0x53, 0x8e, 0xc0, 0xbc, 0xc3, 0xef, 0xac, 0xd7, 0x42, 0x7a, 0x9b, 0xe8, 0x73, 0x93, 0x81, + 0x87, 0xe1, 0xc7, 0xd8, 0x7e, 0xd1, 0xd3, 0xce, 0xd6, 0x9c, 0xbf, 0x0a, 0x48, 0x46, 0x47, 0xe1, 0x5f, 0x85, 0x67, + 0xa5, 0x6d, 0x12, 0x42, 0xfc, 0x03, 0xd1, 0x75, 0x86, 0x33, 0x48, 0xb0, 0x48, 0x5f, 0x2e, 0x6a, 0x17, 0x39, 0x05, + 0x95, 0xf6, 0x99, 0xd5, 0xca, 0xb2, 0x7c, 0x7b, 0xfb, 0x8f, 0x73, 0x6b, 0x53, 0x8d, 0x0b, 0x1e, 0x72, 0x8d, 0xcb, + 0x56, 0x36, 0xfc, 0xa2, 0x8d, 0xf7, 0x96, 0x6c, 0x36, 0x72, 0xd5, 0x57, 0x2e, 0xe1, 0x9f, 0xf8, 0x51, 0x46, 0xb2, + 0xf1, 0x02, 0xac, 0x45, 0x6a, 0x79, 0xe4, 0xea, 0xa9, 0xed, 0x7b, 0xbd, 0x50, 0xac, 0x0c, 0xee, 0xfc, 0xe2, 0x38, + 0x41, 0x92, 0xca, 0x43, 0xfe, 0x9c, 0xaf, 0xe3, 0x1c, 0x3b, 0xab, 0xe9, 0x68, 0x45, 0xef, 0x48, 0x5d, 0x0e, 0x16, + 0x5b, 0x8e, 0x92, 0xf3, 0xc1, 0xb9, 0x6b, 0x86, 0x1e, 0x1c, 0x45, 0x3d, 0x9f, 0x29, 0x89, 0x05, 0x5c, 0x9a, 0xbb, + 0xa7, 0x08, 0x7a, 0x84, 0x48, 0x8c, 0xd0, 0xbb, 0xa0, 0x21, 0x18, 0xb6, 0x39, 0xdf, 0x64, 0x82, 0x36, 0x6b, 0xd1, + 0x2e, 0xe2, 0x17, 0xc3, 0xa2, 0xf0, 0xda, 0xbb, 0x7a, 0xe4, 0x8a, 0xe5, 0x12, 0x1a, 0x03, 0x59, 0x83, 0x62, 0xff, + 0x9a, 0x04, 0x3f, 0xe8, 0x9f, 0xad, 0x16, 0x1a, 0x2b, 0x53, 0xe6, 0xc7, 0x8c, 0x55, 0xea, 0x9c, 0xb5, 0xf4, 0x6e, + 0x6a, 0x17, 0x94, 0x9c, 0x6c, 0xe5, 0xfc, 0x5a, 0xcc, 0xbf, 0x1f, 0xaa, 0x9f, 0x66, 0xca, 0x3b, 0x84, 0x27, 0xcc, + 0xd7, 0x89, 0xa2, 0x80, 0xac, 0x9b, 0x25, 0xce, 0x52, 0x52, 0xc7, 0x2a, 0x89, 0x12, 0x63, 0x3b, 0x87, 0x47, 0x08, + 0x42, 0xd2, 0xd9, 0xac, 0xce, 0x8c, 0xc9, 0x95, 0x14, 0x6f, 0x87, 0x72, 0x25, 0x9c, 0xc5, 0x22, 0x4d, 0x50, 0x74, + 0xf9, 0x86, 0x5c, 0x2a, 0xf4, 0x54, 0x97, 0x76, 0x74, 0xa8, 0xf4, 0x80, 0x7f, 0x74, 0x73, 0x89, 0x99, 0x54, 0xa0, + 0x11, 0x9f, 0xc7, 0x96, 0x54, 0x22, 0x59, 0x15, 0x39, 0x0c, 0xd4, 0xca, 0xe4, 0x27, 0xdb, 0xe7, 0x32, 0x5a, 0x37, + 0x21, 0x70, 0xdd, 0xe6, 0x4a, 0xe2, 0xee, 0x5f, 0x26, 0xf3, 0x74, 0x00, 0xf6, 0xcb, 0x72, 0x9d, 0x37, 0x3a, 0xe1, + 0xf2, 0xe8, 0xec, 0x53, 0x13, 0xec, 0x78, 0x07, 0x2f, 0x26, 0x12, 0x04, 0x07, 0x89, 0x44, 0xa4, 0x82, 0x33, 0x90, + 0x78, 0x02, 0x03, 0x70, 0x72, 0xfe, 0x88, 0x9f, 0x17, 0x64, 0x79, 0x01, 0x5c, 0xe1, 0xa8, 0x02, 0x44, 0x82, 0x04, + 0x8d, 0x2e, 0xbc, 0x9b, 0x63, 0xf5, 0x9a, 0x2d, 0xb7, 0xab, 0xd2, 0x79, 0x50, 0x73, 0x24, 0x85, 0x92, 0x30, 0xe2, + 0x0c, 0x8b, 0x1f, 0x6c, 0x4a, 0x94, 0xaf, 0x1a, 0x81, 0x30, 0xb2, 0x58, 0xe2, 0x85, 0x46, 0x83, 0x00, 0x8f, 0x8f, + 0x90, 0x32, 0xd9, 0x36, 0xe3, 0x98, 0x7d, 0x4d, 0x89, 0x73, 0x86, 0xcc, 0x10, 0x4a, 0x06, 0xe6, 0x68, 0x09, 0x60, + 0x9d, 0xc5, 0x18, 0x4d, 0xa5, 0x29, 0x3e, 0x3f, 0x47, 0xad, 0xd6, 0x91, 0x57, 0x36, 0x43, 0xbd, 0x0d, 0x56, 0x4c, + 0x89, 0x00, 0xc7, 0x21, 0xa4, 0x97, 0xc0, 0x82, 0x32, 0xe6, 0xb6, 0x24, 0x97, 0x22, 0x3f, 0x24, 0x6b, 0x49, 0xd3, + 0x66, 0x60, 0x70, 0xae, 0x9b, 0x7c, 0x31, 0x1f, 0x8e, 0x92, 0x69, 0x15, 0x6c, 0x1a, 0xeb, 0xfe, 0x3d, 0x6c, 0x9a, + 0x6e, 0x72, 0xe5, 0xb6, 0x0a, 0xc6, 0x6a, 0xe6, 0x78, 0xc4, 0xe6, 0x6c, 0xc0, 0xcb, 0xef, 0x41, 0x1a, 0x2e, 0x1e, + 0x42, 0x64, 0xda, 0x4f, 0xfb, 0xa7, 0xd8, 0xbb, 0xe5, 0xf2, 0x64, 0x06, 0xce, 0xda, 0xd8, 0x1d, 0xa7, 0xa8, 0xae, + 0x25, 0x51, 0x2e, 0x09, 0x9b, 0x0f, 0x80, 0xa1, 0xd6, 0xf7, 0xa2, 0xec, 0xff, 0x2e, 0xe9, 0x89, 0xa2, 0xc2, 0x73, + 0x9d, 0xd7, 0x67, 0xa9, 0x3f, 0x80, 0x76, 0x1f, 0xc7, 0xe4, 0xce, 0x38, 0xcc, 0x11, 0x50, 0x99, 0xbd, 0x5f, 0xbf, + 0xa2, 0x83, 0xb6, 0x95, 0xea, 0x4f, 0x28, 0xce, 0x1f, 0x94, 0xd1, 0x3a, 0x5b, 0xe6, 0xfc, 0x6c, 0xc1, 0x40, 0x67, + 0x92, 0x96, 0x92, 0xca, 0x27, 0xfe, 0x87, 0xea, 0xf0, 0x31, 0xb5, 0x47, 0x8c, 0x4d, 0x24, 0x09, 0x7e, 0x92, 0x27, + 0x7c, 0x4c, 0x35, 0x13, 0xb5, 0x3d, 0x43, 0x8a, 0x7a, 0x63, 0x44, 0xa6, 0x52, 0x4d, 0x96, 0x15, 0x9b, 0x58, 0xe4, + 0x04, 0xbb, 0xba, 0xb0, 0x2e, 0x7d, 0xa2, 0x3e, 0xa5, 0xa6, 0xe6, 0x20, 0x5c, 0x18, 0x48, 0x77, 0xdb, 0x55, 0x8f, + 0x16, 0x4b, 0x5a, 0x28, 0x52, 0x12, 0x39, 0x89, 0xa4, 0x69, 0x1c, 0xa9, 0x0b, 0x60, 0x9e, 0xa3, 0xec, 0x56, 0xb2, + 0x06, 0x6b, 0x6b, 0x3c, 0x51, 0x27, 0xd5, 0x91, 0x9b, 0x3a, 0xcc, 0xac, 0xa7, 0x9a, 0xf9, 0x3f, 0x3b, 0x26, 0x52, + 0xd0, 0x71, 0xe5, 0x91, 0x27, 0x94, 0xe6, 0xcd, 0x54, 0xed, 0x64, 0x48, 0x8f, 0x3c, 0xd7, 0xdb, 0xa4, 0x63, 0x9f, + 0x0b, 0x25, 0x0e, 0xdd, 0x74, 0x39, 0xd5, 0x25, 0xf0, 0xf1, 0x55, 0xfc, 0x91, 0x50, 0x2c, 0x49, 0xa4, 0x61, 0xee, + 0x6c, 0x34, 0xb6, 0x34, 0x56, 0x97, 0x8a, 0x2c, 0x0e, 0x4b, 0x43, 0xd5, 0x55, 0x94, 0xc5, 0x1b, 0x35, 0xd8, 0x44, + 0xd4, 0x45, 0x7d, 0x0d, 0x3a, 0xa5, 0xf3, 0x1c, 0x68, 0xa9, 0xc5, 0xdd, 0x53, 0x41, 0xef, 0x27, 0x5c, 0x53, 0x01, + 0x0e, 0x26, 0x1e, 0xb9, 0x78, 0xc1, 0xe9, 0x32, 0xa7, 0x2b, 0xd8, 0x5f, 0x34, 0x52, 0x8d, 0x33, 0x0b, 0x55, 0x39, + 0x33, 0xaa, 0xda, 0x99, 0x75, 0xd3, 0x0d, 0x86, 0x39, 0x57, 0xbb, 0x1c, 0x59, 0x94, 0x25, 0x59, 0x1c, 0x57, 0xa6, + 0x89, 0xe7, 0xf6, 0xca, 0x65, 0xa4, 0xf3, 0x4a, 0xb6, 0x98, 0x8c, 0x89, 0x4b, 0x3d, 0x32, 0x3d, 0x31, 0xb2, 0x6c, + 0xa0, 0xb6, 0x4b, 0xaf, 0xa9, 0x3a, 0xf6, 0x8b, 0x3b, 0x16, 0x85, 0x97, 0xb9, 0xc6, 0xf4, 0x38, 0x09, 0x19, 0xf2, + 0xa5, 0x35, 0x50, 0x62, 0x1b, 0xfc, 0x58, 0x8e, 0xf6, 0xd3, 0x69, 0x09, 0xac, 0x49, 0x44, 0xa0, 0xea, 0xb5, 0x81, + 0xfc, 0xb8, 0x4d, 0x0f, 0xe9, 0xcb, 0x16, 0x2e, 0xca, 0x1f, 0xca, 0x61, 0x73, 0xe0, 0x10, 0x66, 0x02, 0xa3, 0x60, + 0xa1, 0xbc, 0x92, 0xc0, 0x26, 0xf0, 0x3b, 0x46, 0xcd, 0x76, 0xbb, 0xd2, 0xfb, 0x00, 0x32, 0x19, 0x37, 0x21, 0x3c, + 0x80, 0xc2, 0xeb, 0x29, 0x28, 0x57, 0x88, 0x03, 0xcd, 0x14, 0xa0, 0xc3, 0x0f, 0xe9, 0xc3, 0x13, 0x90, 0x1f, 0xd3, + 0xe1, 0x47, 0xb7, 0x72, 0x1b, 0x6d, 0x73, 0x2c, 0x4f, 0x95, 0x87, 0x6a, 0x1c, 0x21, 0x4a, 0x72, 0x61, 0xb1, 0xa8, + 0xdb, 0x2b, 0x57, 0xb4, 0xbd, 0xf1, 0x5e, 0xdf, 0xb0, 0x4d, 0x3a, 0xfe, 0x18, 0xe6, 0xb8, 0xc2, 0xa8, 0x46, 0x15, + 0x6c, 0xe9, 0x1d, 0xb0, 0xd5, 0x5d, 0x25, 0xb0, 0xc7, 0xa6, 0xb1, 0xb9, 0x00, 0x1d, 0x1a, 0xa2, 0x0c, 0xa4, 0x54, + 0x35, 0x0b, 0x64, 0x72, 0xf5, 0x29, 0xec, 0xb6, 0xa6, 0x31, 0x9b, 0x90, 0xf7, 0xbf, 0xa1, 0x79, 0x15, 0x96, 0x7c, + 0xc2, 0xfe, 0x10, 0xc9, 0x67, 0xf8, 0xd2, 0x47, 0x8d, 0xf8, 0x1e, 0xe0, 0x6a, 0x5f, 0x0a, 0x45, 0xa6, 0x38, 0xb6, + 0xc7, 0x6b, 0xd4, 0x26, 0xf3, 0xf0, 0x50, 0x47, 0x17, 0x36, 0xe4, 0x47, 0x38, 0x61, 0xfb, 0x31, 0xce, 0x93, 0x0b, + 0x8c, 0xe8, 0xbb, 0x18, 0x37, 0x07, 0xe8, 0xca, 0x00, 0xbc, 0x2d, 0xa3, 0x5e, 0x2a, 0x1f, 0xec, 0xf0, 0x16, 0x75, + 0xb3, 0xe3, 0x34, 0xd8, 0x66, 0xc4, 0xa1, 0x1c, 0x0a, 0x70, 0x97, 0xbd, 0xaa, 0x52, 0xe4, 0xf4, 0xd6, 0xf4, 0x8e, + 0xeb, 0x0d, 0xf2, 0x45, 0x13, 0x4d, 0x1d, 0x4c, 0x7a, 0x00, 0x13, 0x6a, 0x39, 0xa0, 0x31, 0x7a, 0xb5, 0x25, 0x5b, + 0x5c, 0x0b, 0x9e, 0xd9, 0x02, 0xd2, 0xbc, 0x22, 0xd5, 0x6e, 0x94, 0x46, 0x53, 0x32, 0x34, 0x69, 0x13, 0x8b, 0xd4, + 0x40, 0x32, 0xab, 0x57, 0x75, 0x22, 0x55, 0x83, 0x2d, 0x70, 0x60, 0xb3, 0x20, 0xc3, 0x37, 0xfb, 0x93, 0x01, 0x63, + 0x4b, 0xaf, 0x7d, 0x4f, 0x76, 0x1f, 0x35, 0xe4, 0x1a, 0x12, 0x19, 0xc7, 0x39, 0xd1, 0x6c, 0xaa, 0x88, 0xf8, 0x64, + 0x1d, 0x15, 0xb0, 0x36, 0x97, 0x6d, 0xe5, 0x83, 0x0a, 0x7a, 0x6c, 0xb0, 0xc9, 0x06, 0xd7, 0x8e, 0x19, 0xd6, 0x17, + 0x9b, 0x8a, 0xd4, 0x65, 0xfa, 0x40, 0xc4, 0x34, 0x83, 0x44, 0xa8, 0x3b, 0x36, 0xbe, 0xae, 0x4a, 0xbb, 0x20, 0xec, + 0xfb, 0xab, 0x74, 0xae, 0x61, 0xe3, 0x15, 0x90, 0xb9, 0xbe, 0xea, 0x00, 0x03, 0xbc, 0x02, 0x71, 0x31, 0xe0, 0xe3, + 0x2d, 0x90, 0x29, 0xf9, 0x77, 0xd4, 0x73, 0xa3, 0x94, 0x47, 0x2d, 0xef, 0x86, 0x19, 0xde, 0x6a, 0x2f, 0xf3, 0x0f, + 0x4b, 0x1f, 0xf2, 0x21, 0x41, 0x85, 0x2c, 0xa4, 0xe6, 0x49, 0xd4, 0xcd, 0x1d, 0x88, 0x6d, 0xdd, 0xe7, 0x22, 0xa3, + 0x58, 0xc4, 0xca, 0x23, 0xc0, 0x5d, 0x04, 0xbc, 0xdb, 0xac, 0x94, 0x88, 0x6f, 0x0e, 0xa5, 0x55, 0xde, 0x12, 0xcd, + 0x55, 0x60, 0xde, 0x45, 0x2b, 0x3b, 0x9c, 0xea, 0x90, 0x81, 0x9d, 0x4a, 0xed, 0x94, 0x44, 0xef, 0xb1, 0xc2, 0x5e, + 0xb3, 0x8d, 0xf5, 0x5b, 0x3b, 0xfa, 0x10, 0x56, 0x0b, 0x56, 0xdb, 0x73, 0x85, 0xe6, 0x66, 0xa2, 0x80, 0x58, 0x60, + 0xcd, 0xde, 0xbe, 0x49, 0x14, 0x42, 0xe1, 0x82, 0xcb, 0x52, 0x2a, 0x91, 0x62, 0xdb, 0x01, 0x83, 0x44, 0x13, 0x26, + 0xaa, 0x8a, 0x73, 0x23, 0xf6, 0x3c, 0x6b, 0xb0, 0xc0, 0xb3, 0x92, 0x0c, 0x8a, 0xd0, 0xba, 0xad, 0x76, 0xa9, 0x40, + 0xef, 0x67, 0x81, 0x95, 0x53, 0xe5, 0x04, 0x0e, 0xa9, 0x46, 0x1c, 0x9f, 0x05, 0x23, 0xb7, 0x4a, 0xf9, 0x16, 0x61, + 0xce, 0xe0, 0xcc, 0x7f, 0x5d, 0x7a, 0x6d, 0x7a, 0x52, 0x0a, 0x0e, 0xd3, 0xf7, 0xe7, 0x30, 0xe9, 0x15, 0x18, 0x9d, + 0xf7, 0x9e, 0xf7, 0x4a, 0x16, 0xf8, 0xeb, 0xb5, 0xbe, 0x14, 0x45, 0xb3, 0x29, 0x4f, 0x3d, 0xb9, 0x94, 0xf9, 0x96, + 0x06, 0xae, 0x53, 0x1c, 0xad, 0xee, 0xd9, 0x9b, 0x15, 0x30, 0x13, 0xcb, 0xf0, 0xef, 0xc1, 0xd6, 0xbe, 0x81, 0xf3, + 0x62, 0x09, 0x44, 0x7e, 0x63, 0x7c, 0x7d, 0xc8, 0xd3, 0xe2, 0x85, 0xcf, 0xcf, 0x08, 0x0b, 0x15, 0xe6, 0x8a, 0x84, + 0xc7, 0xa7, 0x4a, 0xab, 0x2c, 0x41, 0xc3, 0xb4, 0x7c, 0xa6, 0xc7, 0x8b, 0xfa, 0x56, 0x39, 0x7a, 0xeb, 0x2f, 0xb3, + 0xd2, 0x98, 0xcf, 0xcf, 0x93, 0x47, 0x4a, 0xbc, 0x7e, 0xf2, 0x89, 0xaa, 0xf2, 0x67, 0xc3, 0x6d, 0xbd, 0xef, 0xe1, + 0xd7, 0xde, 0x7e, 0x90, 0x65, 0x81, 0xc8, 0xaa, 0x71, 0x6d, 0xe3, 0xa8, 0xf2, 0x34, 0xed, 0x85, 0x58, 0x28, 0xc2, + 0x0f, 0x8a, 0x0e, 0x2c, 0x2f, 0x73, 0xa9, 0xe6, 0x5c, 0x85, 0x8a, 0x46, 0xec, 0x69, 0xfc, 0x7c, 0x08, 0x4b, 0x61, + 0x2a, 0x32, 0x08, 0xe3, 0x0e, 0xed, 0x92, 0x64, 0xec, 0x96, 0x32, 0x6d, 0xeb, 0x77, 0x0b, 0xe5, 0x35, 0x15, 0x13, + 0x30, 0x85, 0x77, 0x20, 0xb9, 0x99, 0x2d, 0xb6, 0xd6, 0x39, 0xa9, 0xa3, 0x02, 0xfb, 0x31, 0xa6, 0xc0, 0x61, 0xa7, + 0x9b, 0xe9, 0x73, 0x81, 0x1b, 0x9a, 0xf2, 0x50, 0x6f, 0x3a, 0xc3, 0x95, 0xd7, 0xf1, 0x43, 0x75, 0xe6, 0x6c, 0x81, + 0x96, 0x6b, 0xe4, 0x2b, 0xbe, 0xaa, 0x96, 0x20, 0xf2, 0x40, 0xf2, 0xb7, 0xaf, 0xbe, 0x7b, 0xab, 0x4b, 0x45, 0xd2, + 0x19, 0x6e, 0xd5, 0xb0, 0x3b, 0x58, 0xe8, 0x6e, 0x75, 0x26, 0x91, 0x04, 0x1a, 0x90, 0x5d, 0x1b, 0xde, 0x0b, 0x1b, + 0xe8, 0x4e, 0x3b, 0x5c, 0x3b, 0xa9, 0x22, 0x68, 0x9d, 0x5d, 0x65, 0x0c, 0x6d, 0xd9, 0x45, 0xc4, 0x2d, 0xbb, 0x0e, + 0x47, 0xd1, 0xcd, 0xb4, 0x10, 0xd6, 0xc6, 0xe3, 0x1e, 0x54, 0x6f, 0x33, 0x20, 0x25, 0x22, 0x12, 0x28, 0x17, 0xe2, + 0x6f, 0x5d, 0xa8, 0x59, 0xc6, 0xdd, 0xa6, 0x43, 0xec, 0x26, 0x89, 0xeb, 0x83, 0x66, 0xf0, 0xd6, 0xa5, 0x95, 0xd7, + 0x19, 0x52, 0xf8, 0x48, 0x45, 0x06, 0xce, 0x0d, 0x53, 0x7b, 0xda, 0x65, 0x1d, 0xc6, 0xbc, 0x54, 0xca, 0xaa, 0x08, + 0xb8, 0xd5, 0x00, 0xcf, 0xda, 0xb7, 0x70, 0x4c, 0x13, 0x1b, 0x9a, 0xa7, 0xbe, 0xcf, 0xd1, 0x76, 0x37, 0x5e, 0xb4, + 0xe2, 0xab, 0xd7, 0xd6, 0x71, 0xd9, 0x3c, 0xeb, 0x6e, 0x13, 0x56, 0xb1, 0x9f, 0x22, 0x29, 0x6c, 0x1a, 0x8b, 0xb9, + 0x26, 0x71, 0x4c, 0x02, 0xa3, 0x05, 0xb0, 0x37, 0xd1, 0x2c, 0xbb, 0x58, 0x22, 0xb5, 0x75, 0x58, 0x77, 0x73, 0xc0, + 0xe1, 0xdb, 0xce, 0x57, 0xaa, 0x76, 0x53, 0x83, 0x12, 0x39, 0xe7, 0xc3, 0xfe, 0xc2, 0xed, 0xfe, 0x50, 0xe2, 0x4d, + 0xdd, 0xc6, 0xe2, 0x48, 0x34, 0x16, 0x10, 0x5c, 0x66, 0x8c, 0xda, 0x2c, 0x8b, 0x10, 0x9d, 0x5a, 0x59, 0xff, 0x40, + 0x05, 0x48, 0x25, 0xd4, 0x6a, 0x71, 0x33, 0x81, 0x05, 0xc7, 0xa4, 0xd4, 0xc6, 0xe1, 0xc7, 0x3f, 0x89, 0xa7, 0x54, + 0xb4, 0x69, 0xd4, 0x13, 0xcd, 0x05, 0xfb, 0x72, 0x88, 0x46, 0x20, 0x77, 0x1b, 0xd6, 0x38, 0xf5, 0xa2, 0xb3, 0xb9, + 0x51, 0xe8, 0xb0, 0x32, 0x55, 0x60, 0xfc, 0xad, 0xc2, 0x6c, 0x20, 0xe7, 0x2a, 0x89, 0x95, 0xdb, 0x19, 0x78, 0x61, + 0x84, 0x0e, 0x62, 0x00, 0xbd, 0x9d, 0xfc, 0x54, 0x7f, 0x5a, 0x5d, 0x94, 0x71, 0x22, 0x4c, 0x4e, 0xdf, 0xdb, 0xc1, + 0x83, 0xda, 0x6c, 0xe7, 0x52, 0xbc, 0xe6, 0x39, 0x81, 0xf6, 0x95, 0x9f, 0xfd, 0x5e, 0xbf, 0x3f, 0x71, 0x13, 0x5a, + 0x56, 0x20, 0x75, 0x8a, 0x7f, 0xd5, 0x9d, 0x51, 0xee, 0x76, 0xce, 0xb3, 0x09, 0xcb, 0x63, 0x92, 0xec, 0x1b, 0x7f, + 0x5b, 0xb8, 0xe4, 0x68, 0xc9, 0x9f, 0x38, 0x0f, 0x8c, 0x62, 0x31, 0x4d, 0x16, 0xa6, 0x7e, 0x4d, 0xd2, 0xde, 0xc4, + 0x75, 0x6c, 0xc4, 0xf1, 0x9f, 0xe3, 0x10, 0x3d, 0x49, 0x84, 0xd4, 0x7a, 0x4b, 0x51, 0x3d, 0xaa, 0x7b, 0x27, 0xea, + 0x42, 0x36, 0x0f, 0x39, 0x5a, 0x93, 0x31, 0x9d, 0xd4, 0x5d, 0xaa, 0x91, 0x68, 0x04, 0x4b, 0xb7, 0x76, 0x3b, 0x39, + 0x3c, 0xc4, 0x4b, 0xfb, 0x28, 0x12, 0x15, 0xbd, 0x0b, 0x4d, 0x71, 0x68, 0xa3, 0x58, 0x5f, 0x59, 0x89, 0x05, 0xd6, + 0x5e, 0x2a, 0xad, 0xe6, 0x82, 0xae, 0xbc, 0x1c, 0x15, 0x3a, 0x27, 0x00, 0x5b, 0xcb, 0x39, 0x91, 0x01, 0x74, 0x62, + 0xb1, 0x70, 0x1d, 0x72, 0x03, 0xca, 0x10, 0xa1, 0x72, 0x4b, 0x8f, 0x53, 0x69, 0xd5, 0x28, 0x96, 0x80, 0xc4, 0x70, + 0xc6, 0x7c, 0xeb, 0x63, 0x36, 0x6e, 0x64, 0x0c, 0xae, 0x5a, 0x76, 0x6d, 0x19, 0x6b, 0x6b, 0x85, 0xb4, 0x0e, 0x98, + 0x5a, 0x65, 0x3f, 0x35, 0xbe, 0xf3, 0xe7, 0xbd, 0x23, 0x4d, 0x6f, 0x71, 0x24, 0x11, 0x06, 0x6d, 0xaf, 0x18, 0x0b, + 0x53, 0x84, 0xdb, 0xec, 0xf6, 0x8a, 0xd0, 0xdd, 0x5f, 0x0a, 0x7c, 0x5b, 0xb8, 0x31, 0x15, 0x37, 0x8e, 0x1e, 0x5f, + 0x14, 0x4c, 0x04, 0xe3, 0xd0, 0x54, 0x95, 0xf0, 0x6e, 0xba, 0x0a, 0x0a, 0x72, 0x2a, 0x2a, 0x1c, 0x78, 0xb0, 0x5e, + 0x66, 0xf4, 0x28, 0x12, 0x8a, 0x2d, 0xae, 0x6a, 0x4d, 0x14, 0x77, 0x19, 0x17, 0xa4, 0x2f, 0x87, 0xf9, 0xb7, 0xaa, + 0x1b, 0xba, 0x66, 0x55, 0xba, 0x45, 0xe2, 0x6b, 0x53, 0x8d, 0x46, 0x44, 0xe5, 0x7b, 0xe9, 0x03, 0xf3, 0x58, 0x4b, + 0x77, 0x3e, 0xed, 0x13, 0xae, 0x0c, 0x1c, 0x18, 0xfa, 0x48, 0xf7, 0x57, 0xeb, 0xea, 0x24, 0x1f, 0x57, 0x9f, 0x7c, + 0x0d, 0xcf, 0x1f, 0x8c, 0x9d, 0x76, 0x70, 0xcb, 0x21, 0x7d, 0xcc, 0xf9, 0x35, 0x33, 0xbd, 0x45, 0xab, 0xbd, 0x51, + 0xa3, 0x2e, 0xb0, 0x99, 0x4c, 0xd3, 0x63, 0xfe, 0xe9, 0xad, 0xe8, 0xc1, 0x05, 0x27, 0x89, 0x2f, 0x20, 0xe1, 0x86, + 0xed, 0xd5, 0xc7, 0x47, 0xaa, 0xeb, 0xd6, 0x09, 0x25, 0x76, 0x23, 0x95, 0xed, 0xa0, 0x42, 0xc8, 0x0e, 0xf7, 0xc4, + 0xd5, 0x1b, 0xec, 0x73, 0x88, 0xd3, 0xd1, 0x00, 0x29, 0x93, 0x26, 0xf6, 0x25, 0x8c, 0xf7, 0xc5, 0x1c, 0x36, 0x2b, + 0x48, 0xbc, 0xea, 0xc2, 0x29, 0x94, 0x58, 0xd9, 0xf3, 0xea, 0x78, 0x1d, 0xdd, 0xe4, 0x23, 0x9a, 0x32, 0xd0, 0xdc, + 0xbd, 0xe5, 0x2e, 0x17, 0x18, 0xdd, 0x6b, 0xf3, 0xf1, 0xdd, 0xce, 0x68, 0x4d, 0x12, 0xf9, 0xc6, 0xb7, 0xf9, 0x70, + 0xf3, 0xf8, 0x85, 0x86, 0xe2, 0x00, 0xd7, 0x71, 0xb8, 0xfd, 0xe1, 0xb2, 0x2a, 0xf7, 0xaa, 0x1f, 0xd4, 0xc0, 0x91, + 0x52, 0x4f, 0x0d, 0x67, 0x61, 0x4f, 0x30, 0xe1, 0xd4, 0x81, 0xb3, 0xe6, 0x83, 0x90, 0x73, 0xf9, 0xd7, 0x2e, 0x94, + 0x73, 0x37, 0x6e, 0x16, 0x9e, 0x06, 0x36, 0x76, 0x86, 0x3a, 0x5c, 0xea, 0xce, 0x6c, 0x49, 0x3c, 0xc3, 0x8f, 0xbb, + 0x9a, 0x08, 0x4b, 0xed, 0x81, 0xaf, 0x57, 0xbc, 0x9c, 0xfb, 0xdb, 0xe1, 0x8d, 0xe2, 0x82, 0x30, 0x33, 0xbe, 0x88, + 0x4a, 0x93, 0x2c, 0x69, 0xf8, 0x36, 0xb2, 0x19, 0x75, 0xed, 0xbb, 0x68, 0x45, 0x50, 0x32, 0x22, 0x54, 0x71, 0x68, + 0xc6, 0x50, 0x06, 0xe8, 0x58, 0x45, 0x69, 0xcf, 0xd7, 0x06, 0xb3, 0x4e, 0x36, 0x73, 0x04, 0x74, 0x44, 0xdf, 0x2f, + 0x5f, 0xd4, 0xb3, 0x7f, 0xdd, 0x1f, 0x1e, 0xbe, 0xc8, 0x55, 0x7d, 0xd4, 0x00, 0xfc, 0x8e, 0x54, 0xf5, 0xe8, 0x8d, + 0xe5, 0x57, 0x5a, 0x82, 0xad, 0x66, 0x07, 0x46, 0x9d, 0xa4, 0x8d, 0xd4, 0x86, 0xcc, 0x32, 0x67, 0x52, 0x28, 0x04, + 0x2d, 0x3d, 0x9e, 0x63, 0x31, 0x05, 0x50, 0xd2, 0xe5, 0x8a, 0x88, 0x0b, 0x06, 0x61, 0x15, 0x87, 0x31, 0x2c, 0xa4, + 0x69, 0x3d, 0xdb, 0xce, 0xa2, 0x51, 0x83, 0xd0, 0x35, 0x86, 0x44, 0x85, 0x99, 0xf5, 0x8c, 0x83, 0x5c, 0x6a, 0xbb, + 0x20, 0x4f, 0x7e, 0x73, 0x15, 0x03, 0xd0, 0x63, 0x22, 0x79, 0x5a, 0xd5, 0x44, 0x96, 0x90, 0xcf, 0xa4, 0x61, 0xd3, + 0xfb, 0xc3, 0x37, 0x31, 0x3d, 0xfa, 0xd8, 0xd5, 0xd6, 0x1f, 0xa2, 0xe4, 0xb9, 0x17, 0x29, 0x5f, 0xeb, 0xb4, 0x65, + 0x77, 0xa2, 0x4d, 0xd0, 0x44, 0xdb, 0x82, 0xb0, 0x05, 0x3a, 0xd0, 0x67, 0x3c, 0x1a, 0x2e, 0x1b, 0x51, 0x16, 0x8b, + 0x94, 0x28, 0x87, 0xfc, 0xe6, 0xec, 0x11, 0xe2, 0xb4, 0x16, 0x46, 0x03, 0xcb, 0xbd, 0x60, 0x18, 0x45, 0x17, 0xec, + 0x81, 0x4f, 0x2b, 0x52, 0x5c, 0x7d, 0xbb, 0x58, 0xf3, 0xba, 0x32, 0xd1, 0x36, 0x98, 0xf0, 0x0e, 0x1a, 0x1e, 0x61, + 0xaf, 0x71, 0x4e, 0x83, 0xae, 0xeb, 0xc9, 0xd3, 0x8a, 0x8c, 0x4d, 0x65, 0xa5, 0x1e, 0x01, 0xb7, 0xd8, 0xdb, 0x7e, + 0xd4, 0x36, 0x07, 0x7b, 0x36, 0xb6, 0xea, 0xc6, 0xb6, 0xc7, 0x10, 0xdc, 0x3a, 0x41, 0x3e, 0xdd, 0x29, 0x7d, 0x9e, + 0xe7, 0x4b, 0x6b, 0x13, 0xe8, 0xf0, 0xba, 0x35, 0xed, 0x71, 0x84, 0x11, 0x11, 0xb7, 0x99, 0x2e, 0x58, 0x58, 0x4a, + 0x6f, 0xa9, 0xae, 0x88, 0xe1, 0xd9, 0x0e, 0x59, 0x0d, 0x40, 0x2f, 0xb0, 0x3f, 0x94, 0xe7, 0x25, 0x5c, 0xe8, 0xf9, + 0xf0, 0xd1, 0x45, 0x95, 0x97, 0xa5, 0x1d, 0x66, 0x7b, 0xd6, 0xdd, 0xa0, 0xc2, 0xf5, 0xa9, 0x5a, 0x9d, 0x50, 0x20, + 0x96, 0x9e, 0xa3, 0xbf, 0xef, 0x53, 0x47, 0x3c, 0xcf, 0x08, 0x61, 0x27, 0x36, 0x9b, 0x07, 0x20, 0xf6, 0x41, 0xc7, + 0x04, 0x01, 0x42, 0xd0, 0x10, 0xab, 0x3d, 0xa0, 0x1e, 0xbf, 0x33, 0xf4, 0x7d, 0x44, 0x7a, 0x13, 0xa0, 0x32, 0x05, + 0xc5, 0x89, 0xda, 0xa7, 0x24, 0x22, 0x27, 0x3f, 0xc9, 0x2e, 0x9b, 0xb1, 0xa8, 0x93, 0xc0, 0xf9, 0x88, 0x53, 0xb0, + 0x14, 0x0a, 0xe7, 0xc5, 0x33, 0x01, 0x7c, 0x3a, 0x67, 0x8b, 0x69, 0xe1, 0x8b, 0x1c, 0x94, 0xcd, 0xa4, 0xc7, 0xf5, + 0x38, 0xb7, 0x7d, 0x4c, 0x38, 0x2a, 0xca, 0xd8, 0xd8, 0x5b, 0x75, 0x66, 0x8c, 0xf0, 0xd5, 0x44, 0xa8, 0xf7, 0x63, + 0x9c, 0xb7, 0xf7, 0x3d, 0x3e, 0xe2, 0x52, 0x6c, 0x2a, 0x84, 0xc2, 0x82, 0x9a, 0xa7, 0xf4, 0x47, 0x59, 0xe7, 0xd4, + 0xc8, 0x82, 0xf2, 0xb8, 0x82, 0x91, 0xa2, 0x4c, 0xfb, 0xec, 0xc9, 0x9e, 0x32, 0x48, 0x6c, 0xe7, 0x65, 0xa2, 0x2b, + 0x05, 0x0c, 0xa2, 0x54, 0x0a, 0x76, 0xb5, 0x2d, 0x14, 0xc9, 0x20, 0x1c, 0x43, 0xbb, 0x11, 0x47, 0x55, 0xe6, 0x90, + 0x84, 0x7c, 0xcd, 0xd7, 0x38, 0xb3, 0xdd, 0x1c, 0x52, 0x18, 0x6c, 0x51, 0x4d, 0x46, 0x81, 0xd0, 0x6e, 0x41, 0x40, + 0xe8, 0xd2, 0x85, 0xdf, 0xe0, 0x97, 0x47, 0xa9, 0x6c, 0x26, 0x38, 0x4f, 0x17, 0x6e, 0xe1, 0x97, 0x1e, 0xb5, 0x62, + 0xc7, 0x5b, 0x6b, 0xe3, 0x12, 0xe5, 0xa2, 0x65, 0xfe, 0x23, 0xf6, 0xb8, 0x80, 0x03, 0x5b, 0x60, 0x6d, 0xe8, 0x0e, + 0x95, 0x61, 0x34, 0x70, 0xe2, 0x01, 0x24, 0xb5, 0xbb, 0x61, 0x49, 0x5b, 0xd4, 0x7f, 0x32, 0xd7, 0xea, 0x1a, 0x34, + 0x81, 0x59, 0xab, 0xc5, 0x36, 0x4d, 0x85, 0x1c, 0x32, 0xaa, 0x1a, 0xb0, 0x52, 0x6d, 0x87, 0x34, 0x59, 0x22, 0x87, + 0x24, 0x71, 0x27, 0x73, 0x06, 0x55, 0x56, 0x7a, 0xd1, 0xff, 0xa8, 0x44, 0xe4, 0x43, 0x5e, 0xff, 0xa4, 0x8a, 0x67, + 0x99, 0xd4, 0x8f, 0xc2, 0xa1, 0x4a, 0x63, 0x93, 0x0d, 0x6d, 0x00, 0xa3, 0x0f, 0x73, 0xa8, 0x2c, 0x74, 0x6c, 0x95, + 0xfb, 0x6e, 0x5a, 0xc9, 0xb1, 0x21, 0x9f, 0xcc, 0x18, 0x30, 0xdf, 0x7e, 0x0b, 0x62, 0x8f, 0x5b, 0xcc, 0x38, 0xdc, + 0x4b, 0x7e, 0xbe, 0x4c, 0x44, 0xc1, 0x1f, 0x4e, 0xc3, 0x0e, 0x7c, 0xd3, 0x21, 0xa6, 0xcd, 0x15, 0x33, 0x64, 0x06, + 0xa5, 0x6d, 0x09, 0x31, 0x2d, 0x78, 0x4a, 0xa4, 0xfb, 0xf7, 0xfe, 0xc4, 0xde, 0xd3, 0x7c, 0x21, 0x3f, 0x59, 0x9d, + 0x83, 0xbe, 0x55, 0x97, 0x3e, 0x86, 0x17, 0xc6, 0x7d, 0x00, 0x50, 0xb9, 0xbd, 0x6d, 0xc5, 0x71, 0x7b, 0x5f, 0x85, + 0xf8, 0x83, 0x19, 0x66, 0x1c, 0xaf, 0x52, 0x64, 0x63, 0x81, 0xe9, 0x94, 0x59, 0xa9, 0x5b, 0x35, 0x2b, 0xfb, 0xc7, + 0xf4, 0x5f, 0x1a, 0x0d, 0xb0, 0x2f, 0x17, 0xf9, 0xf9, 0x76, 0x99, 0x28, 0xb0, 0xc2, 0x22, 0xd1, 0x7b, 0x17, 0x40, + 0xba, 0x83, 0x48, 0xc6, 0x9f, 0xf7, 0x70, 0xd1, 0xf0, 0xf0, 0x17, 0x39, 0x68, 0xd9, 0x79, 0xe5, 0x44, 0x69, 0x3e, + 0xaf, 0xd8, 0x09, 0x44, 0x9e, 0x3a, 0x45, 0x98, 0xfe, 0x7d, 0x72, 0xe5, 0xb5, 0x47, 0x4e, 0xce, 0x5e, 0x40, 0xbf, + 0x26, 0x6e, 0x9f, 0x9f, 0x65, 0x1d, 0xfe, 0xb1, 0x44, 0x85, 0xb0, 0x52, 0xe8, 0x41, 0x55, 0x08, 0x5f, 0x70, 0xe2, + 0x00, 0x4e, 0x3d, 0x7c, 0x78, 0xc6, 0xdf, 0x0e, 0x43, 0xfb, 0xf8, 0x99, 0xb3, 0x69, 0x89, 0xf1, 0x12, 0x83, 0x45, + 0xb5, 0xd4, 0x78, 0x7e, 0xff, 0x34, 0xeb, 0xe9, 0x9e, 0xb1, 0x4f, 0x8b, 0x9e, 0xac, 0x6a, 0x9a, 0x37, 0x24, 0xce, + 0x7f, 0xd8, 0xfc, 0x5a, 0x1b, 0x1f, 0xec, 0xdc, 0x56, 0x25, 0x47, 0xd6, 0x05, 0xae, 0xcb, 0xaa, 0x55, 0xd5, 0x37, + 0x03, 0xce, 0x49, 0x8f, 0xc5, 0x4b, 0x9d, 0xdd, 0x2f, 0xe8, 0x8f, 0x66, 0x3a, 0x5a, 0x1f, 0x7d, 0x70, 0x2d, 0x42, + 0xd5, 0xa4, 0x33, 0xba, 0x37, 0xbf, 0xc3, 0x39, 0xe5, 0x33, 0xd7, 0xf1, 0xb9, 0x5b, 0x0b, 0xe5, 0x09, 0x8f, 0x16, + 0x1a, 0x85, 0xa1, 0x3b, 0x77, 0x8f, 0xe0, 0x5a, 0x24, 0xcd, 0xc8, 0xde, 0xc2, 0x39, 0xd3, 0xf8, 0x4c, 0x7f, 0x36, + 0x0b, 0xf5, 0xa7, 0x3e, 0x14, 0x14, 0x11, 0xf3, 0x2b, 0xa6, 0x62, 0x28, 0x25, 0xc1, 0x43, 0x44, 0x04, 0x5a, 0x47, + 0x51, 0x3e, 0x55, 0x57, 0x57, 0xca, 0xea, 0x97, 0xb3, 0x2c, 0x28, 0x92, 0xd9, 0x14, 0x59, 0xb9, 0xe2, 0x8f, 0x4e, + 0x72, 0x96, 0xeb, 0x42, 0x40, 0xce, 0x3e, 0xc0, 0x89, 0xfd, 0x9b, 0x41, 0xe0, 0xb6, 0xb6, 0xd6, 0xfe, 0x48, 0x50, + 0x63, 0x14, 0x7c, 0x8b, 0x00, 0x8c, 0xc4, 0xd0, 0x46, 0xf9, 0xe4, 0x56, 0xba, 0x50, 0x51, 0xbd, 0x3f, 0x71, 0xf7, + 0x3f, 0xbf, 0xbb, 0xc9, 0x0d, 0x1b, 0x77, 0x69, 0x0e, 0x4d, 0xe6, 0xc9, 0x39, 0xda, 0xc8, 0xee, 0xba, 0x5f, 0x06, + 0xf9, 0x2d, 0x5f, 0x92, 0xf4, 0x74, 0x44, 0x60, 0x4b, 0xcb, 0x8f, 0x48, 0x45, 0x49, 0x22, 0x90, 0x63, 0xad, 0x00, + 0x50, 0x33, 0x21, 0x95, 0x8a, 0x1c, 0x45, 0x9e, 0x8c, 0x7a, 0x33, 0xa7, 0x24, 0x2d, 0x69, 0x37, 0xa8, 0x31, 0x2c, + 0x87, 0xaf, 0xb9, 0x36, 0x4b, 0x7d, 0xad, 0xa4, 0xec, 0xc4, 0xf6, 0x82, 0x05, 0x94, 0x38, 0xa6, 0xe0, 0x82, 0xd5, + 0x58, 0x9a, 0x36, 0xaf, 0x27, 0x18, 0xd0, 0x32, 0x97, 0x76, 0xd9, 0x12, 0xf2, 0x95, 0xfa, 0x7d, 0x58, 0x8c, 0x90, + 0x7c, 0x63, 0xa1, 0x58, 0xda, 0xaa, 0x55, 0xb9, 0xf3, 0x1c, 0x3f, 0xd0, 0xa4, 0x48, 0x1d, 0xed, 0x61, 0xfa, 0x16, + 0x8e, 0xc4, 0xe0, 0x66, 0x4e, 0xb9, 0xa4, 0x4c, 0xe3, 0xd2, 0x9f, 0xa4, 0xff, 0xaa, 0x2f, 0x43, 0x3e, 0xc1, 0x51, + 0xac, 0xfe, 0x83, 0x6a, 0xcc, 0x40, 0x40, 0xea, 0x4b, 0x10, 0x15, 0xc3, 0x68, 0xe6, 0x10, 0xdd, 0xa0, 0xf5, 0x99, + 0x3a, 0x91, 0xce, 0x5e, 0x6c, 0x70, 0xd2, 0x97, 0x73, 0xa2, 0x79, 0xe1, 0x3b, 0x8c, 0xf7, 0x81, 0x01, 0x0c, 0x0a, + 0xf3, 0x60, 0x0c, 0xec, 0xb2, 0x26, 0x6d, 0x29, 0xb8, 0x41, 0x0d, 0x34, 0x81, 0x07, 0x78, 0x3a, 0x89, 0x90, 0x8b, + 0x7c, 0x66, 0x71, 0x27, 0xbb, 0x98, 0x52, 0x6b, 0x7e, 0x2c, 0x84, 0x85, 0xfe, 0xdd, 0x62, 0x2b, 0xcb, 0x1d, 0x3c, + 0x13, 0x11, 0x54, 0x05, 0x0a, 0xbc, 0x72, 0x79, 0x43, 0xa7, 0x25, 0x70, 0xf0, 0x3e, 0xb5, 0xe2, 0x06, 0x07, 0xbe, + 0x0d, 0x2a, 0x5d, 0xb0, 0x1f, 0xb4, 0xeb, 0xdf, 0x7b, 0xae, 0xc2, 0x22, 0xea, 0x21, 0xde, 0x6a, 0xae, 0x57, 0x77, + 0xf7, 0xbe, 0xd7, 0xf1, 0x59, 0x53, 0xcb, 0x1e, 0x7f, 0xc6, 0x10, 0x0a, 0x4e, 0xd0, 0x2a, 0x15, 0x12, 0x30, 0xf0, + 0xc7, 0x2d, 0x6c, 0xfc, 0x92, 0xa5, 0xdb, 0x11, 0x4b, 0x7f, 0xfd, 0xba, 0xa2, 0xc9, 0xae, 0xba, 0xa9, 0x27, 0xa0, + 0x88, 0xbd, 0xa3, 0x55, 0x76, 0xb8, 0x4a, 0xcd, 0x7b, 0xc5, 0xbb, 0x1e, 0xf8, 0x94, 0x0e, 0xcc, 0x28, 0xb0, 0x17, + 0xc4, 0x1c, 0x18, 0xeb, 0xc7, 0x46, 0x79, 0xd7, 0x4f, 0xbe, 0x4b, 0xd1, 0x46, 0xad, 0xaf, 0xfc, 0x41, 0x10, 0xdf, + 0x67, 0x46, 0xac, 0xbd, 0x04, 0x66, 0x30, 0xba, 0xd3, 0x36, 0x1d, 0x76, 0xe5, 0x3e, 0x9e, 0x1f, 0xb2, 0xde, 0x41, + 0x40, 0xa5, 0xe8, 0x47, 0x81, 0x4b, 0x26, 0x30, 0x98, 0x83, 0x23, 0xdb, 0x8b, 0x3d, 0xf9, 0x44, 0xcc, 0x85, 0x28, + 0x45, 0x33, 0x46, 0x01, 0xc1, 0xc8, 0x61, 0x85, 0xed, 0x3f, 0xc2, 0x76, 0x01, 0x70, 0x8b, 0x87, 0x0c, 0x7b, 0x5e, + 0xe3, 0x4d, 0xbc, 0x1d, 0x35, 0xcc, 0x99, 0xd4, 0x5b, 0xd0, 0x4e, 0x8f, 0x21, 0xf9, 0x7d, 0x1a, 0x24, 0xa3, 0x22, + 0xf7, 0x28, 0x12, 0x84, 0xd7, 0x45, 0x4e, 0x5a, 0x80, 0x75, 0x77, 0xe8, 0xa6, 0x5f, 0x01, 0x62, 0xfa, 0x5e, 0x02, + 0xfe, 0x44, 0x6e, 0x22, 0x16, 0xbc, 0xdd, 0x34, 0xc4, 0x1d, 0x4c, 0x80, 0xa1, 0x11, 0x9e, 0x41, 0xd0, 0x08, 0x92, + 0x11, 0xdd, 0x6d, 0xee, 0xa7, 0xcc, 0x7f, 0x56, 0xe4, 0xc7, 0xb2, 0x31, 0xae, 0x79, 0xd3, 0xde, 0xc5, 0x6f, 0x11, + 0xa9, 0x00, 0x62, 0x67, 0xca, 0x2c, 0x54, 0x89, 0xc9, 0xd7, 0x85, 0x8d, 0x7d, 0x6e, 0x94, 0x25, 0xdb, 0xe7, 0xf5, + 0xd7, 0x66, 0xd8, 0x92, 0x66, 0xb6, 0xb7, 0x39, 0xe3, 0xb3, 0x8a, 0x89, 0x85, 0x17, 0x05, 0xce, 0xfd, 0xed, 0xd7, + 0xfd, 0xf9, 0x70, 0x95, 0x2d, 0xdb, 0x29, 0x53, 0x8f, 0x23, 0x25, 0xcb, 0x5a, 0x7f, 0xbb, 0x32, 0x93, 0xb7, 0x6e, + 0xd1, 0x13, 0xec, 0xa8, 0x35, 0xf3, 0x25, 0x47, 0xda, 0xba, 0x87, 0x93, 0xec, 0xba, 0xc0, 0x2e, 0xef, 0x04, 0xd0, + 0xb4, 0x74, 0x42, 0xf1, 0x73, 0x25, 0xb4, 0xac, 0x1d, 0xe0, 0x24, 0x7e, 0xfa, 0x62, 0xe2, 0xa5, 0x98, 0xad, 0xc1, + 0x36, 0xbf, 0x62, 0x5e, 0xc4, 0x60, 0xcf, 0x8d, 0x0a, 0xe1, 0x8c, 0xf3, 0xbe, 0x05, 0xb3, 0xf4, 0x1b, 0xaf, 0xdc, + 0xe6, 0x73, 0x82, 0xfd, 0x96, 0x16, 0xc1, 0xc0, 0xc4, 0x5d, 0xf5, 0x5a, 0xe3, 0x2c, 0x84, 0xa8, 0x6b, 0xb9, 0x2f, + 0x62, 0xe6, 0x36, 0xa7, 0xe9, 0x5d, 0xad, 0xc9, 0x8c, 0xfd, 0xe2, 0x4a, 0x33, 0xeb, 0xbb, 0xef, 0x20, 0x6b, 0xad, + 0x2a, 0xf4, 0x2b, 0x52, 0xcf, 0x64, 0xfd, 0x27, 0xb0, 0x19, 0x8b, 0x1d, 0x16, 0x4b, 0x2b, 0x75, 0xe7, 0xaa, 0xf4, + 0x03, 0x9e, 0x54, 0x00, 0x72, 0x11, 0xd0, 0x99, 0x6e, 0x3d, 0x77, 0x8b, 0x45, 0x3d, 0xea, 0xc1, 0xad, 0xbf, 0xb7, + 0x1a, 0x06, 0x41, 0xac, 0x63, 0xbf, 0x22, 0x78, 0x3c, 0x5e, 0x89, 0xdf, 0x0b, 0xaf, 0xc8, 0x0f, 0x5b, 0x1e, 0xff, + 0x7c, 0x01, 0x65, 0xfa, 0x49, 0x34, 0xed, 0xfc, 0x6c, 0xc3, 0xc2, 0xa4, 0x7c, 0x3a, 0x8f, 0xfc, 0x1e, 0xcd, 0xcd, + 0x15, 0xb4, 0xdc, 0xf3, 0x03, 0x97, 0xf2, 0x7f, 0x16, 0x29, 0x4b, 0x6a, 0x85, 0x66, 0xd9, 0x36, 0xc1, 0xd1, 0xdd, + 0x9e, 0xe2, 0xc1, 0x73, 0x9c, 0x50, 0x68, 0x6f, 0x4a, 0xbd, 0x55, 0x85, 0x9a, 0xa8, 0xb5, 0x85, 0x02, 0x65, 0xfd, + 0x88, 0xf6, 0x51, 0x71, 0xc4, 0x0d, 0x23, 0x3d, 0xea, 0x6f, 0x6a, 0x6d, 0x91, 0x5d, 0x47, 0xed, 0x97, 0xb5, 0xfb, + 0x7d, 0x12, 0x24, 0xff, 0x6d, 0x05, 0xc8, 0xac, 0x0d, 0xd5, 0x9b, 0x80, 0x69, 0x44, 0x31, 0x47, 0xc1, 0x8f, 0xd1, + 0x92, 0x42, 0xa3, 0x0c, 0x2e, 0x1c, 0x11, 0x66, 0x2d, 0xb5, 0xe4, 0x19, 0x43, 0xf0, 0xbc, 0xd1, 0xd0, 0x91, 0xf0, + 0xb5, 0xe9, 0x5d, 0x76, 0x66, 0x36, 0x4c, 0xce, 0x3d, 0xa2, 0x21, 0x9b, 0x7a, 0xaa, 0x28, 0x01, 0xf7, 0xcd, 0x72, + 0x7c, 0x75, 0x50, 0xb2, 0x26, 0xb5, 0x57, 0xc1, 0x6e, 0x1f, 0x72, 0x73, 0x19, 0xbd, 0x35, 0x54, 0x6b, 0xf8, 0xde, + 0x48, 0xd6, 0xb0, 0xca, 0x35, 0x90, 0xd8, 0xce, 0x8f, 0x30, 0x49, 0x45, 0x77, 0xf9, 0x16, 0x34, 0xde, 0x51, 0x95, + 0xcb, 0x4e, 0xeb, 0xda, 0xbb, 0x03, 0x37, 0x61, 0xd8, 0xfa, 0xd4, 0x8d, 0x8e, 0xf4, 0xfd, 0x80, 0x0d, 0x9a, 0x95, + 0x4a, 0x03, 0x4e, 0xf9, 0x05, 0x15, 0xad, 0xf3, 0xbb, 0x25, 0x5f, 0xec, 0x19, 0xee, 0x83, 0x11, 0x32, 0x26, 0x8e, + 0xc0, 0x8e, 0x1a, 0xe0, 0x29, 0x61, 0xc6, 0xe1, 0xc7, 0x9e, 0xdb, 0xd7, 0xc6, 0xa0, 0x7f, 0xa5, 0xd9, 0x50, 0x40, + 0x8d, 0xf6, 0xb8, 0x92, 0x54, 0x3a, 0x86, 0x19, 0x93, 0xc2, 0x87, 0x54, 0x28, 0x73, 0xfc, 0xbb, 0x73, 0x4d, 0xb1, + 0x66, 0x38, 0x57, 0x23, 0xd3, 0x86, 0xf3, 0xbf, 0x1a, 0xf3, 0x5b, 0x8e, 0xef, 0x20, 0xaa, 0x9e, 0x8e, 0x41, 0x87, + 0x50, 0x4a, 0x50, 0x76, 0x65, 0x42, 0x55, 0x03, 0xfd, 0xa2, 0x19, 0x6d, 0x9a, 0xd6, 0x8f, 0x91, 0xf3, 0xbf, 0x6e, + 0xbf, 0xb6, 0x93, 0x8b, 0xd6, 0x0a, 0xeb, 0xe2, 0x07, 0x3f, 0x30, 0xe4, 0xb5, 0x7b, 0x7e, 0x76, 0xab, 0x5c, 0xd9, + 0x53, 0x5b, 0x3c, 0x75, 0xc1, 0x97, 0xe9, 0xfa, 0x18, 0xbc, 0x2c, 0x20, 0x35, 0x8d, 0xaa, 0x75, 0xec, 0x13, 0x16, + 0x5a, 0xec, 0x3a, 0x6f, 0xd7, 0x2f, 0x4f, 0xaa, 0x89, 0x57, 0x2c, 0x03, 0x3a, 0x3f, 0xb3, 0x29, 0xf6, 0x91, 0x16, + 0x97, 0x0d, 0xff, 0x32, 0xa0, 0xe7, 0xd9, 0x40, 0x9e, 0xf9, 0x59, 0x7f, 0xae, 0x3f, 0xbe, 0xe5, 0x21, 0xa1, 0x14, + 0xb7, 0x35, 0x4e, 0xef, 0x1a, 0xdb, 0xcc, 0x3b, 0xb3, 0xb4, 0x8f, 0x9d, 0x66, 0x3e, 0xa2, 0x22, 0x5d, 0x70, 0x12, + 0xb6, 0xa7, 0x43, 0xba, 0x92, 0x6d, 0x16, 0x9a, 0x39, 0xf5, 0xa5, 0x71, 0x59, 0x9c, 0xd7, 0x69, 0x73, 0x31, 0xf7, + 0x82, 0xae, 0x03, 0x38, 0xd7, 0x29, 0x47, 0x70, 0x55, 0x11, 0x28, 0x9a, 0x9a, 0xb9, 0xa2, 0x78, 0x68, 0x2d, 0x76, + 0x73, 0x6b, 0xf7, 0x53, 0x8c, 0xb8, 0xd4, 0xa5, 0x2a, 0x51, 0x92, 0x6c, 0x59, 0x29, 0x90, 0xc9, 0x82, 0xac, 0x39, + 0x49, 0x15, 0x0e, 0xfa, 0x37, 0x87, 0x74, 0xf6, 0x62, 0xd8, 0x87, 0x70, 0xe6, 0x6b, 0xa9, 0x0a, 0xa3, 0x59, 0xac, + 0xe6, 0x39, 0x88, 0x54, 0x6d, 0x1f, 0x28, 0xa7, 0xca, 0x7d, 0x5b, 0x40, 0xe6, 0x46, 0xca, 0x4b, 0x51, 0x47, 0x6e, + 0x78, 0x4a, 0xbf, 0x36, 0x3d, 0x10, 0xa3, 0xd5, 0xb0, 0xa3, 0x8d, 0x66, 0xb3, 0x59, 0x14, 0x53, 0x8f, 0x43, 0x9b, + 0xd4, 0x7c, 0x1b, 0x51, 0xaf, 0x50, 0x35, 0xb3, 0x6f, 0x4c, 0x3d, 0x62, 0xc9, 0x9c, 0xe2, 0x35, 0x14, 0x26, 0xc9, + 0x3d, 0x8b, 0x2d, 0xea, 0x16, 0x6d, 0x6e, 0xce, 0x1c, 0x1b, 0x92, 0xb8, 0x8a, 0x4b, 0x99, 0xae, 0x34, 0x1e, 0x05, + 0xc2, 0x61, 0x25, 0xda, 0x4e, 0x30, 0x1b, 0xf3, 0xf4, 0x83, 0x9f, 0x92, 0x9f, 0x7b, 0x04, 0x4c, 0xb3, 0x2f, 0x60, + 0x2b, 0xe9, 0xce, 0x8c, 0xf8, 0x48, 0x41, 0xce, 0xe1, 0x2b, 0x86, 0xe9, 0x7b, 0x9b, 0xc8, 0x72, 0x1f, 0xe7, 0x53, + 0x82, 0x32, 0x39, 0xa9, 0x76, 0x68, 0xbc, 0x81, 0xd8, 0x1b, 0x20, 0x9e, 0x86, 0xb0, 0x04, 0x4f, 0x23, 0x60, 0x90, + 0xf8, 0xbc, 0x5c, 0xc5, 0x43, 0x2d, 0x6e, 0x7c, 0x97, 0x79, 0x08, 0x70, 0xb6, 0x0c, 0x43, 0x6d, 0x62, 0x92, 0xfb, + 0xb3, 0x06, 0x74, 0x27, 0xa2, 0x62, 0x49, 0x66, 0x97, 0x75, 0x15, 0x85, 0xf9, 0x77, 0x5e, 0x2f, 0x53, 0x27, 0x9c, + 0x2d, 0xde, 0xb8, 0x0d, 0x80, 0xe9, 0x42, 0x7b, 0xaa, 0x93, 0x13, 0x93, 0xe2, 0xd7, 0x50, 0x1a, 0xd6, 0x09, 0x0d, + 0x14, 0x89, 0xfa, 0x79, 0xb4, 0x9e, 0x98, 0xa2, 0x38, 0xff, 0x11, 0x91, 0x0c, 0x4c, 0x12, 0xc8, 0x60, 0xb4, 0x7b, + 0xc5, 0x9a, 0x50, 0xac, 0xfd, 0xa4, 0x65, 0xd3, 0x99, 0xfb, 0x36, 0x83, 0x98, 0xbd, 0x1f, 0x04, 0x0f, 0x04, 0xff, + 0xe5, 0x56, 0x27, 0x8c, 0xa0, 0x04, 0xc3, 0xec, 0x30, 0xff, 0x89, 0xac, 0xba, 0x2d, 0xe8, 0xa8, 0x57, 0xe4, 0xaa, + 0x77, 0xe2, 0x52, 0x67, 0x12, 0x55, 0x4f, 0x7e, 0x9e, 0xb8, 0xfb, 0x56, 0x36, 0x46, 0x0b, 0xdc, 0xe7, 0xc8, 0x27, + 0x57, 0x6e, 0x66, 0xdb, 0xc8, 0xa8, 0xa6, 0x28, 0x12, 0x8b, 0x98, 0xca, 0xbc, 0x79, 0x8e, 0xfb, 0xf0, 0xaa, 0xb9, + 0x83, 0xef, 0xb7, 0x39, 0xd8, 0xca, 0xac, 0xdc, 0xe5, 0xf2, 0x6d, 0x7a, 0x68, 0xd0, 0xfd, 0xae, 0x73, 0xa4, 0x4b, + 0xef, 0xa6, 0x72, 0x5b, 0xb7, 0x3b, 0x53, 0xe7, 0x23, 0xf5, 0xd4, 0xf1, 0xf9, 0x99, 0x74, 0x63, 0xb7, 0xfe, 0x53, + 0x35, 0xc1, 0x4f, 0xf9, 0x02, 0xb4, 0x34, 0x55, 0x7c, 0x2c, 0x28, 0xa3, 0x16, 0xdd, 0xc1, 0x57, 0x6e, 0xb9, 0x15, + 0xf4, 0x2b, 0x9f, 0xab, 0xbc, 0x70, 0x8d, 0x5c, 0x3a, 0x7b, 0xe1, 0x04, 0x36, 0xf5, 0xe0, 0x9d, 0xf1, 0x57, 0xc1, + 0x25, 0xe0, 0xda, 0x10, 0x07, 0x23, 0x25, 0x89, 0xf7, 0xd5, 0xc0, 0x1b, 0x11, 0xf1, 0x8f, 0x82, 0xa1, 0x51, 0xfa, + 0x96, 0xfa, 0x18, 0x5b, 0x0c, 0xb3, 0x3e, 0xaa, 0x94, 0xab, 0xb3, 0xb9, 0xe0, 0xd4, 0x39, 0xfa, 0x13, 0x46, 0xdc, + 0xf3, 0x00, 0xe7, 0xac, 0xae, 0x7f, 0x06, 0xe7, 0xb5, 0xfd, 0xcc, 0x38, 0x1f, 0x8a, 0xa6, 0x44, 0xeb, 0xdd, 0x78, + 0xa7, 0x3c, 0x9b, 0x2d, 0xe7, 0x67, 0x15, 0x5e, 0x0d, 0xf7, 0x19, 0x9f, 0x5e, 0xfa, 0x1d, 0x98, 0x3c, 0x0e, 0xba, + 0xf4, 0xbe, 0x72, 0x78, 0xef, 0x4e, 0xc5, 0x4a, 0x15, 0x35, 0xe2, 0xd8, 0xa1, 0x7b, 0x8d, 0xc7, 0xbd, 0x0b, 0xcc, + 0x1a, 0xab, 0x93, 0xc3, 0x43, 0x6b, 0xab, 0x7c, 0x5d, 0x65, 0x84, 0x9d, 0xc4, 0x37, 0xcb, 0xc6, 0x94, 0x48, 0xb0, + 0xbf, 0x0d, 0x94, 0x63, 0x38, 0x10, 0xe1, 0x61, 0xc2, 0x9b, 0xac, 0xc2, 0xbc, 0x96, 0x8a, 0x32, 0xab, 0x1d, 0xfe, + 0x5a, 0x71, 0x8d, 0x68, 0x07, 0x4b, 0xae, 0xa4, 0x0d, 0x66, 0xd1, 0xa5, 0xa4, 0x61, 0x75, 0xc3, 0xa1, 0x5e, 0xdf, + 0x15, 0x5c, 0xd5, 0xb6, 0x66, 0x91, 0xfc, 0x45, 0xd9, 0x8e, 0x95, 0x08, 0x9b, 0x29, 0xf3, 0x3c, 0xfb, 0x3f, 0xa2, + 0x4a, 0x87, 0xdc, 0x02, 0xa6, 0xf6, 0x43, 0xba, 0x42, 0x52, 0x8c, 0x0d, 0xda, 0x4f, 0x4a, 0x57, 0x32, 0xef, 0xf8, + 0x8d, 0xc5, 0x75, 0xcb, 0x50, 0xe4, 0x62, 0x33, 0x56, 0x17, 0x1b, 0xc0, 0xc2, 0x2a, 0x07, 0xcc, 0x46, 0x14, 0xcd, + 0xa2, 0x6c, 0xca, 0xa3, 0xed, 0x16, 0xaf, 0x5b, 0xb4, 0xfa, 0xfb, 0x33, 0xd1, 0x33, 0x5b, 0x27, 0x55, 0x9d, 0x65, + 0xbd, 0x7f, 0x65, 0xc5, 0x5c, 0xe1, 0xe1, 0x47, 0x7b, 0x6e, 0xe7, 0x88, 0xce, 0xfb, 0xbb, 0x9b, 0xfe, 0x85, 0xdd, + 0xfc, 0x7f, 0x49, 0x37, 0x61, 0x86, 0xf5, 0xe4, 0xf6, 0xd3, 0x4b, 0xac, 0x09, 0xe7, 0x3f, 0xb2, 0x89, 0x61, 0xdb, + 0x15, 0xd4, 0x16, 0x35, 0x66, 0x9c, 0x12, 0x3c, 0xf6, 0x81, 0x0a, 0xed, 0x61, 0xe2, 0x0a, 0x61, 0x54, 0x79, 0xaa, + 0x44, 0xfa, 0x5c, 0xfc, 0xb2, 0x4d, 0x64, 0xd0, 0x19, 0x87, 0xb2, 0x81, 0x9d, 0xdb, 0xb5, 0xca, 0xcc, 0xd6, 0xd2, + 0xfa, 0x8f, 0x99, 0x62, 0xf3, 0x7f, 0xc0, 0x12, 0xf5, 0x90, 0x47, 0x7e, 0x59, 0xb5, 0x08, 0xef, 0x0d, 0xe5, 0xe6, + 0x21, 0xc8, 0x2d, 0x8b, 0x0e, 0x7f, 0x60, 0x3e, 0x40, 0x8e, 0x60, 0x8c, 0x1a, 0xb0, 0x52, 0x4e, 0x21, 0x97, 0xf9, + 0x71, 0xaa, 0xc9, 0x50, 0xcb, 0x72, 0x9d, 0xb1, 0x4a, 0x23, 0xaf, 0x59, 0x99, 0xa7, 0x59, 0x91, 0x6b, 0x94, 0x0d, + 0x15, 0xd7, 0x9f, 0x91, 0xa3, 0x51, 0x1b, 0xd0, 0x10, 0xbb, 0xe3, 0x9c, 0xd8, 0x28, 0x73, 0xd4, 0x71, 0x72, 0x4b, + 0x9e, 0x59, 0x57, 0x33, 0x5b, 0x89, 0x93, 0x8b, 0x77, 0x9b, 0xb1, 0x6d, 0x77, 0x34, 0x2e, 0x99, 0x27, 0x8e, 0x73, + 0x74, 0x7d, 0xa3, 0xcd, 0x9e, 0x97, 0xec, 0xb8, 0xf8, 0x3f, 0x48, 0x0e, 0xdd, 0x3c, 0x1a, 0x11, 0xcc, 0xc5, 0x25, + 0x45, 0xa9, 0xe9, 0xe6, 0x48, 0x02, 0x1b, 0x1e, 0xff, 0xb9, 0x89, 0xae, 0xf8, 0x78, 0x6e, 0x56, 0x46, 0x14, 0x5b, + 0x9c, 0xd8, 0x9f, 0xed, 0x61, 0xd5, 0x7a, 0x44, 0xc2, 0x81, 0xb3, 0xce, 0xfa, 0x60, 0x9f, 0xeb, 0xd2, 0xff, 0xe0, + 0x07, 0x36, 0x12, 0x82, 0x8d, 0x61, 0xf5, 0xce, 0xfe, 0xa7, 0x66, 0xc5, 0x85, 0xae, 0x35, 0x3b, 0x5e, 0xf8, 0x57, + 0x5c, 0xe1, 0x2d, 0x49, 0x65, 0x25, 0x37, 0x2e, 0x77, 0x2a, 0xe3, 0x05, 0x55, 0x3a, 0x66, 0x61, 0xe8, 0x58, 0x4c, + 0xaf, 0x0e, 0x4a, 0xaf, 0x08, 0x68, 0xa8, 0xce, 0xb9, 0xab, 0x95, 0xd9, 0x04, 0x97, 0x11, 0x92, 0x4a, 0x81, 0xbb, + 0xc2, 0x90, 0xe9, 0x9d, 0x6f, 0x86, 0x7e, 0x30, 0x14, 0x66, 0x6e, 0x40, 0xd8, 0x32, 0x41, 0xa5, 0xc3, 0x9a, 0x15, + 0x7b, 0x41, 0x9b, 0x0c, 0xe6, 0x3c, 0xa2, 0xde, 0x6b, 0xa4, 0xbf, 0x73, 0xc2, 0x05, 0x38, 0x4a, 0x81, 0xc2, 0x80, + 0x2e, 0x6f, 0x3c, 0x40, 0x72, 0x89, 0x10, 0x63, 0x0d, 0x85, 0xd4, 0x26, 0x7e, 0x39, 0xbf, 0xe2, 0x9e, 0xf7, 0xb3, + 0xe3, 0xac, 0xeb, 0x5b, 0x03, 0x79, 0x98, 0x5f, 0xbf, 0xbd, 0xce, 0x7a, 0x90, 0xb3, 0x21, 0x71, 0xb1, 0xb2, 0xf3, + 0x8a, 0x76, 0x76, 0x45, 0x5b, 0xea, 0x6a, 0x54, 0xe1, 0xb6, 0x86, 0x29, 0x52, 0x54, 0xb1, 0xe1, 0x7a, 0x1b, 0xba, + 0x20, 0xe9, 0x8b, 0x35, 0x85, 0x84, 0x19, 0xbb, 0xa6, 0x30, 0x95, 0x3b, 0xa1, 0x47, 0x67, 0xc3, 0x40, 0x5f, 0x6c, + 0xfd, 0x02, 0xf4, 0xa7, 0x8d, 0x8d, 0x36, 0x7d, 0x4f, 0x54, 0x46, 0xcc, 0x29, 0xfa, 0xbc, 0xc3, 0xec, 0xd3, 0xfe, + 0x44, 0x77, 0xb0, 0x5a, 0x5f, 0xc6, 0x5f, 0x56, 0x6c, 0xd4, 0xc7, 0xd6, 0x33, 0x26, 0x89, 0x53, 0xc9, 0xed, 0x41, + 0x49, 0x41, 0x66, 0xde, 0x44, 0x0d, 0x19, 0x29, 0xad, 0x39, 0x8f, 0x20, 0xfe, 0x77, 0xae, 0x98, 0x99, 0x98, 0xf6, + 0x63, 0x5c, 0x52, 0x1f, 0x7f, 0xf7, 0xc4, 0x5b, 0xbb, 0x77, 0x9a, 0xa1, 0x63, 0xf6, 0x00, 0x81, 0x9c, 0x57, 0x5e, + 0xba, 0x60, 0x68, 0x6e, 0xad, 0x54, 0xb3, 0xa6, 0x51, 0xfe, 0xb3, 0xbb, 0x32, 0x05, 0x03, 0xfb, 0x44, 0xad, 0x3f, + 0xdb, 0xe5, 0x66, 0xea, 0x1b, 0xb3, 0x57, 0x03, 0x4e, 0x04, 0x66, 0x36, 0xdd, 0x54, 0xfa, 0xaf, 0xfb, 0xfe, 0x3b, + 0x16, 0xa0, 0xd8, 0xd9, 0xc8, 0x1f, 0x9a, 0x8a, 0xe0, 0xc6, 0x77, 0x67, 0x2f, 0x86, 0x2d, 0x0a, 0x05, 0x5f, 0x46, + 0x99, 0xee, 0x32, 0xf2, 0x07, 0x0d, 0x6d, 0xf0, 0x4b, 0x7a, 0x63, 0x1b, 0x97, 0x61, 0x1f, 0xed, 0x61, 0x12, 0xbb, + 0x60, 0x68, 0x6b, 0x62, 0x41, 0x50, 0x35, 0x75, 0xde, 0x30, 0x22, 0xa1, 0x6f, 0xad, 0x95, 0xcf, 0xeb, 0xd8, 0x33, + 0xde, 0x71, 0x3e, 0x64, 0x62, 0x04, 0x7e, 0x8b, 0xb6, 0x5b, 0x12, 0xca, 0xb8, 0x74, 0x0c, 0x32, 0xb5, 0x47, 0x6d, + 0xc7, 0xc9, 0xb4, 0xed, 0x76, 0xd4, 0xee, 0xd1, 0xdd, 0xcd, 0x6f, 0x06, 0xa5, 0xed, 0x8e, 0xf0, 0x2d, 0xbc, 0x3a, + 0x73, 0xe4, 0x7e, 0xeb, 0xee, 0x24, 0x5b, 0xa0, 0x37, 0x33, 0x15, 0x14, 0x75, 0xc2, 0xc9, 0x33, 0xd6, 0xf8, 0xbf, + 0xd0, 0x54, 0xc1, 0x10, 0x98, 0xcc, 0x44, 0xb2, 0xdb, 0x82, 0x7c, 0x16, 0xfa, 0xfb, 0x14, 0x6e, 0x15, 0xb2, 0xb4, + 0x2d, 0x66, 0x08, 0xa7, 0x7a, 0xd0, 0x0c, 0x5e, 0x42, 0x81, 0x28, 0xed, 0x9d, 0xa1, 0x32, 0xe8, 0x41, 0xa5, 0x03, + 0x99, 0x28, 0x06, 0x35, 0x4b, 0x61, 0xca, 0x9b, 0x90, 0x7a, 0xf7, 0x7b, 0xbd, 0xf5, 0x77, 0xf9, 0xde, 0x8c, 0x22, + 0x1e, 0xf5, 0xd6, 0x49, 0x02, 0x82, 0x5f, 0x71, 0x20, 0x13, 0xe5, 0xf5, 0x92, 0x18, 0xb1, 0x8e, 0xc7, 0x49, 0xae, + 0x16, 0x1d, 0xaf, 0xc4, 0x39, 0x25, 0x15, 0x42, 0xce, 0x01, 0x0c, 0x13, 0x05, 0xee, 0xe5, 0x38, 0x82, 0xf5, 0x80, + 0x67, 0x72, 0x45, 0x3d, 0x1b, 0x8b, 0xbb, 0xfd, 0xef, 0xe5, 0xd5, 0xed, 0x9a, 0xf6, 0x36, 0x49, 0x01, 0x56, 0x5d, + 0x54, 0x82, 0xef, 0xfe, 0xfc, 0x29, 0xe4, 0xb1, 0x64, 0x87, 0x5a, 0x2a, 0x73, 0x30, 0x5b, 0x74, 0x1d, 0x72, 0xd6, + 0xa7, 0xaa, 0x3a, 0x36, 0x39, 0xa0, 0x86, 0xd3, 0xb4, 0x73, 0xc1, 0x78, 0x9c, 0xb0, 0x86, 0x73, 0xc2, 0x1a, 0x76, + 0xa8, 0x68, 0x23, 0x8c, 0x6e, 0x68, 0x31, 0x96, 0xb4, 0xc6, 0x7c, 0x3b, 0x20, 0x24, 0xf8, 0x7a, 0xa1, 0x95, 0x8b, + 0x8c, 0xe3, 0x8f, 0x2d, 0x06, 0x13, 0xec, 0x12, 0x2b, 0xdd, 0x84, 0x7f, 0x0d, 0xcf, 0x95, 0xbe, 0x95, 0x27, 0x71, + 0x73, 0x6f, 0xce, 0xe1, 0x44, 0xe3, 0x51, 0x93, 0x8c, 0xfc, 0x94, 0xf5, 0xa8, 0x94, 0xe4, 0x3f, 0x37, 0x8f, 0x81, + 0x33, 0x73, 0x8b, 0x7d, 0x25, 0x30, 0x26, 0x54, 0x3a, 0x96, 0xf1, 0x2f, 0x11, 0xf5, 0xd9, 0x68, 0xc4, 0x0c, 0x0a, + 0xe3, 0x5c, 0x25, 0x56, 0xe2, 0x3e, 0xdb, 0xa2, 0x97, 0xf2, 0xae, 0x31, 0x46, 0x25, 0x4c, 0xc5, 0x2f, 0x46, 0xf6, + 0x18, 0xa9, 0xb7, 0x73, 0xb6, 0xfd, 0x5c, 0x13, 0xdd, 0x73, 0x3a, 0x90, 0x04, 0x8d, 0x4b, 0x66, 0x0a, 0x90, 0xc4, + 0x04, 0x63, 0x72, 0x07, 0x2c, 0xda, 0xa6, 0x75, 0x9e, 0xc2, 0xab, 0x56, 0xe3, 0x49, 0x65, 0x7b, 0xdf, 0x65, 0x65, + 0x2e, 0xdb, 0x8e, 0x4e, 0x5b, 0x12, 0x24, 0x8d, 0x1a, 0xa7, 0x48, 0x48, 0xd5, 0xd3, 0xac, 0x0c, 0x0b, 0x84, 0xb5, + 0xe2, 0x9c, 0xbe, 0xb9, 0x35, 0x99, 0x9d, 0x17, 0xb1, 0x57, 0x78, 0x15, 0x85, 0x08, 0x6e, 0x67, 0x13, 0x89, 0x0f, + 0x63, 0xcb, 0x3a, 0x59, 0xc8, 0xd2, 0xb7, 0x6e, 0xad, 0x4b, 0xc0, 0x0f, 0xde, 0xea, 0xb7, 0xfb, 0xf1, 0x38, 0xb4, + 0x30, 0xd6, 0x47, 0xb8, 0xf8, 0xa8, 0x17, 0x2c, 0xad, 0x7c, 0x89, 0x08, 0x4a, 0x9b, 0xa5, 0xd7, 0xbf, 0x60, 0xb1, + 0x29, 0x2f, 0x57, 0x2c, 0x34, 0x36, 0x74, 0x33, 0x0d, 0xd5, 0x32, 0x31, 0x27, 0x15, 0x55, 0x31, 0xc7, 0x00, 0x3d, + 0xee, 0x20, 0x73, 0xcb, 0x22, 0x6b, 0xd2, 0xc3, 0x59, 0x09, 0xcc, 0xd7, 0x60, 0xe7, 0x38, 0x03, 0xea, 0xd8, 0xa4, + 0xea, 0x17, 0x0b, 0xa0, 0x24, 0x6e, 0xe0, 0x5b, 0x21, 0x77, 0xa1, 0xca, 0x1e, 0x29, 0xa4, 0xb0, 0x0e, 0x2c, 0xe1, + 0xac, 0x60, 0xc5, 0xd8, 0x3e, 0x6c, 0xe6, 0x8f, 0x51, 0x6f, 0x01, 0xd3, 0x43, 0x08, 0xf3, 0xdd, 0x1d, 0xb8, 0x11, + 0x1d, 0xad, 0xc9, 0xe4, 0x1e, 0x27, 0xc8, 0xa2, 0x9f, 0xfb, 0x25, 0x31, 0x14, 0x4f, 0xc8, 0xcb, 0x51, 0x33, 0x16, + 0xb5, 0x60, 0x5a, 0xa6, 0xcd, 0x2d, 0xdf, 0x7d, 0x6d, 0x23, 0xaa, 0x47, 0xc4, 0xa5, 0x42, 0x48, 0x1d, 0x14, 0xe8, + 0x0e, 0x73, 0xa9, 0xeb, 0xc9, 0xb3, 0x45, 0xf1, 0x2c, 0x9b, 0xae, 0x12, 0xfc, 0xe9, 0xe3, 0x0d, 0xb5, 0xbd, 0x09, + 0xa8, 0xf4, 0x5e, 0x77, 0x9c, 0x93, 0xde, 0x51, 0x89, 0x88, 0x26, 0x19, 0x7f, 0xfb, 0xc8, 0xbc, 0x05, 0x91, 0x58, + 0xeb, 0xe1, 0xd2, 0xeb, 0xb7, 0xaf, 0x51, 0xb0, 0x6a, 0x22, 0x9c, 0xbd, 0xa5, 0x49, 0x1c, 0xbc, 0x14, 0x21, 0x19, + 0x8a, 0x60, 0xe4, 0xa3, 0x82, 0xd8, 0x8a, 0xad, 0x12, 0x75, 0xb5, 0x86, 0x40, 0xc4, 0x39, 0xd8, 0x20, 0xb3, 0x8c, + 0xce, 0x99, 0xd7, 0xbe, 0x3c, 0x44, 0xf1, 0xd2, 0x14, 0xf5, 0xbf, 0x5a, 0x16, 0x7e, 0xf4, 0x70, 0xe0, 0x75, 0x64, + 0xe5, 0xac, 0x77, 0xbd, 0x54, 0x6e, 0xcb, 0x3a, 0x6e, 0xad, 0x7a, 0x4f, 0x9e, 0x20, 0xa7, 0xd1, 0xa6, 0x97, 0xe2, + 0xd6, 0x21, 0xa9, 0x31, 0xbc, 0x56, 0xb5, 0xa8, 0x8f, 0x0b, 0x77, 0xd8, 0x8b, 0x5a, 0xa9, 0x77, 0x30, 0x11, 0x5d, + 0xf7, 0xed, 0x9f, 0x88, 0x6a, 0xc8, 0x98, 0x8e, 0x35, 0xe4, 0x0e, 0x6c, 0xc1, 0xf4, 0x54, 0xd2, 0x77, 0x02, 0xf1, + 0xf8, 0x48, 0xb2, 0xab, 0xff, 0x94, 0xd1, 0xfd, 0x85, 0x8c, 0x81, 0x91, 0xd1, 0x1d, 0x61, 0x2d, 0xc2, 0xbd, 0x34, + 0xe8, 0x18, 0x23, 0x94, 0x4f, 0x89, 0x66, 0x66, 0xd9, 0x6d, 0x5e, 0x90, 0xd8, 0xe7, 0x5a, 0xcd, 0xde, 0x72, 0x9d, + 0x48, 0xd0, 0xa2, 0x04, 0xe2, 0xe5, 0x96, 0x19, 0x17, 0x80, 0xae, 0x8d, 0x9b, 0x14, 0x71, 0xb8, 0xb1, 0xd9, 0xdb, + 0x00, 0xa0, 0x7d, 0xfe, 0xfd, 0x4c, 0xe9, 0xe2, 0x76, 0x41, 0x09, 0x9b, 0x1f, 0x2c, 0x26, 0x8b, 0x5b, 0x19, 0x14, + 0x62, 0x23, 0x04, 0x0f, 0x64, 0x13, 0x8d, 0xdd, 0x7a, 0x8a, 0xd8, 0x3c, 0x5f, 0x20, 0x6d, 0x51, 0x78, 0x26, 0x67, + 0x93, 0xfd, 0x8b, 0x76, 0xb0, 0x81, 0xb1, 0x6e, 0x52, 0x94, 0xdf, 0x95, 0xa6, 0xa3, 0x8c, 0xda, 0xc7, 0x2f, 0x37, + 0x5c, 0x94, 0xa5, 0x26, 0x30, 0x9a, 0x46, 0xdd, 0xf2, 0xf7, 0x89, 0x13, 0x0c, 0x5d, 0x19, 0x01, 0xca, 0xb9, 0x94, + 0x09, 0x9f, 0xb3, 0x6f, 0x90, 0x16, 0x00, 0xf2, 0x9b, 0x1f, 0xb5, 0xe3, 0x63, 0x73, 0xbd, 0xfc, 0xd2, 0xb6, 0xa5, + 0x44, 0xf4, 0x5f, 0xda, 0x2a, 0xdb, 0xb1, 0x0f, 0x54, 0xf1, 0x30, 0x6a, 0x44, 0xcb, 0x9a, 0x0f, 0x59, 0xfb, 0x14, + 0x0f, 0x9b, 0x7b, 0x6f, 0x76, 0xa6, 0xc8, 0x86, 0xda, 0x25, 0xfb, 0xcb, 0x4b, 0x3a, 0x2f, 0xaf, 0xd6, 0x0c, 0x5e, + 0xed, 0x11, 0xea, 0x2a, 0x02, 0x05, 0x8f, 0xc1, 0x01, 0xbe, 0x36, 0xfb, 0x9e, 0x2d, 0x28, 0xf0, 0xcf, 0x8e, 0x9d, + 0xbf, 0x3c, 0x9f, 0x43, 0x02, 0x59, 0x9f, 0x35, 0x49, 0x04, 0x44, 0x24, 0x74, 0x3a, 0xdb, 0x1a, 0x82, 0x3c, 0x8c, + 0x2c, 0x1e, 0xb1, 0x59, 0xc6, 0x7f, 0xb1, 0x98, 0x8b, 0xcb, 0x7b, 0x36, 0xb9, 0x9f, 0x9b, 0xb7, 0xce, 0x00, 0xa9, + 0x6d, 0x9a, 0xc9, 0x48, 0x75, 0x64, 0x1a, 0x40, 0x05, 0xed, 0x85, 0x52, 0x4a, 0x46, 0xa9, 0x1c, 0x23, 0xb6, 0x6b, + 0x23, 0xe3, 0xe2, 0x64, 0x49, 0xc3, 0xb0, 0x24, 0xf8, 0x35, 0x11, 0x04, 0xbd, 0x54, 0x44, 0xf5, 0x70, 0x51, 0xca, + 0xdb, 0x21, 0x8f, 0x06, 0xd0, 0x52, 0xe3, 0x6d, 0x92, 0xa7, 0xdd, 0x8b, 0x73, 0x17, 0x59, 0x71, 0xf3, 0xa7, 0xc4, + 0x0f, 0x95, 0x63, 0x3c, 0x29, 0x90, 0x18, 0xe7, 0x5d, 0xb9, 0xf3, 0xa0, 0x0e, 0xc4, 0x1c, 0x13, 0x3c, 0xd2, 0xb3, + 0xaa, 0x3d, 0x98, 0x19, 0x68, 0x53, 0x1a, 0x4d, 0x15, 0xb5, 0x01, 0xe5, 0xff, 0x80, 0xbe, 0xca, 0xa7, 0xe5, 0x91, + 0x6b, 0x10, 0x86, 0xd2, 0x7a, 0x4b, 0xc3, 0x4b, 0x42, 0x68, 0x71, 0xae, 0x4c, 0x32, 0x08, 0xbc, 0xf1, 0xa1, 0xd7, + 0x35, 0x7e, 0x10, 0x25, 0x40, 0x73, 0xe6, 0x27, 0x1f, 0x3e, 0x9e, 0x03, 0x14, 0xce, 0x5a, 0x32, 0xfa, 0xb3, 0xab, + 0x09, 0x4b, 0xba, 0x5d, 0x34, 0xbb, 0x11, 0xca, 0x57, 0x29, 0x58, 0x5a, 0x58, 0x8a, 0xde, 0xa2, 0x3c, 0x30, 0x6c, + 0xb7, 0xb2, 0x7d, 0xfb, 0x5f, 0x1e, 0xde, 0x2b, 0x74, 0x91, 0xb0, 0x1d, 0xe2, 0xa7, 0xa8, 0xe9, 0x2f, 0x3e, 0x9c, + 0x9e, 0x8c, 0x61, 0xbb, 0x2b, 0x61, 0xee, 0x30, 0xcf, 0xb1, 0xbf, 0x74, 0xe4, 0x86, 0xb6, 0x12, 0x31, 0xf9, 0x5a, + 0x36, 0x61, 0x11, 0x07, 0x0c, 0x64, 0xae, 0x06, 0xb9, 0x83, 0x23, 0x04, 0xa6, 0xd6, 0x7c, 0xf2, 0xff, 0x54, 0x2d, + 0x1e, 0x9f, 0x2d, 0x8b, 0x4a, 0x82, 0x7c, 0x2b, 0xed, 0xf3, 0xd8, 0x87, 0xa4, 0x1d, 0xd8, 0xf7, 0x08, 0x16, 0xbd, + 0xdd, 0x61, 0x51, 0x68, 0xa1, 0x83, 0xb8, 0xa4, 0xce, 0xa7, 0xf0, 0xea, 0xe5, 0x32, 0x85, 0xd0, 0x29, 0x0b, 0x3c, + 0x5f, 0x45, 0x38, 0xa6, 0xf7, 0xc7, 0x03, 0x95, 0x05, 0xa5, 0x5c, 0x4e, 0xf0, 0x29, 0x6f, 0xea, 0x70, 0x06, 0xd4, + 0x90, 0xf6, 0xa9, 0x70, 0xc5, 0x3f, 0x4a, 0x59, 0x17, 0x3a, 0xb3, 0x90, 0xaa, 0x30, 0xd9, 0x91, 0xf0, 0xbf, 0x54, + 0xcc, 0x90, 0xe1, 0x85, 0x50, 0xa5, 0x0d, 0x7c, 0x6d, 0x8b, 0xae, 0x94, 0x17, 0x6d, 0x0b, 0x7d, 0x2c, 0x76, 0x65, + 0x4e, 0x00, 0xba, 0x01, 0x5a, 0x7b, 0xed, 0x82, 0xbb, 0x1b, 0xee, 0x65, 0x9f, 0x15, 0xf7, 0x6e, 0xda, 0x00, 0x07, + 0x5f, 0x20, 0xa7, 0xbe, 0x7f, 0x45, 0x71, 0xfe, 0x69, 0x2b, 0x1e, 0x2d, 0xc4, 0x94, 0x80, 0x09, 0x24, 0xe4, 0x1b, + 0x3e, 0xb6, 0x66, 0xc4, 0x3e, 0x7e, 0x08, 0x37, 0x4a, 0x09, 0x2b, 0x8d, 0x3c, 0x38, 0xca, 0xed, 0x37, 0x55, 0x86, + 0xe4, 0xb6, 0x9c, 0x83, 0xc2, 0x10, 0x0b, 0x07, 0xdc, 0x65, 0xae, 0x6c, 0x7f, 0xbc, 0x4a, 0x8f, 0xc2, 0x9e, 0xb8, + 0x50, 0xb1, 0x18, 0x6a, 0x64, 0xc4, 0x2b, 0x1e, 0xaa, 0xb3, 0xd2, 0xc4, 0x00, 0x19, 0x61, 0x80, 0x8e, 0x29, 0x6d, + 0x84, 0x40, 0x09, 0x01, 0x5b, 0x7e, 0xa8, 0xa3, 0x42, 0x13, 0xa1, 0x08, 0xa1, 0x25, 0xd2, 0x1c, 0x1d, 0x64, 0x65, + 0x86, 0xa4, 0xd2, 0x63, 0x76, 0x4c, 0x07, 0x96, 0x05, 0x58, 0x52, 0x29, 0x0a, 0x20, 0x9f, 0x8c, 0x51, 0xab, 0x88, + 0x50, 0xe2, 0xae, 0xbc, 0x4c, 0x1a, 0x0e, 0x58, 0xc3, 0x5c, 0x34, 0x17, 0x4b, 0xd6, 0x75, 0x38, 0x94, 0x21, 0x4d, + 0xae, 0x5a, 0x05, 0x79, 0xa7, 0x3f, 0x4f, 0x63, 0xce, 0x57, 0x04, 0x42, 0x9b, 0xfb, 0x91, 0xcb, 0x05, 0xc2, 0x8f, + 0x74, 0x6c, 0x8c, 0x91, 0x91, 0xb4, 0x76, 0x20, 0x75, 0x51, 0x22, 0x24, 0xc4, 0x95, 0x74, 0x41, 0x73, 0x3e, 0x14, + 0x22, 0x3e, 0x3b, 0x61, 0xae, 0x0f, 0x12, 0xb3, 0x44, 0xe5, 0xdf, 0x37, 0xcb, 0x61, 0xf5, 0x42, 0xf0, 0xb0, 0xd8, + 0xae, 0xaa, 0x1c, 0x28, 0x24, 0x12, 0xd6, 0xa8, 0x13, 0xe6, 0xce, 0x1b, 0xcb, 0xdf, 0x14, 0xc1, 0x9e, 0x27, 0x64, + 0x26, 0x18, 0xa5, 0x57, 0x51, 0xae, 0x54, 0xef, 0x94, 0x39, 0x8c, 0xdc, 0xf0, 0xee, 0xa6, 0xf8, 0xc1, 0x81, 0xbc, + 0x67, 0x53, 0x7a, 0xc4, 0xdb, 0xfd, 0x50, 0x4b, 0x9c, 0x53, 0x24, 0x39, 0x41, 0x29, 0xe8, 0xfe, 0xc3, 0x6b, 0x47, + 0x25, 0x31, 0xfe, 0xd0, 0xa2, 0xf4, 0x5b, 0x8b, 0xa7, 0xb9, 0x96, 0x33, 0x6d, 0xd2, 0xcc, 0x9c, 0x6f, 0x46, 0x15, + 0x9b, 0x2b, 0x63, 0x68, 0x5d, 0x70, 0x20, 0x00, 0x37, 0x83, 0x75, 0x2a, 0xad, 0xcf, 0xf5, 0x07, 0x08, 0x7d, 0xe3, + 0x3e, 0x28, 0xb3, 0x1d, 0x3c, 0x1a, 0x63, 0xc8, 0xeb, 0x67, 0x57, 0x75, 0xd0, 0x65, 0x44, 0x82, 0x00, 0x16, 0x7a, + 0xc8, 0xe1, 0x95, 0xba, 0x9c, 0xd9, 0xca, 0xec, 0xd1, 0xe6, 0xb5, 0x1c, 0x6f, 0x1d, 0x69, 0x38, 0x2e, 0x8e, 0x67, + 0x1f, 0x2c, 0x9d, 0x47, 0xe8, 0x48, 0xca, 0x8d, 0xf7, 0x4a, 0x20, 0xdf, 0x10, 0x19, 0xa8, 0xe7, 0xa2, 0x02, 0xb0, + 0x2b, 0x8b, 0xaa, 0xe4, 0x75, 0x78, 0xe8, 0xf9, 0x38, 0x32, 0x8f, 0x18, 0xe3, 0x10, 0x55, 0x46, 0x1e, 0x9d, 0xdc, + 0x2e, 0x2d, 0x32, 0x6a, 0x2e, 0x98, 0x5a, 0xcd, 0xbb, 0xea, 0x94, 0x07, 0xb2, 0xc9, 0xd7, 0x2b, 0x2d, 0xb4, 0x1e, + 0x89, 0x15, 0xdd, 0xac, 0x8a, 0x7a, 0x58, 0x20, 0x62, 0xbd, 0xff, 0x04, 0x91, 0x47, 0x2c, 0x1f, 0x64, 0xd4, 0x22, + 0x6d, 0xae, 0xc4, 0x4a, 0x29, 0x60, 0x76, 0x81, 0x42, 0x2b, 0xef, 0x10, 0x5c, 0xf9, 0x4d, 0x85, 0x44, 0xda, 0xc5, + 0x5d, 0x07, 0xea, 0x11, 0xbf, 0x35, 0xb2, 0x59, 0x1f, 0xa8, 0xe5, 0x7c, 0x2b, 0x2a, 0x1a, 0x22, 0x23, 0xd2, 0xd1, + 0x6f, 0x38, 0x01, 0xc3, 0x82, 0x0c, 0xe9, 0xf4, 0x3c, 0xf5, 0x58, 0xa0, 0xc5, 0x50, 0xe5, 0x54, 0x8c, 0x65, 0x52, + 0xdd, 0x0a, 0x96, 0x29, 0xb3, 0x50, 0x12, 0x5d, 0x41, 0xcb, 0xec, 0x35, 0xd8, 0x7c, 0xcf, 0x6a, 0x5b, 0x64, 0x44, + 0xc2, 0x35, 0xc2, 0x1f, 0x86, 0x31, 0x00, 0xaf, 0x12, 0xa5, 0xf3, 0xc0, 0x68, 0xc5, 0x24, 0xe6, 0x71, 0x0a, 0xaf, + 0x9b, 0x2a, 0x79, 0x81, 0x5b, 0xf3, 0xd4, 0xd4, 0x58, 0x7e, 0xff, 0xfa, 0xfb, 0x41, 0x53, 0x65, 0xad, 0x40, 0x7e, + 0xb2, 0x6e, 0xfd, 0xfb, 0x2e, 0xf7, 0x20, 0x6f, 0xd3, 0xfb, 0x7e, 0x1c, 0xf2, 0x0d, 0x04, 0x82, 0x51, 0x0a, 0xd3, + 0xc5, 0xfa, 0xb4, 0xc2, 0xe8, 0x7a, 0x49, 0xbb, 0x32, 0x7d, 0x40, 0xc2, 0xfb, 0x7a, 0xfb, 0x19, 0xa1, 0xcc, 0x12, + 0xfb, 0x50, 0x10, 0xc5, 0x4a, 0x94, 0x47, 0xe6, 0x67, 0x73, 0x6f, 0x45, 0x5c, 0x30, 0x53, 0xfd, 0x62, 0xf2, 0x30, + 0x24, 0x1c, 0x98, 0x99, 0x08, 0x07, 0xd6, 0xb4, 0xf0, 0xec, 0x6a, 0xc1, 0x9f, 0x96, 0x12, 0xe0, 0x11, 0xeb, 0x2a, + 0xfd, 0xbd, 0x8c, 0xa5, 0x18, 0xb1, 0xbd, 0x9d, 0xa1, 0xf4, 0x9c, 0x59, 0x07, 0x1d, 0x5e, 0x89, 0x82, 0x2d, 0xce, + 0xf4, 0x03, 0x33, 0x39, 0x8a, 0x0b, 0xaa, 0x25, 0xfc, 0xed, 0xad, 0xe2, 0xbe, 0x54, 0x3c, 0xa7, 0xb0, 0xef, 0x49, + 0xbe, 0xf8, 0x20, 0xed, 0xbd, 0xa8, 0x82, 0x56, 0x38, 0xb5, 0xc1, 0x0d, 0xf1, 0xd1, 0x9d, 0xf2, 0x50, 0x29, 0x42, + 0x08, 0xa3, 0xe8, 0x17, 0xf5, 0x08, 0x79, 0x81, 0xd7, 0xcb, 0xb7, 0x75, 0x7d, 0x48, 0x98, 0x82, 0xb8, 0xbe, 0xdb, + 0xc2, 0x19, 0x7d, 0x6a, 0x46, 0xcd, 0xdd, 0x71, 0xf5, 0x17, 0xea, 0x16, 0xef, 0x40, 0xb4, 0xc3, 0x74, 0x0f, 0x8f, + 0xeb, 0xfa, 0x68, 0x72, 0xd4, 0x85, 0x26, 0x6e, 0x35, 0xdb, 0xaf, 0xb4, 0x64, 0x9e, 0x06, 0x30, 0x68, 0x8c, 0xfa, + 0x7d, 0xc9, 0x1e, 0x8d, 0xef, 0x78, 0x4b, 0x64, 0x43, 0xb8, 0x0d, 0xe8, 0x70, 0x70, 0xa1, 0xa7, 0xfe, 0x46, 0xf6, + 0x6b, 0xb9, 0x04, 0xc7, 0x4b, 0xf1, 0x63, 0xdd, 0xf0, 0x47, 0x99, 0xd0, 0xec, 0x24, 0xa6, 0xc1, 0xfd, 0xb6, 0xb8, + 0xb2, 0xc2, 0x65, 0x12, 0x0a, 0x0b, 0x68, 0x40, 0x60, 0x01, 0x45, 0xd0, 0xe7, 0x30, 0x49, 0x94, 0x3d, 0x9c, 0xb3, + 0xdd, 0xdb, 0x7b, 0x71, 0x4c, 0x24, 0x9d, 0xd2, 0xe4, 0xe8, 0x07, 0x5e, 0x4c, 0x67, 0x51, 0x93, 0x68, 0xa4, 0x5f, + 0x31, 0x27, 0x2f, 0x1b, 0xe9, 0xc8, 0x00, 0x31, 0x67, 0x15, 0xa2, 0x0b, 0x69, 0xdf, 0x3f, 0x23, 0x32, 0xa0, 0x62, + 0x50, 0x37, 0xc3, 0x0e, 0xb1, 0x29, 0xe7, 0xb5, 0xeb, 0x07, 0x46, 0x4b, 0x7c, 0xb1, 0x3c, 0xca, 0xb0, 0xfc, 0x31, + 0x1f, 0xa3, 0xef, 0xbc, 0x95, 0x65, 0xe8, 0xc2, 0x72, 0x7e, 0x17, 0x93, 0xa5, 0x3a, 0x5c, 0x3d, 0x09, 0xe9, 0x7e, + 0x6a, 0xcd, 0x4b, 0xff, 0xb3, 0xe9, 0x82, 0xb4, 0xcf, 0x3c, 0xa4, 0x7e, 0x92, 0xc7, 0x68, 0xf7, 0x55, 0x2f, 0xac, + 0xd3, 0x85, 0x11, 0x66, 0xfa, 0xa8, 0x9a, 0x85, 0x0f, 0x55, 0x66, 0xcb, 0xc6, 0xd3, 0x52, 0xcc, 0x1f, 0x1d, 0xc1, + 0x1a, 0x82, 0x26, 0x24, 0x1b, 0xf7, 0x25, 0xf1, 0x83, 0xea, 0x82, 0xf1, 0x60, 0x87, 0xe5, 0xf5, 0xfc, 0x66, 0xd7, + 0xbe, 0xd3, 0xf2, 0xa9, 0xe0, 0x1f, 0x3c, 0xc4, 0xca, 0x9f, 0x0a, 0x87, 0x25, 0x2e, 0xbe, 0xb0, 0x52, 0x67, 0xbd, + 0x28, 0xa4, 0xad, 0xd5, 0xac, 0xa8, 0x65, 0x37, 0x64, 0xe5, 0x55, 0xde, 0xf2, 0x52, 0x0a, 0x7e, 0x4d, 0x45, 0x4e, + 0x72, 0x9d, 0x72, 0x31, 0x18, 0x78, 0x33, 0x27, 0xfd, 0x7a, 0x42, 0x1b, 0xb9, 0x81, 0x71, 0xfa, 0x3a, 0xb6, 0x2e, + 0x90, 0x04, 0x76, 0xf5, 0x91, 0x0b, 0x4f, 0x20, 0x91, 0xd5, 0xc7, 0x73, 0x36, 0xd6, 0xcd, 0x4e, 0xbe, 0x97, 0x77, + 0x19, 0x47, 0xf0, 0x4c, 0x21, 0xde, 0x9c, 0xd7, 0x06, 0xfc, 0x23, 0xef, 0x70, 0x6e, 0xe5, 0x7d, 0x50, 0x8d, 0xa1, + 0x35, 0x6c, 0x69, 0xe0, 0xab, 0x0b, 0x8c, 0x61, 0x02, 0xd5, 0x24, 0x08, 0x8e, 0xd6, 0x62, 0xbb, 0x20, 0x38, 0x96, + 0x51, 0x78, 0xb1, 0x3a, 0xe5, 0x17, 0x66, 0x53, 0x11, 0x45, 0x26, 0xfc, 0xc2, 0x0e, 0xd5, 0x66, 0x84, 0x43, 0xfc, + 0x58, 0x11, 0xe5, 0x43, 0x8d, 0x07, 0x20, 0x0e, 0x60, 0x7a, 0xd3, 0x88, 0x68, 0x7f, 0x8d, 0x1a, 0x05, 0x35, 0x3c, + 0x73, 0x97, 0xde, 0x59, 0x23, 0x2e, 0xeb, 0x6f, 0x0a, 0xcc, 0x2b, 0xb1, 0x6c, 0xaf, 0xac, 0xcb, 0x92, 0xf3, 0x3d, + 0x68, 0xe2, 0xb8, 0xd3, 0xce, 0x92, 0xb3, 0xe4, 0x00, 0x6d, 0xbb, 0xa7, 0xcd, 0x7c, 0x42, 0x72, 0x71, 0xb5, 0xeb, + 0x14, 0xa4, 0x32, 0xaf, 0x62, 0x23, 0x95, 0xe2, 0xbc, 0x73, 0x09, 0x38, 0xdc, 0x4f, 0xf1, 0xff, 0x55, 0xa2, 0xbe, + 0x9b, 0x5f, 0x10, 0xc6, 0x76, 0x52, 0xbf, 0x1c, 0x36, 0x6d, 0x61, 0x76, 0x70, 0xdd, 0xe2, 0xa6, 0xdd, 0x10, 0x55, + 0xd9, 0x5b, 0x6b, 0xbe, 0x34, 0x0f, 0x79, 0x61, 0x39, 0xb3, 0xd0, 0xea, 0xf3, 0xb8, 0x65, 0x44, 0xf9, 0xd4, 0x85, + 0xc3, 0xb1, 0xd0, 0x90, 0xcb, 0x9b, 0x43, 0x5d, 0xe4, 0xc7, 0xa4, 0xed, 0x01, 0x03, 0x49, 0x3d, 0xd1, 0xc6, 0x87, + 0x6e, 0xe6, 0xb6, 0x3b, 0x03, 0xe9, 0x7a, 0x39, 0x0d, 0x25, 0xb3, 0x18, 0xb8, 0x70, 0x34, 0xe6, 0xa9, 0x43, 0xa7, + 0x5d, 0xb1, 0x11, 0xd6, 0x1d, 0x0c, 0x57, 0x62, 0x54, 0x75, 0x18, 0xbb, 0xa6, 0xd5, 0x39, 0x56, 0xaa, 0xc7, 0xde, + 0x67, 0x1d, 0x91, 0xe0, 0x09, 0x05, 0x47, 0x1e, 0x78, 0x86, 0xcf, 0xea, 0xa0, 0xc3, 0xa3, 0x8e, 0xc0, 0xa1, 0xba, + 0x40, 0x5f, 0x1d, 0xc6, 0x40, 0x39, 0x82, 0x50, 0x44, 0xbe, 0x7b, 0xa0, 0x0e, 0xe1, 0x6b, 0x7e, 0x82, 0x99, 0x52, + 0x7a, 0x3e, 0x66, 0x7b, 0xf0, 0xe5, 0x80, 0xfb, 0xfd, 0x17, 0x9e, 0xd2, 0x35, 0x8c, 0xc3, 0x0f, 0xbf, 0xd5, 0x62, + 0xf9, 0xfd, 0x00, 0xf3, 0xed, 0x20, 0xd5, 0x25, 0x1c, 0xe5, 0x2a, 0xc0, 0x1f, 0x6d, 0x19, 0x77, 0x0d, 0x86, 0xf5, + 0x11, 0x14, 0x11, 0x1e, 0x71, 0x30, 0xdc, 0x2c, 0x05, 0x80, 0xe2, 0x3c, 0xac, 0x80, 0xc8, 0x42, 0x34, 0x3f, 0x2f, + 0x97, 0x58, 0x57, 0x65, 0x68, 0x4b, 0x4b, 0x36, 0x8f, 0x13, 0x31, 0x6c, 0x26, 0x49, 0x25, 0x44, 0xaf, 0x88, 0x18, + 0x11, 0x33, 0x43, 0xeb, 0xa5, 0xfd, 0x9e, 0xba, 0x2a, 0x08, 0xa3, 0xd6, 0x6d, 0xb8, 0xd7, 0xf5, 0x55, 0x2f, 0x6a, + 0xb5, 0x5f, 0x6b, 0x65, 0x00, 0xfb, 0x96, 0x7c, 0x80, 0x22, 0x09, 0x5b, 0xda, 0xf1, 0x6f, 0x07, 0x72, 0xd1, 0x3f, + 0x84, 0xb0, 0x89, 0x4d, 0x90, 0x73, 0x78, 0xa9, 0x75, 0xf6, 0x36, 0x10, 0xc2, 0x24, 0xd6, 0x6a, 0x3d, 0x82, 0x17, + 0x4d, 0x00, 0xa9, 0xd0, 0x3e, 0x63, 0xf9, 0x88, 0x54, 0x9c, 0x3f, 0x1f, 0x5a, 0x36, 0xb7, 0x3f, 0xe5, 0x13, 0x2b, + 0x47, 0x9c, 0xad, 0x5f, 0x2c, 0x49, 0x56, 0xf0, 0x5d, 0x22, 0xc1, 0x37, 0x96, 0xa1, 0xfa, 0xb4, 0x0f, 0xa0, 0x49, + 0x21, 0xd0, 0xc1, 0xe5, 0x5d, 0x8d, 0x0c, 0xb5, 0x58, 0x46, 0x75, 0xb4, 0xc7, 0x22, 0xd3, 0x17, 0x2f, 0xca, 0xea, + 0xb3, 0x08, 0x0d, 0x27, 0x16, 0xc3, 0x28, 0x95, 0x5e, 0x6c, 0xd1, 0xc6, 0x9f, 0xf4, 0x3f, 0xe6, 0x06, 0xa5, 0xea, + 0x78, 0x85, 0x5b, 0x35, 0x54, 0x87, 0xae, 0xd0, 0x1b, 0xd9, 0xca, 0xb1, 0x7f, 0x79, 0x67, 0x51, 0xc7, 0x9a, 0x36, + 0x08, 0x5e, 0x07, 0xfd, 0xcd, 0x14, 0x9c, 0xec, 0xfc, 0x9a, 0xe8, 0x14, 0x06, 0x08, 0x0a, 0x66, 0x08, 0xf6, 0x19, + 0xcd, 0xa6, 0xa5, 0x74, 0x67, 0xcd, 0x89, 0x3a, 0x36, 0xce, 0x8c, 0xb2, 0x76, 0x29, 0x9e, 0xda, 0x78, 0xeb, 0x05, + 0x3d, 0xf8, 0x5a, 0xbc, 0x58, 0x71, 0x52, 0x5b, 0x46, 0xc4, 0x0b, 0x8e, 0x87, 0xeb, 0x98, 0x43, 0xb5, 0x71, 0x6b, + 0xc1, 0x84, 0x09, 0xad, 0x86, 0xcd, 0xce, 0x5a, 0x4e, 0xf9, 0x5a, 0x1e, 0x27, 0x95, 0x4b, 0x7f, 0xac, 0x90, 0x00, + 0x08, 0x1d, 0x2d, 0x22, 0x09, 0x7c, 0x56, 0x18, 0xc6, 0x1c, 0x0f, 0x92, 0x25, 0xf3, 0x63, 0x25, 0x8f, 0x00, 0x33, + 0x31, 0x5c, 0xbc, 0x0d, 0xbd, 0x7a, 0x82, 0x2e, 0xd9, 0xc1, 0x46, 0xdd, 0x20, 0x08, 0x12, 0xec, 0x00, 0x7f, 0xe1, + 0x7d, 0x17, 0x26, 0xe7, 0xfc, 0x66, 0xeb, 0xf0, 0xff, 0x04, 0x4f, 0xe6, 0x61, 0x6d, 0xbb, 0x1f, 0x6c, 0xd4, 0x97, + 0xff, 0x3f, 0xd5, 0x35, 0xb4, 0x0e, 0x7c, 0xf8, 0xc0, 0x85, 0xc7, 0xab, 0x55, 0x3d, 0x5a, 0x6d, 0xed, 0x80, 0x21, + 0x99, 0x38, 0x51, 0x56, 0xec, 0xa8, 0xde, 0xa1, 0xee, 0x1f, 0x1c, 0xee, 0x8f, 0x1c, 0xa0, 0xfe, 0xc1, 0xc4, 0xdb, + 0x48, 0x23, 0xdd, 0xfd, 0x22, 0x64, 0x62, 0x3d, 0xea, 0x20, 0x57, 0x29, 0xfd, 0xfc, 0xdc, 0xb3, 0x75, 0x84, 0xa5, + 0xab, 0x74, 0x70, 0x7f, 0xd1, 0x59, 0x7b, 0xb0, 0xc1, 0xe5, 0x8b, 0xec, 0x56, 0xad, 0x7d, 0x52, 0xba, 0xca, 0x1a, + 0x6f, 0x02, 0x10, 0x60, 0xab, 0xcc, 0x64, 0xe5, 0xe9, 0x9e, 0x52, 0xc2, 0xbb, 0xd6, 0xe6, 0xec, 0xaf, 0xd7, 0xc1, + 0x29, 0x63, 0x6d, 0x77, 0x71, 0x6b, 0xbd, 0x00, 0x41, 0x39, 0xf7, 0x1a, 0xca, 0x29, 0x84, 0x78, 0x49, 0x0d, 0x2e, + 0x87, 0xb1, 0xf1, 0x10, 0x39, 0x72, 0x88, 0x6e, 0x23, 0x82, 0x75, 0x95, 0xb6, 0x2a, 0x8e, 0xbd, 0x96, 0x27, 0x66, + 0x0b, 0xe3, 0x26, 0xa6, 0x14, 0x16, 0x15, 0x18, 0x79, 0x1a, 0x76, 0x38, 0xdb, 0x11, 0x7a, 0x34, 0x6b, 0x5b, 0x90, + 0x26, 0xec, 0x97, 0xfa, 0x7d, 0x58, 0x82, 0xb1, 0xf9, 0xaa, 0x85, 0xd0, 0x4b, 0xe0, 0x34, 0x79, 0x6f, 0xc8, 0xaf, + 0x2e, 0xf4, 0x8c, 0x70, 0x59, 0x24, 0xf7, 0x58, 0x08, 0x42, 0x65, 0x6b, 0xbb, 0x4c, 0xba, 0x99, 0x63, 0x88, 0xe7, + 0x19, 0x63, 0x68, 0xe1, 0x05, 0x81, 0x4c, 0x13, 0x94, 0x32, 0xfc, 0x16, 0xe1, 0x71, 0x86, 0xb3, 0x43, 0x6e, 0xa6, + 0xc3, 0x48, 0xb8, 0xc2, 0xed, 0x04, 0x99, 0xa5, 0xf9, 0x44, 0xe9, 0xc7, 0x54, 0x75, 0xd8, 0x67, 0x26, 0x21, 0x6a, + 0x8f, 0x58, 0x8f, 0xa7, 0x34, 0x6c, 0x67, 0xfc, 0x9c, 0xe7, 0x52, 0x6c, 0x20, 0xee, 0xae, 0xdc, 0x25, 0xd7, 0xc4, + 0x29, 0xb1, 0xb4, 0xca, 0x38, 0xa4, 0xd0, 0x8e, 0x85, 0xb6, 0xf1, 0x90, 0x1e, 0x44, 0xda, 0xae, 0x0f, 0x49, 0x95, + 0x4e, 0x1e, 0xf3, 0x23, 0x62, 0xc8, 0x4c, 0xbf, 0xc0, 0xda, 0xfe, 0x72, 0xf3, 0x21, 0x04, 0x2a, 0x12, 0x3b, 0x77, + 0x04, 0x3e, 0x0d, 0xb0, 0x79, 0x29, 0x2d, 0x85, 0x56, 0x85, 0xce, 0x55, 0x5b, 0xbd, 0x34, 0x94, 0x0d, 0x51, 0x04, + 0x92, 0x59, 0x96, 0xf0, 0x51, 0xd6, 0x30, 0xc8, 0xa9, 0xdf, 0x35, 0x20, 0xdb, 0x1e, 0x06, 0xeb, 0x7b, 0xaa, 0x2c, + 0xf5, 0xfd, 0xd9, 0x4f, 0x9f, 0xf0, 0xb1, 0x0e, 0x61, 0x95, 0x01, 0xd7, 0x6c, 0x5e, 0xe3, 0xa1, 0x77, 0x9f, 0xcc, + 0xa0, 0x7e, 0xc2, 0x91, 0xbe, 0xc1, 0xd7, 0xc8, 0xfd, 0xcc, 0xcb, 0xf2, 0xca, 0x7b, 0x49, 0x9e, 0x6d, 0x53, 0xea, + 0x27, 0x2d, 0x56, 0xbc, 0x81, 0x3f, 0x75, 0x7e, 0x68, 0xc5, 0xf7, 0x65, 0x75, 0x67, 0xdb, 0x99, 0x33, 0x2c, 0x33, + 0xd8, 0x83, 0x19, 0xba, 0xeb, 0xa3, 0x56, 0x2a, 0xa4, 0x5e, 0xe9, 0xcb, 0x07, 0xef, 0x53, 0xef, 0x53, 0x26, 0x4d, + 0x74, 0x13, 0x98, 0xa2, 0xd2, 0xd7, 0x21, 0xca, 0x0e, 0x69, 0x62, 0xda, 0xa1, 0x4a, 0x14, 0x1d, 0x5e, 0x98, 0x65, + 0x29, 0xc0, 0xf0, 0x8d, 0xe5, 0x53, 0x86, 0x6b, 0x25, 0xa9, 0x20, 0xd4, 0x1a, 0xc4, 0x67, 0x93, 0xe9, 0x7d, 0x99, + 0x1b, 0x0a, 0x58, 0xb0, 0xf8, 0x3a, 0x86, 0x5d, 0xa4, 0x7f, 0x38, 0x7e, 0x27, 0xc1, 0x39, 0xe1, 0x70, 0x64, 0x03, + 0x01, 0x94, 0x69, 0xbb, 0xe0, 0xe2, 0x7e, 0x83, 0x3d, 0xcc, 0xd2, 0x7f, 0x42, 0x6a, 0xc1, 0x69, 0xa0, 0x97, 0xe8, + 0xff, 0xba, 0x33, 0x7f, 0x2a, 0x5f, 0x2f, 0x2c, 0xe6, 0x44, 0xb8, 0xc5, 0xd9, 0x57, 0x96, 0x59, 0xe5, 0x8a, 0xfb, + 0x03, 0x23, 0x13, 0xad, 0x5d, 0x9f, 0x1f, 0xac, 0x56, 0xd4, 0x2a, 0xd4, 0xd0, 0x57, 0xee, 0x7f, 0xa6, 0x7b, 0xb9, + 0x67, 0xc6, 0x3c, 0x14, 0x73, 0x87, 0x75, 0xd1, 0xd0, 0xf8, 0x0c, 0xd1, 0x10, 0xa5, 0xc6, 0x6a, 0xc0, 0x66, 0x4c, + 0xea, 0xd7, 0x83, 0x08, 0x4b, 0xe9, 0x9c, 0x18, 0x55, 0x6a, 0x91, 0x41, 0x82, 0xc9, 0xf1, 0x5c, 0xda, 0x1c, 0x0a, + 0x44, 0xd0, 0xcc, 0x6b, 0x68, 0xf4, 0x35, 0x1e, 0x56, 0xb8, 0xd1, 0xcd, 0x1e, 0x31, 0x64, 0x04, 0x41, 0x65, 0xd9, + 0x18, 0xd9, 0x2e, 0x46, 0x51, 0x38, 0xf5, 0xb3, 0x43, 0x41, 0xf1, 0xcb, 0x99, 0x2f, 0x4d, 0x76, 0xdc, 0x3d, 0x1a, + 0x80, 0xa2, 0x58, 0x97, 0x78, 0xd9, 0x66, 0x22, 0x37, 0xb9, 0xc1, 0x94, 0x20, 0x88, 0x39, 0xfc, 0x09, 0xb2, 0xa4, + 0x88, 0xe9, 0x22, 0x6e, 0x2e, 0xcd, 0xc5, 0xa8, 0x4c, 0x76, 0xf5, 0xc0, 0x6d, 0x68, 0x54, 0xab, 0x89, 0x5e, 0x6b, + 0xdd, 0x0f, 0x0a, 0xd1, 0x09, 0x8b, 0x27, 0xf2, 0x8a, 0x85, 0x48, 0x82, 0x81, 0x00, 0x45, 0xdb, 0xc2, 0x28, 0x0a, + 0xbd, 0x16, 0xd3, 0xd9, 0x72, 0x7e, 0x2e, 0xd3, 0x50, 0x34, 0x3a, 0xe3, 0x96, 0x81, 0x55, 0x3f, 0x6d, 0x96, 0x1f, + 0x78, 0xfc, 0x4f, 0x62, 0xc2, 0xdb, 0x1e, 0x7a, 0x06, 0xe2, 0x53, 0x0f, 0x28, 0xd3, 0x5d, 0x02, 0x85, 0xe9, 0xb9, + 0x8b, 0x50, 0x22, 0x29, 0xea, 0xc6, 0x9c, 0x58, 0x72, 0x1f, 0x95, 0xf8, 0xbe, 0x1a, 0x1f, 0xc2, 0x11, 0xd5, 0x87, + 0xc4, 0x15, 0x05, 0x2c, 0xf2, 0xac, 0x64, 0xf3, 0x39, 0xcb, 0xb8, 0x0e, 0x8b, 0x35, 0x73, 0xce, 0x1b, 0x5e, 0x96, + 0xfa, 0xa0, 0x64, 0x04, 0xef, 0x07, 0x73, 0x08, 0x55, 0xae, 0xc8, 0x0c, 0xf9, 0x55, 0x89, 0xda, 0x0a, 0x58, 0xe3, + 0x0a, 0xc2, 0x6c, 0xed, 0x15, 0xaf, 0x6f, 0x8b, 0x95, 0xfe, 0x40, 0xae, 0x11, 0xf7, 0x70, 0x08, 0x00, 0x0c, 0xfb, + 0xdd, 0x09, 0x8a, 0x91, 0x0a, 0x17, 0xe6, 0x62, 0x68, 0x40, 0xc2, 0x36, 0x48, 0x99, 0xed, 0xc7, 0xf9, 0xf0, 0xee, + 0x9f, 0xd6, 0x9c, 0x1d, 0xac, 0x95, 0x70, 0xed, 0xe8, 0x2a, 0x13, 0xe4, 0xe5, 0x43, 0x94, 0x9d, 0xb9, 0x6d, 0xae, + 0x96, 0x05, 0x51, 0x69, 0x31, 0x9e, 0xad, 0xc4, 0xfd, 0x32, 0x85, 0xc7, 0xce, 0x22, 0xd8, 0x99, 0x97, 0xe0, 0x12, + 0x10, 0x7d, 0x90, 0xf1, 0x91, 0x0d, 0x12, 0xbd, 0xf2, 0x6c, 0xfc, 0x59, 0x75, 0xef, 0x51, 0x9b, 0x2a, 0x52, 0xbb, + 0x1e, 0xb8, 0x3b, 0x94, 0xa4, 0x82, 0x69, 0x77, 0x03, 0xd6, 0xcc, 0xeb, 0x89, 0xc9, 0x37, 0x0a, 0xe2, 0x06, 0x38, + 0xfb, 0x6e, 0x1c, 0x68, 0x1a, 0x58, 0x6f, 0x3e, 0xa2, 0x68, 0x10, 0x6a, 0x44, 0x9c, 0x9b, 0xf5, 0xfa, 0xa5, 0xdf, + 0x81, 0x00, 0xa4, 0x60, 0x56, 0x12, 0xbc, 0x77, 0xe5, 0xa4, 0x10, 0x84, 0xd8, 0x02, 0x88, 0xe9, 0x06, 0x12, 0xc7, + 0x11, 0xe5, 0x1a, 0xcf, 0xbe, 0x59, 0x7a, 0xf4, 0xa2, 0x23, 0x76, 0x7f, 0x09, 0xac, 0xe9, 0x65, 0x07, 0xdb, 0xb9, + 0x09, 0x49, 0x85, 0x32, 0xf4, 0xaa, 0x9e, 0xdd, 0xb0, 0xb9, 0x75, 0x2c, 0x0b, 0x3d, 0x7a, 0x08, 0x72, 0xc9, 0xbc, + 0xb7, 0xd5, 0x18, 0x08, 0xa5, 0x9b, 0x5f, 0x08, 0x14, 0x47, 0xeb, 0x5f, 0x68, 0x93, 0x0c, 0x6d, 0x9c, 0xdb, 0xb4, + 0xb3, 0x66, 0xb7, 0x29, 0xac, 0x5c, 0x72, 0x73, 0xbd, 0x38, 0xa6, 0xa8, 0xa7, 0xf2, 0xbd, 0xd6, 0xb2, 0x67, 0x63, + 0xa0, 0x86, 0xf6, 0xc8, 0x27, 0x85, 0xd0, 0x1b, 0xb6, 0x62, 0x69, 0x24, 0x93, 0xe6, 0xce, 0x3b, 0x27, 0xd4, 0xe6, + 0x61, 0x87, 0xc4, 0x09, 0x73, 0xeb, 0xbf, 0xcf, 0x23, 0x29, 0x8b, 0xf7, 0x29, 0x14, 0x6e, 0x86, 0xea, 0x86, 0xb1, + 0xe8, 0x38, 0x01, 0xb7, 0x95, 0xf5, 0xd3, 0x8c, 0x44, 0xac, 0x16, 0x16, 0xc6, 0x33, 0x80, 0xa9, 0x98, 0x22, 0x6e, + 0x55, 0x30, 0xd4, 0x20, 0x39, 0x57, 0x83, 0x60, 0xa6, 0xd7, 0x8c, 0x9d, 0x79, 0x99, 0xb7, 0xd0, 0xd6, 0xc6, 0x2c, + 0x2c, 0xd4, 0x6c, 0x4c, 0xcd, 0x93, 0x49, 0x01, 0x4b, 0x23, 0xe8, 0xf6, 0x98, 0x1e, 0xee, 0xae, 0x91, 0xef, 0x96, + 0x23, 0x67, 0x17, 0x83, 0xf9, 0xd8, 0xcb, 0xec, 0x71, 0xea, 0xc1, 0xcb, 0x04, 0x33, 0x42, 0x85, 0xad, 0xe2, 0x02, + 0xda, 0xb3, 0xa6, 0xff, 0xc0, 0x37, 0xf1, 0x31, 0x07, 0x37, 0x66, 0xec, 0xad, 0x59, 0xba, 0xe2, 0x3d, 0x1d, 0x23, + 0x64, 0x11, 0x23, 0xf2, 0x9c, 0x35, 0xc5, 0xdc, 0x4a, 0x15, 0xe3, 0x1e, 0x14, 0x82, 0xe5, 0x2b, 0x4c, 0x05, 0x10, + 0x0e, 0x66, 0x37, 0x1a, 0x1c, 0x62, 0x7d, 0xdc, 0xba, 0x5b, 0x20, 0x04, 0x06, 0x50, 0x5d, 0x9c, 0x73, 0x34, 0xd1, + 0x01, 0x90, 0xfc, 0x3e, 0x12, 0x00, 0x49, 0x60, 0x86, 0x22, 0x01, 0x46, 0xaf, 0x5a, 0xfa, 0x9a, 0x17, 0x6b, 0x8c, + 0x8c, 0xd8, 0x23, 0x08, 0xb6, 0x72, 0x8f, 0x2c, 0x90, 0x66, 0x73, 0xaf, 0x75, 0xc2, 0xb7, 0x67, 0x45, 0x25, 0x0e, + 0x2e, 0xbf, 0x2a, 0x23, 0xe9, 0x9f, 0x0c, 0x6a, 0xac, 0x63, 0xe5, 0x29, 0xfd, 0x98, 0xa9, 0xa3, 0x47, 0x77, 0x69, + 0x9a, 0x4e, 0xe6, 0xa0, 0xd8, 0x43, 0x36, 0x28, 0xab, 0x64, 0xec, 0xc4, 0x39, 0x74, 0x22, 0xa9, 0x7f, 0x1c, 0xbd, + 0xbc, 0x53, 0x8f, 0xa2, 0x34, 0xb7, 0xeb, 0x09, 0xb5, 0x72, 0xaa, 0xdd, 0x08, 0xbc, 0x49, 0x79, 0x26, 0x75, 0xc6, + 0x96, 0xfa, 0xa5, 0x42, 0x2a, 0x3b, 0x35, 0x26, 0xb1, 0x93, 0xf3, 0x32, 0xe7, 0xe8, 0x29, 0xbf, 0x10, 0xc6, 0x81, + 0xb1, 0x3f, 0x9d, 0xb6, 0xde, 0xaf, 0xd9, 0x19, 0xe2, 0xf1, 0x6a, 0xaa, 0xf6, 0x21, 0x5d, 0xab, 0x26, 0xa6, 0x40, + 0xd3, 0x9e, 0xa6, 0xff, 0xc9, 0x80, 0x3e, 0x0f, 0xc1, 0x9e, 0xe9, 0x27, 0x21, 0xd5, 0x0e, 0xa2, 0xfd, 0x41, 0x0b, + 0xaf, 0xf0, 0x35, 0x4a, 0xa8, 0xfe, 0xce, 0x09, 0xd0, 0xf1, 0xbd, 0x6e, 0x10, 0x5b, 0x92, 0xb8, 0x98, 0x8b, 0x54, + 0x76, 0x8e, 0x19, 0xd5, 0x40, 0x2e, 0x88, 0x14, 0xcf, 0x75, 0x1a, 0x95, 0x85, 0x2c, 0x79, 0x83, 0x1b, 0x3f, 0xfb, + 0x35, 0x53, 0x28, 0xfc, 0x6a, 0x38, 0x08, 0x58, 0x06, 0x90, 0x30, 0xef, 0x5e, 0x69, 0xce, 0x99, 0x9d, 0x8d, 0x18, + 0xb2, 0x00, 0x2e, 0x75, 0xec, 0x23, 0x74, 0x12, 0x00, 0x10, 0x1d, 0x13, 0x63, 0x20, 0xaf, 0x76, 0x54, 0xff, 0x03, + 0x1c, 0x7a, 0x27, 0xbd, 0x5a, 0x73, 0x37, 0x81, 0x28, 0x42, 0x40, 0x80, 0xc4, 0xde, 0x50, 0x10, 0x45, 0xcb, 0x41, + 0x24, 0x55, 0x62, 0x27, 0xb4, 0x15, 0x9a, 0x05, 0x37, 0xb2, 0x11, 0x69, 0x04, 0xd0, 0x2b, 0xb8, 0x10, 0x33, 0x02, + 0x65, 0x1e, 0x47, 0x1a, 0xbf, 0xa0, 0xc4, 0xcc, 0x4b, 0xc5, 0xe8, 0x73, 0x8a, 0x7a, 0xef, 0x41, 0x74, 0xcf, 0xcd, + 0xa3, 0xd6, 0xc7, 0x84, 0x10, 0x3d, 0x01, 0x6b, 0x28, 0xab, 0x9f, 0xa2, 0x0c, 0x30, 0x1a, 0xa0, 0x6c, 0xef, 0x70, + 0x1e, 0xb0, 0x7c, 0xa9, 0x79, 0x52, 0x0f, 0x1d, 0xf3, 0x88, 0x5c, 0x3a, 0x9f, 0xf7, 0xeb, 0xb8, 0x5e, 0xd4, 0x0e, + 0x2a, 0x04, 0x3c, 0x56, 0x2f, 0xd5, 0x8d, 0x20, 0x37, 0x14, 0xff, 0x45, 0xc5, 0xd4, 0x18, 0xf6, 0x95, 0x5f, 0x4c, + 0x27, 0x1d, 0x6a, 0x87, 0xf5, 0x2e, 0x72, 0x75, 0x2a, 0x4a, 0x00, 0x4e, 0xbb, 0x1d, 0xda, 0x39, 0xb3, 0xe3, 0x6f, + 0x77, 0xfd, 0x68, 0x95, 0x95, 0x6a, 0x51, 0xe7, 0x59, 0x43, 0x41, 0x79, 0x39, 0x1d, 0xff, 0x0b, 0x4f, 0xf6, 0xf2, + 0x64, 0x30, 0xa7, 0x16, 0x61, 0x9c, 0xba, 0xf3, 0xec, 0xfd, 0xc1, 0xdb, 0x61, 0x4b, 0x88, 0x9d, 0xea, 0xe6, 0xd7, + 0x7a, 0xe4, 0x8b, 0xa9, 0xdb, 0x7a, 0x82, 0x1b, 0x37, 0xb7, 0xce, 0xd8, 0xab, 0xc7, 0xd0, 0x31, 0x01, 0xe0, 0xad, + 0x25, 0x8a, 0xa2, 0x22, 0xfc, 0xfb, 0xe3, 0xe9, 0xa1, 0xe6, 0xf4, 0xa0, 0x6f, 0xe3, 0x9d, 0x18, 0x34, 0x05, 0x26, + 0x58, 0x07, 0x0c, 0xf3, 0x01, 0xfd, 0xae, 0xa4, 0x1a, 0xdf, 0xf7, 0xa2, 0xc8, 0x41, 0x4c, 0x66, 0xf0, 0xa5, 0xf1, + 0x4d, 0x56, 0x64, 0x7c, 0x51, 0x01, 0xda, 0xbc, 0x4c, 0xb6, 0x0b, 0xc7, 0x30, 0x85, 0x69, 0xb7, 0xee, 0x7b, 0xec, + 0xa0, 0x5c, 0xdc, 0x1a, 0xc6, 0x6f, 0x24, 0x82, 0xec, 0x8d, 0x65, 0xe6, 0x03, 0xc5, 0xaf, 0x9f, 0x3a, 0x26, 0xb9, + 0xe7, 0x0d, 0xf7, 0x87, 0xa8, 0xa9, 0xd0, 0xf9, 0x5c, 0x79, 0xb7, 0x9c, 0xdf, 0x85, 0xd7, 0xaa, 0x50, 0x77, 0x64, + 0xc9, 0x48, 0xd3, 0x69, 0xe8, 0xe9, 0x5a, 0x2d, 0xda, 0x9c, 0xb8, 0x0e, 0xda, 0xb6, 0xd8, 0x04, 0x12, 0x4b, 0xce, + 0x60, 0xa6, 0xe4, 0x4d, 0x2c, 0x08, 0x0e, 0x1d, 0x41, 0xa1, 0xbb, 0x28, 0x0e, 0x91, 0x30, 0x60, 0xb3, 0x43, 0x85, + 0xdd, 0x47, 0xb0, 0xf1, 0xd5, 0xbc, 0x50, 0xe4, 0x19, 0xab, 0xc2, 0x9c, 0xa9, 0x66, 0x56, 0xb5, 0x5f, 0x0c, 0x70, + 0xf6, 0x4f, 0xb8, 0x97, 0x4e, 0x0c, 0xd6, 0x83, 0x4c, 0x49, 0x0d, 0x9d, 0x72, 0x8a, 0x6a, 0x1e, 0xb1, 0x8d, 0x35, + 0x14, 0x50, 0x3b, 0x3c, 0x32, 0x1c, 0x6e, 0x7a, 0x6f, 0x7c, 0x3e, 0xf0, 0x2c, 0x36, 0xf0, 0xce, 0x21, 0xb0, 0x10, + 0xfb, 0x51, 0xbb, 0x38, 0xb4, 0x35, 0x9b, 0xa1, 0xb0, 0x8d, 0x86, 0x42, 0x3a, 0xb3, 0x17, 0x24, 0xd3, 0x41, 0x1a, + 0x48, 0x5b, 0x87, 0x89, 0xc6, 0xde, 0x57, 0x07, 0xba, 0x03, 0x8d, 0x37, 0x3d, 0xa2, 0xd0, 0xc6, 0xae, 0x1a, 0xc0, + 0x2b, 0x3a, 0x97, 0xe8, 0x66, 0xdf, 0x12, 0xd8, 0xaf, 0x36, 0x83, 0x1b, 0xcb, 0x97, 0x00, 0xa6, 0x14, 0x90, 0x6d, + 0xd9, 0xf8, 0xdc, 0x73, 0x3e, 0x90, 0x4d, 0x98, 0x1c, 0x8d, 0xa3, 0x76, 0x11, 0x76, 0x69, 0x8d, 0xb7, 0x1c, 0x4d, + 0xf5, 0xcf, 0xa5, 0x08, 0x0b, 0xac, 0x2e, 0x98, 0xb6, 0xc5, 0x47, 0x23, 0xac, 0x49, 0x51, 0xb7, 0x87, 0x05, 0xd4, + 0xd8, 0x61, 0x21, 0x95, 0xd6, 0x6b, 0x89, 0x8b, 0x95, 0x18, 0x5f, 0xd3, 0x53, 0x28, 0x0e, 0x2c, 0x3b, 0x47, 0x40, + 0xe3, 0xc1, 0x3a, 0xdf, 0x0a, 0x5f, 0x2a, 0xdc, 0x2f, 0xe5, 0xa8, 0xe4, 0x99, 0x26, 0xce, 0xf5, 0x02, 0xce, 0x08, + 0x89, 0xcb, 0x0f, 0xd8, 0x38, 0x96, 0x00, 0x5b, 0xa6, 0xf7, 0xff, 0x50, 0x87, 0x05, 0xdb, 0x21, 0xc0, 0x2b, 0xee, + 0xa2, 0x7d, 0xa0, 0x5c, 0x01, 0xa4, 0xc9, 0x51, 0x86, 0x5c, 0x7e, 0xd6, 0xbd, 0x42, 0xc6, 0xb4, 0x4f, 0xa2, 0x63, + 0xcb, 0x76, 0x76, 0x20, 0x99, 0x23, 0x43, 0xc2, 0xb8, 0xf5, 0x2a, 0x65, 0xe1, 0x6e, 0x80, 0x63, 0x44, 0xb1, 0xb4, + 0x62, 0x7e, 0x99, 0x29, 0xf2, 0x0a, 0xd8, 0x8d, 0x93, 0xa6, 0xbd, 0x36, 0xa6, 0x4b, 0x64, 0x63, 0x30, 0x76, 0xaa, + 0x08, 0x2c, 0x8c, 0x41, 0x55, 0xb2, 0x20, 0xfc, 0x63, 0x4f, 0x38, 0xe0, 0x7f, 0x72, 0x26, 0x28, 0x3f, 0x1e, 0xcb, + 0x79, 0x52, 0x61, 0x1e, 0xc8, 0x14, 0xf6, 0x70, 0xfa, 0xd2, 0xbf, 0xa2, 0x4f, 0xa9, 0xc0, 0x9a, 0x24, 0xc2, 0xb8, + 0x01, 0x06, 0x55, 0x1b, 0x00, 0x74, 0x83, 0x14, 0x6f, 0x12, 0xd0, 0xe4, 0x26, 0xb4, 0x0a, 0xe5, 0x28, 0x1d, 0xb0, + 0x52, 0xe4, 0x66, 0xd4, 0x14, 0xc1, 0x46, 0xda, 0x41, 0x8a, 0x12, 0x0c, 0x3b, 0xa9, 0x06, 0x69, 0x95, 0x7a, 0x38, + 0x08, 0x7f, 0x4d, 0x80, 0x0e, 0x40, 0x1e, 0x96, 0xff, 0x65, 0x32, 0xc2, 0xcb, 0x23, 0x2b, 0xb9, 0x47, 0xcd, 0x51, + 0x63, 0x62, 0x1a, 0x3a, 0xb9, 0xa5, 0x13, 0xde, 0xd5, 0x1c, 0x71, 0x36, 0x0e, 0x6a, 0xab, 0x9a, 0x0f, 0x06, 0x43, + 0xa7, 0x4e, 0x3a, 0x82, 0xc2, 0x47, 0x39, 0x06, 0x13, 0xda, 0xcd, 0x14, 0x3d, 0x0f, 0x7b, 0x99, 0x97, 0x93, 0x6e, + 0x36, 0x53, 0x00, 0xb6, 0x5a, 0x0a, 0x5b, 0x86, 0x81, 0x31, 0x8c, 0x3f, 0x02, 0x72, 0xc3, 0xa7, 0xcf, 0x4b, 0x23, + 0x8f, 0x4a, 0x2f, 0x6f, 0x7e, 0xf8, 0xf8, 0x31, 0x80, 0xc1, 0x50, 0xd1, 0xe0, 0xc3, 0x67, 0x7d, 0x35, 0xbe, 0x93, + 0xc9, 0xc6, 0x1a, 0xe3, 0x1c, 0x44, 0x79, 0x16, 0xda, 0x91, 0x9f, 0x95, 0x75, 0x51, 0x0c, 0xdb, 0xd7, 0x17, 0x95, + 0xd7, 0x97, 0x22, 0xa4, 0x6a, 0x41, 0x2d, 0x6f, 0x71, 0x8a, 0x8d, 0x43, 0x28, 0x34, 0xe6, 0xa0, 0x08, 0x19, 0x8e, + 0x22, 0x6e, 0xee, 0x34, 0x14, 0x03, 0x52, 0x22, 0x12, 0xe6, 0xd4, 0x69, 0xed, 0x6d, 0x4e, 0xb0, 0xd9, 0x3b, 0x53, + 0x4b, 0x0d, 0xaf, 0x31, 0x61, 0x65, 0xe7, 0x48, 0x91, 0xc0, 0xa5, 0x2d, 0xbb, 0xb5, 0xc8, 0x54, 0xdd, 0x8d, 0x86, + 0xcc, 0x99, 0x15, 0x14, 0xab, 0x97, 0xcf, 0x0a, 0x87, 0x56, 0x50, 0xfc, 0xa6, 0xc1, 0x06, 0xc4, 0x38, 0x76, 0x7f, + 0x7c, 0xae, 0x82, 0x7f, 0xf8, 0x39, 0xdc, 0x93, 0x3f, 0xa6, 0x17, 0xac, 0x52, 0xcc, 0x06, 0x35, 0xdf, 0x25, 0xca, + 0xf4, 0xda, 0x9c, 0x09, 0x4c, 0x1c, 0xf3, 0x82, 0x1e, 0x3e, 0x4b, 0x5f, 0x14, 0xa3, 0xcd, 0xa0, 0x22, 0x65, 0x52, + 0x01, 0x44, 0x34, 0xe9, 0x0e, 0xa9, 0xd7, 0x58, 0xa4, 0x2c, 0x9b, 0x62, 0x9b, 0xe6, 0x4a, 0x6d, 0x1f, 0x3b, 0x6a, + 0x6a, 0xdd, 0x90, 0x48, 0x3c, 0xa4, 0xf9, 0x0f, 0xc5, 0xd7, 0x31, 0x16, 0x84, 0x48, 0x21, 0xad, 0x2f, 0xce, 0x74, + 0x7a, 0x7c, 0x36, 0x8a, 0x3b, 0x1a, 0xc7, 0xa3, 0xfc, 0x6b, 0x8d, 0xe9, 0x54, 0xb0, 0x1f, 0xd2, 0x19, 0x12, 0x47, + 0x9e, 0x1b, 0x17, 0xdd, 0xad, 0xcf, 0x3a, 0x7c, 0x10, 0xb5, 0xbc, 0xe4, 0x1d, 0xbb, 0x21, 0x54, 0x32, 0x98, 0xeb, + 0x94, 0x40, 0x6b, 0xea, 0x0f, 0xfc, 0x0d, 0x5f, 0xb8, 0x51, 0x5d, 0x3a, 0x24, 0x5a, 0xd8, 0x90, 0x60, 0x3a, 0x49, + 0x8a, 0xb4, 0xe0, 0x12, 0x26, 0x9b, 0xa0, 0x58, 0xbb, 0xb0, 0xe0, 0xb3, 0xb0, 0x38, 0x64, 0xf3, 0x8b, 0x2e, 0xc4, + 0x78, 0x07, 0xd6, 0x19, 0xaa, 0x3c, 0x73, 0x8e, 0x41, 0x33, 0x78, 0x61, 0x61, 0xaf, 0x0a, 0x72, 0x88, 0x0b, 0xeb, + 0x80, 0xca, 0xc7, 0xa8, 0x91, 0xf1, 0xe8, 0xad, 0x4d, 0x21, 0x3d, 0xd0, 0x7d, 0xff, 0xce, 0x6f, 0xaf, 0x22, 0x67, + 0x60, 0x88, 0x0b, 0x91, 0x17, 0x1e, 0xcd, 0x6c, 0x70, 0x81, 0x71, 0xe1, 0x6d, 0x8e, 0x7a, 0xf1, 0x1c, 0xce, 0xdb, + 0x37, 0x54, 0x6a, 0x78, 0xc5, 0x94, 0x8e, 0x13, 0x14, 0xdc, 0xa4, 0x97, 0x15, 0xc8, 0xbb, 0x93, 0xb4, 0xd8, 0x35, + 0xbb, 0x14, 0xb2, 0x21, 0x45, 0x67, 0xed, 0x6e, 0x70, 0xca, 0xdc, 0x9e, 0x49, 0x87, 0x65, 0x4e, 0xd1, 0xcc, 0x24, + 0x0a, 0x3d, 0x87, 0x28, 0xc6, 0x8c, 0xa1, 0x49, 0xb5, 0x65, 0x5e, 0xd4, 0x92, 0x0d, 0x95, 0xba, 0x46, 0x50, 0xd6, + 0xcd, 0x91, 0xfd, 0x73, 0xe6, 0xe3, 0xdc, 0xb9, 0xc1, 0xd0, 0x03, 0x79, 0x18, 0x90, 0xb1, 0x3a, 0xc7, 0xfd, 0x85, + 0x8f, 0x69, 0xde, 0xed, 0x5e, 0x73, 0xfc, 0x6b, 0x04, 0x6d, 0x99, 0x7c, 0xd3, 0xfa, 0x07, 0xd5, 0xff, 0x65, 0x03, + 0x46, 0xd6, 0x3a, 0x3e, 0x2c, 0x36, 0xad, 0x37, 0x55, 0x4d, 0x76, 0xd0, 0xd8, 0x99, 0x26, 0x6d, 0x2c, 0x1c, 0xd4, + 0xdd, 0xdb, 0xc8, 0x24, 0x38, 0x6c, 0xde, 0x1c, 0xc2, 0x40, 0x56, 0xc6, 0x77, 0x1b, 0xa1, 0x75, 0xeb, 0xb4, 0xa9, + 0xc3, 0x2f, 0x43, 0x13, 0x0f, 0x7b, 0x8d, 0x56, 0x0c, 0x61, 0x9e, 0x4d, 0x19, 0xbb, 0x02, 0xde, 0x14, 0x41, 0x11, + 0x4f, 0xe3, 0x9a, 0x23, 0x98, 0xd2, 0x6a, 0x60, 0xc8, 0xaa, 0x11, 0xcf, 0x51, 0xa5, 0x6b, 0xd4, 0x73, 0xfb, 0xa6, + 0x07, 0x0c, 0xb9, 0x90, 0xb3, 0x5f, 0x4e, 0x6b, 0x42, 0x67, 0xeb, 0x76, 0xf4, 0x18, 0xf0, 0x0a, 0x91, 0xe8, 0xf3, + 0x41, 0x96, 0x01, 0xb1, 0xc5, 0x64, 0xa5, 0x43, 0x21, 0xc1, 0xe3, 0x76, 0xf8, 0x8c, 0xd5, 0xa7, 0xbc, 0xa7, 0x4f, + 0x59, 0xec, 0x86, 0xa6, 0xd6, 0xc1, 0xdf, 0xa9, 0x12, 0x22, 0x05, 0xae, 0xa5, 0xba, 0xcb, 0x90, 0x55, 0xa5, 0x1e, + 0xfd, 0x41, 0x91, 0x96, 0x46, 0xac, 0xc4, 0x52, 0xa9, 0x1a, 0xd3, 0xfa, 0xef, 0xb4, 0x15, 0x9d, 0xa8, 0xff, 0xb4, + 0xb0, 0x5a, 0x7d, 0x45, 0xac, 0xab, 0x84, 0xe3, 0x45, 0xaf, 0xb6, 0xe8, 0xc8, 0x10, 0x41, 0x02, 0x9f, 0x75, 0xad, + 0x37, 0x1b, 0x3a, 0x0d, 0x68, 0xbc, 0xcd, 0xe3, 0xbf, 0xea, 0xf9, 0x8c, 0xec, 0x05, 0x9a, 0xae, 0x52, 0x9b, 0x02, + 0x5d, 0x41, 0xe0, 0x9c, 0x25, 0xd6, 0x5c, 0xa5, 0x81, 0x09, 0xa6, 0x79, 0xb1, 0xe5, 0x0b, 0xa8, 0x4e, 0xb7, 0x40, + 0x1a, 0x08, 0xd2, 0x50, 0x7a, 0xa9, 0x55, 0xea, 0x36, 0xa6, 0xd3, 0x41, 0x79, 0xd6, 0x06, 0xa5, 0xf8, 0x01, 0xe5, + 0xc0, 0x95, 0x5b, 0xbd, 0x44, 0x09, 0xb2, 0xea, 0xd0, 0xbc, 0x95, 0xbd, 0x66, 0x1e, 0x52, 0xb8, 0x61, 0x4d, 0xc6, + 0x05, 0x6f, 0xfb, 0xdb, 0xdd, 0x40, 0xe6, 0x28, 0x5a, 0x47, 0x4d, 0xc9, 0x42, 0xf7, 0x88, 0xab, 0x08, 0xe9, 0xe7, + 0x85, 0xd2, 0x2d, 0xb0, 0xd3, 0xed, 0xef, 0xd0, 0xba, 0x5b, 0xd4, 0xc5, 0xf2, 0xd0, 0xc3, 0xce, 0x91, 0x7f, 0x04, + 0xef, 0x8f, 0x02, 0x16, 0xc3, 0x4f, 0x13, 0x65, 0x07, 0x6d, 0xf6, 0xfa, 0x7e, 0xb0, 0x4d, 0xbf, 0xa9, 0xb1, 0x1b, + 0xbc, 0x1d, 0xf0, 0x57, 0x1d, 0xac, 0xc2, 0x61, 0x0f, 0xcc, 0x27, 0x16, 0xbf, 0x3f, 0xbf, 0xd8, 0xd7, 0xe0, 0xf8, + 0x44, 0xb8, 0xcd, 0x54, 0x3e, 0xc0, 0xdb, 0x24, 0xb7, 0x74, 0xbd, 0x30, 0xf2, 0xea, 0x02, 0x52, 0x56, 0xce, 0x89, + 0xeb, 0x8b, 0x02, 0x57, 0xa0, 0x85, 0x12, 0x8f, 0x6e, 0x0b, 0xde, 0xb3, 0x86, 0xb4, 0x77, 0x1b, 0x63, 0xd3, 0x69, + 0xef, 0x38, 0x15, 0x87, 0x99, 0x2c, 0xcd, 0x26, 0xe4, 0xdf, 0xae, 0xa8, 0x53, 0x35, 0x70, 0x9f, 0x5f, 0xd7, 0x57, + 0xb3, 0x74, 0x37, 0xee, 0xe7, 0x4f, 0xd9, 0x9e, 0xb0, 0x95, 0x0d, 0x63, 0x76, 0xc8, 0xef, 0x0b, 0x0d, 0xe8, 0x7a, + 0x34, 0x8b, 0x90, 0xfd, 0x40, 0x80, 0xc1, 0x57, 0xe7, 0x8c, 0xa5, 0x59, 0xd9, 0x37, 0xfd, 0xc2, 0x43, 0x49, 0x41, + 0x8c, 0xca, 0x5e, 0x03, 0x64, 0xb4, 0x24, 0x5b, 0xa7, 0xc5, 0x7b, 0x61, 0x01, 0xa3, 0xef, 0x53, 0xe8, 0x54, 0x9f, + 0x64, 0x08, 0xab, 0x2e, 0xd1, 0x78, 0x4e, 0x7b, 0xc4, 0xd7, 0x79, 0x3e, 0x7c, 0x1d, 0x8b, 0x3d, 0x57, 0x58, 0x66, + 0xd8, 0x4c, 0x8c, 0x43, 0x03, 0xf1, 0xc4, 0xa1, 0xfb, 0x91, 0xd1, 0x62, 0xdc, 0x5a, 0x50, 0xc3, 0xe3, 0x2f, 0xe7, + 0x89, 0xa5, 0xeb, 0xdb, 0x80, 0x0c, 0x53, 0x44, 0x9c, 0x59, 0xd4, 0xeb, 0x8b, 0x6a, 0xd8, 0xbd, 0x2e, 0x2a, 0x08, + 0x9a, 0xcd, 0xb7, 0xf5, 0xe0, 0x06, 0xdb, 0x45, 0xed, 0x87, 0x2c, 0xd1, 0xda, 0xba, 0xdf, 0xb4, 0x1b, 0x64, 0x9f, + 0x33, 0x0e, 0xda, 0x40, 0xc0, 0xf8, 0xde, 0xbf, 0xb4, 0xd5, 0xd9, 0xde, 0x3a, 0xcd, 0x5f, 0x88, 0x8b, 0xbf, 0x93, + 0xb0, 0xbc, 0x9b, 0xe1, 0xa0, 0x94, 0x90, 0xe1, 0x04, 0x11, 0xa6, 0xc2, 0xba, 0xb8, 0xe4, 0xb3, 0x0b, 0x13, 0x2b, + 0x23, 0xac, 0x88, 0x76, 0xc4, 0x37, 0x7c, 0x9f, 0xb7, 0x05, 0x04, 0xb1, 0xb3, 0x65, 0xcc, 0x78, 0x46, 0x44, 0x89, + 0x8c, 0xec, 0x30, 0xb1, 0x69, 0x36, 0x61, 0xea, 0xd8, 0x6d, 0x32, 0x68, 0xea, 0x60, 0x9c, 0xc2, 0x06, 0xba, 0x1b, + 0xaa, 0xad, 0xb6, 0x92, 0x8c, 0xf9, 0x85, 0xac, 0x9d, 0xed, 0x8f, 0xea, 0x65, 0x5e, 0x6c, 0x3f, 0xdb, 0x86, 0xe6, + 0x55, 0x31, 0x86, 0x76, 0x20, 0xb3, 0x23, 0xdc, 0x65, 0xea, 0x1e, 0xd2, 0x78, 0xa2, 0x50, 0x6d, 0x25, 0x08, 0x07, + 0xa0, 0x90, 0xa6, 0x39, 0xe3, 0x02, 0xb3, 0xe8, 0xa3, 0x2a, 0xbc, 0xd3, 0x41, 0x59, 0x2d, 0x0d, 0xa0, 0x04, 0x88, + 0xe3, 0x4e, 0x3a, 0x8c, 0xe4, 0xc1, 0x5e, 0xa9, 0x3b, 0xb5, 0x6a, 0x9d, 0xb9, 0x58, 0xec, 0x47, 0x97, 0x5a, 0xec, + 0x11, 0xa6, 0x59, 0x52, 0xeb, 0x07, 0x5a, 0x68, 0xcf, 0x37, 0x5b, 0x13, 0xf3, 0x94, 0x71, 0x48, 0x7b, 0x68, 0x3f, + 0x14, 0xd4, 0x7a, 0x09, 0x8f, 0x95, 0x41, 0x89, 0x64, 0xa7, 0xa6, 0x9d, 0x95, 0x69, 0x0a, 0xde, 0x65, 0x99, 0x78, + 0x15, 0xfa, 0x1d, 0xe1, 0xdd, 0x96, 0x59, 0xdb, 0xc8, 0xc4, 0xcd, 0x49, 0xa4, 0x58, 0x0e, 0xce, 0x35, 0xbc, 0x2d, + 0x3a, 0x76, 0xf1, 0xdb, 0x4c, 0xad, 0x5f, 0xb0, 0x8c, 0x38, 0x5a, 0xc2, 0xcd, 0x0f, 0xe9, 0x91, 0x88, 0x02, 0xa3, + 0xfe, 0x4d, 0x5e, 0xc5, 0x89, 0x1b, 0x66, 0xfc, 0x8e, 0x86, 0x38, 0x54, 0x87, 0xba, 0x67, 0x89, 0x15, 0x88, 0x85, + 0xf3, 0xf7, 0xd5, 0x4d, 0x20, 0x97, 0xde, 0x56, 0xab, 0xe6, 0x61, 0xd4, 0x65, 0xdf, 0x83, 0x3f, 0x85, 0x31, 0x35, + 0x9f, 0xbc, 0x2a, 0xdf, 0x70, 0xcf, 0x64, 0x29, 0x97, 0xf0, 0xba, 0xde, 0x52, 0x73, 0x9c, 0xcb, 0x37, 0x5c, 0x56, + 0x59, 0x6a, 0x33, 0x82, 0x49, 0xdf, 0x5a, 0xe1, 0x38, 0x82, 0x35, 0x14, 0x91, 0xb8, 0x39, 0xa8, 0x83, 0xcd, 0xb1, + 0x0e, 0xe5, 0x36, 0x13, 0xac, 0x43, 0x36, 0xd8, 0x03, 0xc2, 0xa9, 0xc5, 0x66, 0x8e, 0x7d, 0x6c, 0x08, 0x07, 0x74, + 0xdc, 0x9a, 0x22, 0x4a, 0x4e, 0xdd, 0x89, 0xa7, 0x96, 0xe5, 0xbb, 0x19, 0xd3, 0x74, 0x7c, 0x87, 0x18, 0x59, 0x12, + 0x8b, 0xdc, 0xcb, 0x10, 0x97, 0x43, 0x8b, 0xf6, 0x2c, 0x55, 0x21, 0x37, 0xe8, 0xd6, 0x2d, 0x37, 0x1c, 0x3e, 0x7d, + 0x38, 0x2e, 0x7a, 0x22, 0xda, 0x4a, 0x7c, 0x39, 0x85, 0x54, 0x4a, 0xf6, 0x93, 0x0a, 0x2f, 0x54, 0x48, 0xa7, 0xcf, + 0x8a, 0x04, 0xdc, 0xdc, 0x99, 0x0b, 0x57, 0x53, 0xae, 0x65, 0x8d, 0x76, 0x93, 0x9c, 0x08, 0x15, 0x96, 0xcc, 0x18, + 0xbd, 0x78, 0x3f, 0x8b, 0x16, 0xdb, 0x39, 0xc5, 0x76, 0xd8, 0xd4, 0xd4, 0x79, 0x87, 0x22, 0xa7, 0xa1, 0xb2, 0x8c, + 0xc4, 0x2c, 0xca, 0x21, 0x3e, 0x3a, 0x75, 0xbe, 0xc7, 0x28, 0x75, 0x05, 0x73, 0xaa, 0x4d, 0xc9, 0x83, 0xd3, 0xc5, + 0xf9, 0x17, 0x6e, 0x37, 0xfd, 0xe3, 0x28, 0xe8, 0xb7, 0x5a, 0x35, 0x51, 0xad, 0x1c, 0x9a, 0x5d, 0xd7, 0x6e, 0x34, + 0x26, 0xc7, 0x0e, 0x70, 0x67, 0x13, 0xc4, 0x18, 0x3e, 0x2e, 0xc3, 0x2c, 0x9a, 0xe7, 0x53, 0x7d, 0xfd, 0xf3, 0x20, + 0xca, 0xb6, 0x4c, 0xdd, 0x0a, 0xa3, 0xc0, 0xa5, 0x81, 0x07, 0xe3, 0xa8, 0x21, 0x86, 0xcd, 0xa3, 0xa3, 0x6d, 0x22, + 0x93, 0x74, 0xcf, 0x6a, 0xae, 0x00, 0x4d, 0xa7, 0x20, 0xf2, 0xef, 0x71, 0x9b, 0x2f, 0x38, 0x8e, 0x4e, 0xe9, 0xf9, + 0x17, 0xa5, 0x1f, 0x69, 0xf0, 0xc6, 0x78, 0x75, 0xc1, 0xaa, 0x27, 0x23, 0xef, 0x88, 0x87, 0x39, 0x0a, 0xe4, 0x07, + 0xf0, 0x4d, 0xe9, 0x82, 0x73, 0x63, 0xf7, 0x3d, 0xc8, 0x96, 0xed, 0x68, 0x55, 0xc4, 0x1a, 0xf9, 0x1a, 0x27, 0x2e, + 0xb5, 0xfe, 0x92, 0xcc, 0x3a, 0x09, 0xf6, 0x35, 0xf2, 0x78, 0x47, 0xbc, 0xcf, 0xf6, 0x76, 0x68, 0xe3, 0x35, 0x92, + 0xb0, 0xef, 0xb6, 0xeb, 0xaa, 0x2b, 0x4e, 0x7f, 0x62, 0x01, 0xe1, 0x02, 0x36, 0x16, 0xe8, 0x9b, 0xdb, 0x86, 0xd2, + 0xb9, 0xee, 0x1a, 0x7c, 0xaf, 0xf1, 0xce, 0x3b, 0x3b, 0xb8, 0x8a, 0x93, 0x44, 0x60, 0x76, 0xa1, 0x34, 0x26, 0xea, + 0x17, 0x86, 0xe7, 0x6a, 0xcf, 0xb7, 0xea, 0x65, 0x54, 0xb3, 0x67, 0x02, 0xfe, 0x3a, 0x1a, 0x1f, 0x95, 0x79, 0x83, + 0x85, 0xdb, 0x3a, 0x85, 0xee, 0xfc, 0x7d, 0x00, 0xaf, 0xbc, 0x6b, 0xb6, 0x2c, 0xe6, 0x3b, 0xce, 0x82, 0xa5, 0xcd, + 0xdb, 0x5d, 0x99, 0xe0, 0x97, 0x1f, 0x70, 0xaf, 0xf8, 0xd2, 0xc1, 0x93, 0x7e, 0xdd, 0x52, 0xc0, 0x5d, 0xc0, 0xe4, + 0xa5, 0x1b, 0x6e, 0x42, 0xdc, 0xac, 0x72, 0xd3, 0xb7, 0x48, 0xf9, 0xe9, 0xe9, 0xac, 0x79, 0xea, 0x10, 0xde, 0x78, + 0x54, 0x87, 0xca, 0x68, 0x6e, 0xdd, 0xc2, 0x95, 0xfe, 0x05, 0xb7, 0x35, 0x77, 0x30, 0xc3, 0x89, 0x2c, 0x89, 0xc7, + 0x93, 0xee, 0x1e, 0xa0, 0xcc, 0x03, 0x2f, 0x22, 0x29, 0xa2, 0x6d, 0x60, 0x07, 0x2d, 0x28, 0x6b, 0x0d, 0xa2, 0xd6, + 0xde, 0x23, 0x66, 0x7b, 0x69, 0xf7, 0x43, 0xf7, 0xe1, 0x03, 0x0f, 0xd6, 0x44, 0xc8, 0x19, 0x4c, 0x28, 0x7e, 0x97, + 0xf6, 0x73, 0xd8, 0x33, 0xc2, 0x95, 0x56, 0x28, 0x78, 0x8e, 0x1b, 0x54, 0x7d, 0x9f, 0x2d, 0x20, 0x5a, 0x24, 0x20, + 0x67, 0xbb, 0x16, 0xd6, 0xa8, 0x18, 0xf9, 0x49, 0x0c, 0x95, 0xd4, 0xb6, 0x7c, 0x83, 0xa5, 0xbf, 0xae, 0x02, 0x49, + 0x20, 0x31, 0x27, 0xf5, 0xbc, 0xb6, 0x2d, 0x16, 0x37, 0x4b, 0x36, 0x0f, 0x15, 0xa5, 0xe7, 0xe5, 0xd8, 0x79, 0x07, + 0xf5, 0x28, 0xb8, 0x2c, 0xdb, 0xeb, 0xdc, 0x2e, 0xed, 0xae, 0x75, 0x18, 0x4e, 0x52, 0x4e, 0x31, 0xe0, 0xa1, 0x6d, + 0x3d, 0x72, 0xb1, 0xf8, 0xf7, 0x40, 0x1a, 0xde, 0x5d, 0x7e, 0x78, 0x73, 0x4b, 0x6b, 0x4c, 0xd4, 0x66, 0x2a, 0x12, + 0xe1, 0xe4, 0x86, 0x15, 0xc9, 0x90, 0x26, 0x12, 0x3d, 0x7c, 0x91, 0x6f, 0xae, 0x35, 0xac, 0x12, 0xd9, 0x90, 0x61, + 0xb3, 0xd9, 0xcd, 0x64, 0xdc, 0xca, 0x76, 0x7f, 0x3a, 0xf4, 0x26, 0xc8, 0xd6, 0x89, 0xd2, 0x3c, 0x77, 0xd8, 0x62, + 0xed, 0x9e, 0xb1, 0xa8, 0x9f, 0x1c, 0x65, 0x8e, 0x78, 0x43, 0x4c, 0xb4, 0x4d, 0xb9, 0xb6, 0x66, 0x1e, 0x21, 0x70, + 0x6f, 0xed, 0x3f, 0x2b, 0xf6, 0xf1, 0x1a, 0xd3, 0xc7, 0xf4, 0x0b, 0xee, 0xf9, 0xc4, 0xc3, 0xcf, 0x34, 0xc9, 0x0d, + 0xb1, 0xdd, 0x45, 0x3c, 0xe4, 0xa3, 0xbb, 0x61, 0xca, 0x84, 0x65, 0xda, 0x5c, 0xb5, 0x9c, 0x1b, 0x83, 0x54, 0xa1, + 0x48, 0xe5, 0x3e, 0xa2, 0xeb, 0xb0, 0x1f, 0xce, 0xf2, 0x3d, 0x48, 0x64, 0xbe, 0x4f, 0x22, 0x10, 0xeb, 0x1f, 0x05, + 0x71, 0x8e, 0x25, 0x2f, 0x8d, 0x22, 0x8d, 0xfe, 0x84, 0x52, 0x96, 0x72, 0x08, 0xf8, 0xe6, 0x80, 0x73, 0x15, 0x5b, + 0xff, 0x7d, 0xf6, 0x7d, 0x98, 0x76, 0x7d, 0xce, 0xee, 0x16, 0x12, 0xde, 0x72, 0xe2, 0x4e, 0xe5, 0xf1, 0xa9, 0x90, + 0x7b, 0x0f, 0x72, 0x2d, 0xbf, 0xed, 0x86, 0x86, 0xce, 0xf1, 0xb2, 0x45, 0x71, 0x55, 0xa7, 0xf2, 0x47, 0x51, 0xab, + 0xaa, 0xa4, 0x2a, 0x7b, 0x1e, 0x39, 0x93, 0x93, 0xb3, 0xdc, 0xab, 0x0a, 0x43, 0x03, 0x8f, 0x38, 0x5b, 0x62, 0x9d, + 0xbd, 0xc7, 0xcc, 0xe2, 0x84, 0x8f, 0x42, 0x03, 0xc1, 0x2a, 0xe9, 0x13, 0x8e, 0xe8, 0x0b, 0xb4, 0xd3, 0xfa, 0xd2, + 0xcd, 0xed, 0x47, 0x96, 0xb2, 0x83, 0xa3, 0xf1, 0xca, 0x8a, 0x7c, 0x9d, 0x02, 0x31, 0x53, 0xac, 0x62, 0xe4, 0xf3, + 0x1b, 0x84, 0x44, 0xf3, 0x8b, 0xe9, 0x82, 0xf6, 0x2e, 0x6b, 0x51, 0x1d, 0x3e, 0x6b, 0xf6, 0xca, 0x0b, 0x40, 0x1e, + 0x57, 0x15, 0x87, 0x48, 0x7b, 0x83, 0x38, 0x4d, 0x88, 0x7a, 0x76, 0xdb, 0xb3, 0x85, 0x38, 0x31, 0xcb, 0xd1, 0x62, + 0xd2, 0xab, 0x12, 0xd9, 0x6f, 0x4f, 0x4f, 0x20, 0x25, 0x3a, 0x63, 0x2c, 0x19, 0x69, 0x4b, 0xf9, 0x86, 0xe6, 0xf4, + 0x1e, 0xd6, 0x31, 0x11, 0xb1, 0x5a, 0x00, 0x52, 0x0d, 0x29, 0xf4, 0x35, 0x6a, 0x47, 0x44, 0xf8, 0xf9, 0xc8, 0x52, + 0x29, 0x32, 0xc6, 0x37, 0xf6, 0x23, 0x6d, 0x31, 0xab, 0x88, 0x53, 0xc4, 0xac, 0x61, 0x18, 0xd9, 0xb8, 0xc4, 0x28, + 0xa8, 0x56, 0x8f, 0x37, 0xf8, 0x04, 0x83, 0x94, 0x62, 0xab, 0xeb, 0x71, 0xdc, 0xc4, 0x4f, 0x47, 0x22, 0xc2, 0x7d, + 0x82, 0x38, 0x29, 0x81, 0x8d, 0xe7, 0x6f, 0xce, 0x54, 0xe7, 0x7c, 0x69, 0xb0, 0xe7, 0x48, 0xf5, 0x38, 0xc3, 0x40, + 0x13, 0x7c, 0xf5, 0xa3, 0xd9, 0x1f, 0xcb, 0x37, 0x32, 0x8b, 0x3b, 0x09, 0xa9, 0x95, 0xd3, 0xad, 0x36, 0x5b, 0x22, + 0x7e, 0x80, 0x0b, 0x67, 0xbc, 0x47, 0x35, 0x77, 0xf9, 0xa5, 0x26, 0x06, 0x56, 0x98, 0x13, 0x72, 0x37, 0x47, 0x92, + 0xbf, 0x00, 0xe3, 0x66, 0x0d, 0x6d, 0x2e, 0xc7, 0x2e, 0x38, 0x09, 0xe4, 0xea, 0xa6, 0xf4, 0x9b, 0xcf, 0x9e, 0x81, + 0xe9, 0x61, 0xf9, 0xd1, 0xef, 0x32, 0xff, 0x65, 0xac, 0x53, 0x13, 0xfc, 0xc8, 0x51, 0x59, 0x9f, 0xcb, 0xe3, 0xdf, + 0x01, 0xb7, 0x67, 0x7d, 0xe3, 0xcf, 0x93, 0x20, 0xce, 0x35, 0x7f, 0x66, 0xc1, 0x44, 0x36, 0xbb, 0xd6, 0xde, 0xcf, + 0x9f, 0xc1, 0x9b, 0xfb, 0x3d, 0x42, 0xe5, 0x6b, 0xe7, 0x14, 0xea, 0xa6, 0x01, 0x16, 0x7a, 0xf5, 0x60, 0x1f, 0x90, + 0x9b, 0x7d, 0x88, 0x0a, 0x14, 0x39, 0xa2, 0xd2, 0x35, 0xe3, 0x1a, 0xb6, 0x55, 0x3b, 0x94, 0x3d, 0xc3, 0x79, 0x4f, + 0x9b, 0x5d, 0xb5, 0xad, 0x57, 0x69, 0x13, 0x52, 0x98, 0xc2, 0x1a, 0xe8, 0x56, 0x37, 0xec, 0xdc, 0x1c, 0x2c, 0xdf, + 0x5a, 0x06, 0x1e, 0x16, 0xae, 0x98, 0xbd, 0x98, 0xf9, 0x8e, 0x0e, 0xfc, 0x59, 0xca, 0x2b, 0x0a, 0xb5, 0xfa, 0x8d, + 0xe3, 0xa8, 0x87, 0x36, 0xe0, 0x0d, 0x7f, 0x9d, 0x50, 0x9d, 0x3d, 0x67, 0x0b, 0x23, 0x17, 0xe2, 0xd7, 0xba, 0xc1, + 0x27, 0x74, 0xa9, 0x0a, 0x26, 0xe0, 0x4b, 0x9b, 0x1d, 0x0f, 0x5e, 0x87, 0x96, 0xa4, 0xf2, 0xb8, 0x79, 0x8c, 0xe5, + 0xdc, 0x4e, 0xb9, 0x54, 0x2b, 0xf3, 0xa3, 0x1b, 0x25, 0xd8, 0x20, 0x90, 0x24, 0x58, 0x84, 0xf0, 0x4f, 0x30, 0x67, + 0x1c, 0x89, 0x21, 0x7b, 0xa3, 0x62, 0x8a, 0x16, 0x14, 0x0c, 0x91, 0x52, 0xb6, 0x58, 0x60, 0xb3, 0xf7, 0x08, 0x7f, + 0x7f, 0xdf, 0x3a, 0xf5, 0xb4, 0xef, 0x9d, 0x02, 0xed, 0xdb, 0x27, 0xa5, 0xb4, 0xef, 0x9f, 0x14, 0x9d, 0xa5, 0x56, + 0x19, 0xfb, 0x6a, 0xc9, 0xbe, 0x1a, 0xd9, 0x63, 0x3b, 0xdb, 0x43, 0x2b, 0x8e, 0x6b, 0xd0, 0x8e, 0xc5, 0x82, 0x6f, + 0xc9, 0x01, 0xc7, 0x11, 0xcb, 0x92, 0xf5, 0x63, 0xa1, 0xb9, 0x77, 0xb5, 0x1e, 0xf9, 0xf6, 0x00, 0x15, 0x85, 0xb7, + 0xc3, 0xda, 0x8d, 0xac, 0x2c, 0xcf, 0x34, 0xb7, 0x4e, 0xe2, 0xd4, 0xd9, 0xde, 0x42, 0xf1, 0x74, 0xea, 0x08, 0x7e, + 0xd6, 0xe4, 0x70, 0x86, 0x4a, 0x13, 0xf8, 0x8f, 0x1c, 0x3f, 0x36, 0x5a, 0x5b, 0xd1, 0x78, 0xf3, 0xbd, 0x27, 0x1a, + 0x9a, 0xbf, 0xb8, 0x8a, 0x58, 0x98, 0x96, 0x93, 0x7b, 0x07, 0x53, 0x1f, 0x4a, 0xd6, 0xe2, 0x76, 0x07, 0x64, 0xb6, + 0x94, 0x97, 0x39, 0x41, 0x08, 0xa7, 0xba, 0x85, 0xff, 0x2c, 0xa0, 0x31, 0x27, 0x2d, 0x17, 0x53, 0xf9, 0xbc, 0xd5, + 0x86, 0xda, 0xce, 0x59, 0x83, 0x78, 0xec, 0xca, 0x74, 0x57, 0x82, 0x29, 0x3c, 0x9f, 0xc5, 0x15, 0x90, 0xfa, 0x85, + 0x35, 0xff, 0x46, 0xd9, 0xc3, 0xe5, 0x4e, 0x4c, 0x83, 0xff, 0xf7, 0x64, 0x3b, 0x08, 0x27, 0x74, 0x35, 0x4e, 0xb9, + 0x24, 0xe2, 0x7e, 0x6e, 0xa5, 0xed, 0x99, 0x37, 0x72, 0x7d, 0xfb, 0x8c, 0x61, 0x7a, 0xae, 0x42, 0x20, 0x53, 0x68, + 0x3f, 0xdd, 0x3f, 0x8f, 0x71, 0x48, 0xb0, 0x62, 0xcf, 0x09, 0x56, 0x19, 0x98, 0x92, 0x77, 0x33, 0x99, 0x9e, 0xbf, + 0xfc, 0x5f, 0xe6, 0xf7, 0x92, 0xf5, 0xa0, 0x37, 0x7b, 0xcb, 0x36, 0xb7, 0x85, 0x75, 0x69, 0x31, 0xb3, 0x7e, 0x34, + 0xd3, 0xfa, 0x5f, 0x71, 0x80, 0xb7, 0xdb, 0xe9, 0x8a, 0x3f, 0xaa, 0x0c, 0xd2, 0x38, 0x64, 0xdd, 0x6f, 0x4b, 0xd6, + 0x03, 0xde, 0xed, 0xed, 0xf3, 0x5b, 0x1c, 0xfe, 0xb1, 0x09, 0x7c, 0x4b, 0x17, 0x00, 0x02, 0xf8, 0x91, 0xbb, 0x1c, + 0xbb, 0x9c, 0xc9, 0x9d, 0xeb, 0x0a, 0x0b, 0xaa, 0x96, 0x43, 0x16, 0x2e, 0x96, 0x6c, 0x41, 0x3e, 0x5e, 0x13, 0xd9, + 0xe8, 0xc0, 0xec, 0x12, 0xb1, 0xf7, 0x44, 0x09, 0xde, 0x6c, 0xf3, 0x29, 0xd1, 0xf3, 0xe7, 0x04, 0xda, 0x66, 0xd7, + 0x89, 0x0d, 0xb9, 0xb6, 0x38, 0x15, 0x27, 0x00, 0xec, 0x7b, 0xe6, 0xc2, 0xe0, 0x6c, 0xa4, 0xf6, 0x41, 0x0b, 0xa7, + 0xb7, 0x04, 0xfa, 0xd9, 0x38, 0x45, 0xde, 0xef, 0x00, 0x95, 0x52, 0x78, 0xe2, 0x10, 0x13, 0x2a, 0x76, 0xf8, 0x9c, + 0x52, 0x9f, 0x43, 0x8e, 0x9c, 0x77, 0xc0, 0x89, 0x5b, 0xda, 0x74, 0x63, 0x79, 0xbb, 0xe1, 0xbb, 0x8a, 0x74, 0x65, + 0xf5, 0xb0, 0x84, 0xb6, 0xcd, 0xd1, 0x19, 0x67, 0x94, 0xa0, 0xc4, 0x08, 0x29, 0xc3, 0xf3, 0x63, 0x30, 0x48, 0x26, + 0xe3, 0x30, 0xd1, 0x6b, 0xce, 0x0a, 0xa3, 0xff, 0x44, 0x08, 0x23, 0x84, 0x9f, 0x5f, 0x67, 0x6c, 0xdf, 0xa3, 0xbb, + 0xb6, 0xe9, 0x5e, 0x6f, 0x15, 0xb1, 0xcf, 0x2b, 0x0d, 0x4a, 0xa5, 0xd2, 0x58, 0xdb, 0x8c, 0x7f, 0x75, 0x52, 0x9d, + 0x46, 0x1d, 0x8e, 0x70, 0x76, 0xa6, 0xcd, 0xf9, 0xbf, 0x9e, 0x99, 0xb9, 0xd7, 0xce, 0xcb, 0x9e, 0x19, 0xeb, 0xc6, + 0x25, 0x91, 0x16, 0xe4, 0x26, 0x0d, 0x25, 0xeb, 0xb1, 0xd1, 0xde, 0x94, 0x4c, 0xfd, 0xc8, 0xa4, 0x80, 0x8d, 0xb1, + 0xda, 0xe1, 0x32, 0x3e, 0xcd, 0xfb, 0xa8, 0x24, 0x0b, 0x2e, 0xc4, 0xf9, 0x70, 0xbb, 0xb1, 0x22, 0x85, 0x1d, 0x68, + 0xf2, 0xeb, 0x4f, 0xc9, 0xae, 0xf0, 0x9d, 0xdf, 0x81, 0x64, 0xd6, 0x17, 0x2b, 0xc5, 0x06, 0x75, 0x05, 0x7a, 0x03, + 0x2e, 0xdf, 0xd1, 0x1b, 0x65, 0x34, 0x7c, 0x5d, 0x4a, 0x54, 0x92, 0x50, 0x63, 0xa5, 0x60, 0xb7, 0x58, 0x97, 0x51, + 0x14, 0x17, 0x6b, 0xa2, 0x5f, 0x15, 0x4c, 0x16, 0xdb, 0x6e, 0xe0, 0xdc, 0x04, 0xea, 0x51, 0x07, 0x27, 0xfa, 0x3b, + 0x44, 0xde, 0x16, 0xc5, 0x86, 0x6c, 0x1b, 0x88, 0xa1, 0x15, 0xd7, 0x75, 0xaa, 0xb1, 0x3e, 0xf2, 0x80, 0x1e, 0xf7, + 0xaf, 0x8e, 0x21, 0x78, 0xf8, 0x69, 0xc0, 0x52, 0x57, 0x9d, 0x3a, 0xbc, 0x7d, 0x74, 0x63, 0x49, 0xea, 0x71, 0x7e, + 0xf3, 0x4f, 0xc5, 0x36, 0x2d, 0x4f, 0xb7, 0xc8, 0x0d, 0x89, 0xe0, 0x5d, 0x41, 0xf6, 0xd3, 0xc1, 0x6d, 0x7b, 0xa6, + 0x28, 0x0a, 0x2f, 0x94, 0xb4, 0xbc, 0x29, 0x3c, 0x8c, 0xe3, 0x46, 0x8a, 0x1a, 0x2a, 0x32, 0xfe, 0x31, 0x36, 0x2c, + 0x92, 0x9d, 0xad, 0x0f, 0x63, 0xe7, 0x95, 0x90, 0x8f, 0x2b, 0xd7, 0xea, 0x46, 0xa6, 0x63, 0x5b, 0x14, 0x39, 0x7a, + 0x9d, 0x07, 0xa0, 0xf2, 0x47, 0x61, 0x12, 0x50, 0x1c, 0x89, 0x00, 0xa2, 0x3a, 0xf1, 0xa2, 0xe8, 0x81, 0xcd, 0xa1, + 0x19, 0x56, 0x83, 0x7c, 0x2a, 0xfa, 0x1b, 0x15, 0x9d, 0xd9, 0xf1, 0x50, 0xd8, 0x8b, 0x4c, 0xac, 0x3e, 0xe6, 0xae, + 0x43, 0x91, 0x36, 0x72, 0xea, 0x3b, 0xd7, 0x98, 0x37, 0xd0, 0xc3, 0x22, 0x14, 0x7f, 0x61, 0x83, 0x98, 0xb2, 0x01, + 0x2b, 0x64, 0xec, 0x79, 0x04, 0x30, 0x4b, 0xf2, 0x45, 0x12, 0x78, 0xd3, 0xaf, 0x40, 0xbf, 0xc1, 0x7d, 0xb5, 0x6f, + 0x26, 0x4d, 0xb3, 0x70, 0x77, 0x63, 0xbf, 0x2f, 0x36, 0x72, 0x4f, 0x08, 0x22, 0x58, 0x92, 0xd9, 0xb2, 0x96, 0xde, + 0x03, 0x7e, 0xa5, 0xe0, 0x19, 0x78, 0xc8, 0x00, 0x8e, 0x98, 0x96, 0xb8, 0xae, 0xd6, 0x93, 0xab, 0xc3, 0x56, 0x6e, + 0x0b, 0x25, 0x38, 0x44, 0xd2, 0xe8, 0x96, 0x4a, 0xb3, 0x94, 0xee, 0x99, 0xad, 0xbb, 0x91, 0x71, 0xfc, 0x65, 0x50, + 0x9e, 0x58, 0x8f, 0x5f, 0x93, 0xc8, 0x96, 0x44, 0x14, 0x97, 0x5f, 0xe7, 0x90, 0xdd, 0x58, 0xa9, 0x98, 0x0c, 0x54, + 0x3d, 0x79, 0xaa, 0x09, 0xde, 0x60, 0x71, 0x17, 0x1c, 0xe9, 0x03, 0xed, 0x09, 0xc5, 0x49, 0xaa, 0x4a, 0xa9, 0x87, + 0x7e, 0xc2, 0x57, 0xd8, 0xe7, 0xa2, 0xb7, 0xd9, 0x31, 0xcd, 0xd8, 0x67, 0x34, 0xc1, 0x80, 0x3a, 0x09, 0x4e, 0xbb, + 0x66, 0x78, 0xe4, 0x48, 0x6c, 0x3a, 0x90, 0x8f, 0xf1, 0x54, 0xe8, 0x36, 0x6e, 0x56, 0x21, 0x8e, 0x86, 0xd0, 0xfd, + 0x30, 0x14, 0x46, 0x3f, 0xa3, 0x74, 0xac, 0x3e, 0xed, 0xd7, 0x5c, 0xd4, 0xce, 0x98, 0xa2, 0x29, 0x2f, 0xbb, 0xa6, + 0x00, 0x6f, 0xa4, 0xbc, 0xc1, 0x0a, 0xf8, 0xfe, 0xb2, 0x59, 0x57, 0x8f, 0x17, 0x36, 0xf9, 0x0f, 0xae, 0x83, 0x8d, + 0x84, 0x09, 0xfc, 0x11, 0x92, 0x99, 0x2d, 0x82, 0x35, 0x8c, 0xf3, 0x92, 0x58, 0x38, 0x7a, 0x9c, 0xef, 0x07, 0xd9, + 0x1f, 0x57, 0x8d, 0x07, 0x61, 0x0b, 0x0f, 0xad, 0xe4, 0x9c, 0xa8, 0xd7, 0xd4, 0xa9, 0x91, 0x0f, 0x22, 0x93, 0xc0, + 0x84, 0xf2, 0x3c, 0xc1, 0x34, 0xab, 0xb3, 0x59, 0x50, 0xdb, 0x44, 0xc5, 0xa0, 0xd0, 0x8d, 0xdb, 0x99, 0x20, 0xc9, + 0x86, 0xe0, 0x94, 0x97, 0x65, 0xc3, 0xed, 0x75, 0x6b, 0xe6, 0x45, 0xf3, 0xd7, 0x64, 0x87, 0xa5, 0xdf, 0x05, 0x1d, + 0x6b, 0xe3, 0xf5, 0x60, 0x7b, 0xd0, 0x79, 0x58, 0xbc, 0x50, 0x3a, 0x8d, 0xaa, 0x9b, 0x7a, 0x11, 0x37, 0xfb, 0x39, + 0x75, 0x35, 0xd1, 0x6c, 0x09, 0x48, 0x67, 0xa3, 0x7c, 0x8f, 0x9d, 0xb8, 0x46, 0x51, 0x5a, 0x4b, 0xab, 0x5b, 0xe6, + 0x1d, 0x8b, 0x91, 0xbb, 0x81, 0x51, 0x62, 0xed, 0x22, 0x86, 0x9a, 0x9f, 0xc3, 0xdc, 0x9e, 0x98, 0x40, 0x45, 0xff, + 0x3a, 0x9f, 0xcc, 0xec, 0x62, 0x9a, 0x4a, 0x32, 0xcc, 0x07, 0xa5, 0x6f, 0x89, 0xe6, 0xee, 0xf1, 0x9c, 0x93, 0xd2, + 0xb6, 0xad, 0x62, 0x1d, 0xba, 0x85, 0x81, 0x0f, 0x5c, 0x4d, 0x03, 0xc1, 0x15, 0xbd, 0xc5, 0x98, 0x67, 0xf0, 0x7c, + 0xc0, 0xec, 0x1b, 0x79, 0x3e, 0x2f, 0x45, 0xde, 0x3e, 0x91, 0x19, 0xbe, 0x50, 0xa0, 0x98, 0xde, 0xe9, 0x7c, 0x6f, + 0x9f, 0x87, 0x1b, 0x77, 0x59, 0xe0, 0xbe, 0x24, 0x0e, 0x19, 0xfe, 0x75, 0x17, 0x5b, 0xc6, 0x1a, 0xcf, 0x9c, 0xfb, + 0x2d, 0x89, 0x09, 0xa5, 0xda, 0xae, 0x1f, 0xa2, 0xbc, 0x16, 0x61, 0x52, 0x85, 0xa5, 0xdb, 0x2a, 0xa4, 0x32, 0xf4, + 0x45, 0xa4, 0x8a, 0xc7, 0x99, 0x9b, 0x9d, 0xa1, 0x34, 0x82, 0x0c, 0x05, 0x13, 0x64, 0xb5, 0x4f, 0xa2, 0x79, 0x56, + 0xf2, 0xa0, 0x4d, 0x13, 0xf9, 0xf0, 0xba, 0x2a, 0x63, 0xe1, 0x71, 0xe6, 0xde, 0x76, 0xc4, 0xdc, 0xba, 0x8e, 0xf3, + 0xea, 0x03, 0x75, 0x2b, 0x47, 0xe6, 0xb9, 0x62, 0x6c, 0xc5, 0xd8, 0x3d, 0xa8, 0x45, 0xa0, 0x0c, 0x45, 0x12, 0x0e, + 0x6c, 0x31, 0x7a, 0x7b, 0xa1, 0xce, 0x06, 0xc2, 0xad, 0xb2, 0x3e, 0xaa, 0xc5, 0x6b, 0xda, 0xb6, 0x52, 0x0a, 0x4e, + 0xa0, 0x10, 0x4e, 0x34, 0xf6, 0x9c, 0x0f, 0xff, 0xfc, 0x5c, 0xa7, 0x1e, 0xff, 0x99, 0x10, 0x9b, 0xfd, 0x97, 0xf7, + 0xaf, 0x63, 0x0a, 0xd0, 0xeb, 0x9e, 0x75, 0x45, 0x7a, 0xad, 0xeb, 0x35, 0xd2, 0xab, 0xaf, 0x57, 0xb5, 0x39, 0xe1, + 0x59, 0x2d, 0xb5, 0x51, 0x1b, 0x77, 0x6e, 0x14, 0xeb, 0x30, 0x94, 0x94, 0xd4, 0x7e, 0x4f, 0x4f, 0x3f, 0x8d, 0x55, + 0x99, 0x6f, 0xcd, 0xa4, 0x98, 0x4d, 0x5f, 0x1c, 0xab, 0xf5, 0xb8, 0x8c, 0x10, 0xbb, 0x17, 0x43, 0x6d, 0xa5, 0x3a, + 0x35, 0x75, 0x9b, 0xcf, 0x2f, 0xc6, 0xc5, 0xe5, 0xcb, 0xbf, 0x22, 0xc4, 0xf3, 0x11, 0xe3, 0xa1, 0x8d, 0x76, 0xde, + 0x37, 0x48, 0x8d, 0xcb, 0xcd, 0x11, 0xe7, 0x68, 0x56, 0x15, 0xc6, 0x88, 0xa7, 0x55, 0xe7, 0x2e, 0xb8, 0xf6, 0x20, + 0xf0, 0x73, 0x71, 0x55, 0xa9, 0x48, 0x52, 0xdf, 0x36, 0xb0, 0x2e, 0x37, 0x4b, 0x93, 0xc3, 0xdf, 0x0b, 0x2c, 0xe8, + 0x63, 0x53, 0x95, 0xeb, 0x07, 0x25, 0xc4, 0xd8, 0x29, 0x62, 0xca, 0xa9, 0xf4, 0x2e, 0xac, 0x7c, 0x51, 0x5f, 0x4f, + 0x99, 0xfa, 0x36, 0x88, 0x31, 0x8b, 0x31, 0x27, 0x4b, 0x31, 0x27, 0x79, 0x60, 0xfb, 0x3c, 0x06, 0xc6, 0xc5, 0x24, + 0x10, 0xf9, 0x70, 0xe3, 0xca, 0x58, 0xbe, 0x08, 0x18, 0xac, 0xa2, 0x0d, 0x04, 0xd3, 0x3b, 0x33, 0xed, 0xe2, 0x1f, + 0xf3, 0xcb, 0x41, 0x64, 0x5c, 0x89, 0x21, 0xac, 0x8e, 0xf8, 0xad, 0xd3, 0x0b, 0x64, 0x62, 0xe5, 0x8c, 0x66, 0x09, + 0x98, 0x75, 0xd3, 0x34, 0x38, 0x56, 0x4d, 0x83, 0x4a, 0x33, 0xaf, 0xb0, 0x0c, 0x24, 0x31, 0x30, 0x95, 0x6a, 0xf8, + 0x95, 0x16, 0x09, 0xce, 0xf9, 0xfb, 0xae, 0xcf, 0x29, 0x90, 0xc6, 0x99, 0x46, 0xa5, 0xc0, 0xe7, 0x0e, 0xf8, 0x05, + 0xfa, 0x15, 0x9f, 0x88, 0xe3, 0x34, 0xe9, 0x91, 0xc9, 0xe8, 0x81, 0xda, 0x81, 0x90, 0x59, 0x4b, 0x46, 0x61, 0x1a, + 0x42, 0x28, 0x05, 0x44, 0x7c, 0x3f, 0xcc, 0x45, 0x95, 0x35, 0xaf, 0xc6, 0x02, 0xb7, 0x10, 0x31, 0x23, 0xaa, 0x06, + 0x22, 0xc9, 0x48, 0xae, 0x1b, 0x32, 0x2c, 0x96, 0x26, 0x2d, 0xc6, 0xe0, 0x04, 0xc9, 0x3c, 0x9d, 0x08, 0xfe, 0x65, + 0x48, 0xc6, 0x05, 0x6f, 0x7a, 0xaf, 0x80, 0xbe, 0xc6, 0xc5, 0x64, 0xe7, 0xcd, 0xbc, 0xe8, 0x89, 0xb4, 0x5c, 0xe5, + 0x43, 0xa2, 0xd0, 0xdf, 0xd7, 0x7d, 0xef, 0x58, 0x3d, 0x48, 0xc1, 0xbc, 0x2d, 0x6a, 0x8b, 0x96, 0xed, 0xb5, 0x95, + 0xc7, 0x72, 0xdc, 0x3d, 0x4a, 0x50, 0x00, 0xdd, 0x66, 0xd1, 0x0a, 0x3c, 0x49, 0xd6, 0xd8, 0xa9, 0x4f, 0x44, 0x74, + 0x74, 0x1b, 0x25, 0xb3, 0x23, 0x5b, 0x17, 0x3f, 0x90, 0x91, 0xe4, 0xcc, 0x5c, 0x89, 0xce, 0xff, 0x59, 0xf2, 0x26, + 0x17, 0x33, 0x5b, 0x75, 0xc8, 0x01, 0x6e, 0x3a, 0x13, 0x61, 0x8a, 0xf6, 0x56, 0x66, 0x23, 0x44, 0x86, 0x93, 0x49, + 0x16, 0x64, 0xea, 0xc5, 0x5f, 0x8c, 0x14, 0xfc, 0x47, 0xc4, 0x86, 0x96, 0x3c, 0xd2, 0xff, 0x70, 0x0d, 0xe1, 0x5b, + 0x39, 0x1c, 0x24, 0xd5, 0x7b, 0x2d, 0xb8, 0x2d, 0x2d, 0x1b, 0x66, 0x83, 0x24, 0x3c, 0x3e, 0xbb, 0x7c, 0xe6, 0xdb, + 0x83, 0xfc, 0x43, 0x44, 0x08, 0x84, 0xfa, 0xdf, 0xf4, 0xaa, 0x76, 0xf1, 0x32, 0x2a, 0x4e, 0x83, 0xf2, 0xf5, 0xf8, + 0x8c, 0xc3, 0x1b, 0x2a, 0x2f, 0xe0, 0xb7, 0x6f, 0xe7, 0x1c, 0x98, 0x81, 0x2f, 0x63, 0xab, 0xb1, 0x80, 0xbd, 0x70, + 0xd8, 0x63, 0x28, 0x59, 0xc4, 0xa1, 0xed, 0x6c, 0x84, 0xdb, 0xd0, 0xf5, 0x36, 0xdb, 0xaf, 0x94, 0x72, 0x75, 0xc6, + 0xf9, 0x3b, 0xdb, 0xaa, 0xe6, 0x66, 0xd7, 0x0d, 0x5b, 0x2a, 0xe9, 0x69, 0x5f, 0x6e, 0x30, 0x75, 0x43, 0xf6, 0x36, + 0xd4, 0x5a, 0xbe, 0x19, 0xd6, 0x95, 0x37, 0x0b, 0x83, 0x42, 0xc0, 0x98, 0x61, 0xcd, 0x15, 0xb9, 0xd6, 0xca, 0x7e, + 0x30, 0xc5, 0xfe, 0x30, 0x08, 0x89, 0xa8, 0x8a, 0x24, 0x67, 0x83, 0x1e, 0xe7, 0x6a, 0xed, 0x59, 0x3d, 0x02, 0x4b, + 0x27, 0x96, 0x63, 0xcd, 0x0a, 0x06, 0x43, 0xa9, 0xaa, 0xd5, 0x52, 0x77, 0xb8, 0x4a, 0x9f, 0x6a, 0x79, 0xc5, 0x0b, + 0x12, 0xf6, 0x0b, 0x88, 0x4e, 0x7c, 0x77, 0xf7, 0x24, 0xf2, 0x9d, 0x59, 0x7d, 0x63, 0x2a, 0x4d, 0x94, 0x67, 0xc5, + 0x0a, 0x4a, 0xd6, 0x3b, 0xc0, 0x50, 0x51, 0x63, 0x6c, 0xe8, 0x0e, 0x0d, 0xd6, 0xe6, 0x38, 0xdc, 0x17, 0xf6, 0xdb, + 0x82, 0xec, 0x47, 0xfd, 0x9c, 0x93, 0xfb, 0x68, 0xb3, 0xa8, 0x97, 0xf5, 0x56, 0x63, 0xe4, 0x08, 0xaf, 0x37, 0x27, + 0x55, 0xb6, 0xa0, 0xc9, 0x5e, 0x83, 0xd3, 0x4b, 0x33, 0x75, 0xa1, 0xec, 0xc4, 0x8c, 0x28, 0xd3, 0x41, 0x24, 0x09, + 0xba, 0xb3, 0x1e, 0x04, 0xd7, 0x2c, 0x0b, 0x6b, 0x93, 0x91, 0x7b, 0x30, 0x9c, 0x23, 0x15, 0xd1, 0x25, 0x14, 0xc5, + 0x39, 0x9b, 0xd7, 0x3f, 0x30, 0xe4, 0x28, 0x8f, 0xc5, 0xb2, 0x64, 0x41, 0xbd, 0x6f, 0x61, 0xa4, 0x26, 0xfb, 0x74, + 0x2c, 0xa5, 0x90, 0x1d, 0xc0, 0xc6, 0x8e, 0xb6, 0x73, 0xc1, 0x9c, 0xda, 0xba, 0x04, 0x3b, 0xd9, 0xa9, 0xb9, 0x5b, + 0x91, 0x01, 0x91, 0x07, 0x42, 0x14, 0x06, 0x7c, 0x7f, 0x5e, 0x11, 0xc0, 0x9a, 0xe3, 0x14, 0x89, 0x3f, 0x08, 0xe3, + 0x97, 0x1f, 0x14, 0x83, 0x84, 0xe5, 0xae, 0xe7, 0x70, 0xfa, 0x3a, 0x80, 0x56, 0xea, 0xc5, 0xe6, 0x7b, 0x26, 0xca, + 0x46, 0xfe, 0x2a, 0xd6, 0x3a, 0x62, 0x88, 0x70, 0xe0, 0xcb, 0x66, 0x43, 0xd2, 0x78, 0xb3, 0x5c, 0x9c, 0x8d, 0x9a, + 0xae, 0xab, 0x23, 0xee, 0x23, 0x15, 0x84, 0xb1, 0xd9, 0xf0, 0xd0, 0x8d, 0xf3, 0x43, 0xd6, 0x6e, 0x06, 0x87, 0x41, + 0x04, 0x7e, 0x6d, 0xba, 0x56, 0x97, 0x70, 0xb5, 0xa6, 0x99, 0x78, 0x2f, 0xce, 0xa6, 0xfb, 0xba, 0xd7, 0x35, 0xc2, + 0xbf, 0x5c, 0xe2, 0x80, 0xf9, 0x37, 0x52, 0xc5, 0x41, 0x8b, 0x39, 0x7a, 0x8d, 0x4b, 0x9a, 0xe9, 0xa9, 0x21, 0x77, + 0x57, 0xca, 0x7b, 0x28, 0x07, 0xaa, 0x63, 0x3c, 0x3d, 0x64, 0x37, 0x87, 0x5b, 0x80, 0xda, 0x8e, 0x10, 0x57, 0x06, + 0xea, 0x09, 0x80, 0x2b, 0x09, 0x84, 0x65, 0x1e, 0xcf, 0x90, 0xbe, 0x67, 0xd2, 0x09, 0x68, 0xe8, 0x40, 0xb9, 0xe9, + 0x49, 0x99, 0x43, 0xea, 0xa1, 0x0e, 0x52, 0x4c, 0x78, 0xd0, 0xcb, 0xae, 0x96, 0xea, 0x3a, 0x1a, 0x21, 0x69, 0x42, + 0x41, 0xfc, 0x82, 0xa0, 0xe8, 0xab, 0x21, 0xf2, 0x97, 0x89, 0xca, 0xba, 0xaa, 0xf0, 0x06, 0xff, 0x12, 0x2d, 0xb2, + 0xfa, 0xce, 0xcc, 0xec, 0x48, 0x5d, 0x56, 0xa2, 0xf6, 0x02, 0xb0, 0x0e, 0x87, 0xe0, 0x40, 0x22, 0x62, 0x9e, 0x44, + 0x13, 0xd9, 0x54, 0x28, 0x7f, 0xe6, 0xd0, 0x28, 0x80, 0xcb, 0x79, 0x24, 0x68, 0x22, 0xf0, 0xb1, 0x13, 0xe0, 0xcc, + 0x0c, 0x3c, 0x9c, 0xad, 0x26, 0x8d, 0xc0, 0x98, 0x6b, 0xe5, 0xa5, 0x66, 0x1f, 0x33, 0xa2, 0x1c, 0x17, 0x73, 0x23, + 0xbb, 0x6b, 0xf2, 0x70, 0x88, 0x79, 0x62, 0x63, 0x0e, 0xdf, 0xd7, 0x9e, 0x19, 0xd3, 0xbf, 0xcc, 0xc0, 0x27, 0x25, + 0xea, 0x4e, 0x0d, 0x8a, 0xd7, 0xed, 0x9d, 0xd7, 0x56, 0xbb, 0x86, 0x5c, 0x16, 0x1d, 0x06, 0xab, 0xb5, 0xff, 0xd7, + 0x7f, 0x8a, 0xe3, 0xbe, 0x72, 0x3e, 0x06, 0x57, 0x3c, 0x04, 0x87, 0x35, 0x43, 0xcd, 0xaf, 0xeb, 0xe2, 0x39, 0x3e, + 0x6d, 0x1f, 0xe6, 0xc6, 0xd3, 0xdd, 0x81, 0x97, 0xb9, 0x90, 0xfa, 0xcc, 0x12, 0xa2, 0x0f, 0x43, 0x8b, 0x67, 0x63, + 0x54, 0x89, 0xc6, 0x97, 0x0e, 0x29, 0x96, 0x2d, 0x9e, 0xee, 0x04, 0xe2, 0xe5, 0x70, 0x77, 0xb6, 0x40, 0xac, 0x28, + 0x11, 0xe6, 0x74, 0x22, 0xd2, 0x38, 0x02, 0xc6, 0x2b, 0xf1, 0xc0, 0x10, 0x18, 0x69, 0x94, 0x59, 0xd3, 0xfe, 0xb0, + 0x11, 0xd9, 0xe7, 0x90, 0x68, 0x32, 0x6c, 0xca, 0x3b, 0x9b, 0x51, 0x7b, 0x25, 0x12, 0x8a, 0x86, 0x75, 0xdf, 0x4d, + 0x33, 0x2a, 0xef, 0xc5, 0x38, 0x24, 0x0e, 0xe1, 0xa4, 0x77, 0x3f, 0x6d, 0x1f, 0x4a, 0x1e, 0x7e, 0x0e, 0xfb, 0xc3, + 0x0f, 0xfe, 0xe1, 0xe7, 0x70, 0x77, 0x7e, 0xf0, 0x9d, 0x9f, 0x43, 0xde, 0xf9, 0x41, 0xbc, 0x54, 0x9a, 0xbe, 0xd2, + 0x9e, 0x07, 0x63, 0xc5, 0x50, 0x2e, 0xcb, 0xc8, 0x56, 0xaa, 0xe0, 0x17, 0x3f, 0x24, 0xdc, 0xe7, 0x12, 0x29, 0x39, + 0x25, 0x2e, 0x58, 0x89, 0x4a, 0x56, 0x86, 0x4e, 0x81, 0x7d, 0x1a, 0xd0, 0xc3, 0xea, 0xed, 0xe7, 0xfc, 0xcb, 0x6d, + 0x50, 0x74, 0x26, 0xe2, 0x01, 0x24, 0x43, 0xb9, 0x33, 0x07, 0x2f, 0x4c, 0x49, 0x18, 0x65, 0x39, 0x62, 0xb4, 0xa2, + 0xd2, 0x8e, 0xb3, 0x44, 0xef, 0xdc, 0x01, 0x16, 0x82, 0xbe, 0x5d, 0xe8, 0xb5, 0x72, 0x58, 0x7f, 0xff, 0x05, 0x28, + 0x55, 0x57, 0x0c, 0x78, 0x60, 0x1f, 0x7b, 0x71, 0x1f, 0x69, 0xe5, 0xd5, 0xa4, 0x8a, 0x1a, 0x5c, 0x93, 0x83, 0x31, + 0x46, 0x48, 0xdc, 0xd3, 0xbf, 0x64, 0x4d, 0x72, 0xe6, 0xe6, 0xad, 0x66, 0xe1, 0x1e, 0xa3, 0xe7, 0x80, 0xe6, 0xc4, + 0xa8, 0x9a, 0x19, 0xb6, 0x88, 0x5a, 0xb3, 0x9a, 0x33, 0x8b, 0x38, 0x59, 0x8a, 0xad, 0xab, 0xb0, 0xe7, 0x3d, 0x7e, + 0xca, 0xef, 0xe0, 0x2a, 0x37, 0x43, 0x1a, 0xec, 0x8b, 0x0c, 0xec, 0x83, 0x2b, 0x6c, 0x6b, 0x0d, 0xa6, 0x27, 0x9c, + 0xad, 0xc5, 0xf5, 0xd5, 0x14, 0xbe, 0x20, 0xad, 0xa1, 0x2d, 0x45, 0x34, 0xba, 0x4b, 0x26, 0x36, 0x52, 0xda, 0xfa, + 0xe1, 0x6b, 0x0b, 0x8d, 0x36, 0x2b, 0x96, 0x60, 0xc9, 0xee, 0x37, 0x2f, 0xb9, 0x0f, 0x4d, 0xe6, 0x2c, 0xc8, 0x44, + 0xd5, 0x4d, 0x90, 0x36, 0x05, 0xbe, 0x38, 0x59, 0x61, 0x3c, 0x02, 0x59, 0xe4, 0x36, 0x17, 0x87, 0x53, 0x47, 0x2d, + 0xa3, 0xaa, 0x84, 0x48, 0x7d, 0x56, 0xae, 0x93, 0x4b, 0xd0, 0xf1, 0xe2, 0x40, 0x04, 0x97, 0xc3, 0x84, 0x54, 0x6a, + 0x3a, 0x6d, 0xd7, 0x68, 0x6f, 0x21, 0xcf, 0xa1, 0x4e, 0x3f, 0x0d, 0x36, 0x84, 0x21, 0xaa, 0x31, 0xf8, 0x32, 0xf3, + 0xf4, 0x9a, 0x2e, 0x4d, 0xdb, 0xc7, 0x01, 0x04, 0x7a, 0xb1, 0x3d, 0x95, 0xce, 0x5d, 0x9f, 0x92, 0x48, 0x20, 0x91, + 0xf8, 0x02, 0xe0, 0x03, 0x80, 0xaf, 0x7a, 0x89, 0xaa, 0x45, 0x26, 0xbd, 0x54, 0x81, 0x9e, 0x29, 0xb8, 0x03, 0x32, + 0x43, 0x2b, 0x40, 0xe5, 0x8f, 0x48, 0xf1, 0xb5, 0x43, 0xb2, 0x98, 0xf0, 0xd2, 0x50, 0xbc, 0x8e, 0x09, 0xed, 0x7c, + 0x98, 0x9a, 0x5e, 0x22, 0x77, 0x81, 0x94, 0x8e, 0xd8, 0xa2, 0x9f, 0xbe, 0x3c, 0xbb, 0x69, 0xe1, 0x24, 0x8f, 0x2c, + 0xbf, 0xd6, 0xfe, 0x2d, 0x6b, 0xdb, 0x55, 0xf5, 0x47, 0xa6, 0xa4, 0x0e, 0xb4, 0x21, 0x94, 0xeb, 0x99, 0xb2, 0xa7, + 0xf4, 0x15, 0xec, 0x2c, 0x86, 0x45, 0xaf, 0x2d, 0xb3, 0xdd, 0x1c, 0x3e, 0x74, 0xd1, 0x03, 0xd1, 0x84, 0xdb, 0xd7, + 0x48, 0xa0, 0xb9, 0x44, 0xb0, 0x18, 0x9e, 0xe1, 0xd2, 0x6e, 0xfc, 0x92, 0x53, 0x14, 0xc4, 0x2a, 0xf0, 0x21, 0x7d, + 0xff, 0x01, 0x43, 0x86, 0xb2, 0xdd, 0x86, 0xc3, 0x55, 0x0d, 0x34, 0x5f, 0xf7, 0x71, 0xd8, 0xab, 0x13, 0xb0, 0xb6, + 0x64, 0xbe, 0xda, 0xb4, 0x51, 0xec, 0x35, 0x97, 0xa7, 0xbd, 0xb6, 0x52, 0xe0, 0xcf, 0xc5, 0x47, 0x7f, 0x7b, 0x5e, + 0xd4, 0x2c, 0xcb, 0x8b, 0xd2, 0x7b, 0x5b, 0xd5, 0xec, 0xb4, 0x06, 0xa3, 0x3f, 0x4e, 0x85, 0x90, 0x58, 0x0e, 0x93, + 0xd2, 0xf3, 0xd1, 0xa8, 0x16, 0xbb, 0xd7, 0x64, 0x1e, 0x1f, 0x26, 0xa1, 0x9a, 0x4d, 0x8d, 0x3c, 0xb8, 0xd7, 0x9b, + 0x0b, 0x7d, 0x8f, 0x02, 0xd5, 0xbd, 0x16, 0x4e, 0xd5, 0x55, 0x29, 0x41, 0x4c, 0x46, 0x46, 0x33, 0xcd, 0xc6, 0xbc, + 0x0c, 0xdc, 0x9a, 0xa9, 0x7e, 0x41, 0x9f, 0x48, 0xc9, 0x61, 0xd8, 0x59, 0x59, 0x94, 0x8a, 0x49, 0x4a, 0x00, 0x8b, + 0xed, 0x67, 0x71, 0x72, 0x60, 0x50, 0xb5, 0xea, 0x3c, 0x60, 0x24, 0x8e, 0xc5, 0xe2, 0x23, 0x50, 0xf1, 0x5b, 0x07, + 0xa8, 0x12, 0x4e, 0x8f, 0x55, 0x71, 0x1e, 0x7e, 0x10, 0xa5, 0x52, 0x4f, 0x40, 0xa0, 0xa6, 0x4e, 0x5e, 0xe6, 0x5e, + 0xb0, 0x7c, 0x33, 0xa7, 0x8d, 0xbd, 0x30, 0x2b, 0x1d, 0x90, 0x6b, 0xd3, 0x48, 0x0c, 0x45, 0xfc, 0x93, 0x63, 0xe3, + 0x36, 0xba, 0xb0, 0xea, 0x85, 0xe5, 0x5e, 0x54, 0x07, 0xa1, 0x41, 0xe8, 0x90, 0xa7, 0xca, 0x6d, 0x19, 0xd6, 0xe7, + 0x81, 0x97, 0x27, 0xfd, 0x0b, 0x4f, 0x0f, 0x36, 0x3d, 0xfc, 0x80, 0x45, 0x2b, 0x89, 0x34, 0x54, 0xb1, 0x49, 0xe1, + 0x8e, 0x48, 0x95, 0xe5, 0xce, 0xd3, 0x1e, 0xdd, 0x6b, 0x33, 0x0f, 0xd2, 0xd5, 0x47, 0x05, 0x45, 0x6b, 0x68, 0x09, + 0xa5, 0x2e, 0x60, 0x0a, 0xa3, 0x2c, 0xde, 0xe9, 0xab, 0xf5, 0x93, 0x5d, 0x4a, 0xc2, 0x01, 0x1f, 0xc3, 0x60, 0x26, + 0xf0, 0xef, 0x87, 0x48, 0x07, 0x37, 0xb5, 0x6e, 0x85, 0x32, 0x86, 0xb4, 0x42, 0x30, 0x1f, 0x49, 0x74, 0x98, 0xe0, + 0xfb, 0xc1, 0xa4, 0xc8, 0x49, 0xc1, 0x46, 0xe3, 0x37, 0xe3, 0x1a, 0x43, 0xc7, 0x99, 0xf1, 0x9d, 0x9f, 0xae, 0xd8, + 0xdb, 0x72, 0x5c, 0x1d, 0x42, 0xc0, 0xe5, 0x58, 0xee, 0x75, 0x5d, 0x90, 0x75, 0x8c, 0x76, 0x14, 0x3e, 0x23, 0x71, + 0xa9, 0x0b, 0x3d, 0xa5, 0xda, 0x91, 0x33, 0x86, 0x25, 0x38, 0x5d, 0xcd, 0x9f, 0xd8, 0xc6, 0x15, 0xb4, 0xed, 0xec, + 0x34, 0x50, 0xb7, 0x57, 0xc0, 0x83, 0x5d, 0x63, 0x4a, 0x94, 0x25, 0x56, 0x05, 0x34, 0x18, 0x01, 0x6d, 0x59, 0x60, + 0x54, 0x13, 0x31, 0xd1, 0x28, 0x8c, 0x12, 0xa9, 0xa5, 0x94, 0x1d, 0xcb, 0x1f, 0x75, 0x92, 0x4c, 0x92, 0x75, 0x28, + 0x4e, 0x7a, 0x62, 0x92, 0xd4, 0x6a, 0x5d, 0xb6, 0x78, 0x79, 0x21, 0xf6, 0x8b, 0x54, 0x7a, 0x62, 0xef, 0xa0, 0x05, + 0x72, 0xb3, 0xef, 0x69, 0x48, 0x0d, 0x8d, 0xce, 0xf6, 0x46, 0xe7, 0xe5, 0xa9, 0x6c, 0xbe, 0xd5, 0x51, 0xcb, 0xf8, + 0xc6, 0x98, 0xa2, 0x0a, 0xa8, 0x3f, 0xd6, 0x82, 0xf4, 0xfd, 0x4b, 0xb1, 0xce, 0x50, 0x34, 0x4c, 0x5d, 0xf6, 0x58, + 0x8c, 0x74, 0x9d, 0xe6, 0x89, 0x90, 0xe0, 0xde, 0x1d, 0x18, 0x78, 0x44, 0x99, 0x3b, 0x19, 0xd3, 0x09, 0xc2, 0x10, + 0x91, 0x75, 0xb2, 0xe6, 0x7d, 0x6e, 0xfd, 0x7c, 0x14, 0x87, 0x61, 0x0c, 0x1b, 0x4c, 0xae, 0xf6, 0x53, 0x7a, 0xef, + 0xc7, 0x62, 0xa4, 0x76, 0x9d, 0xd9, 0xcd, 0x78, 0x61, 0xa9, 0x3d, 0x16, 0xf6, 0x3f, 0x64, 0x3e, 0xf5, 0x58, 0xe9, + 0xbd, 0xb4, 0x86, 0x34, 0x9e, 0x59, 0x63, 0xd5, 0x5f, 0x82, 0x76, 0xe4, 0x12, 0xed, 0xc4, 0x4e, 0xa9, 0x2a, 0x48, + 0x28, 0x48, 0x8c, 0xa9, 0xed, 0x1c, 0x0c, 0x34, 0x63, 0x9d, 0xb9, 0x63, 0x8b, 0xbe, 0x3d, 0xe5, 0xa4, 0x1c, 0xa0, + 0xbc, 0x14, 0xfe, 0xd9, 0x76, 0x50, 0x62, 0x1f, 0xc7, 0x18, 0x5b, 0x81, 0x7d, 0x48, 0x20, 0x55, 0xc1, 0x84, 0x56, + 0x93, 0x07, 0x74, 0x71, 0x4a, 0xc7, 0x9f, 0x19, 0xe6, 0x4f, 0xb0, 0xfa, 0x9a, 0x27, 0xb6, 0xd9, 0x85, 0x63, 0x4c, + 0xa9, 0xd7, 0xd9, 0x11, 0xeb, 0xa7, 0x74, 0x61, 0x8b, 0xb5, 0x31, 0xa4, 0x6c, 0xc9, 0xd6, 0xb5, 0x45, 0xc8, 0x84, + 0x21, 0xeb, 0x3a, 0x52, 0x71, 0x03, 0xe7, 0x37, 0xe4, 0x02, 0x5e, 0xef, 0xe7, 0x5c, 0xa9, 0x67, 0x11, 0xcd, 0x32, + 0x41, 0xbb, 0x04, 0x72, 0xa4, 0xf3, 0xa2, 0xfe, 0xbf, 0x95, 0x10, 0xa2, 0x4b, 0x6b, 0xba, 0x2d, 0xa1, 0x4e, 0xf2, + 0xd9, 0x59, 0xb4, 0x80, 0xc7, 0x6e, 0x94, 0x1b, 0xe7, 0xb1, 0xb4, 0x09, 0x9e, 0x0d, 0x22, 0x81, 0x0d, 0xcb, 0x29, + 0x51, 0x0d, 0xab, 0xad, 0xee, 0x7a, 0x18, 0x9f, 0xdd, 0xde, 0x28, 0xc4, 0x50, 0x61, 0xf6, 0x77, 0xa0, 0xa4, 0xe2, + 0x5e, 0x97, 0xd4, 0x3a, 0x2a, 0xff, 0x1b, 0xa5, 0x0d, 0x41, 0xe1, 0x8b, 0x9b, 0x82, 0x1d, 0xdc, 0xeb, 0x9e, 0x1a, + 0x8a, 0xfd, 0xfd, 0x42, 0x85, 0x69, 0xa7, 0x0f, 0xca, 0x04, 0x4d, 0x78, 0x0b, 0x72, 0x39, 0xf2, 0xfd, 0x6c, 0x2a, + 0xbf, 0xc8, 0x2f, 0x7d, 0x7b, 0x6d, 0x08, 0x5b, 0xd1, 0x4a, 0x2b, 0x56, 0x47, 0xf9, 0x61, 0x78, 0x13, 0xb7, 0x45, + 0x06, 0x45, 0x7d, 0x5e, 0x63, 0xef, 0x90, 0xaa, 0xc4, 0x6e, 0x7b, 0xe2, 0x06, 0x61, 0x39, 0xe9, 0x12, 0x5c, 0x58, + 0x23, 0x11, 0xa3, 0xd4, 0x9c, 0xe1, 0x54, 0x8b, 0xda, 0xc2, 0x72, 0xae, 0xd5, 0x11, 0x15, 0x10, 0xaa, 0xef, 0xa9, + 0x52, 0xd6, 0xc0, 0xb0, 0x77, 0x9e, 0x86, 0xc1, 0xcb, 0xb1, 0xab, 0x6b, 0xe5, 0xe8, 0x34, 0x5d, 0xf7, 0xb4, 0x40, + 0x81, 0x36, 0xa5, 0xb7, 0x76, 0x59, 0x8e, 0xb7, 0xea, 0x02, 0x17, 0x43, 0x0b, 0x9e, 0x3b, 0xef, 0x00, 0xbe, 0x4a, + 0x1e, 0x29, 0x3c, 0x58, 0xba, 0x76, 0x05, 0xb4, 0x30, 0x99, 0x04, 0x1e, 0x9c, 0xc5, 0x5a, 0x25, 0x6b, 0x51, 0xe1, + 0x35, 0x21, 0x0c, 0xc8, 0x59, 0x1f, 0x6c, 0xbb, 0x31, 0x72, 0x89, 0xda, 0xeb, 0x47, 0x1a, 0x5a, 0x64, 0xfd, 0xa0, + 0x49, 0xcf, 0x03, 0x45, 0xe5, 0xa8, 0x7a, 0x77, 0xa7, 0x8c, 0xbe, 0xc4, 0x3c, 0x61, 0xd4, 0x27, 0x06, 0x8d, 0xf4, + 0x85, 0x3a, 0x22, 0xe4, 0xfc, 0xc4, 0x66, 0xcd, 0x57, 0xfb, 0xf0, 0x9e, 0x10, 0xc6, 0x6a, 0xd3, 0x91, 0xcf, 0x13, + 0x68, 0xcf, 0x96, 0xae, 0x5f, 0xd4, 0x90, 0xe1, 0xb5, 0xe9, 0x72, 0x48, 0xc6, 0x82, 0xa7, 0x66, 0x08, 0x83, 0x5a, + 0xc9, 0x38, 0x4d, 0xec, 0x73, 0x16, 0x26, 0xd2, 0x55, 0xb9, 0x86, 0x00, 0xd7, 0x2f, 0x9c, 0x49, 0xb3, 0xd8, 0x72, + 0x8b, 0x92, 0xd1, 0xa5, 0x26, 0xc4, 0x16, 0x4d, 0x44, 0x06, 0x00, 0xbd, 0x1c, 0xf6, 0x11, 0x90, 0xf0, 0x6d, 0xc2, + 0xb9, 0x79, 0x62, 0x4b, 0x1b, 0xd7, 0x5c, 0x50, 0x18, 0xee, 0xe8, 0xc9, 0x5e, 0x6c, 0x2a, 0x62, 0xcf, 0x60, 0x1e, + 0x9a, 0x8d, 0x65, 0x36, 0x7f, 0xe4, 0xa7, 0xe3, 0x50, 0x0c, 0xa4, 0xff, 0xc0, 0x82, 0xf8, 0x9f, 0xa1, 0x42, 0x5c, + 0x71, 0x41, 0xfe, 0x80, 0x2b, 0x69, 0xf8, 0x82, 0x74, 0x3b, 0x9d, 0xf9, 0xd9, 0xf4, 0xa9, 0x5a, 0x40, 0x50, 0x1e, + 0x08, 0x85, 0x34, 0x17, 0x90, 0xc6, 0x0b, 0x1c, 0x58, 0x2f, 0xec, 0x90, 0x04, 0xb6, 0x9e, 0x8e, 0x64, 0xd2, 0x48, + 0xa7, 0x78, 0xe0, 0x53, 0xbd, 0xb6, 0x3f, 0xd5, 0x31, 0xa5, 0x37, 0xe5, 0x69, 0xd3, 0x3c, 0x15, 0x0f, 0x3d, 0x6b, + 0xab, 0x08, 0x13, 0x06, 0x4f, 0x85, 0x13, 0x5e, 0xef, 0xe9, 0x5a, 0xbb, 0x86, 0xaf, 0xe0, 0x8b, 0x9e, 0x0d, 0xe6, + 0xc2, 0xe6, 0x5a, 0x24, 0xe8, 0x20, 0x4c, 0x17, 0x3e, 0x3e, 0xc2, 0xc8, 0x74, 0x29, 0xbd, 0xa2, 0x1f, 0x0d, 0x0a, + 0xc5, 0xdb, 0xf5, 0x87, 0xf6, 0x2e, 0x82, 0x83, 0xb3, 0x05, 0xd9, 0x98, 0x76, 0x07, 0x20, 0x0f, 0x69, 0x51, 0xd5, + 0x18, 0x23, 0xa4, 0x42, 0x1c, 0x43, 0xc4, 0xe9, 0xf6, 0x55, 0x5b, 0x1e, 0xba, 0xe5, 0x97, 0x3c, 0x23, 0xff, 0x5e, + 0xfc, 0x99, 0xf9, 0xae, 0x6f, 0xd0, 0x15, 0xd7, 0x79, 0x0e, 0xf1, 0xbd, 0xdf, 0xb5, 0x46, 0x42, 0x94, 0x84, 0x7f, + 0x0c, 0x1e, 0x20, 0x66, 0x3c, 0x58, 0x03, 0xf6, 0xbc, 0xba, 0x91, 0x93, 0xe0, 0xbe, 0x60, 0xe8, 0x6d, 0xf3, 0xb5, + 0x7e, 0x3c, 0x26, 0xf1, 0x16, 0x6d, 0x11, 0xbb, 0x52, 0x07, 0x33, 0x76, 0xe2, 0x9c, 0x0f, 0x93, 0xd9, 0x7f, 0x8c, + 0xb0, 0xc0, 0x11, 0x0a, 0x6a, 0x2d, 0xfc, 0xb2, 0x15, 0xc0, 0xad, 0xfe, 0x83, 0x91, 0x02, 0x37, 0xd1, 0x13, 0x3f, + 0xdb, 0x3d, 0xc5, 0x26, 0x38, 0x11, 0x7b, 0x45, 0x6c, 0xcf, 0x81, 0x5a, 0xad, 0x6a, 0x0a, 0xd5, 0xad, 0xd3, 0x41, + 0xe8, 0x62, 0x51, 0x98, 0xeb, 0x75, 0x14, 0xf8, 0xac, 0x5a, 0x56, 0x1d, 0x86, 0x6c, 0x57, 0xa1, 0xf6, 0x24, 0x1b, + 0x16, 0x25, 0x2a, 0x72, 0xe3, 0x78, 0x53, 0xac, 0x03, 0xea, 0xb7, 0x7a, 0x6d, 0x82, 0x5b, 0x2f, 0x78, 0x74, 0x2c, + 0xc8, 0xb5, 0x14, 0x31, 0x78, 0x82, 0xc8, 0xe0, 0x55, 0xb9, 0x40, 0x07, 0xbd, 0x74, 0x5f, 0x37, 0x1f, 0x5a, 0xe3, + 0xe9, 0x6e, 0x1a, 0x3e, 0xfb, 0xb9, 0xf7, 0xd6, 0x88, 0xed, 0x9a, 0x31, 0x32, 0x2e, 0x92, 0x16, 0x3d, 0x75, 0x8d, + 0xcb, 0x35, 0x98, 0x3d, 0xb4, 0x3a, 0x66, 0x98, 0xbf, 0x5c, 0x69, 0x31, 0xc6, 0xef, 0x44, 0x31, 0xed, 0x41, 0x37, + 0x2b, 0xc4, 0x3d, 0xbd, 0x60, 0xc0, 0x5a, 0x4b, 0xbc, 0x69, 0xf5, 0x56, 0x5b, 0x9f, 0x2d, 0xcb, 0x20, 0xfa, 0x46, + 0x53, 0xbe, 0x9b, 0x85, 0x2c, 0x97, 0x29, 0xd6, 0x68, 0x13, 0xf6, 0xe5, 0x72, 0x6f, 0x37, 0xb6, 0x95, 0xf1, 0x6f, + 0x51, 0xf5, 0x64, 0x48, 0x24, 0x2d, 0x51, 0x2a, 0x15, 0x38, 0xe9, 0xc2, 0x10, 0x6b, 0x3a, 0x6a, 0xb9, 0x4e, 0x82, + 0xf9, 0xbe, 0x3b, 0x75, 0x58, 0xfe, 0xf8, 0x9c, 0x17, 0x69, 0xe5, 0x93, 0x22, 0xf6, 0xd9, 0xe1, 0x62, 0x42, 0x39, + 0x85, 0x33, 0xb2, 0xfb, 0x6f, 0x78, 0xb5, 0x2b, 0x80, 0x9a, 0x60, 0xf4, 0x72, 0xc9, 0xd5, 0x50, 0x94, 0x7e, 0x3a, + 0x19, 0xa6, 0x20, 0xac, 0xaf, 0xd6, 0xc2, 0x6b, 0xaf, 0x48, 0x74, 0x89, 0xbf, 0x92, 0x5e, 0x1a, 0x82, 0xa4, 0xed, + 0x50, 0x5f, 0xd5, 0x25, 0x08, 0x74, 0x88, 0x57, 0x12, 0xe0, 0x66, 0xde, 0x82, 0x26, 0x13, 0x19, 0x17, 0x6f, 0x5c, + 0x00, 0x17, 0xc6, 0xdb, 0xa7, 0x1b, 0x48, 0xd6, 0x5a, 0x62, 0x27, 0xa1, 0x9b, 0x5e, 0x1a, 0x9c, 0x00, 0x09, 0x76, + 0x3c, 0x81, 0x26, 0xef, 0x84, 0xcf, 0x5c, 0xaf, 0x26, 0xa6, 0x20, 0x88, 0xe8, 0xde, 0x73, 0xb0, 0x9b, 0xeb, 0x59, + 0x56, 0xd8, 0x84, 0xd8, 0xec, 0xa8, 0xfa, 0x7e, 0xaa, 0xc0, 0xeb, 0xa5, 0x49, 0xc5, 0x46, 0xa1, 0xeb, 0xe4, 0x0e, + 0xc7, 0x01, 0xa6, 0xb3, 0xe4, 0x50, 0xc3, 0x95, 0x8f, 0x65, 0x39, 0x49, 0x09, 0x2d, 0x85, 0x03, 0xce, 0x40, 0x72, + 0xf0, 0x3f, 0x96, 0x74, 0x90, 0x75, 0xf8, 0x89, 0x69, 0x0b, 0xfe, 0x4c, 0x5a, 0xd3, 0xb4, 0x88, 0x56, 0x7b, 0x1d, + 0x6b, 0xd0, 0xbc, 0x4a, 0x9e, 0x4f, 0x0c, 0x60, 0xb3, 0x5a, 0xc8, 0xea, 0xc7, 0x5e, 0x5b, 0xfe, 0x48, 0xf9, 0x29, + 0x0b, 0xb5, 0xa7, 0x7a, 0x6c, 0x85, 0x64, 0xa7, 0x69, 0x51, 0x11, 0xc5, 0xf5, 0x64, 0xbb, 0x21, 0x7e, 0xf8, 0x22, + 0x11, 0x94, 0x4b, 0x05, 0xc4, 0x90, 0x00, 0x04, 0x83, 0x19, 0xd4, 0x90, 0xd0, 0x51, 0x5f, 0x6f, 0x9e, 0x8e, 0x7b, + 0x08, 0x34, 0x4f, 0x85, 0x02, 0x62, 0xba, 0x62, 0x76, 0xbe, 0x0b, 0xa8, 0xe2, 0xfd, 0x1b, 0x6c, 0x9b, 0x56, 0xdf, + 0xd6, 0xb4, 0xca, 0x4f, 0xd6, 0x7f, 0xd4, 0xb9, 0x29, 0xb0, 0x21, 0x36, 0xa8, 0x52, 0x24, 0xac, 0x32, 0x06, 0x88, + 0x46, 0xcf, 0xdc, 0x64, 0x9a, 0xc2, 0xfe, 0xee, 0x3c, 0x5d, 0xf5, 0x75, 0x6a, 0xf3, 0x5d, 0xcf, 0xa5, 0xc4, 0x12, + 0x2e, 0xb3, 0xd0, 0xc7, 0x72, 0x00, 0x64, 0xa6, 0x87, 0xa5, 0x83, 0x06, 0x5f, 0x83, 0x57, 0x57, 0x2c, 0x55, 0xd7, + 0xec, 0x7e, 0xc8, 0xf8, 0xeb, 0x9b, 0xf4, 0x8a, 0xde, 0xc9, 0xc8, 0x7c, 0x73, 0xaf, 0x77, 0xd7, 0xea, 0xfa, 0x85, + 0xf5, 0x8c, 0xba, 0x54, 0x2d, 0x4f, 0x7f, 0x6f, 0xf7, 0x7d, 0x71, 0x67, 0xed, 0x4f, 0x41, 0x19, 0xdb, 0x93, 0x7c, + 0xa0, 0x9a, 0x1b, 0xff, 0x02, 0xcd, 0x9b, 0x82, 0x5a, 0x46, 0xa6, 0xbc, 0xad, 0xfd, 0x92, 0x1b, 0xf2, 0xf6, 0x44, + 0xc6, 0x11, 0xe7, 0x8e, 0x21, 0xef, 0x4b, 0xdb, 0xf8, 0xdc, 0xeb, 0x08, 0x14, 0x7e, 0x79, 0x3a, 0xa5, 0x80, 0xd6, + 0x84, 0x4b, 0xc4, 0x11, 0x5a, 0x5e, 0x97, 0x2e, 0x8a, 0x41, 0xe4, 0xe8, 0x03, 0xd8, 0xd2, 0x86, 0xe0, 0xd3, 0x22, + 0xfc, 0x6c, 0x26, 0xd4, 0x93, 0xad, 0x40, 0xad, 0x88, 0x2a, 0x7b, 0x48, 0x56, 0x02, 0xcb, 0x89, 0xe4, 0xa4, 0x27, + 0x75, 0x26, 0x90, 0x60, 0xea, 0x15, 0xef, 0xbb, 0x60, 0xc8, 0x62, 0x97, 0x2b, 0x0c, 0x2c, 0xe2, 0x64, 0xa1, 0x7e, + 0xbd, 0x3c, 0x95, 0x46, 0x0b, 0x0c, 0x01, 0x4c, 0x73, 0x2f, 0x2f, 0x1b, 0x23, 0x9e, 0xfe, 0xee, 0x86, 0x4c, 0x17, + 0x78, 0xf0, 0xcd, 0x8b, 0x9b, 0xd4, 0x52, 0x80, 0x9e, 0x9b, 0xfc, 0x6e, 0xa4, 0x9d, 0xc8, 0x09, 0xa9, 0xcd, 0x19, + 0x0e, 0x01, 0xaa, 0x9a, 0x3d, 0xc4, 0x5c, 0x2a, 0x65, 0x27, 0xae, 0x81, 0x2c, 0xbf, 0x89, 0xc0, 0x97, 0x6f, 0xe7, + 0xd8, 0x3b, 0x15, 0x95, 0xad, 0xd0, 0x0e, 0xa1, 0xa2, 0x36, 0xac, 0xee, 0xe6, 0xe1, 0x31, 0x47, 0xb0, 0xf3, 0x87, + 0x79, 0xdc, 0xd7, 0x0d, 0x8f, 0x10, 0x60, 0x05, 0xc2, 0x27, 0x04, 0x1f, 0x60, 0x88, 0x66, 0xba, 0xb5, 0xef, 0xef, + 0x55, 0x52, 0x55, 0x3c, 0x05, 0x38, 0x3e, 0xc0, 0xf0, 0xce, 0xd4, 0x63, 0xb3, 0x04, 0x9b, 0x79, 0x04, 0x86, 0x90, + 0x9b, 0xe6, 0x54, 0x53, 0x6e, 0x80, 0xf9, 0x2e, 0x62, 0x98, 0xe2, 0x91, 0xee, 0xd1, 0xf0, 0x01, 0xed, 0xc6, 0x9b, + 0x3b, 0x2f, 0xf0, 0xd3, 0x2c, 0x62, 0xd9, 0xf3, 0x64, 0x94, 0xc1, 0x27, 0x22, 0xdf, 0x22, 0x85, 0xcc, 0xfd, 0xc4, + 0x29, 0xac, 0xb6, 0x69, 0x7d, 0x51, 0x88, 0xdc, 0x5c, 0xdd, 0x98, 0x68, 0x0d, 0x5c, 0xa8, 0x4d, 0x54, 0x27, 0xd0, + 0xda, 0x66, 0x7b, 0xb8, 0xea, 0x4c, 0x24, 0x83, 0x27, 0xc2, 0xfc, 0x1b, 0xaf, 0xee, 0x64, 0xeb, 0x90, 0x8b, 0xd3, + 0xa3, 0x30, 0x57, 0x7b, 0x6b, 0xcf, 0x5b, 0xf7, 0x2d, 0x77, 0xd5, 0x9a, 0x3c, 0xa7, 0x45, 0x28, 0xb1, 0x93, 0x0c, + 0xa0, 0x08, 0xee, 0x9b, 0x41, 0xef, 0x3d, 0xd4, 0x89, 0x0c, 0x2e, 0x54, 0x31, 0xe3, 0xcc, 0x38, 0xca, 0xf3, 0x2b, + 0xae, 0x39, 0xb8, 0xfd, 0xbc, 0x71, 0x31, 0x10, 0xa0, 0xd0, 0x01, 0x99, 0xfa, 0x51, 0x99, 0xda, 0x9a, 0x26, 0xc7, + 0x7c, 0x05, 0x0b, 0x44, 0x86, 0x20, 0x00, 0x59, 0x78, 0xda, 0x56, 0xe9, 0x3e, 0x9e, 0x0c, 0x07, 0xca, 0x1b, 0x81, + 0x19, 0x19, 0x74, 0x10, 0xcd, 0x58, 0xdb, 0x99, 0x44, 0x44, 0x98, 0x84, 0x1b, 0x8b, 0x1a, 0xfe, 0xc5, 0x53, 0x52, + 0x3e, 0xe6, 0xa1, 0x87, 0x11, 0xd3, 0x62, 0x5e, 0x51, 0x7c, 0x49, 0x41, 0x3a, 0x97, 0x56, 0xdf, 0xb2, 0x4c, 0xce, + 0xa9, 0x97, 0xa1, 0xd0, 0x45, 0xc2, 0xa8, 0xb0, 0x49, 0x3d, 0x91, 0x01, 0x24, 0x63, 0x95, 0x19, 0xca, 0x15, 0x5e, + 0x8f, 0x2a, 0x79, 0x5c, 0xf2, 0x6f, 0xcc, 0xca, 0xb8, 0x1c, 0x5b, 0xd6, 0x0d, 0xeb, 0x1c, 0x1c, 0xaf, 0x54, 0xcb, + 0xe4, 0x9b, 0xa2, 0x38, 0xf1, 0xe2, 0x23, 0x06, 0xe2, 0xfd, 0xac, 0xde, 0x66, 0x9e, 0x7d, 0x58, 0xee, 0xda, 0xc2, + 0x95, 0x49, 0xc5, 0x20, 0x96, 0x30, 0x11, 0xb4, 0x28, 0x8d, 0xdf, 0x71, 0x30, 0xc5, 0x29, 0x40, 0x1b, 0x0b, 0xdf, + 0x1b, 0x49, 0x55, 0xe5, 0xb0, 0x5c, 0x46, 0x6f, 0xa5, 0xa8, 0xb3, 0x59, 0x5e, 0x46, 0x9b, 0x79, 0x12, 0x10, 0xe0, + 0xea, 0x4c, 0x59, 0xcd, 0x6e, 0x0e, 0x1d, 0x86, 0x33, 0xac, 0x2c, 0xe5, 0x84, 0x29, 0x9a, 0x35, 0x96, 0x12, 0x61, + 0xdc, 0x66, 0xb7, 0x2f, 0x8e, 0xdf, 0xd5, 0x72, 0x67, 0xfa, 0x0d, 0xdc, 0xe5, 0xae, 0x59, 0x40, 0x78, 0xe0, 0x11, + 0x9d, 0x93, 0xcb, 0x80, 0xaf, 0x8c, 0xea, 0x0d, 0x1a, 0xb0, 0x25, 0xeb, 0xa5, 0xf9, 0x58, 0x95, 0x87, 0xbe, 0x8a, + 0x5d, 0xbc, 0xd4, 0x25, 0xb4, 0x3a, 0xd4, 0xfa, 0xb0, 0xb7, 0xff, 0xb4, 0x57, 0xed, 0x34, 0xa0, 0x03, 0x62, 0x5f, + 0xeb, 0xf1, 0x65, 0x97, 0xff, 0xd5, 0x1f, 0xb7, 0x45, 0xa2, 0xed, 0x94, 0xba, 0x81, 0x0a, 0x41, 0xee, 0x40, 0xb0, + 0x95, 0xce, 0x67, 0xe5, 0x38, 0xe8, 0x85, 0x25, 0xa1, 0x16, 0x5e, 0x97, 0x97, 0x4a, 0xf0, 0x60, 0x4a, 0x49, 0xac, + 0x71, 0xaf, 0x37, 0x87, 0x01, 0x7d, 0xb8, 0xc5, 0x5a, 0x4d, 0x4c, 0x7f, 0x42, 0x54, 0x99, 0x48, 0x0f, 0x6c, 0x2f, + 0x9a, 0x98, 0xf0, 0xb0, 0x1f, 0x54, 0xa4, 0x84, 0xea, 0x40, 0xd0, 0x06, 0xca, 0xc4, 0x1c, 0x5f, 0x76, 0x28, 0x79, + 0x2e, 0xb4, 0xc0, 0x27, 0x06, 0xfb, 0x8e, 0xab, 0xb1, 0x50, 0xb1, 0x03, 0xc9, 0x31, 0x65, 0x0e, 0x37, 0xd8, 0x22, + 0xf6, 0x27, 0xd5, 0x40, 0xe9, 0xaf, 0xc6, 0x75, 0xdf, 0x56, 0x01, 0x94, 0xba, 0xe6, 0xc7, 0x7d, 0x8d, 0x42, 0x0f, + 0x16, 0xf1, 0x76, 0x08, 0xcf, 0x64, 0xbb, 0xa6, 0x22, 0xd6, 0x7c, 0x96, 0xec, 0xb9, 0x61, 0xc3, 0xdf, 0x57, 0x04, + 0x32, 0x46, 0x9a, 0x0e, 0x65, 0x6c, 0xc6, 0x2f, 0x65, 0x14, 0x53, 0x84, 0x7d, 0xe1, 0x77, 0x92, 0x10, 0x21, 0x42, + 0xc6, 0x30, 0xcd, 0x11, 0xb4, 0x33, 0x9f, 0x27, 0xb5, 0x40, 0x75, 0x4d, 0x42, 0xdf, 0xd3, 0xc3, 0x8a, 0x78, 0x90, + 0xa3, 0x47, 0x25, 0x00, 0xea, 0xbf, 0xc5, 0xbd, 0x27, 0x59, 0x31, 0x82, 0xb4, 0xe2, 0x44, 0x1a, 0x57, 0xe0, 0x38, + 0xc7, 0x27, 0x2d, 0x24, 0x88, 0x97, 0xea, 0x4e, 0x42, 0x5f, 0xb4, 0x71, 0x6a, 0xf0, 0x02, 0xb9, 0x28, 0x56, 0x2a, + 0x00, 0xb5, 0x5b, 0xf0, 0x66, 0x09, 0x33, 0x66, 0x48, 0x8f, 0xbc, 0x07, 0x6b, 0x1e, 0xf2, 0x52, 0x2e, 0x8f, 0x39, + 0x39, 0x87, 0xa8, 0xb9, 0x28, 0x92, 0x1a, 0x73, 0x05, 0x7d, 0x0d, 0x8a, 0x53, 0xe8, 0x63, 0x4c, 0xac, 0x36, 0x4f, + 0x7d, 0xaa, 0x86, 0xa2, 0xf4, 0x6c, 0x56, 0x17, 0xeb, 0x88, 0x2d, 0xb0, 0x0b, 0xcd, 0x18, 0x82, 0x5f, 0xc9, 0x24, + 0x87, 0x83, 0xb4, 0x4c, 0x04, 0x1d, 0x95, 0x17, 0x43, 0x27, 0x33, 0xda, 0xbb, 0xf4, 0x84, 0x3b, 0x7a, 0x28, 0x39, + 0x7d, 0x81, 0xd2, 0x43, 0x08, 0xd0, 0x5f, 0x8d, 0x68, 0xdc, 0xfe, 0x0a, 0x27, 0xc5, 0x8b, 0x09, 0x1f, 0x24, 0x51, + 0x84, 0x87, 0x70, 0x46, 0x14, 0x32, 0x12, 0xed, 0x43, 0xc1, 0xcc, 0x3b, 0xdb, 0xd6, 0x94, 0xf7, 0x45, 0x9d, 0x3a, + 0xcd, 0xc1, 0xcb, 0xf7, 0xe2, 0xb5, 0x5c, 0x4e, 0x3d, 0x7a, 0xec, 0xcb, 0x96, 0x90, 0x9d, 0x07, 0x00, 0x02, 0xe4, + 0x8b, 0x1d, 0x32, 0x26, 0x68, 0xc3, 0x9a, 0x96, 0x64, 0x4d, 0x3f, 0x5a, 0x84, 0x7e, 0x54, 0x7d, 0x9c, 0x66, 0x99, + 0x90, 0x6a, 0x0b, 0x63, 0x40, 0x84, 0x9e, 0x2a, 0x94, 0x60, 0x45, 0xee, 0x83, 0x97, 0xb8, 0x9a, 0x00, 0xdb, 0xb6, + 0x18, 0x9e, 0xb4, 0x37, 0x43, 0x60, 0x3b, 0x22, 0xa0, 0xd3, 0x0c, 0x89, 0x42, 0x6c, 0xb8, 0x8f, 0xd1, 0x4c, 0x52, + 0xc1, 0x98, 0x26, 0x2a, 0x1f, 0xfc, 0x07, 0xb5, 0x11, 0x37, 0x69, 0xaf, 0xe2, 0x79, 0x84, 0x3d, 0xc7, 0xa1, 0xeb, + 0xc2, 0x65, 0x40, 0x54, 0xd9, 0x72, 0x59, 0x73, 0x3d, 0x5a, 0x9e, 0x91, 0x41, 0x95, 0x48, 0xfd, 0x85, 0x5b, 0x07, + 0x95, 0x06, 0xd4, 0xb3, 0xf8, 0x64, 0xe0, 0xb9, 0x25, 0xb4, 0xdc, 0x9f, 0x23, 0x89, 0x07, 0xe0, 0xd4, 0xa3, 0x39, + 0xc2, 0x4b, 0x77, 0x87, 0x00, 0xf7, 0x56, 0x75, 0xbb, 0x69, 0x09, 0x28, 0x63, 0x27, 0xe1, 0xaa, 0xad, 0x52, 0x92, + 0x5a, 0x83, 0x12, 0xf3, 0xef, 0xf2, 0x4b, 0x3d, 0x76, 0x15, 0x1b, 0x96, 0x21, 0xd0, 0xb5, 0x42, 0xfd, 0xe5, 0x13, + 0xda, 0x49, 0xe1, 0xc6, 0xe1, 0x0d, 0xb2, 0x68, 0xf3, 0x11, 0xb5, 0x60, 0x2e, 0x50, 0x77, 0x5c, 0xd4, 0xbd, 0xf9, + 0x1b, 0xc1, 0x4d, 0x51, 0x53, 0xe8, 0x42, 0xc9, 0x46, 0x8f, 0x37, 0x12, 0x33, 0x40, 0x73, 0xb9, 0xd2, 0x0a, 0xcf, + 0xaa, 0x07, 0x6a, 0xbf, 0x21, 0x71, 0x6b, 0xbd, 0xbe, 0x0d, 0x1b, 0x3d, 0x44, 0xab, 0xc9, 0x82, 0x36, 0x46, 0x92, + 0xc7, 0xcc, 0xa1, 0xb5, 0x22, 0xd3, 0x35, 0x49, 0xb0, 0x2c, 0xa9, 0xf5, 0x6a, 0xd7, 0xf0, 0xf3, 0xb7, 0x3e, 0x40, + 0x58, 0x30, 0xb0, 0x5a, 0x49, 0xef, 0xb0, 0xdd, 0xca, 0xa5, 0x85, 0xab, 0x4d, 0xfe, 0x2c, 0x95, 0x43, 0x40, 0x9b, + 0x2c, 0xbf, 0xc4, 0xa5, 0xa7, 0x28, 0x88, 0xd4, 0x69, 0xab, 0xab, 0x84, 0x84, 0x60, 0xa5, 0x52, 0x3f, 0x1d, 0x98, + 0x90, 0x23, 0x2a, 0x47, 0x64, 0xf7, 0xba, 0x9c, 0xf3, 0x53, 0x03, 0xd2, 0xdd, 0x88, 0x48, 0xc8, 0xe9, 0x8d, 0x01, + 0x5c, 0x16, 0x1a, 0xfb, 0xdb, 0x80, 0x2b, 0x7c, 0x88, 0xe0, 0xb4, 0xef, 0x4a, 0xb9, 0x2e, 0x82, 0xfb, 0xbe, 0x40, + 0x8a, 0xaa, 0x22, 0x82, 0x05, 0xd5, 0x8e, 0x6c, 0xce, 0x8e, 0xfc, 0xc6, 0x8c, 0x02, 0xe7, 0xe6, 0x78, 0xd7, 0x28, + 0x42, 0xe9, 0x62, 0xe7, 0xbe, 0x62, 0x20, 0x4a, 0x12, 0x3e, 0x3b, 0x46, 0x68, 0xad, 0x75, 0x3e, 0xf1, 0x7e, 0xc0, + 0xb3, 0x24, 0x9c, 0x7f, 0x60, 0x93, 0xf7, 0xa5, 0x38, 0x2f, 0xaf, 0x36, 0x75, 0x5b, 0x30, 0x02, 0x50, 0x5f, 0x78, + 0xde, 0x56, 0x1e, 0xdc, 0x60, 0x64, 0x90, 0x27, 0x73, 0x81, 0xf1, 0xcc, 0xd5, 0x60, 0x9e, 0x1f, 0x3b, 0x2a, 0x04, + 0x2c, 0x04, 0xf2, 0x54, 0x53, 0x9b, 0xd6, 0x4a, 0x6c, 0xd1, 0x8e, 0xd9, 0x6f, 0xd9, 0x00, 0x27, 0xc0, 0xe9, 0x70, + 0xbc, 0xb4, 0x0d, 0xde, 0x90, 0x4b, 0x7a, 0x6b, 0x19, 0x05, 0xd9, 0x85, 0x7f, 0x1b, 0xb4, 0x06, 0xe5, 0x15, 0x08, + 0x15, 0x49, 0x1d, 0x1b, 0x25, 0xa5, 0x48, 0x1a, 0xa1, 0x65, 0xb6, 0x05, 0x59, 0x71, 0xb6, 0x47, 0x7c, 0xd5, 0xcc, + 0xe1, 0xa6, 0xc8, 0x6d, 0x91, 0xce, 0x1a, 0xee, 0x8b, 0x40, 0xc5, 0xa6, 0x90, 0x66, 0x5a, 0x23, 0xdb, 0xb8, 0x27, + 0xab, 0xb4, 0x77, 0x1b, 0x51, 0x33, 0x68, 0x44, 0xdf, 0xd2, 0x54, 0xf9, 0x7d, 0x2d, 0xaa, 0x97, 0x62, 0xa0, 0xcc, + 0x21, 0xa6, 0x6b, 0x5a, 0xc1, 0xa4, 0x4a, 0x2d, 0x8e, 0xf3, 0x36, 0x9f, 0x3e, 0x5c, 0x28, 0x87, 0xe4, 0xc0, 0x09, + 0x25, 0x47, 0x0c, 0xd9, 0x0a, 0x43, 0x70, 0x2b, 0x67, 0x13, 0xc9, 0x72, 0x23, 0x72, 0x99, 0x35, 0x46, 0x77, 0xfc, + 0x83, 0x05, 0xa0, 0xd0, 0x17, 0x1b, 0x14, 0xf4, 0x63, 0xad, 0xf5, 0x89, 0x3a, 0x52, 0x6a, 0x52, 0x7c, 0xba, 0x70, + 0x13, 0x95, 0x43, 0xcd, 0xd5, 0xab, 0xa2, 0x01, 0xb5, 0x26, 0x74, 0xc0, 0xf5, 0x08, 0x83, 0x0d, 0x84, 0xd1, 0x1f, + 0x4d, 0x21, 0x2c, 0xf7, 0x55, 0xdc, 0xb4, 0x9b, 0xbc, 0x7b, 0x3a, 0xdb, 0x63, 0xa4, 0x06, 0x15, 0x69, 0x59, 0x71, + 0x0c, 0xa7, 0x07, 0x9c, 0x83, 0xc7, 0x8e, 0x19, 0x36, 0x1b, 0xa7, 0xc7, 0x18, 0x03, 0x2c, 0x59, 0x61, 0xb1, 0x4d, + 0xa5, 0xb5, 0x22, 0x42, 0x6a, 0x9b, 0xd5, 0x4b, 0x9b, 0x3b, 0x45, 0x7e, 0xfb, 0x33, 0x00, 0xcc, 0xab, 0x26, 0xd3, + 0x3a, 0x8a, 0x29, 0x62, 0x94, 0xb4, 0x59, 0x1c, 0x2f, 0xc4, 0xca, 0x8b, 0x8f, 0x05, 0xee, 0x8f, 0x54, 0xb9, 0xb2, + 0xec, 0xb8, 0x3a, 0x93, 0xfb, 0xe1, 0xe6, 0x87, 0xcc, 0x49, 0xc4, 0x03, 0x16, 0xfa, 0x8c, 0xd9, 0x70, 0x75, 0xe2, + 0x1d, 0xa9, 0xc3, 0x2c, 0x26, 0xf7, 0xba, 0x78, 0xcb, 0xc7, 0xb9, 0x0b, 0xa8, 0xec, 0x41, 0xec, 0xb6, 0x2a, 0x63, + 0xbd, 0xce, 0xc8, 0x20, 0xe1, 0x5b, 0x2a, 0xf6, 0x4a, 0xc6, 0x4e, 0x7c, 0x06, 0x99, 0x1e, 0x2c, 0xc3, 0xc2, 0x53, + 0x46, 0x72, 0xfb, 0x4c, 0x15, 0xb5, 0xeb, 0x29, 0x95, 0xeb, 0xa2, 0x3b, 0xaf, 0xb9, 0xb7, 0x15, 0xee, 0xd4, 0xcc, + 0xa4, 0x13, 0xaf, 0x0b, 0x50, 0xe7, 0x83, 0x97, 0x16, 0xe9, 0x9c, 0x37, 0xb0, 0x6a, 0x85, 0xc2, 0x75, 0xa9, 0x46, + 0x9f, 0x5d, 0xee, 0xa3, 0x2d, 0x8e, 0x4d, 0x77, 0x7e, 0x5d, 0xf6, 0x68, 0xf2, 0x59, 0x87, 0x40, 0xec, 0x29, 0x22, + 0x3e, 0xa5, 0xc1, 0xad, 0x75, 0x98, 0x69, 0xab, 0xad, 0x0c, 0x54, 0x9b, 0xa4, 0x16, 0xf8, 0x49, 0x9b, 0xd2, 0xec, + 0x70, 0x6a, 0x79, 0xd7, 0x20, 0x96, 0xf8, 0x05, 0x0e, 0xab, 0x62, 0xf5, 0xec, 0xf1, 0x2d, 0xae, 0xac, 0x0c, 0x73, + 0xbf, 0x1e, 0x55, 0x0e, 0xb3, 0xb9, 0xe2, 0x78, 0x53, 0x1d, 0x91, 0x48, 0x6d, 0x3f, 0xf7, 0xf3, 0x27, 0x43, 0x45, + 0x8f, 0x83, 0x81, 0x38, 0x50, 0x55, 0xe4, 0x4c, 0x89, 0xb0, 0x0a, 0xa7, 0x25, 0x9a, 0x86, 0xc6, 0x3a, 0x14, 0x04, + 0x64, 0xd4, 0xff, 0x81, 0x70, 0x10, 0x99, 0xb7, 0x4e, 0x48, 0xaa, 0x2a, 0x35, 0x2c, 0xd1, 0x5e, 0xec, 0x7b, 0x48, + 0xe1, 0x21, 0x4f, 0xb6, 0x3e, 0x6f, 0xbf, 0xce, 0x91, 0x05, 0x0f, 0x04, 0xa3, 0x4c, 0x12, 0x03, 0x5b, 0x47, 0x97, + 0x7a, 0xd9, 0x8b, 0xbb, 0x4c, 0x40, 0x4f, 0x77, 0x1e, 0x7f, 0x84, 0x43, 0x51, 0xda, 0x9c, 0xbf, 0x6a, 0x49, 0x36, + 0xf3, 0xe8, 0xb6, 0x6a, 0xac, 0x43, 0x24, 0x36, 0x97, 0x1c, 0x2d, 0xe7, 0x45, 0x9e, 0x72, 0x74, 0xf9, 0x00, 0x8c, + 0x85, 0x77, 0xe7, 0x5c, 0x35, 0x17, 0x52, 0x4d, 0x5f, 0x1c, 0x13, 0xb8, 0x0e, 0x8f, 0xd8, 0x4a, 0xdb, 0x06, 0xeb, + 0xc1, 0x72, 0x88, 0xe7, 0xdc, 0x50, 0xae, 0x3f, 0xd4, 0x92, 0x6a, 0x52, 0xcf, 0x60, 0x1a, 0x2b, 0x75, 0x82, 0x26, + 0x65, 0xce, 0x2b, 0x9e, 0x3a, 0x98, 0x3a, 0x74, 0x93, 0x44, 0xf4, 0xd7, 0x91, 0x39, 0x91, 0xa4, 0x49, 0x3f, 0xb6, + 0x8d, 0x0a, 0x08, 0x80, 0x8e, 0x56, 0x08, 0x68, 0xf7, 0xbd, 0x5b, 0x7d, 0x26, 0xc9, 0x87, 0x67, 0x3d, 0x8a, 0xb9, + 0xd6, 0xd1, 0x56, 0xd7, 0xb0, 0x7c, 0x7b, 0x45, 0x18, 0xcd, 0xdb, 0x03, 0xb3, 0xc2, 0xd9, 0x88, 0x14, 0x63, 0xe7, + 0x2d, 0x20, 0x61, 0x1e, 0x22, 0xc7, 0xbb, 0x2e, 0x6a, 0xdc, 0x4a, 0xe7, 0xe8, 0xbc, 0x08, 0x4f, 0x9b, 0x2b, 0x16, + 0x4a, 0xd1, 0x4b, 0xe5, 0xd8, 0x6f, 0xde, 0x99, 0x51, 0x43, 0x5e, 0xf2, 0xb0, 0xf1, 0x7e, 0x94, 0xa7, 0xa7, 0x30, + 0x3a, 0x3f, 0xc4, 0x61, 0xee, 0x48, 0x5f, 0xa9, 0x03, 0xf4, 0x7a, 0x4f, 0x0e, 0xdf, 0xae, 0xef, 0x65, 0x27, 0x38, + 0x5c, 0x18, 0x2e, 0x8a, 0xf3, 0x05, 0xa9, 0x24, 0xe6, 0x28, 0xf5, 0x78, 0x51, 0x4f, 0xf1, 0x81, 0x78, 0xe5, 0x04, + 0xdb, 0x5e, 0xf6, 0xfd, 0xdf, 0x85, 0x33, 0x29, 0xbf, 0xef, 0x2e, 0x81, 0xaf, 0x07, 0x7f, 0xe8, 0xf6, 0x0b, 0x1c, + 0x89, 0xc8, 0x61, 0x1c, 0xee, 0xd8, 0x56, 0x71, 0xbd, 0x6f, 0xc3, 0x16, 0xa9, 0xd7, 0x1f, 0x27, 0x84, 0x5c, 0x37, + 0xe4, 0xa8, 0x3b, 0x28, 0xe2, 0x65, 0x09, 0x4c, 0xdc, 0x14, 0x42, 0x14, 0xe3, 0xbf, 0x5c, 0xcd, 0x53, 0x84, 0x5f, + 0x35, 0xa2, 0xb0, 0x55, 0x53, 0x53, 0x70, 0x57, 0x60, 0x00, 0x56, 0xf2, 0x04, 0x77, 0xa0, 0xe5, 0x43, 0x59, 0x78, + 0x85, 0x8e, 0xd5, 0xa2, 0xac, 0x04, 0x6a, 0x99, 0x21, 0x8f, 0x08, 0x4e, 0xd0, 0x5e, 0x84, 0x59, 0xd7, 0x30, 0x29, + 0xf7, 0x60, 0xf2, 0xb6, 0x6e, 0xe1, 0x75, 0xb7, 0xa9, 0xd3, 0xc3, 0xfb, 0x55, 0x69, 0xb1, 0xab, 0xb2, 0xc7, 0x03, + 0xe4, 0x28, 0x39, 0xbd, 0x03, 0x30, 0xe7, 0x61, 0x12, 0xd8, 0xea, 0xd2, 0x1c, 0xb6, 0x76, 0x97, 0xd0, 0x6f, 0x33, + 0x7c, 0xba, 0x43, 0x66, 0xa3, 0xa4, 0x9d, 0x7d, 0xfe, 0x53, 0x05, 0x8b, 0xa1, 0x37, 0x00, 0x9e, 0xb0, 0xee, 0x64, + 0xb5, 0xb0, 0x81, 0x7b, 0xfc, 0xd3, 0x87, 0xa6, 0x28, 0xa4, 0x25, 0xa6, 0xb1, 0x8b, 0xa3, 0x9a, 0x6c, 0xad, 0xf6, + 0x1a, 0x39, 0xbb, 0x21, 0x71, 0x55, 0x4a, 0x88, 0x2e, 0x47, 0xb4, 0x42, 0xb2, 0x47, 0x14, 0xc1, 0x6a, 0xef, 0x2c, + 0xdd, 0x46, 0x5f, 0xc3, 0x74, 0x05, 0x18, 0x4d, 0xc0, 0xb0, 0x41, 0xa5, 0xbd, 0x13, 0x00, 0x18, 0xa5, 0x55, 0x53, + 0xa7, 0xf4, 0x2e, 0x76, 0xd9, 0xe5, 0xe6, 0x41, 0xa6, 0xd4, 0x13, 0x35, 0x93, 0xdb, 0x03, 0x2a, 0x6b, 0x2d, 0x54, + 0xb2, 0x5f, 0x72, 0xc5, 0xa7, 0x51, 0x89, 0x56, 0xe8, 0x6a, 0x46, 0x07, 0xdd, 0x4c, 0xd1, 0x51, 0x22, 0xb6, 0x4c, + 0x4c, 0xbb, 0xf2, 0x66, 0x98, 0x78, 0xa9, 0xd8, 0x2a, 0x33, 0x22, 0x4d, 0xd9, 0xa2, 0x96, 0x23, 0xe2, 0xfc, 0xa8, + 0xbd, 0x66, 0x55, 0xa7, 0x36, 0xd6, 0x5a, 0x78, 0xba, 0x38, 0xc4, 0xe4, 0xea, 0x43, 0xb4, 0xdd, 0x07, 0x29, 0x38, + 0xd3, 0xa6, 0x8d, 0x2b, 0xb5, 0xcd, 0xbe, 0x88, 0x32, 0x5e, 0x91, 0x71, 0x11, 0xb3, 0xd9, 0xed, 0x93, 0xa5, 0x1d, + 0x26, 0xca, 0xe3, 0x98, 0x4c, 0x46, 0x0e, 0x54, 0xd2, 0x06, 0xaa, 0x25, 0xf7, 0x92, 0x15, 0x17, 0x71, 0xf1, 0xdf, + 0x68, 0xd9, 0xe6, 0xd9, 0xc2, 0xc0, 0x82, 0x16, 0x66, 0x89, 0x02, 0xb3, 0x54, 0x4a, 0x07, 0x25, 0x1c, 0x45, 0x64, + 0x27, 0x09, 0xd3, 0xcb, 0x92, 0x36, 0xf8, 0xa0, 0x91, 0xee, 0x4e, 0x26, 0x0d, 0x09, 0x97, 0x6b, 0x9c, 0xb5, 0x2d, + 0x26, 0x32, 0xe2, 0xa9, 0x6f, 0x4a, 0x26, 0xc2, 0x48, 0x3c, 0xc8, 0x94, 0x98, 0x0b, 0xcf, 0x06, 0x52, 0xe2, 0x8b, + 0x9c, 0x7e, 0xae, 0x17, 0xb3, 0xd1, 0x22, 0x8d, 0xfe, 0xa1, 0xe7, 0x97, 0xc5, 0xce, 0x6e, 0x47, 0x8c, 0x7a, 0x7b, + 0x5c, 0x79, 0x56, 0x53, 0x6b, 0xd7, 0x8c, 0x1c, 0x33, 0x06, 0x34, 0x52, 0x08, 0x14, 0xd2, 0x27, 0x23, 0x9c, 0x16, + 0x97, 0x03, 0x1b, 0x36, 0xbe, 0x53, 0x8e, 0x67, 0x0a, 0xb7, 0x17, 0x43, 0xc3, 0x73, 0x87, 0x44, 0x10, 0xa1, 0xf1, + 0x06, 0x67, 0xce, 0x50, 0xff, 0xe1, 0xe9, 0xbc, 0x35, 0xd7, 0xfd, 0x0f, 0x8a, 0x9e, 0xa5, 0x45, 0x44, 0x80, 0xf3, + 0x45, 0x45, 0x9a, 0xdb, 0x7b, 0x27, 0x8b, 0xac, 0xc7, 0x37, 0xcd, 0xfa, 0x15, 0x01, 0xdc, 0x49, 0x02, 0x42, 0x80, + 0x86, 0xd7, 0xf5, 0x7c, 0x38, 0x4b, 0x58, 0x1e, 0x60, 0xba, 0xab, 0xe0, 0xef, 0xc4, 0x4d, 0xce, 0x4b, 0x13, 0xfa, + 0xb1, 0xa8, 0xe0, 0x83, 0x9d, 0x2c, 0x10, 0x6e, 0x01, 0x96, 0x10, 0x04, 0x82, 0x92, 0x99, 0x29, 0xa6, 0x12, 0xfa, + 0x0b, 0x29, 0x21, 0x43, 0x02, 0x4c, 0x47, 0xe3, 0x82, 0x1b, 0x24, 0xd5, 0x46, 0x3c, 0xad, 0x62, 0x36, 0x9c, 0x34, + 0x0c, 0x88, 0xf5, 0xc7, 0x30, 0xd8, 0x2a, 0x26, 0xc9, 0xb0, 0xbf, 0xb3, 0x37, 0x9e, 0x0c, 0xa7, 0x4b, 0x14, 0x72, + 0xb1, 0xcf, 0x98, 0x3c, 0xa1, 0xe9, 0x17, 0x85, 0xa8, 0x2f, 0xeb, 0x82, 0xd3, 0x7b, 0x76, 0x74, 0x07, 0x4f, 0xae, + 0x32, 0xd2, 0xdb, 0x38, 0xb0, 0xdc, 0x42, 0x22, 0xc0, 0xbc, 0xdf, 0x03, 0xcd, 0x48, 0x32, 0x64, 0x28, 0x03, 0xcc, + 0x35, 0x66, 0x4f, 0x0d, 0x4d, 0x0f, 0x65, 0x47, 0x72, 0x6d, 0x12, 0xac, 0x1e, 0xe6, 0xbe, 0xbc, 0xb2, 0x6e, 0x73, + 0xbd, 0x03, 0xb9, 0x6e, 0x6b, 0x08, 0xd8, 0xe5, 0x88, 0x34, 0xa7, 0x26, 0xb7, 0x09, 0xd5, 0x03, 0x14, 0x48, 0x35, + 0xd5, 0xb4, 0x0e, 0x0e, 0x37, 0x7c, 0xd0, 0x01, 0xe1, 0x26, 0xc4, 0x46, 0xe5, 0x11, 0x7a, 0xad, 0xc6, 0x3e, 0xd1, + 0xd7, 0x92, 0x6b, 0x9a, 0x6f, 0x90, 0x3a, 0x72, 0xa9, 0xea, 0x3c, 0x4e, 0xd4, 0xb5, 0xb6, 0xda, 0x82, 0x2d, 0xc2, + 0x00, 0x8b, 0x55, 0x0c, 0x87, 0xe8, 0x54, 0xa8, 0x68, 0x89, 0x7b, 0x1b, 0x73, 0xd5, 0xcb, 0x9d, 0xb7, 0x55, 0x97, + 0x7a, 0xa7, 0x06, 0x8d, 0xc8, 0xf4, 0x50, 0x01, 0x0e, 0x84, 0x8c, 0xb5, 0x7d, 0xb0, 0x8c, 0xe3, 0x8c, 0x54, 0x65, + 0xd8, 0x08, 0x46, 0xd3, 0x01, 0xca, 0x5a, 0xf5, 0x38, 0x9c, 0x03, 0x62, 0x79, 0x48, 0x6e, 0x9a, 0xcc, 0x10, 0xd9, + 0x22, 0x9b, 0x5f, 0x6a, 0xf2, 0xe4, 0x0a, 0x1d, 0x0d, 0xfb, 0x1e, 0xd0, 0xae, 0xee, 0xc0, 0x40, 0x76, 0xf8, 0xaa, + 0x93, 0xce, 0x72, 0x09, 0xb4, 0x39, 0x86, 0xce, 0x85, 0xc5, 0x29, 0x9f, 0xa7, 0x23, 0x1b, 0xee, 0x1d, 0xe0, 0x45, + 0x47, 0xd7, 0x0b, 0xf0, 0xdb, 0xc1, 0xc5, 0x1d, 0x63, 0x0f, 0x6e, 0xca, 0xa3, 0x2c, 0x3b, 0x95, 0x30, 0x95, 0x47, + 0x13, 0x17, 0xeb, 0x9c, 0x0b, 0x5d, 0xce, 0xe6, 0x75, 0xba, 0xf5, 0x27, 0x2a, 0x86, 0x9b, 0xb5, 0x73, 0x06, 0xcf, + 0x55, 0x4e, 0x87, 0x24, 0x62, 0x49, 0x8b, 0x73, 0xf4, 0x85, 0x44, 0x9e, 0xd6, 0xf9, 0xfd, 0x42, 0x81, 0xce, 0xa9, + 0x83, 0x6a, 0x1d, 0xe3, 0xcc, 0x4e, 0x0f, 0x3a, 0xef, 0x95, 0xc6, 0xa2, 0xb1, 0x4a, 0x59, 0xf1, 0x1f, 0x38, 0xb7, + 0xf4, 0xf6, 0x84, 0xb0, 0x49, 0x2a, 0xa4, 0x50, 0x96, 0x09, 0xb7, 0x3d, 0x0e, 0x34, 0x6d, 0xe7, 0x44, 0x76, 0x5b, + 0xdf, 0xbe, 0x93, 0x24, 0x22, 0x71, 0xdb, 0x0b, 0xa2, 0xf0, 0x0c, 0xd0, 0x18, 0x92, 0xb3, 0xe7, 0x9d, 0x75, 0xf9, + 0xd2, 0xcb, 0x72, 0xbc, 0xc2, 0xde, 0x15, 0x83, 0xb1, 0xb0, 0x42, 0x0b, 0x0b, 0x37, 0x0d, 0xd4, 0xb1, 0x93, 0x24, + 0x76, 0x59, 0x12, 0x3f, 0xb6, 0xfc, 0x33, 0x69, 0x6e, 0x44, 0x9e, 0x8a, 0x8e, 0x75, 0xc8, 0x3e, 0x73, 0xaa, 0x54, + 0xf7, 0x5a, 0xe5, 0x41, 0x39, 0xe6, 0xa9, 0x1a, 0x31, 0x67, 0x6e, 0x33, 0x45, 0x3e, 0x92, 0x3e, 0x6f, 0xae, 0x67, + 0x94, 0x28, 0x10, 0xa9, 0x0b, 0xbd, 0xca, 0x9c, 0xab, 0xd0, 0x91, 0x42, 0x4a, 0xb7, 0x46, 0xb3, 0x89, 0x39, 0x0e, + 0x67, 0x3f, 0x55, 0xd9, 0x13, 0x7c, 0xed, 0x3d, 0x6f, 0xed, 0xc3, 0x66, 0x83, 0xeb, 0x50, 0xf3, 0x21, 0x3d, 0x60, + 0xa6, 0x99, 0x3b, 0x53, 0x20, 0x0b, 0xdb, 0xaf, 0xec, 0x48, 0x94, 0x32, 0xfd, 0x63, 0xa3, 0x75, 0x7d, 0xd9, 0x47, + 0x75, 0x4c, 0xfe, 0xfd, 0x2d, 0x5d, 0xc3, 0x55, 0x07, 0x45, 0x8e, 0xe1, 0x58, 0xd1, 0x6e, 0xa5, 0x3b, 0x00, 0xe1, + 0x35, 0x3b, 0x8c, 0xdc, 0x72, 0x36, 0x45, 0xbd, 0x55, 0x57, 0xc1, 0x02, 0x6a, 0xd4, 0x91, 0x94, 0xbd, 0x51, 0x58, + 0x44, 0xfd, 0x9a, 0x5d, 0x8b, 0x2b, 0x8a, 0x6e, 0x59, 0xe3, 0x7e, 0xc8, 0xec, 0xa8, 0x3f, 0xe2, 0x5a, 0xb9, 0xc3, + 0x0c, 0xd9, 0xe1, 0x1a, 0x53, 0x48, 0xea, 0x8d, 0xc6, 0xcd, 0xb6, 0xd5, 0xf3, 0x4c, 0xc3, 0xb8, 0x6d, 0xcd, 0xd2, + 0x26, 0x76, 0x50, 0x0d, 0xb7, 0x75, 0xc1, 0x54, 0xb5, 0x5d, 0xf8, 0xfa, 0xd5, 0x6e, 0x25, 0xb2, 0x26, 0xb4, 0xe1, + 0x68, 0x6b, 0x60, 0x9a, 0x16, 0xf9, 0x5c, 0xf4, 0xec, 0x6a, 0xb0, 0xc5, 0xbe, 0x0b, 0xd9, 0xbc, 0xfb, 0x6b, 0x95, + 0x84, 0x2a, 0xb9, 0x72, 0x1f, 0x97, 0xe4, 0x27, 0x9d, 0xac, 0xc2, 0x33, 0xb5, 0x8d, 0xfc, 0x0e, 0x27, 0xda, 0x87, + 0x95, 0xe6, 0x69, 0x25, 0xb3, 0x10, 0x10, 0x85, 0xae, 0xf0, 0x2a, 0x04, 0xba, 0x05, 0x0b, 0xff, 0x07, 0x3a, 0x76, + 0x65, 0x5c, 0x0a, 0xe9, 0x8d, 0xca, 0x39, 0x74, 0x43, 0x42, 0x3e, 0xb4, 0xb0, 0x9c, 0x9c, 0x97, 0x1a, 0x74, 0xb5, + 0x35, 0x74, 0x64, 0x79, 0x20, 0x02, 0xfc, 0x44, 0x0e, 0x79, 0xa6, 0x26, 0xd8, 0xfd, 0x24, 0x70, 0x96, 0x66, 0xb3, + 0x08, 0xbf, 0x18, 0x70, 0x86, 0xa4, 0xf6, 0x9e, 0x7e, 0xf9, 0x14, 0x68, 0x0f, 0xbf, 0x5c, 0x68, 0x7d, 0x72, 0x66, + 0xce, 0x51, 0x4b, 0xc7, 0x0d, 0x1c, 0xc2, 0x45, 0x69, 0xc0, 0xf7, 0x48, 0x35, 0x86, 0x29, 0x22, 0x4c, 0x0e, 0xe0, + 0x5c, 0xb9, 0x6d, 0x78, 0x56, 0x6e, 0x02, 0x33, 0x1d, 0x29, 0xa5, 0x15, 0xc7, 0xa8, 0xfb, 0xb6, 0xf7, 0xa3, 0x24, + 0xe9, 0xcd, 0xc7, 0xcb, 0xac, 0x50, 0xfa, 0x9e, 0x99, 0x85, 0xae, 0xe2, 0x77, 0x26, 0xb9, 0xab, 0x4b, 0xe8, 0xa4, + 0x5a, 0xce, 0x80, 0x51, 0xae, 0x56, 0x58, 0xee, 0x84, 0x40, 0x0e, 0x9b, 0xfb, 0xe9, 0x66, 0x90, 0x26, 0x5b, 0x51, + 0x95, 0x18, 0x23, 0x52, 0x68, 0xbf, 0xd9, 0x9d, 0xfb, 0xa3, 0xd5, 0x0c, 0x3a, 0xea, 0x3b, 0x66, 0x5c, 0xcd, 0xb7, + 0x62, 0xbb, 0xd8, 0xb0, 0x83, 0x69, 0x14, 0x75, 0x98, 0xe6, 0x01, 0x42, 0xf7, 0x2c, 0x1d, 0xa8, 0x5f, 0x10, 0x9f, + 0xf2, 0x76, 0x55, 0x6d, 0x1d, 0xe4, 0x62, 0xa6, 0xa2, 0x7c, 0x8a, 0x1a, 0x14, 0xb0, 0x68, 0xdd, 0x2e, 0x4d, 0xc0, + 0x14, 0x59, 0x48, 0xb7, 0x90, 0x82, 0x28, 0x59, 0x08, 0x66, 0x50, 0xf1, 0x99, 0xbf, 0x4c, 0x7c, 0xad, 0x8f, 0x16, + 0x3c, 0xa5, 0x27, 0x6c, 0x15, 0x72, 0x75, 0xc7, 0x68, 0x31, 0xab, 0x4e, 0x3b, 0x4e, 0x13, 0x87, 0x0e, 0x35, 0xea, + 0x88, 0xd8, 0x75, 0x7c, 0xf0, 0x54, 0x32, 0x79, 0x83, 0xec, 0x2f, 0x27, 0x01, 0x3f, 0xd6, 0xb3, 0x5f, 0x32, 0x7b, + 0x88, 0x55, 0x69, 0xc6, 0xe3, 0x85, 0xb2, 0x47, 0xe5, 0xa8, 0xa8, 0x35, 0xf6, 0x73, 0x17, 0xa7, 0xb5, 0x51, 0x49, + 0x21, 0x77, 0x1e, 0x2e, 0xe4, 0x2b, 0xa7, 0x70, 0xee, 0x46, 0x25, 0xa2, 0x3c, 0x80, 0x99, 0xb0, 0x39, 0x71, 0xa3, + 0xe2, 0x16, 0x50, 0x39, 0xd3, 0x93, 0x26, 0x31, 0x9d, 0x95, 0x88, 0x31, 0xa3, 0x4b, 0xb8, 0x1e, 0x87, 0x68, 0x0c, + 0xcd, 0x30, 0xa7, 0xf7, 0x31, 0x7a, 0x82, 0x1c, 0x50, 0x0f, 0xed, 0x5a, 0x43, 0x88, 0x99, 0x54, 0xf8, 0x56, 0xad, + 0x88, 0x2d, 0xb3, 0x4f, 0x04, 0xb5, 0x6d, 0x2e, 0xf3, 0x88, 0x28, 0x6f, 0x29, 0x7c, 0x9f, 0xfb, 0xcb, 0x77, 0x8c, + 0x57, 0x72, 0xe8, 0x9d, 0x8e, 0x92, 0x9f, 0xc3, 0xfc, 0xec, 0x37, 0x0e, 0x60, 0x01, 0x11, 0xe7, 0x92, 0x9c, 0x7a, + 0x4a, 0x96, 0xe6, 0x3a, 0xeb, 0x75, 0x13, 0xc1, 0x2c, 0x99, 0x06, 0x4c, 0xac, 0x65, 0x16, 0x40, 0x07, 0x52, 0x09, + 0x9c, 0x15, 0x95, 0x75, 0x34, 0x93, 0x47, 0x0b, 0xbd, 0x37, 0xf1, 0xf0, 0x45, 0x29, 0xc6, 0x02, 0xfc, 0xb1, 0xa5, + 0xc6, 0xa2, 0x4c, 0xdf, 0xbc, 0x08, 0x54, 0xcd, 0x5a, 0x1e, 0x87, 0x74, 0xe9, 0xf5, 0x8a, 0x9a, 0x55, 0x49, 0x6b, + 0xa9, 0x2e, 0xd0, 0x76, 0x40, 0x8e, 0x51, 0x8b, 0xea, 0x0c, 0xd2, 0x50, 0xb4, 0x07, 0x4a, 0x5f, 0xc3, 0x84, 0x1e, + 0xf0, 0x4b, 0x35, 0x90, 0xd9, 0xe0, 0x9d, 0x4d, 0xb7, 0xb8, 0x98, 0x1c, 0x39, 0xeb, 0x06, 0x10, 0x70, 0xbb, 0xde, + 0x96, 0x9a, 0x08, 0xa9, 0x70, 0x83, 0x71, 0x59, 0x24, 0xea, 0x2f, 0x9a, 0xc3, 0xda, 0x15, 0x92, 0x3a, 0xc4, 0x3a, + 0xb4, 0x30, 0x01, 0xad, 0x19, 0x17, 0x1b, 0x5a, 0x94, 0x9d, 0xc8, 0x81, 0xb5, 0x59, 0x24, 0x19, 0x87, 0x3d, 0x9a, + 0x69, 0x33, 0x91, 0x6b, 0x09, 0x9e, 0x4a, 0x44, 0x6f, 0xd1, 0xf4, 0xeb, 0x07, 0x15, 0x36, 0x37, 0x99, 0x54, 0xca, + 0x4c, 0x8f, 0x86, 0x40, 0xbb, 0x76, 0x07, 0x7c, 0x87, 0x0a, 0xfe, 0x12, 0x3e, 0x18, 0x45, 0xf7, 0xfb, 0xec, 0x69, + 0x07, 0x7c, 0x08, 0xa7, 0x4e, 0xfb, 0x45, 0x80, 0x75, 0x0e, 0x94, 0x62, 0x5d, 0x98, 0xe3, 0x8c, 0xa3, 0x76, 0x35, + 0xa3, 0x8d, 0xfd, 0xc4, 0x18, 0x02, 0x85, 0xc3, 0xb7, 0x3d, 0x5a, 0x79, 0xd5, 0xcc, 0xd6, 0x4c, 0x2f, 0x69, 0x47, + 0x3e, 0xa2, 0x46, 0x30, 0x09, 0x22, 0x69, 0x99, 0x40, 0x68, 0xc6, 0xe8, 0x2d, 0x5c, 0xc1, 0xda, 0x9c, 0x01, 0x2d, + 0x75, 0xbd, 0x50, 0xe8, 0x81, 0xa7, 0xe7, 0x4c, 0x4c, 0x0a, 0xf3, 0x01, 0x2e, 0x69, 0xff, 0xde, 0x1c, 0x66, 0x0d, + 0xd5, 0x6a, 0x6d, 0xb7, 0x65, 0x7d, 0x97, 0x28, 0x10, 0xb6, 0x1f, 0xda, 0x45, 0xf7, 0x23, 0x3f, 0xbb, 0x16, 0xa0, + 0xce, 0x62, 0xdb, 0x35, 0x9e, 0xf4, 0xd7, 0x5e, 0xb7, 0x04, 0x1f, 0xfb, 0x2b, 0x0d, 0x9f, 0x54, 0x0c, 0xcb, 0x92, + 0x09, 0xd3, 0x95, 0xe5, 0x18, 0x67, 0xa5, 0xb8, 0xcf, 0xcb, 0x98, 0x74, 0x77, 0x28, 0x31, 0x89, 0xaf, 0x3b, 0x1b, + 0xd0, 0xb7, 0x8c, 0xe8, 0x65, 0xfd, 0x56, 0xcf, 0xb0, 0xd7, 0x25, 0x80, 0x98, 0x7a, 0x45, 0xc5, 0x78, 0x98, 0xe8, + 0x8b, 0x87, 0xa0, 0x30, 0x7e, 0x94, 0xb9, 0x18, 0x7c, 0x52, 0x6f, 0x5b, 0x48, 0x84, 0x9f, 0xc6, 0xa3, 0xb8, 0x98, + 0xb5, 0x68, 0xd8, 0x76, 0x3d, 0x29, 0x0e, 0x84, 0x84, 0xd6, 0xcc, 0xa7, 0x49, 0x5a, 0x73, 0x29, 0x0c, 0xbf, 0x59, + 0x88, 0x8d, 0x66, 0xe3, 0x28, 0x5a, 0x0b, 0x60, 0x74, 0x55, 0x73, 0xc5, 0x62, 0xe0, 0x61, 0xc1, 0x43, 0xf9, 0xd2, + 0x12, 0x96, 0x3d, 0x7f, 0x9f, 0x4e, 0xe4, 0x9b, 0xbb, 0x9c, 0x6e, 0xbf, 0x77, 0x9d, 0xbd, 0xb9, 0x4b, 0x27, 0xca, + 0xea, 0x17, 0x1d, 0x95, 0xa8, 0xc6, 0xfa, 0xd8, 0xfa, 0x70, 0x97, 0x5b, 0xfd, 0x44, 0x72, 0xda, 0xd9, 0x0e, 0x18, + 0xb7, 0x14, 0xb0, 0x65, 0xda, 0x1e, 0x36, 0xe5, 0x3f, 0xde, 0xba, 0x38, 0xd2, 0x28, 0x48, 0x7c, 0xc2, 0x9c, 0x21, + 0x49, 0xf1, 0xd8, 0x64, 0x00, 0xa3, 0x96, 0x01, 0xf5, 0x08, 0xf6, 0x75, 0x63, 0x47, 0xbe, 0xb9, 0x8c, 0x71, 0xa9, + 0x4e, 0xbb, 0x0e, 0x64, 0xda, 0xe5, 0x21, 0xb0, 0x71, 0x9b, 0xbb, 0x1c, 0x28, 0x12, 0x07, 0x2a, 0x62, 0xa6, 0xfd, + 0x22, 0xf5, 0xa7, 0x1b, 0xc4, 0x46, 0xed, 0xc0, 0xf5, 0x39, 0xd8, 0x14, 0xfb, 0x64, 0xb1, 0xdf, 0xca, 0x3b, 0x3b, + 0xec, 0x8d, 0xf4, 0x47, 0x9c, 0x9b, 0xcf, 0x38, 0x30, 0xa2, 0x4a, 0x73, 0x31, 0x2b, 0x91, 0x2a, 0xb2, 0xa7, 0x95, + 0xef, 0x2f, 0xa4, 0x49, 0xe0, 0xdc, 0xad, 0x0f, 0x3d, 0x9c, 0xcc, 0x9e, 0x08, 0x93, 0x39, 0xe4, 0x3b, 0x78, 0x49, + 0x89, 0xa6, 0x0b, 0x8d, 0xb6, 0x5b, 0x07, 0x04, 0x76, 0x02, 0xe6, 0x69, 0x89, 0xbc, 0x4e, 0xc9, 0x4b, 0x7e, 0xff, + 0xf6, 0xcf, 0xd2, 0xb2, 0x80, 0xe1, 0xc8, 0x53, 0x5c, 0xa5, 0x00, 0x11, 0xc7, 0x71, 0xfe, 0x6a, 0xdd, 0x67, 0x24, + 0xc6, 0xfa, 0xf3, 0xbb, 0x1f, 0xec, 0x3e, 0x41, 0xae, 0xa4, 0xa1, 0xf0, 0xcc, 0xcd, 0x91, 0x9d, 0x83, 0xec, 0xca, + 0xb8, 0x62, 0xb7, 0x41, 0x3f, 0x89, 0x2c, 0x2a, 0xd1, 0x4c, 0xeb, 0xcd, 0x29, 0x16, 0x49, 0x49, 0x2b, 0x2c, 0x6a, + 0xc9, 0x17, 0x0c, 0xe5, 0x30, 0x59, 0x96, 0xb6, 0x9d, 0x39, 0x0e, 0xc5, 0x5a, 0x96, 0x00, 0xd9, 0xc5, 0x12, 0x9c, + 0x2b, 0x8a, 0x5c, 0x86, 0x95, 0x35, 0xb7, 0x02, 0xe3, 0xc0, 0x14, 0x7e, 0xf2, 0x8f, 0x12, 0xed, 0xef, 0x64, 0x58, + 0xb2, 0x8b, 0x3f, 0xa7, 0x2b, 0xf4, 0xda, 0xb9, 0x17, 0xcc, 0x60, 0x32, 0x44, 0xef, 0xb1, 0x84, 0x79, 0xb9, 0x13, + 0xaf, 0x4a, 0x96, 0xa5, 0x71, 0xe0, 0xa0, 0x59, 0x37, 0x6b, 0x75, 0xdf, 0x22, 0x48, 0xcb, 0x86, 0xab, 0x46, 0xac, + 0xb4, 0x12, 0x2a, 0xa1, 0x29, 0xe8, 0x88, 0x92, 0xbc, 0x44, 0x98, 0x19, 0x80, 0xb2, 0x93, 0x88, 0xca, 0x08, 0x82, + 0x63, 0x58, 0xb1, 0x98, 0x69, 0x5c, 0xd9, 0x80, 0xd5, 0xa9, 0xf1, 0x51, 0x1e, 0xba, 0x5e, 0xc0, 0x50, 0x7d, 0xed, + 0x4d, 0xc6, 0x39, 0xc6, 0xbc, 0xd6, 0x4c, 0x73, 0x72, 0xec, 0x7f, 0xdc, 0x55, 0x13, 0x24, 0x2d, 0xbe, 0x1f, 0xed, + 0x67, 0x0c, 0x4d, 0x33, 0x20, 0x96, 0x3d, 0x7c, 0xc2, 0x56, 0x07, 0x6c, 0xd2, 0x75, 0xd8, 0x48, 0x12, 0x25, 0xe8, + 0x4d, 0x9c, 0x66, 0xfb, 0x26, 0x80, 0xa1, 0xba, 0x34, 0xc8, 0x9e, 0x47, 0x46, 0xbc, 0x35, 0x96, 0x03, 0x4b, 0xbe, + 0x02, 0xba, 0xa0, 0x3c, 0xf3, 0x08, 0xce, 0xb6, 0x73, 0x20, 0x0a, 0x63, 0x2d, 0x8a, 0x4b, 0x9c, 0xf0, 0x3b, 0x92, + 0x43, 0x59, 0x32, 0x43, 0x61, 0xca, 0xe7, 0xe0, 0x5c, 0x99, 0x0f, 0x1f, 0xfe, 0x90, 0x7f, 0xfc, 0x4c, 0x57, 0x97, + 0x22, 0xf6, 0xf9, 0x71, 0x8e, 0xcf, 0xbf, 0x4d, 0x7a, 0xca, 0xed, 0x2c, 0xfc, 0x06, 0xde, 0x59, 0x42, 0xce, 0xbb, + 0x1f, 0x7e, 0xd4, 0x2d, 0x0e, 0x8a, 0x85, 0xce, 0x62, 0x8b, 0x5a, 0x70, 0xfe, 0xf9, 0x79, 0x31, 0x17, 0x55, 0x1e, + 0x13, 0x38, 0x53, 0x49, 0x59, 0xfd, 0xa6, 0x48, 0x81, 0xb4, 0x8d, 0x4a, 0xc2, 0xc6, 0xff, 0x18, 0x45, 0xf1, 0xff, + 0xa3, 0x0c, 0x85, 0x86, 0xac, 0xfd, 0xf1, 0x96, 0x45, 0x65, 0x83, 0xcd, 0xff, 0x98, 0x68, 0xad, 0x56, 0x9f, 0x09, + 0x50, 0x49, 0x5b, 0x49, 0xa5, 0x0f, 0x0a, 0x3c, 0xd3, 0xd1, 0xe4, 0x0c, 0x35, 0xc8, 0x88, 0x27, 0x8c, 0x33, 0x60, + 0x68, 0x9b, 0x75, 0xc9, 0xbb, 0x6d, 0x13, 0x7f, 0x8d, 0xc2, 0x9b, 0x32, 0xb5, 0xd1, 0x18, 0x24, 0xa7, 0x0a, 0x90, + 0xe6, 0x38, 0x5b, 0x85, 0xae, 0x68, 0xc3, 0x39, 0x37, 0x6b, 0x2d, 0x38, 0x1b, 0xc6, 0x56, 0xc3, 0x97, 0xbf, 0x20, + 0x41, 0x60, 0xd7, 0xa4, 0x0e, 0xaa, 0xb2, 0xe6, 0xc5, 0x4d, 0xf8, 0x27, 0x6c, 0x2f, 0x31, 0x98, 0xc9, 0x4b, 0x9a, + 0x77, 0xa6, 0x23, 0xa4, 0x79, 0x84, 0x9c, 0xd9, 0xfc, 0x8f, 0x62, 0x26, 0xcb, 0x43, 0x19, 0xcd, 0x7c, 0x98, 0x18, + 0xff, 0xe6, 0x49, 0x02, 0xfb, 0x99, 0xf3, 0x61, 0x14, 0x99, 0x58, 0x1e, 0xdb, 0xc6, 0x0b, 0x72, 0x1f, 0x43, 0x37, + 0x5a, 0xac, 0xb2, 0x2c, 0x63, 0x5f, 0x29, 0xb3, 0xb4, 0xb4, 0xe0, 0xf0, 0x74, 0x03, 0xa2, 0x0a, 0x9d, 0x0d, 0x21, + 0xcf, 0xa5, 0x7f, 0x59, 0xa5, 0xc2, 0xf4, 0xa1, 0xcc, 0x58, 0xeb, 0x2d, 0x10, 0x7b, 0x3d, 0x51, 0x7f, 0x72, 0x87, + 0x44, 0x9b, 0xdc, 0x68, 0xf9, 0xe0, 0x14, 0x56, 0x93, 0x74, 0x1e, 0x99, 0x88, 0x47, 0xf8, 0xce, 0xb6, 0x9f, 0xb7, + 0x4a, 0x7a, 0x7e, 0xf0, 0x11, 0x76, 0xbb, 0x34, 0xf6, 0x5e, 0xf2, 0x3b, 0xf9, 0x39, 0xfa, 0x30, 0xb8, 0x23, 0x27, + 0x25, 0xb5, 0xfd, 0xa1, 0x8f, 0x71, 0x1d, 0x28, 0xbb, 0xff, 0x41, 0xe3, 0x39, 0x64, 0x51, 0xf1, 0x68, 0x92, 0xce, + 0x30, 0x07, 0x4b, 0xfd, 0x30, 0x73, 0xe1, 0x6f, 0xd2, 0x04, 0x67, 0xd1, 0x8d, 0x5e, 0x1e, 0x4c, 0xeb, 0xc9, 0x3f, + 0x22, 0x2b, 0x7f, 0x9a, 0x65, 0x93, 0xc3, 0x69, 0xb8, 0xe0, 0x47, 0x32, 0xfa, 0xed, 0xac, 0x6e, 0x4f, 0x96, 0xad, + 0x5b, 0xed, 0x21, 0x60, 0xfa, 0x91, 0x86, 0x48, 0xde, 0x2c, 0x53, 0x85, 0x81, 0xa8, 0x18, 0x5c, 0xd0, 0x1a, 0x74, + 0x29, 0x35, 0xb5, 0x55, 0xe0, 0x8c, 0x4e, 0x04, 0x1d, 0x54, 0x70, 0xb4, 0x5c, 0xf9, 0xea, 0x07, 0x0d, 0x8b, 0x93, + 0x8a, 0xed, 0xb6, 0x28, 0x92, 0x3d, 0x83, 0xe3, 0x68, 0x11, 0x15, 0x99, 0xf1, 0xef, 0x32, 0x5a, 0x29, 0xa5, 0x54, + 0x82, 0xe0, 0x8e, 0xbe, 0xd0, 0xc5, 0x65, 0x94, 0x86, 0xfc, 0x90, 0x4a, 0xb6, 0xd0, 0x80, 0x4a, 0x29, 0x0e, 0x54, + 0x8d, 0xcb, 0x44, 0xb8, 0x32, 0x36, 0x17, 0x8d, 0x2b, 0x5a, 0x15, 0xaf, 0x62, 0x87, 0xf4, 0xfa, 0x3a, 0x51, 0x85, + 0x21, 0xcb, 0xc0, 0xe1, 0xe1, 0x1c, 0x65, 0xcd, 0x93, 0x6d, 0x28, 0xc9, 0x33, 0x15, 0x73, 0x30, 0xe3, 0x5c, 0x3d, + 0xa9, 0xc6, 0x06, 0x34, 0x54, 0x54, 0x67, 0x31, 0x9d, 0xad, 0x0e, 0xa8, 0x53, 0x02, 0x02, 0xb3, 0xf0, 0x18, 0x8f, + 0xa3, 0x10, 0x77, 0xa5, 0x0c, 0xbb, 0x70, 0x9b, 0x25, 0x58, 0x8f, 0x93, 0xe1, 0x70, 0x47, 0x1b, 0x3b, 0x17, 0x75, + 0xf8, 0x51, 0xb0, 0x4b, 0x31, 0x68, 0x48, 0x23, 0x24, 0xb9, 0xd8, 0x39, 0x75, 0x2c, 0x9a, 0x70, 0x27, 0x0b, 0x02, + 0x20, 0xf2, 0x30, 0xf5, 0xc1, 0xe2, 0xf2, 0xa8, 0xb3, 0x80, 0x89, 0x79, 0xae, 0xec, 0xa2, 0xbc, 0x81, 0xaf, 0xd6, + 0xa1, 0x1c, 0x62, 0x99, 0xc4, 0x58, 0x69, 0x33, 0xfe, 0x77, 0x59, 0x1e, 0xa5, 0x37, 0x96, 0xd5, 0xb4, 0x45, 0xf5, + 0xa0, 0xd1, 0x1d, 0xae, 0x1d, 0x31, 0x36, 0x96, 0x59, 0x27, 0x86, 0x05, 0xf3, 0xdf, 0x67, 0x86, 0xc5, 0x46, 0x55, + 0xcb, 0x37, 0x39, 0xdf, 0xbd, 0xe3, 0x53, 0x08, 0x66, 0x51, 0xc3, 0x03, 0xee, 0x1e, 0x96, 0x31, 0x2c, 0x16, 0x04, + 0xb3, 0xec, 0xc1, 0xc3, 0xba, 0x1a, 0x82, 0x34, 0xe3, 0x51, 0x92, 0x40, 0x37, 0x62, 0x28, 0x46, 0x72, 0x46, 0xc0, + 0x26, 0x29, 0xc4, 0xe0, 0x15, 0xb0, 0x3f, 0xd6, 0x25, 0xa4, 0x82, 0x23, 0x3c, 0x20, 0x1c, 0xaa, 0xb8, 0xfc, 0xb0, + 0x88, 0x61, 0x20, 0x86, 0x54, 0xbc, 0x98, 0x95, 0x4f, 0x0b, 0x80, 0x91, 0x35, 0xaa, 0x78, 0x48, 0x86, 0xc8, 0xc8, + 0x9b, 0x16, 0x19, 0x75, 0xf0, 0xc6, 0xf0, 0x1b, 0x11, 0x03, 0x5e, 0xc9, 0x19, 0xe4, 0x31, 0x27, 0x2b, 0x73, 0xf9, + 0x32, 0x77, 0xe9, 0xb7, 0xe3, 0xa9, 0x1c, 0xb7, 0xcd, 0x3c, 0xb4, 0xe9, 0x65, 0xac, 0x73, 0x51, 0x71, 0x70, 0xbd, + 0xcc, 0x31, 0xa2, 0xa7, 0xf9, 0xc2, 0xb5, 0x45, 0xf6, 0xb4, 0x86, 0xeb, 0xd7, 0x2a, 0xfb, 0xf0, 0x09, 0x19, 0x5b, + 0x40, 0x86, 0x87, 0x9d, 0xba, 0x6d, 0x64, 0x0c, 0x23, 0xf0, 0xdf, 0xc6, 0xf7, 0x13, 0xe7, 0x98, 0x2e, 0x73, 0x41, + 0x72, 0x98, 0x17, 0xf8, 0xb6, 0x30, 0xfe, 0x92, 0x73, 0x1c, 0x8d, 0xc9, 0xba, 0x87, 0x1a, 0xdd, 0xbd, 0xb4, 0xe1, + 0x0b, 0x26, 0xe8, 0xfc, 0x12, 0x1d, 0x5f, 0x93, 0x06, 0xcb, 0x7d, 0xde, 0xd7, 0x33, 0x64, 0x1a, 0x0f, 0x63, 0x4c, + 0xc8, 0x35, 0x9e, 0x33, 0xd1, 0x8d, 0x7a, 0xcf, 0x96, 0xb1, 0x96, 0xd8, 0x2a, 0xa2, 0xcd, 0x36, 0x58, 0x39, 0xaa, + 0xfe, 0xf5, 0x5d, 0x24, 0x82, 0x11, 0x35, 0xed, 0xd3, 0x5a, 0xdf, 0xa8, 0x3c, 0xf3, 0xdb, 0x99, 0x77, 0x1b, 0x56, + 0x86, 0x19, 0x04, 0x33, 0xbe, 0x62, 0xce, 0xd0, 0xcf, 0x23, 0x73, 0x0f, 0xbc, 0xdd, 0x4b, 0xef, 0xc6, 0x9a, 0x35, + 0xfa, 0x61, 0xba, 0x53, 0x92, 0x59, 0x60, 0x3b, 0xfe, 0x4d, 0xd0, 0x53, 0x21, 0xf5, 0xa3, 0x3a, 0xb0, 0xf8, 0x9a, + 0x93, 0x98, 0x90, 0x0c, 0x39, 0x58, 0x90, 0xab, 0xe6, 0xbd, 0xa7, 0xdb, 0xb6, 0x8c, 0x0a, 0x71, 0xe9, 0x74, 0xf5, + 0xe5, 0xf5, 0xda, 0x0b, 0xb4, 0xa3, 0xfa, 0xd1, 0xc6, 0xcb, 0x78, 0xf1, 0x78, 0x03, 0x77, 0x22, 0x7e, 0x43, 0x6e, + 0x68, 0x8c, 0xaf, 0xc2, 0xa3, 0xb5, 0xda, 0x2b, 0xae, 0xbd, 0x69, 0xee, 0xf1, 0x8b, 0xb9, 0x56, 0x67, 0x4e, 0xb5, + 0x57, 0x66, 0x5c, 0x99, 0xb8, 0x58, 0x51, 0x92, 0x0f, 0x5f, 0x10, 0x5c, 0xc7, 0xcf, 0xd6, 0x41, 0xb8, 0xeb, 0xf1, + 0x9d, 0x1c, 0x2c, 0xc5, 0xc0, 0x74, 0x03, 0xd7, 0x81, 0x18, 0xc3, 0xd8, 0x22, 0x01, 0x92, 0xfa, 0xa1, 0x6c, 0xc5, + 0x28, 0x18, 0xbf, 0x3e, 0x5e, 0xb6, 0xea, 0x1d, 0xff, 0x61, 0x09, 0xe0, 0xd8, 0x46, 0x38, 0x02, 0xcd, 0xac, 0x38, + 0xe5, 0x52, 0x5c, 0xe8, 0x23, 0x38, 0xb3, 0x29, 0xfb, 0x80, 0xe3, 0x90, 0x4d, 0x30, 0xed, 0x8f, 0x86, 0xca, 0xef, + 0x9f, 0xc8, 0x8f, 0x6b, 0x77, 0xbf, 0x57, 0xd7, 0x16, 0x9e, 0xfe, 0x73, 0x87, 0xde, 0x49, 0x47, 0x5e, 0x3b, 0x1d, + 0x75, 0xb8, 0x02, 0xd9, 0x71, 0x53, 0x7b, 0x56, 0x57, 0x5f, 0xc9, 0xf1, 0x63, 0x7f, 0x5b, 0x95, 0x6e, 0xe3, 0xfd, + 0xf3, 0xb2, 0xaa, 0xec, 0x6c, 0xaf, 0x5c, 0x17, 0x7f, 0x59, 0x69, 0xf2, 0xda, 0x7f, 0xd1, 0x6f, 0xe7, 0xa4, 0x1f, + 0xe6, 0x9b, 0x45, 0x8b, 0xbc, 0x61, 0x9b, 0x01, 0xfe, 0xf1, 0xa0, 0xf1, 0x50, 0x44, 0xd8, 0xdc, 0x75, 0xbf, 0xb5, + 0xa1, 0x41, 0x31, 0x27, 0xef, 0x04, 0x29, 0x0a, 0x20, 0x71, 0xc7, 0x5a, 0x01, 0x38, 0x06, 0x86, 0xc1, 0x73, 0xef, + 0x53, 0xeb, 0xc6, 0x14, 0x75, 0xb9, 0x7a, 0xa2, 0xb1, 0x9b, 0xad, 0x37, 0xf4, 0x35, 0x6e, 0xf4, 0x1f, 0x91, 0x0b, + 0x11, 0x18, 0x3c, 0x3f, 0x80, 0xfb, 0xc7, 0x29, 0x7b, 0xd1, 0x62, 0x52, 0x79, 0xc3, 0xe7, 0xf6, 0xf5, 0xe1, 0xb5, + 0x7c, 0x9a, 0xcd, 0x05, 0x12, 0xbd, 0x3e, 0x36, 0xb5, 0xd0, 0x14, 0xb9, 0x96, 0x3b, 0xd9, 0xc5, 0xd1, 0x34, 0xc4, + 0x68, 0x01, 0x50, 0x28, 0x03, 0xc6, 0x4f, 0xb0, 0x86, 0x3a, 0xe3, 0x9f, 0xcf, 0xa7, 0x3c, 0xa7, 0xfb, 0xcd, 0x5b, + 0x33, 0xbd, 0xa5, 0x39, 0xe0, 0xdb, 0x90, 0xff, 0xdb, 0x3f, 0xd1, 0xad, 0x63, 0xac, 0xf6, 0x98, 0x1d, 0x5c, 0x9b, + 0x6b, 0x59, 0xf4, 0x6f, 0x6b, 0xe2, 0xca, 0xeb, 0xd1, 0x0f, 0xf8, 0x75, 0xee, 0x0b, 0x81, 0xd1, 0x14, 0x9e, 0xb1, + 0x98, 0xb4, 0x55, 0xae, 0xef, 0x7a, 0xc2, 0x6c, 0x1b, 0x9d, 0x22, 0x35, 0x04, 0xd7, 0xfb, 0x18, 0x57, 0x1b, 0x4f, + 0xa2, 0xb2, 0xda, 0xbe, 0x79, 0x2a, 0xc0, 0x85, 0xc6, 0xf2, 0x4f, 0xd4, 0x79, 0xbb, 0x47, 0x6d, 0x72, 0xda, 0x3f, + 0x69, 0xed, 0x9e, 0x4b, 0x0f, 0x1d, 0xe9, 0xb1, 0xe9, 0x53, 0x6b, 0xde, 0x10, 0xec, 0x5b, 0xd2, 0x62, 0x2f, 0x00, + 0xdc, 0x01, 0x9e, 0xa8, 0x36, 0xd1, 0xb3, 0xaa, 0x7f, 0xec, 0x01, 0x69, 0x8c, 0xef, 0x31, 0x49, 0x95, 0x1b, 0xb9, + 0x50, 0xb3, 0x48, 0x50, 0x74, 0x1c, 0x1f, 0xdf, 0x31, 0xad, 0xd6, 0xc3, 0xf3, 0x62, 0x55, 0x0a, 0x63, 0xcb, 0xdc, + 0x9b, 0x79, 0x90, 0xd3, 0x54, 0x1f, 0xbc, 0x16, 0xee, 0x1b, 0xba, 0x14, 0x3e, 0x16, 0x8f, 0x5a, 0xed, 0x80, 0x9c, + 0x6c, 0x41, 0x08, 0x47, 0x74, 0xfe, 0x52, 0x32, 0x53, 0x80, 0xd7, 0x81, 0xbb, 0xe2, 0x18, 0x3d, 0xb6, 0xe3, 0x6e, + 0x54, 0xdc, 0xc2, 0x9f, 0x1d, 0x44, 0x11, 0xd6, 0x55, 0xbb, 0x35, 0x61, 0xce, 0xcb, 0x14, 0x46, 0xa9, 0x90, 0x80, + 0x70, 0xb8, 0xcc, 0x0d, 0x50, 0x42, 0x49, 0x40, 0x5b, 0x15, 0xd5, 0x1f, 0xca, 0xdc, 0x76, 0xbb, 0x51, 0x73, 0x1e, + 0x89, 0x87, 0x81, 0x8a, 0xf5, 0x98, 0xd6, 0x5a, 0x92, 0x03, 0x0a, 0x51, 0xb3, 0xc9, 0xf3, 0xf2, 0x8f, 0xf5, 0x48, + 0x2e, 0x05, 0x8f, 0x44, 0x2c, 0xde, 0x96, 0xe4, 0x9b, 0xfc, 0xf1, 0x0c, 0x99, 0xbd, 0xe5, 0xe4, 0x87, 0x39, 0x4c, + 0x27, 0x76, 0x19, 0xf0, 0x04, 0x05, 0xac, 0x51, 0x8f, 0xb6, 0xa2, 0xa7, 0x80, 0x74, 0x98, 0x15, 0x0c, 0x08, 0x4e, + 0xa9, 0x5f, 0xe6, 0x47, 0xbe, 0xd9, 0x96, 0x42, 0x55, 0x89, 0x68, 0x29, 0x0b, 0xbe, 0xdc, 0x9e, 0x6f, 0x26, 0x94, + 0xac, 0xb8, 0xa6, 0xb6, 0x99, 0xad, 0xa2, 0x45, 0x2b, 0x08, 0x7f, 0x5c, 0xcd, 0x8c, 0xa8, 0xbf, 0x90, 0x6e, 0xd6, + 0xb4, 0x7d, 0x80, 0xb4, 0x9a, 0x53, 0x3b, 0x3b, 0x47, 0x73, 0x41, 0x03, 0xf5, 0x18, 0xc1, 0xc6, 0xe2, 0x52, 0x93, + 0x72, 0xd6, 0x39, 0xaf, 0xc6, 0x1b, 0x86, 0xdb, 0x4d, 0x52, 0x2f, 0x8a, 0x1b, 0x57, 0x37, 0x3a, 0xe9, 0x4b, 0xd0, + 0xc1, 0xa0, 0x03, 0x86, 0x94, 0x5a, 0x85, 0x8a, 0xec, 0xce, 0x62, 0x5d, 0x38, 0x4d, 0x48, 0x3a, 0x5d, 0xf1, 0x72, + 0x52, 0xbc, 0x67, 0x84, 0x38, 0xfa, 0x01, 0x29, 0x93, 0x47, 0xa8, 0x49, 0x5e, 0xfb, 0x80, 0x21, 0xf3, 0x67, 0xd4, + 0xe2, 0xb0, 0xa1, 0x0d, 0xa2, 0x7f, 0x10, 0x38, 0x1e, 0x47, 0x90, 0x0a, 0xd6, 0x53, 0x32, 0xba, 0x04, 0x48, 0x7a, + 0x09, 0x9f, 0x1e, 0xb1, 0x60, 0x6a, 0xee, 0x94, 0x82, 0xe2, 0xc9, 0x00, 0x43, 0x5b, 0x69, 0x54, 0x96, 0x54, 0x4e, + 0xf4, 0x40, 0x03, 0xef, 0x29, 0x14, 0x10, 0x46, 0x9c, 0x3d, 0xf6, 0x39, 0x2f, 0x62, 0x50, 0xec, 0xad, 0x41, 0xe8, + 0x3e, 0x03, 0xd8, 0xc8, 0x33, 0x0c, 0x16, 0x79, 0x5e, 0x21, 0x47, 0x65, 0x2f, 0xab, 0xb9, 0xff, 0x72, 0x46, 0xd9, + 0xc0, 0xe0, 0x51, 0x3d, 0xe9, 0xe4, 0x5a, 0xbf, 0x0e, 0x27, 0xc8, 0x59, 0xfa, 0x94, 0xd5, 0xa3, 0x76, 0x6e, 0xca, + 0x98, 0xac, 0x2b, 0xf5, 0x67, 0xee, 0x61, 0x24, 0xdf, 0xca, 0x99, 0x51, 0x16, 0xa9, 0x88, 0x17, 0x7e, 0x00, 0xa5, + 0x9f, 0x67, 0x1d, 0x83, 0xc2, 0x13, 0x0b, 0x0d, 0x81, 0x38, 0xc4, 0x35, 0x36, 0xb8, 0x71, 0x20, 0x18, 0x35, 0x68, + 0x4c, 0x6e, 0x51, 0xad, 0x29, 0x72, 0x2d, 0xd4, 0xa7, 0x06, 0x43, 0x6d, 0x9c, 0x79, 0x66, 0x25, 0x98, 0xd0, 0xf0, + 0x92, 0x4f, 0x95, 0xac, 0xa3, 0xb8, 0xc2, 0x2f, 0x57, 0x80, 0xd9, 0xc0, 0x34, 0x77, 0x1d, 0x60, 0xb0, 0xd2, 0x9c, + 0x9a, 0x91, 0x67, 0xe7, 0x0e, 0xa1, 0xd4, 0x8d, 0x5e, 0xc0, 0x04, 0x30, 0x1c, 0x02, 0xda, 0xa0, 0x97, 0x17, 0x3e, + 0x5c, 0x90, 0xaa, 0x1d, 0x19, 0x70, 0xb4, 0xc8, 0x89, 0xb2, 0x75, 0x88, 0xff, 0x99, 0x48, 0x48, 0xda, 0xec, 0x40, + 0xbc, 0x39, 0x76, 0x53, 0xc7, 0xaa, 0xe7, 0x20, 0xbf, 0xba, 0xc1, 0x5e, 0x2b, 0xae, 0x4c, 0x93, 0x1a, 0x7a, 0x35, + 0x1a, 0x87, 0x82, 0xb4, 0xbc, 0x98, 0xdd, 0x78, 0xd2, 0x24, 0xba, 0x2c, 0xdd, 0x34, 0xe8, 0x21, 0xbc, 0x33, 0x0f, + 0xf9, 0x1d, 0xef, 0xeb, 0xc9, 0xfe, 0x81, 0xa2, 0x43, 0x60, 0xb7, 0x21, 0xd7, 0x15, 0x5f, 0x3f, 0x21, 0xc5, 0x32, + 0xda, 0xa7, 0x5d, 0x5b, 0xf3, 0xd4, 0xe2, 0x04, 0x86, 0xbd, 0x9c, 0x8d, 0x0b, 0x6e, 0x55, 0x84, 0x61, 0x6a, 0x28, + 0x25, 0x97, 0x9d, 0x6e, 0x49, 0x4e, 0xae, 0x05, 0x1a, 0x83, 0x40, 0x71, 0x1e, 0xf7, 0x9f, 0xd3, 0x97, 0x12, 0x6c, + 0xc7, 0x0e, 0x46, 0x27, 0xe9, 0x3d, 0xad, 0x93, 0xa3, 0xa2, 0xb0, 0xdd, 0x29, 0xdd, 0x38, 0xf0, 0xc7, 0x89, 0x2a, + 0xb5, 0x25, 0x06, 0x9e, 0xef, 0xae, 0x4d, 0x42, 0x5b, 0x73, 0x0e, 0xb0, 0x12, 0x00, 0x28, 0x2f, 0x06, 0x55, 0xbb, + 0x78, 0xe0, 0xa6, 0xa5, 0x6d, 0x70, 0xd3, 0x40, 0x8d, 0x44, 0x04, 0x51, 0x40, 0xc2, 0xd4, 0x3f, 0x37, 0x25, 0x3b, + 0xf1, 0x8e, 0x79, 0x27, 0x0a, 0x15, 0x92, 0x06, 0xda, 0x79, 0xf5, 0xf0, 0xe8, 0x63, 0x42, 0x58, 0x63, 0x9c, 0x18, + 0xdb, 0x80, 0x7d, 0xd7, 0x5a, 0xd1, 0x5c, 0x17, 0xf4, 0xb6, 0xee, 0x14, 0xd5, 0x1c, 0xa0, 0x93, 0x70, 0x3b, 0x0f, + 0x3e, 0x42, 0x46, 0x95, 0xbc, 0xdf, 0xe5, 0xc8, 0xe3, 0x12, 0x04, 0x39, 0xdf, 0x36, 0x54, 0x1e, 0x83, 0x07, 0x51, + 0xe0, 0x83, 0x2a, 0x06, 0x53, 0xe5, 0xc4, 0x4d, 0x20, 0xd5, 0x08, 0x32, 0xaf, 0x22, 0xc4, 0x2b, 0x3a, 0xf9, 0x7d, + 0x8f, 0x40, 0xac, 0x12, 0x9e, 0x24, 0xf3, 0x2a, 0xf9, 0x34, 0x23, 0xae, 0x6a, 0x83, 0x61, 0x66, 0xd8, 0x6e, 0xc9, + 0x69, 0x8c, 0x88, 0xa7, 0xe3, 0x66, 0x6f, 0xe2, 0xb9, 0x11, 0xd8, 0xc2, 0x51, 0xc4, 0xf2, 0x35, 0xd1, 0x19, 0x98, + 0x21, 0x55, 0x85, 0xd8, 0x5c, 0xf2, 0x19, 0x91, 0x8d, 0xc2, 0xf5, 0xb5, 0xf8, 0xbb, 0x4a, 0x29, 0x11, 0x19, 0xea, + 0x6b, 0xf5, 0x4f, 0xd1, 0x08, 0xdb, 0xa9, 0x62, 0xf4, 0xfa, 0xf1, 0x81, 0x7a, 0x04, 0x3a, 0x53, 0xaa, 0x8d, 0x52, + 0x2d, 0x98, 0xd7, 0x5a, 0xa1, 0x61, 0xa1, 0x75, 0xd4, 0xa7, 0x26, 0xc3, 0xe2, 0xc7, 0xab, 0xdc, 0x2e, 0x06, 0x80, + 0x4e, 0x02, 0x94, 0xec, 0x0f, 0x2d, 0xf5, 0xcd, 0x2a, 0x4b, 0x12, 0x80, 0xcc, 0x01, 0xd8, 0xe3, 0x38, 0xa9, 0x59, + 0x91, 0x1d, 0xfd, 0x42, 0x54, 0x4e, 0xd8, 0xe1, 0x8b, 0xa5, 0x69, 0xfe, 0x00, 0x57, 0x09, 0xcc, 0x08, 0x31, 0x19, + 0xd7, 0x51, 0x67, 0x83, 0xd0, 0x02, 0xa0, 0x5b, 0xda, 0xa9, 0x87, 0xe4, 0xed, 0x7a, 0x0c, 0x52, 0xf6, 0x01, 0xea, + 0xbc, 0xab, 0xe1, 0xfa, 0x08, 0xc3, 0x9c, 0x1d, 0x24, 0xa0, 0x9d, 0xaa, 0xd0, 0x9f, 0x9a, 0xa6, 0x72, 0x44, 0xaf, + 0xa7, 0x4d, 0x07, 0x95, 0xbb, 0x89, 0x2a, 0x13, 0xe0, 0xe0, 0x4d, 0x3c, 0x49, 0xe2, 0xb0, 0xdb, 0x4b, 0x4d, 0x53, + 0x3f, 0x99, 0xb8, 0xb2, 0x5a, 0x4d, 0xf9, 0x76, 0x6e, 0x95, 0xd0, 0xd2, 0xe3, 0x42, 0x88, 0x79, 0xbc, 0xe7, 0x81, + 0xeb, 0x65, 0xdf, 0xc8, 0x1a, 0x2c, 0xee, 0x9b, 0x95, 0x51, 0x95, 0xd3, 0x11, 0x1a, 0x93, 0x62, 0x9e, 0xfc, 0x05, + 0x88, 0xd1, 0xdd, 0x4e, 0xd3, 0xeb, 0x10, 0x42, 0x44, 0x37, 0xfb, 0xf6, 0x9e, 0x1a, 0xeb, 0x88, 0x3d, 0x21, 0x2c, + 0x73, 0xc3, 0x4b, 0x74, 0x0c, 0xdc, 0xf6, 0xae, 0x2c, 0xc9, 0x74, 0xf9, 0xdc, 0x17, 0x20, 0x7c, 0x1d, 0x30, 0x43, + 0x0a, 0x54, 0x4a, 0xec, 0x83, 0xcd, 0xf7, 0x91, 0xd0, 0x3c, 0x3d, 0x17, 0xb6, 0x11, 0x7a, 0xbe, 0xec, 0xb3, 0xf5, + 0x5b, 0x38, 0x62, 0x6b, 0xab, 0x60, 0x0f, 0x7b, 0xb9, 0x6e, 0x91, 0xd1, 0x3c, 0xf8, 0x85, 0xe9, 0x2c, 0x0b, 0x89, + 0x57, 0x1b, 0xf5, 0x0d, 0xeb, 0x1d, 0x5b, 0xfa, 0x4c, 0x66, 0x4d, 0x3c, 0x4c, 0xd6, 0xd3, 0xc8, 0xc3, 0xc9, 0xa9, + 0x3c, 0xc7, 0xe6, 0xa9, 0xb0, 0xc0, 0x1b, 0xba, 0x7a, 0x7a, 0xcb, 0xb8, 0xf7, 0xa6, 0x21, 0x79, 0x89, 0xcf, 0xce, + 0xa2, 0x05, 0xa0, 0x98, 0xa8, 0x9c, 0x5e, 0xbb, 0xc0, 0x09, 0xf6, 0x7a, 0x51, 0x41, 0x83, 0x63, 0xe4, 0xd8, 0x96, + 0xe0, 0xe9, 0x70, 0x26, 0x67, 0x9d, 0x0b, 0x48, 0x5f, 0x33, 0xa9, 0x39, 0x0b, 0x73, 0x4e, 0x4a, 0x11, 0xf8, 0xe8, + 0x51, 0x9c, 0xa3, 0x79, 0xba, 0x01, 0x04, 0x86, 0x8a, 0xf7, 0x5d, 0x60, 0x8f, 0x37, 0x1c, 0xa9, 0x8b, 0x1c, 0xac, + 0xe4, 0x3d, 0x31, 0xcc, 0x0a, 0xfd, 0xeb, 0xe7, 0x07, 0x2b, 0x85, 0x8a, 0x5c, 0x8e, 0x51, 0x88, 0x62, 0xf7, 0x8c, + 0x08, 0xcc, 0x4d, 0xa5, 0x3a, 0x08, 0xd4, 0xf2, 0x0f, 0xb6, 0x5f, 0x08, 0x57, 0x4a, 0x70, 0xeb, 0x41, 0x5d, 0x5a, + 0x42, 0xc6, 0x1e, 0xce, 0xea, 0x2d, 0xd2, 0x58, 0x40, 0xb0, 0xc7, 0x5c, 0x6b, 0x7a, 0x98, 0x03, 0xc9, 0xac, 0x06, + 0x18, 0x6d, 0x89, 0x20, 0xf5, 0x82, 0xc1, 0x2e, 0x15, 0xdd, 0xd7, 0x05, 0x45, 0xba, 0xcb, 0xa8, 0x31, 0x95, 0x56, + 0x72, 0x7c, 0x1e, 0x62, 0x7f, 0xad, 0xa9, 0x5a, 0xea, 0xab, 0xec, 0x6b, 0x72, 0xba, 0xbb, 0x5f, 0x6c, 0xfc, 0x48, + 0xf8, 0x79, 0xae, 0x98, 0x41, 0x95, 0x8c, 0xa3, 0x5d, 0xc2, 0xa4, 0xa1, 0x7a, 0xa5, 0x38, 0x6e, 0x2c, 0x37, 0x9e, + 0x6e, 0x5f, 0x74, 0xc6, 0x56, 0xe9, 0xbf, 0xbb, 0x05, 0x3e, 0x27, 0xdd, 0x6b, 0x32, 0x4f, 0x49, 0x6c, 0xf0, 0x43, + 0xf7, 0x20, 0x9d, 0x28, 0xcf, 0xfd, 0xcb, 0xf3, 0xe3, 0x39, 0x29, 0x62, 0xdb, 0x56, 0xe4, 0x95, 0x15, 0xa0, 0x1c, + 0xd2, 0x6e, 0x02, 0xea, 0x4b, 0x37, 0xea, 0x4d, 0xe4, 0x8d, 0x0d, 0xbc, 0x84, 0xd4, 0x1a, 0x28, 0x76, 0x61, 0xec, + 0xab, 0xd3, 0x51, 0x48, 0x93, 0x33, 0xd9, 0x43, 0x42, 0x31, 0x61, 0x80, 0xfe, 0x69, 0x71, 0x34, 0xa3, 0x82, 0xd6, + 0xfb, 0xbd, 0xa8, 0x8e, 0x65, 0xe7, 0x1a, 0x08, 0x99, 0xd9, 0x68, 0x96, 0xbf, 0xc8, 0xf0, 0xc6, 0x21, 0xf2, 0x55, + 0x66, 0x3a, 0x3a, 0xf0, 0xd7, 0x94, 0x3b, 0xa9, 0xc3, 0xc6, 0x55, 0x76, 0x24, 0x81, 0xff, 0x2e, 0x73, 0x22, 0x14, + 0xbe, 0x99, 0x2d, 0x0f, 0xe4, 0x6b, 0x5d, 0xf9, 0x5f, 0x33, 0xea, 0xb3, 0xc2, 0x1d, 0x6d, 0xcb, 0xd5, 0x8c, 0xc3, + 0xd9, 0x70, 0x20, 0xf3, 0xf1, 0x81, 0x0b, 0x5e, 0x79, 0xaa, 0xca, 0x7e, 0x13, 0x0e, 0xc9, 0x03, 0x7b, 0x36, 0x39, + 0x4a, 0x4b, 0x47, 0xed, 0x7f, 0xe5, 0xb4, 0xe8, 0x50, 0x34, 0x2c, 0x5a, 0x17, 0x05, 0xa2, 0x56, 0x1b, 0xcb, 0xcb, + 0x3c, 0x22, 0x41, 0xed, 0x8b, 0xc5, 0x43, 0x7b, 0xe0, 0xa3, 0x29, 0x46, 0xbe, 0xcf, 0x58, 0x07, 0x12, 0x7d, 0x7f, + 0x44, 0x90, 0x32, 0x50, 0x3a, 0x74, 0x06, 0xa5, 0x89, 0x29, 0x1e, 0x93, 0x3c, 0x67, 0xb1, 0xc2, 0x5e, 0xf2, 0x3a, + 0x2a, 0x07, 0x2b, 0x92, 0x7f, 0x8e, 0x08, 0x70, 0x14, 0x0c, 0x1e, 0x45, 0x9e, 0xfa, 0x25, 0xb8, 0xe5, 0xbe, 0x3f, + 0x60, 0x44, 0x56, 0xd2, 0x58, 0x5b, 0x8c, 0x5e, 0x88, 0x91, 0xf9, 0x08, 0x8e, 0xc7, 0xef, 0x9b, 0xa3, 0x14, 0x94, + 0xbe, 0xb4, 0x2b, 0x50, 0xdc, 0x04, 0xba, 0xb4, 0x9b, 0x1a, 0xa7, 0x81, 0x9c, 0xc8, 0xb4, 0xb5, 0x1d, 0xf7, 0xdd, + 0xd9, 0xb1, 0xa0, 0x2d, 0x41, 0xc6, 0x74, 0x17, 0x9a, 0x39, 0x0a, 0x0c, 0xef, 0xb7, 0x1a, 0x47, 0xc0, 0x80, 0x5d, + 0x63, 0x3d, 0xfc, 0x52, 0x4c, 0xfb, 0x54, 0xe9, 0x87, 0x2b, 0x9c, 0xb3, 0x4b, 0x3a, 0xbd, 0xf9, 0xfd, 0x40, 0x06, + 0xc4, 0xc5, 0x1b, 0x31, 0xf5, 0x05, 0xcc, 0x2f, 0x83, 0x02, 0x10, 0xa6, 0x12, 0x58, 0xfa, 0xbf, 0x98, 0x0b, 0xbc, + 0x13, 0x87, 0x35, 0x83, 0x03, 0x83, 0x88, 0x8f, 0x3b, 0xb8, 0xc5, 0x5f, 0x87, 0xff, 0x68, 0x80, 0xba, 0x72, 0xf7, + 0x19, 0x65, 0xcd, 0xf7, 0x49, 0x29, 0x32, 0x7d, 0xf9, 0xee, 0x65, 0x2b, 0xd4, 0x41, 0x8e, 0x6d, 0x6e, 0x55, 0xf3, + 0xda, 0xe2, 0xf7, 0xd3, 0x58, 0xcd, 0x4d, 0x7e, 0xd3, 0xdb, 0x55, 0x57, 0x4f, 0x8d, 0x1a, 0xf5, 0x84, 0x60, 0xf4, + 0xe6, 0x66, 0xd8, 0xad, 0xf1, 0xcb, 0x59, 0x09, 0x68, 0x64, 0xb3, 0x57, 0xbf, 0x47, 0x41, 0xae, 0xaf, 0xf5, 0xf3, + 0xbc, 0xac, 0x32, 0x2e, 0xbe, 0x09, 0xc0, 0x53, 0xe3, 0x43, 0xa2, 0x4a, 0xb5, 0x2c, 0x0d, 0x51, 0x93, 0x00, 0x82, + 0xc3, 0x1f, 0x74, 0x0b, 0x2e, 0xed, 0x57, 0x72, 0x9b, 0x55, 0x79, 0x6d, 0x45, 0xd0, 0x81, 0x45, 0x9f, 0xae, 0x0c, + 0x76, 0x30, 0xe0, 0xe1, 0x14, 0xfd, 0x43, 0xf1, 0x87, 0x89, 0xed, 0x9d, 0x6d, 0x4a, 0x28, 0x1f, 0x9a, 0xb9, 0x17, + 0x77, 0xf6, 0x4c, 0x11, 0xcd, 0x22, 0xd4, 0xac, 0x82, 0x19, 0x2c, 0x1b, 0x6a, 0xe7, 0x1a, 0x12, 0xf6, 0x08, 0x52, + 0x4c, 0xc1, 0xb8, 0xd1, 0xde, 0x90, 0xee, 0x88, 0x6b, 0x06, 0xe5, 0xb0, 0x50, 0x94, 0xf9, 0xcd, 0x08, 0xc9, 0x38, + 0xa7, 0xe9, 0x0d, 0x0a, 0xfc, 0x30, 0xe0, 0x73, 0x79, 0xb0, 0x20, 0xcf, 0x1f, 0x55, 0xe9, 0x74, 0x14, 0xfb, 0x56, + 0x12, 0x31, 0x63, 0xda, 0x81, 0x2d, 0xaf, 0xf7, 0xca, 0x74, 0x61, 0xb3, 0x4f, 0x3a, 0xd6, 0x1d, 0xe2, 0xed, 0x29, + 0x71, 0x1d, 0xc4, 0x9e, 0xa6, 0x1c, 0x36, 0x79, 0x3d, 0x99, 0xe3, 0x68, 0x51, 0x76, 0x5d, 0xac, 0xa6, 0x33, 0x14, + 0x7a, 0x0b, 0xc9, 0x46, 0x1b, 0x7a, 0xfe, 0xa4, 0x72, 0x8c, 0xf3, 0xc3, 0xe5, 0x24, 0x86, 0xe9, 0x4b, 0xa9, 0x21, + 0x5a, 0xb7, 0x94, 0xee, 0xb1, 0xbe, 0x63, 0x05, 0x5b, 0xb3, 0xf7, 0x8f, 0x44, 0x96, 0x26, 0x96, 0xa9, 0xd4, 0x96, + 0x9d, 0xba, 0x71, 0xef, 0x59, 0x7b, 0xfc, 0xde, 0x62, 0xb1, 0x46, 0xea, 0x6c, 0xb4, 0x31, 0xcd, 0x40, 0x3e, 0x1a, + 0xda, 0x07, 0x5f, 0x30, 0x65, 0x0b, 0xfd, 0x70, 0xde, 0x6d, 0xd0, 0x16, 0xe3, 0x33, 0x86, 0xa6, 0xd9, 0x9d, 0x0f, + 0xbc, 0xfa, 0x2c, 0x8b, 0x2e, 0x17, 0x1d, 0x4f, 0x73, 0x8c, 0x18, 0x75, 0xff, 0x5f, 0x1e, 0xf6, 0x52, 0x86, 0xbb, + 0x3c, 0x21, 0xc3, 0x4e, 0xee, 0xdb, 0x29, 0xab, 0x80, 0x7c, 0x8c, 0xad, 0xf4, 0xbc, 0x72, 0x30, 0xa2, 0xd2, 0x51, + 0x9c, 0xe9, 0x3f, 0x7c, 0xe5, 0xd7, 0x32, 0x8d, 0xda, 0xf4, 0xa3, 0xcb, 0x92, 0xbf, 0xb2, 0x1a, 0x88, 0x36, 0x4f, + 0x88, 0x4c, 0xfe, 0x4f, 0x24, 0x25, 0x47, 0x06, 0xe2, 0xd1, 0x01, 0x14, 0x30, 0x53, 0x27, 0x93, 0xd3, 0x62, 0x70, + 0x02, 0x22, 0x4b, 0x34, 0x87, 0x73, 0x00, 0x93, 0xb4, 0x04, 0x13, 0x1e, 0xd7, 0x6a, 0xdf, 0x63, 0xc6, 0x01, 0x7f, + 0x99, 0x47, 0x73, 0x70, 0xf7, 0x01, 0x2d, 0x9a, 0x80, 0x64, 0x24, 0x61, 0x58, 0x6b, 0xdb, 0x79, 0x38, 0xd9, 0x4e, + 0xf0, 0xac, 0x7a, 0x7d, 0xc0, 0x8f, 0xb5, 0x82, 0xcb, 0x9d, 0x28, 0x45, 0x75, 0x1f, 0x7c, 0xd9, 0xea, 0xcd, 0x21, + 0xd4, 0x59, 0x0f, 0xf5, 0xcc, 0x40, 0x71, 0xdb, 0xce, 0x66, 0x54, 0xf3, 0x05, 0xff, 0xf8, 0xcb, 0xc2, 0x50, 0x2c, + 0x9a, 0x35, 0x64, 0xc0, 0x00, 0xdc, 0xc6, 0x9c, 0xef, 0x75, 0xfc, 0x97, 0x4f, 0x90, 0xb0, 0x17, 0x11, 0xf6, 0x26, + 0xc5, 0x28, 0xe1, 0x97, 0x13, 0x06, 0x04, 0xf1, 0xda, 0x13, 0x25, 0x88, 0xf4, 0xa0, 0x3e, 0x99, 0x66, 0x5c, 0x66, + 0x33, 0x48, 0xd6, 0xb0, 0x08, 0xba, 0xdd, 0x35, 0xeb, 0x32, 0xe3, 0x4f, 0x7e, 0xc8, 0x70, 0x0d, 0xf4, 0x4f, 0x26, + 0x4a, 0x3a, 0x37, 0x24, 0xa8, 0xf8, 0x20, 0x5e, 0xe6, 0x50, 0x79, 0xde, 0x33, 0xe4, 0xe9, 0xf9, 0x47, 0x7f, 0xdf, + 0xcc, 0x1c, 0xca, 0x53, 0xd6, 0xe4, 0xef, 0x9e, 0xea, 0xfe, 0xa7, 0xc8, 0x2b, 0x3a, 0xf3, 0xd5, 0xac, 0xb3, 0xe2, + 0x3a, 0xe3, 0xec, 0x88, 0x54, 0x70, 0x6a, 0x45, 0xeb, 0x1d, 0x0f, 0xb1, 0x69, 0xfc, 0xb5, 0x40, 0xea, 0xec, 0x91, + 0xb9, 0x67, 0x07, 0x15, 0xa3, 0x25, 0x14, 0x58, 0x2f, 0xa2, 0x06, 0xbe, 0x1d, 0xb5, 0x19, 0x33, 0x7d, 0x4e, 0x0a, + 0xb4, 0x68, 0x09, 0x36, 0x6d, 0x17, 0xa3, 0x26, 0x5e, 0x96, 0xcc, 0x15, 0x27, 0xfc, 0xe9, 0x32, 0x53, 0xec, 0x87, + 0x8c, 0xd4, 0xc1, 0x9e, 0x17, 0x2b, 0x96, 0x2c, 0x97, 0x4f, 0xd7, 0x0f, 0xc9, 0x2e, 0xf7, 0x1e, 0x11, 0x33, 0x5e, + 0x3f, 0x5e, 0xb2, 0x4b, 0x09, 0x28, 0x91, 0x91, 0x0d, 0xe3, 0x36, 0x12, 0x6a, 0x14, 0x15, 0xa3, 0x2b, 0x50, 0x72, + 0xac, 0x53, 0x11, 0x00, 0xf0, 0xc7, 0xf4, 0x52, 0xd8, 0xc0, 0x83, 0xd3, 0x89, 0x02, 0x94, 0x91, 0xa7, 0xef, 0x4c, + 0xc6, 0x82, 0xe8, 0xa8, 0x99, 0xc3, 0xef, 0x84, 0xb1, 0x7a, 0xe6, 0xde, 0xeb, 0xa3, 0x48, 0xb0, 0x7b, 0xd9, 0x08, + 0x03, 0x89, 0x65, 0xd9, 0x64, 0x1c, 0xb6, 0x6e, 0x2b, 0xfc, 0xb4, 0x58, 0x81, 0x34, 0x05, 0x68, 0xde, 0xd3, 0x46, + 0xc0, 0x69, 0x18, 0xb3, 0x2f, 0x13, 0x48, 0xa9, 0x82, 0xb1, 0xfc, 0xa4, 0x64, 0xc3, 0xb3, 0x49, 0xde, 0xfd, 0xc4, + 0xd3, 0x5c, 0x20, 0xe4, 0xc5, 0x02, 0xdb, 0x9a, 0xa9, 0x13, 0xbf, 0x19, 0xe5, 0x66, 0x3f, 0x56, 0xcd, 0xa2, 0x0d, + 0x47, 0x1e, 0x95, 0xe5, 0xa6, 0x1b, 0xdd, 0xda, 0x2d, 0x58, 0xb5, 0x10, 0xa9, 0xe6, 0x78, 0x19, 0x80, 0x8d, 0xe8, + 0x97, 0x94, 0x61, 0xf5, 0x83, 0x4e, 0x81, 0x64, 0x61, 0xc8, 0xb6, 0xcd, 0x92, 0x32, 0x18, 0x82, 0xf2, 0xa8, 0x9a, + 0x02, 0xac, 0x91, 0xf2, 0x4d, 0x0a, 0xa3, 0xc9, 0xbf, 0x6a, 0x8b, 0xfe, 0x93, 0xff, 0x29, 0xd6, 0x7b, 0x26, 0x88, + 0x64, 0x7b, 0x38, 0x9f, 0x9d, 0xa6, 0x05, 0x33, 0x68, 0x14, 0x84, 0xf6, 0x60, 0x4a, 0xcd, 0x49, 0x24, 0x06, 0x25, + 0x17, 0x22, 0xfb, 0x93, 0xea, 0x2d, 0xc7, 0x47, 0x1e, 0xb2, 0xaf, 0x6f, 0x92, 0x26, 0x9d, 0x56, 0xa7, 0xca, 0x08, + 0xee, 0x0a, 0x9c, 0xa0, 0x04, 0xb3, 0x01, 0xfd, 0x93, 0x9f, 0x3f, 0x85, 0x24, 0xfa, 0xd0, 0x05, 0x84, 0x52, 0x67, + 0xcf, 0x88, 0xdc, 0x2c, 0x3c, 0xa2, 0x55, 0x88, 0x62, 0x5c, 0x20, 0x07, 0xc8, 0xfc, 0xb7, 0x91, 0x05, 0xbd, 0x86, + 0xfd, 0x42, 0x37, 0xa2, 0x7d, 0x08, 0x8b, 0x11, 0x9b, 0x2b, 0xde, 0xe8, 0x3d, 0x90, 0x67, 0x88, 0x1b, 0xf7, 0x34, + 0x2e, 0x68, 0xe9, 0x2a, 0x9b, 0x95, 0x02, 0xdd, 0xc4, 0xa3, 0x3e, 0x09, 0x1d, 0xb5, 0x5a, 0xde, 0x0c, 0xd1, 0x3b, + 0xd0, 0xf3, 0x7a, 0xff, 0x04, 0xdf, 0x0e, 0x08, 0x10, 0x51, 0xb8, 0xa3, 0x33, 0xf9, 0xc1, 0xe1, 0x77, 0xae, 0x3c, + 0xff, 0xc8, 0x44, 0x3d, 0x52, 0x99, 0xef, 0x96, 0xf8, 0xbb, 0x5b, 0xde, 0xff, 0xa1, 0x29, 0x53, 0x82, 0xf2, 0x83, + 0x60, 0x60, 0x64, 0x85, 0x0f, 0xae, 0x9d, 0x0e, 0xbf, 0x91, 0x79, 0x89, 0xe2, 0x85, 0xe4, 0xb2, 0x85, 0xdb, 0x2b, + 0xc6, 0x55, 0xac, 0xe2, 0xca, 0x0e, 0xda, 0x67, 0xdc, 0x7a, 0xfc, 0x10, 0x35, 0xc6, 0x1a, 0x47, 0xe7, 0x1c, 0x94, + 0x06, 0x84, 0x04, 0xd3, 0xc0, 0x26, 0x3d, 0x5a, 0x60, 0x99, 0x16, 0x48, 0x09, 0x42, 0x48, 0x2a, 0xba, 0x1f, 0x43, + 0x53, 0x89, 0xcd, 0x8c, 0x20, 0xad, 0x2a, 0x76, 0xa8, 0xc4, 0x29, 0x67, 0x1f, 0xa6, 0x58, 0x23, 0x7c, 0xaa, 0xe9, + 0x3b, 0x88, 0x92, 0xc8, 0x7b, 0x4e, 0x2e, 0x2e, 0x1d, 0x68, 0x45, 0xa6, 0x4a, 0x49, 0xdf, 0x79, 0xc1, 0xad, 0xbf, + 0xd6, 0x3e, 0x20, 0xd6, 0x41, 0x15, 0xf4, 0xac, 0x8a, 0xbf, 0xdc, 0x62, 0xae, 0xa4, 0x25, 0x56, 0xb1, 0xa7, 0x2c, + 0xf6, 0x73, 0x5a, 0x71, 0x1e, 0xce, 0x69, 0xe8, 0x2e, 0x39, 0x77, 0xa9, 0xb8, 0x27, 0x9d, 0xf5, 0x12, 0xe1, 0xbe, + 0x65, 0x07, 0xd3, 0x67, 0x25, 0xfc, 0xf8, 0xbb, 0x39, 0x29, 0x29, 0xd7, 0x81, 0x46, 0xcf, 0x61, 0xe0, 0x65, 0xd0, + 0xa2, 0xee, 0xd4, 0xc0, 0x3d, 0x91, 0xe8, 0x5b, 0x7f, 0x60, 0xc6, 0x66, 0xd9, 0x12, 0x19, 0x14, 0xcf, 0xd4, 0xff, + 0x24, 0x48, 0xe2, 0xb1, 0xfc, 0x23, 0x2f, 0x0e, 0x49, 0x22, 0xa9, 0x3e, 0x80, 0x3e, 0x09, 0x9e, 0x58, 0x80, 0x57, + 0x7f, 0xa0, 0x80, 0xa1, 0x28, 0x57, 0x39, 0x32, 0x77, 0xc2, 0x1c, 0xf2, 0x74, 0x57, 0xbd, 0x53, 0x07, 0x38, 0x7d, + 0xb5, 0x9e, 0x4d, 0x40, 0xa7, 0x85, 0x1e, 0xa0, 0xc4, 0x99, 0x11, 0xa5, 0x19, 0x07, 0xa7, 0x86, 0x39, 0xfc, 0xaf, + 0x57, 0x12, 0x61, 0xec, 0xc1, 0xc3, 0x41, 0xe3, 0x41, 0x05, 0xf9, 0xd9, 0x8e, 0xa6, 0x34, 0x0c, 0x48, 0xc2, 0xb9, + 0x16, 0xab, 0x64, 0x19, 0x5e, 0x3c, 0xf2, 0xca, 0x0c, 0xe1, 0x04, 0xd6, 0x9d, 0x3e, 0x95, 0x0e, 0x82, 0x71, 0x09, + 0x17, 0x2a, 0xaf, 0x39, 0x35, 0x1c, 0x69, 0xb9, 0x40, 0xf1, 0x57, 0x9a, 0xa8, 0x6b, 0x11, 0x4f, 0xe6, 0x47, 0x5c, + 0x35, 0x10, 0x61, 0xda, 0x05, 0x01, 0x96, 0x97, 0xc8, 0xad, 0x85, 0x72, 0xed, 0xb7, 0x1e, 0x36, 0x30, 0x06, 0xeb, + 0xe6, 0xd7, 0x4b, 0x7e, 0x7d, 0xd3, 0xb4, 0xf6, 0xe2, 0x2d, 0x2a, 0x34, 0x9d, 0xe8, 0xe9, 0x90, 0x22, 0x3c, 0x1d, + 0x77, 0x11, 0x19, 0x46, 0x03, 0x4c, 0xdf, 0x56, 0xd5, 0x62, 0x26, 0xed, 0x00, 0xfa, 0xb9, 0x20, 0xcd, 0x01, 0xa0, + 0x29, 0x42, 0xd9, 0x01, 0x70, 0x15, 0xaa, 0xf5, 0xba, 0x5f, 0x69, 0x63, 0x63, 0x3c, 0xe0, 0x11, 0x81, 0x59, 0xf1, + 0x94, 0x42, 0xc9, 0x79, 0x02, 0x79, 0xb1, 0x4d, 0x55, 0xba, 0x99, 0x96, 0xcd, 0xfa, 0xdd, 0xfa, 0x47, 0x96, 0x00, + 0xa2, 0x26, 0x79, 0x64, 0x32, 0x81, 0x0d, 0x15, 0xd2, 0x14, 0xc7, 0xa4, 0x56, 0x02, 0xae, 0xf9, 0xb0, 0x8f, 0x6c, + 0x09, 0x38, 0x3b, 0x70, 0x2d, 0x88, 0xc3, 0x59, 0x33, 0x64, 0xb2, 0x3c, 0xa7, 0xad, 0xd1, 0x3f, 0x5b, 0xad, 0xb1, + 0xb5, 0xff, 0x43, 0x4b, 0x71, 0x3f, 0x19, 0x0b, 0x4d, 0x0c, 0x48, 0x6d, 0x8f, 0xbf, 0xbb, 0x95, 0x74, 0xe6, 0x6d, + 0xc1, 0x49, 0xff, 0x37, 0xd3, 0xe6, 0x74, 0x9e, 0x3d, 0x39, 0x8c, 0x7c, 0xc0, 0x98, 0x0a, 0x61, 0x8c, 0x93, 0xf0, + 0x62, 0x3b, 0xbc, 0x68, 0x0c, 0x6a, 0xff, 0xe5, 0x0e, 0x86, 0x9c, 0xea, 0xd8, 0x7b, 0x1f, 0x44, 0xc9, 0xbe, 0x98, + 0x5b, 0x34, 0x56, 0x87, 0xb4, 0x28, 0x6e, 0xfb, 0x00, 0x32, 0xf0, 0xd2, 0xfd, 0xff, 0xb8, 0x75, 0x88, 0x63, 0xb0, + 0x09, 0x79, 0x89, 0x4b, 0x12, 0xb3, 0x4d, 0x1f, 0x05, 0xf5, 0xfa, 0xb4, 0x11, 0x2e, 0xd1, 0x5c, 0xe9, 0xfe, 0x07, + 0x2f, 0x5b, 0x54, 0x77, 0x29, 0x0f, 0xf7, 0x0e, 0x8c, 0x69, 0x7c, 0x73, 0xf3, 0x3d, 0x0d, 0xa6, 0x14, 0xba, 0x19, + 0xef, 0x60, 0x13, 0xbb, 0xde, 0x56, 0x56, 0x6c, 0x17, 0x99, 0xa2, 0xa2, 0xa9, 0xd1, 0x47, 0x33, 0xd8, 0xec, 0xd0, + 0x80, 0xf6, 0x6f, 0x31, 0xc9, 0x60, 0xf1, 0x70, 0x6b, 0x2e, 0x44, 0xcb, 0xeb, 0x9c, 0xed, 0x28, 0x38, 0x27, 0x23, + 0x8e, 0x24, 0x48, 0x93, 0xee, 0x3b, 0x8e, 0x1e, 0xd4, 0x41, 0xd5, 0x88, 0x3b, 0x6d, 0xc9, 0x7e, 0x45, 0x7d, 0x97, + 0x3e, 0xae, 0x0b, 0x79, 0xe5, 0x1c, 0x48, 0xc4, 0x67, 0x85, 0x37, 0x27, 0x44, 0x46, 0x6d, 0x1b, 0xa9, 0x15, 0x59, + 0x91, 0x5f, 0x21, 0x25, 0xea, 0x5f, 0x51, 0x2b, 0xc8, 0x62, 0x0e, 0xc0, 0xc0, 0x36, 0x00, 0xab, 0xdf, 0xac, 0x18, + 0xb2, 0xa5, 0x80, 0xc6, 0x2f, 0x67, 0xdb, 0x7c, 0xe2, 0x96, 0xec, 0xe8, 0x17, 0x44, 0x6d, 0x6b, 0x45, 0x13, 0x9c, + 0x77, 0x2f, 0xac, 0x9e, 0x89, 0xdf, 0x53, 0xcf, 0xb7, 0xc0, 0x36, 0x90, 0x4f, 0xd2, 0xfd, 0xce, 0x99, 0x3e, 0x60, + 0x0f, 0xc6, 0x58, 0xc7, 0x60, 0x57, 0xd8, 0x63, 0xa3, 0x37, 0x55, 0xe5, 0x39, 0x68, 0x57, 0xb7, 0x1c, 0x15, 0xf1, + 0xf8, 0x2d, 0xcb, 0x3a, 0x18, 0x66, 0x18, 0x3d, 0xf3, 0x05, 0x94, 0x2d, 0xda, 0x11, 0x99, 0x93, 0x5c, 0x46, 0xdb, + 0x54, 0x0e, 0x28, 0x81, 0x05, 0x31, 0xa9, 0x71, 0x4a, 0xdd, 0x2d, 0x9b, 0x97, 0xae, 0xa3, 0x09, 0xf1, 0xd6, 0x5f, + 0x67, 0x3e, 0xd7, 0x83, 0xa3, 0xf2, 0x3c, 0x44, 0x60, 0x1a, 0xc8, 0xc3, 0x02, 0x0e, 0x23, 0x79, 0x5e, 0x8a, 0x40, + 0x01, 0xef, 0x06, 0x7d, 0xb6, 0x19, 0x28, 0x72, 0x0a, 0x91, 0x77, 0x9e, 0x83, 0x05, 0xba, 0xc1, 0x53, 0x44, 0x19, + 0x87, 0x87, 0xff, 0x2e, 0x70, 0x19, 0x1e, 0x92, 0x25, 0x8c, 0xef, 0x1d, 0x4e, 0x24, 0x27, 0xa9, 0x8b, 0xa4, 0xf5, + 0x4b, 0x78, 0xa6, 0xb6, 0x71, 0x6b, 0xfe, 0x22, 0xfb, 0x24, 0x76, 0xaf, 0xbc, 0x80, 0xf9, 0x18, 0x35, 0xd9, 0x65, + 0xfe, 0xc2, 0x3c, 0x26, 0x3d, 0x33, 0xaf, 0xd1, 0x6a, 0x0d, 0x78, 0x20, 0x69, 0x45, 0x58, 0xca, 0x2c, 0x99, 0x73, + 0x19, 0x00, 0xe8, 0xda, 0x78, 0xd8, 0x1a, 0x42, 0x7c, 0x22, 0xd7, 0x77, 0x45, 0x42, 0x65, 0xaa, 0x59, 0x96, 0x23, + 0xf7, 0xc9, 0x4d, 0x08, 0x4b, 0xb5, 0xcb, 0x12, 0xb7, 0x99, 0xe6, 0xb6, 0x36, 0x3c, 0xf7, 0xca, 0xf2, 0xbd, 0xc0, + 0x14, 0xf5, 0xa0, 0xbf, 0xb3, 0x8d, 0x38, 0x45, 0x10, 0x22, 0x66, 0x70, 0x87, 0xa3, 0x11, 0x64, 0x53, 0x4e, 0xf4, + 0x67, 0xbb, 0xc4, 0xe6, 0xa7, 0x97, 0xa9, 0xaa, 0x70, 0x39, 0x62, 0x32, 0xb1, 0x39, 0x1b, 0xb0, 0x98, 0x83, 0x57, + 0x7b, 0x72, 0x9b, 0xdb, 0xb2, 0xec, 0x8d, 0x08, 0x56, 0x83, 0x16, 0xce, 0x1d, 0x2c, 0x15, 0xfa, 0x4e, 0x66, 0xbd, + 0xab, 0x83, 0x9b, 0xd9, 0x6f, 0xd2, 0xee, 0x8f, 0x1c, 0x7d, 0x55, 0x69, 0xdc, 0x81, 0x6d, 0x2c, 0x81, 0x0d, 0x8f, + 0x11, 0x29, 0x87, 0x44, 0xf5, 0xa9, 0x4f, 0x6c, 0x1e, 0xd5, 0x98, 0xe4, 0x38, 0xc8, 0x1d, 0x26, 0xae, 0x68, 0x3a, + 0x9b, 0xb4, 0x10, 0xbb, 0x52, 0x21, 0x3d, 0x9d, 0x85, 0xfc, 0x16, 0x73, 0xd3, 0x75, 0x92, 0xc8, 0xd6, 0xb5, 0x0f, + 0xf9, 0xa2, 0x25, 0x75, 0x68, 0x60, 0xa7, 0xc7, 0x1c, 0xfd, 0x78, 0xb5, 0x95, 0xaf, 0xd4, 0xd6, 0x71, 0x4e, 0x92, + 0x8f, 0x71, 0xbc, 0x68, 0xf8, 0xe7, 0xa2, 0xa2, 0xd1, 0xc2, 0x93, 0xd8, 0xfa, 0x61, 0x27, 0xaf, 0x5f, 0xd1, 0x62, + 0x36, 0x1c, 0xb5, 0x5e, 0x96, 0x57, 0x1c, 0xee, 0xdd, 0xb6, 0x14, 0x4b, 0x58, 0x1f, 0xe3, 0x72, 0xc9, 0xd3, 0xa8, + 0x5a, 0x3a, 0xfa, 0xcb, 0x1b, 0xb8, 0x25, 0xef, 0x04, 0xc0, 0x44, 0x52, 0x1f, 0x61, 0x41, 0x7b, 0x19, 0x31, 0x42, + 0xec, 0x05, 0xd9, 0x27, 0x68, 0x7b, 0xb1, 0xaf, 0x76, 0x3d, 0x0c, 0xd9, 0x92, 0x64, 0x77, 0x6f, 0x46, 0xf8, 0x42, + 0xdd, 0x3d, 0xb2, 0x1a, 0x87, 0x6b, 0xf2, 0xe2, 0x32, 0x44, 0xb1, 0x97, 0x70, 0xc3, 0xa8, 0x2d, 0xc5, 0xdc, 0x82, + 0x1b, 0x49, 0x8b, 0x89, 0xad, 0x51, 0x46, 0x0d, 0x9b, 0x43, 0x9b, 0x43, 0x69, 0xef, 0x15, 0xdf, 0xf0, 0xdf, 0x10, + 0xef, 0x7c, 0x69, 0x4b, 0x12, 0x75, 0xef, 0x42, 0x5a, 0xe6, 0x45, 0xba, 0x92, 0xf5, 0xf3, 0x76, 0x62, 0x43, 0x71, + 0x37, 0xc7, 0x80, 0xf5, 0xc4, 0x41, 0x76, 0x69, 0xf2, 0x81, 0xc4, 0x26, 0x4a, 0x56, 0x5a, 0xfd, 0xcf, 0xee, 0xfa, + 0xb0, 0xe0, 0xa1, 0x89, 0x46, 0xc7, 0xb6, 0x43, 0x37, 0x62, 0x1e, 0x7e, 0x8d, 0x67, 0xaa, 0x16, 0x90, 0x1c, 0xe6, + 0x26, 0x51, 0xea, 0x66, 0x84, 0xea, 0xc4, 0x8d, 0x17, 0x88, 0x7a, 0xda, 0xf5, 0x4c, 0xc7, 0xd2, 0xfb, 0xbb, 0x0c, + 0xa1, 0xa9, 0x21, 0x04, 0x0f, 0x21, 0x39, 0x3f, 0x09, 0x6f, 0x46, 0x27, 0xe2, 0x1b, 0xa6, 0xcb, 0x19, 0x72, 0x0f, + 0x5f, 0xa0, 0x75, 0x27, 0xc1, 0xc2, 0xe1, 0x86, 0x90, 0x22, 0x15, 0x04, 0xc8, 0xf6, 0x31, 0x80, 0x85, 0x46, 0xf6, + 0xa2, 0xc9, 0xd4, 0x80, 0xc8, 0x66, 0x6d, 0x4b, 0x98, 0x63, 0x33, 0x35, 0x68, 0xc1, 0xd6, 0xfc, 0x12, 0x28, 0x1b, + 0xda, 0xe2, 0x2d, 0xfd, 0x4f, 0x5e, 0x13, 0x41, 0x8c, 0x69, 0x6a, 0xd3, 0xcc, 0x7a, 0xe5, 0xda, 0xde, 0xf5, 0x29, + 0x16, 0x0b, 0xe4, 0xc0, 0x75, 0x43, 0x69, 0x6c, 0x8d, 0xd5, 0x25, 0x0d, 0x68, 0xb9, 0xa8, 0x2e, 0x08, 0x84, 0xc4, + 0x10, 0xf3, 0xaa, 0xa1, 0x90, 0x92, 0x84, 0x6a, 0x6e, 0xdd, 0x89, 0x6d, 0x82, 0xc2, 0xec, 0xb8, 0x33, 0x79, 0xe8, + 0xe7, 0x70, 0xfe, 0xfe, 0xc6, 0x2c, 0x40, 0x51, 0xb8, 0xe2, 0xa5, 0x8c, 0x06, 0x89, 0x7e, 0xb3, 0x1e, 0x7a, 0xfe, + 0x83, 0x03, 0xda, 0x9d, 0xca, 0x32, 0xa3, 0xd4, 0xa9, 0x9e, 0x09, 0x4e, 0x6f, 0x0d, 0xd0, 0x88, 0x48, 0x80, 0x09, + 0xfc, 0xa8, 0x3f, 0x0a, 0x15, 0x0b, 0x98, 0xb5, 0x95, 0x53, 0xaf, 0xef, 0x31, 0x10, 0x29, 0xec, 0xb0, 0x71, 0xce, + 0xa2, 0x55, 0x8d, 0x78, 0x42, 0x82, 0x3e, 0x48, 0xc8, 0xce, 0x59, 0xf5, 0x8c, 0xaf, 0x93, 0x0b, 0xbe, 0x60, 0x77, + 0xfc, 0xb5, 0x06, 0x50, 0x8e, 0x7f, 0xb1, 0xf7, 0x86, 0xd7, 0xc3, 0x16, 0xd7, 0x23, 0xe6, 0x8b, 0x32, 0x2f, 0x7f, + 0x78, 0xd0, 0x72, 0xfa, 0xf7, 0xe7, 0x69, 0x80, 0x2a, 0x7f, 0xb1, 0x84, 0x01, 0xa9, 0x3c, 0xbc, 0xf5, 0x46, 0xe4, + 0x4a, 0x66, 0x14, 0x8d, 0x59, 0x3b, 0x6e, 0x09, 0x3b, 0xb8, 0x28, 0x8e, 0x20, 0x54, 0xfc, 0xf3, 0x16, 0x40, 0x62, + 0x2b, 0x68, 0x99, 0xd1, 0xa0, 0x11, 0xed, 0x81, 0x3a, 0x2b, 0x6c, 0xcc, 0x0b, 0xb6, 0x2e, 0x5f, 0xde, 0xad, 0xe0, + 0x20, 0x4b, 0x48, 0x82, 0x87, 0xf5, 0xf6, 0xcd, 0x26, 0xd3, 0xa5, 0x87, 0xa9, 0xd7, 0x1d, 0xbf, 0x67, 0x56, 0x20, + 0xa4, 0xd9, 0x43, 0x64, 0x6d, 0x37, 0x12, 0xd3, 0x1b, 0x4f, 0x6d, 0x3b, 0x62, 0x5e, 0xb7, 0x13, 0x91, 0x2b, 0x75, + 0x6c, 0x9b, 0x87, 0xc8, 0x08, 0x2b, 0x8c, 0x24, 0xb8, 0xfc, 0x32, 0x20, 0x36, 0x51, 0xd0, 0xd8, 0xc7, 0xe2, 0x52, + 0x16, 0x93, 0xec, 0xa3, 0xf8, 0x4b, 0x59, 0xeb, 0x5f, 0x22, 0xd5, 0xd9, 0x13, 0xf8, 0x15, 0x43, 0x7b, 0x0f, 0xa1, + 0xb1, 0x4e, 0x83, 0xbb, 0x16, 0x3c, 0xb2, 0x80, 0x72, 0x1f, 0x1a, 0x12, 0x42, 0x71, 0xba, 0x1d, 0x16, 0xd9, 0xae, + 0x25, 0x46, 0x80, 0x8f, 0x92, 0x5e, 0xa9, 0x4d, 0xc6, 0x70, 0x05, 0x05, 0x70, 0x79, 0xae, 0xc7, 0xf3, 0xd1, 0xcd, + 0xf6, 0x4a, 0x23, 0x09, 0x7d, 0x37, 0xac, 0x78, 0xb9, 0xb9, 0xee, 0x2a, 0x8b, 0x36, 0x9f, 0x62, 0x1c, 0xeb, 0x02, + 0x91, 0x19, 0x21, 0x62, 0x6e, 0xd9, 0xa0, 0x20, 0x1d, 0x6c, 0x17, 0x03, 0xf4, 0xb1, 0x81, 0xe1, 0x0c, 0x56, 0xba, + 0xaa, 0xad, 0x9d, 0xa7, 0xc8, 0xf4, 0x6f, 0xb6, 0x98, 0xc0, 0xcf, 0x17, 0x17, 0x24, 0x04, 0x24, 0x2c, 0xf4, 0xcc, + 0x83, 0x59, 0x0f, 0x27, 0x79, 0xf6, 0x12, 0x13, 0x2e, 0x64, 0xa8, 0x70, 0xfc, 0xa0, 0xad, 0xe6, 0x82, 0xe6, 0xf8, + 0xf5, 0x4c, 0x5b, 0x95, 0xaf, 0x95, 0x34, 0xc9, 0x82, 0x43, 0x5e, 0x38, 0x5d, 0xde, 0x32, 0x44, 0xf1, 0xa9, 0x76, + 0xdd, 0x77, 0xb8, 0xf9, 0x4c, 0x8a, 0x9c, 0x54, 0xda, 0x89, 0x40, 0xa5, 0x21, 0x93, 0xb7, 0x7b, 0x01, 0xb0, 0x6d, + 0x88, 0xbe, 0x68, 0x36, 0x32, 0x53, 0x99, 0x8e, 0xae, 0x96, 0x87, 0x70, 0x6c, 0x0f, 0x6f, 0x06, 0xc3, 0x10, 0xf0, + 0xfa, 0xb4, 0x66, 0xff, 0xba, 0x67, 0x25, 0x55, 0x15, 0x4d, 0x8c, 0x8a, 0xb8, 0xb9, 0x60, 0x72, 0x0f, 0x2a, 0xa6, + 0xc1, 0x43, 0x38, 0x69, 0xc0, 0xe9, 0x38, 0x53, 0xd9, 0x20, 0x79, 0x81, 0x49, 0x10, 0x7b, 0x02, 0x2d, 0x4d, 0xc0, + 0xbc, 0xa2, 0xec, 0x38, 0xda, 0x8c, 0xed, 0x88, 0x50, 0xce, 0x9c, 0x44, 0x45, 0xfc, 0x98, 0x7b, 0xd2, 0x0a, 0xb0, + 0xcf, 0x40, 0x77, 0xbd, 0xc6, 0x4f, 0x6a, 0x41, 0xd1, 0xb7, 0xb6, 0xff, 0x5f, 0x86, 0x41, 0xd8, 0x9e, 0xb6, 0x73, + 0x00, 0x0a, 0xb2, 0x84, 0x00, 0xfe, 0xf2, 0x82, 0xbe, 0x04, 0x59, 0x92, 0x0a, 0x3f, 0x90, 0x57, 0x8f, 0xad, 0x3e, + 0x55, 0xce, 0xbe, 0x3a, 0xfb, 0xf5, 0xb7, 0xec, 0x97, 0xf4, 0xc1, 0x25, 0x27, 0x77, 0xfb, 0x54, 0x62, 0x73, 0xbd, + 0x53, 0x2a, 0x1c, 0x9d, 0x63, 0xb9, 0xac, 0xaf, 0xc4, 0x70, 0x39, 0x95, 0x10, 0xfc, 0x87, 0x0f, 0xc4, 0xef, 0xb3, + 0x72, 0xb7, 0x4f, 0x21, 0xbf, 0x9f, 0x4b, 0x2b, 0xf9, 0xc9, 0xe1, 0x46, 0xba, 0x4f, 0xab, 0x28, 0xc7, 0x5c, 0x7f, + 0x5b, 0x4a, 0xe5, 0xac, 0xb3, 0xb3, 0x4b, 0xb9, 0xb9, 0x9c, 0x73, 0x78, 0xaa, 0xcb, 0xbd, 0x5e, 0xb5, 0xd6, 0xff, + 0x57, 0x66, 0x0d, 0x65, 0xb9, 0x19, 0x94, 0xcc, 0x7b, 0x28, 0x08, 0x72, 0x37, 0xb1, 0x4e, 0x2f, 0x72, 0xe7, 0xb8, + 0x43, 0x39, 0x96, 0xb6, 0xbe, 0x2a, 0x53, 0x8f, 0xcc, 0x45, 0x8c, 0xf3, 0x15, 0xf1, 0xb2, 0x9a, 0xbc, 0x6d, 0xd0, + 0x6f, 0x4f, 0xc8, 0xfc, 0xe7, 0xd7, 0x90, 0x64, 0x3f, 0xc6, 0x2f, 0xea, 0xfe, 0x02, 0x5c, 0xc3, 0x9b, 0x72, 0xe4, + 0x05, 0x3b, 0xae, 0xab, 0xa7, 0x6d, 0xb2, 0xae, 0x85, 0x63, 0xdb, 0xe5, 0xc0, 0x6b, 0x8b, 0x38, 0x04, 0x44, 0x69, + 0x65, 0xdc, 0x73, 0x7a, 0xd7, 0xe9, 0x77, 0xa6, 0x3a, 0x86, 0xdd, 0x80, 0x20, 0x11, 0x0c, 0x28, 0x30, 0x1f, 0x83, + 0xba, 0x93, 0x51, 0xe5, 0xc4, 0x9e, 0x35, 0x10, 0x4a, 0x60, 0x45, 0xf3, 0x35, 0x12, 0x80, 0x96, 0x76, 0xe0, 0x65, + 0xad, 0xa2, 0x93, 0x25, 0x6b, 0x10, 0x1c, 0xf4, 0xff, 0x88, 0xc1, 0x11, 0x07, 0xdf, 0x24, 0x21, 0xce, 0x0a, 0x45, + 0x62, 0x4e, 0xb3, 0x47, 0x1f, 0xb3, 0x8f, 0x72, 0x09, 0xd2, 0xec, 0x47, 0x60, 0x80, 0x60, 0x19, 0x8e, 0x63, 0x91, + 0xa0, 0x64, 0xbe, 0x2a, 0xc8, 0x92, 0x9a, 0xf7, 0x9f, 0x60, 0x6c, 0xff, 0x46, 0xb7, 0x8d, 0xec, 0xef, 0x9a, 0x4a, + 0x6e, 0x7f, 0xe5, 0xdd, 0xf2, 0xeb, 0xfa, 0x7a, 0x79, 0xa1, 0xfe, 0xfc, 0xba, 0x69, 0x81, 0x77, 0x72, 0xf7, 0x52, + 0x0e, 0x35, 0x3f, 0x5f, 0x67, 0xc4, 0x58, 0x30, 0x40, 0xec, 0x53, 0xc7, 0x87, 0x92, 0xee, 0xb7, 0x9e, 0x0d, 0xac, + 0x89, 0xfd, 0x1a, 0xb7, 0xa8, 0x5e, 0xce, 0x0b, 0x6c, 0x56, 0xe3, 0x1a, 0xba, 0xe7, 0x85, 0xd6, 0x3c, 0x17, 0x66, + 0xa9, 0xa0, 0x14, 0x5b, 0x53, 0xc0, 0x27, 0xb8, 0xeb, 0xca, 0x4d, 0x6a, 0xa2, 0xea, 0x4d, 0x78, 0x92, 0xa0, 0xa2, + 0x03, 0x17, 0x4d, 0x5f, 0x3d, 0xb5, 0x2d, 0x36, 0x86, 0x3f, 0x13, 0x74, 0x35, 0x86, 0xac, 0x46, 0x39, 0x66, 0x2d, + 0x56, 0x7a, 0xa1, 0xb5, 0xbc, 0x5a, 0xea, 0x6e, 0x5f, 0x03, 0xbd, 0xf2, 0x82, 0x32, 0xe0, 0x1e, 0x80, 0xac, 0x57, + 0xf4, 0x94, 0x56, 0x91, 0x2d, 0xd9, 0x27, 0x14, 0xdc, 0x3c, 0x9e, 0xe0, 0xb0, 0xf4, 0x51, 0xdd, 0x23, 0x4d, 0x62, + 0x2b, 0x5c, 0xc3, 0xde, 0x64, 0x55, 0xe9, 0x65, 0xf3, 0x84, 0x07, 0x98, 0xd3, 0x82, 0xfd, 0x1b, 0xdb, 0x62, 0xf9, + 0x71, 0x12, 0x68, 0xbb, 0x68, 0x14, 0x37, 0xca, 0x00, 0x88, 0xd2, 0x3d, 0xbd, 0x01, 0x07, 0xa2, 0x5d, 0xd7, 0x42, + 0x7d, 0x9b, 0xd8, 0x0e, 0xe7, 0x26, 0x13, 0x6a, 0xe1, 0xc2, 0x1a, 0xcd, 0xa6, 0x0b, 0x27, 0x6a, 0xef, 0xd2, 0x9e, + 0x27, 0x83, 0x8c, 0xff, 0x2a, 0x83, 0x98, 0xf4, 0xbd, 0xc0, 0xa3, 0x83, 0x70, 0x0f, 0xa1, 0x27, 0x61, 0x91, 0x8a, + 0xd6, 0x14, 0x6c, 0x83, 0x15, 0xa5, 0x71, 0x00, 0xa0, 0xbd, 0x8b, 0xb8, 0x01, 0x07, 0x37, 0x6c, 0x0c, 0x1d, 0x1b, + 0xb7, 0xe4, 0x95, 0x64, 0x82, 0xa0, 0xf2, 0x66, 0x89, 0xcd, 0x78, 0xb2, 0x13, 0x95, 0x6f, 0x70, 0xb3, 0x73, 0x27, + 0x14, 0xf6, 0x3b, 0x9d, 0x11, 0x4c, 0x59, 0x59, 0xed, 0xd0, 0x37, 0x23, 0x5e, 0x70, 0x98, 0x43, 0xb2, 0x20, 0x22, + 0x19, 0xb1, 0xaa, 0x1b, 0xbf, 0xf3, 0x7e, 0x94, 0x9b, 0x89, 0x6d, 0xb1, 0x5e, 0xf1, 0x8c, 0x60, 0xbd, 0x83, 0xa3, + 0x73, 0xf2, 0xdc, 0xcd, 0xc8, 0x5c, 0xe1, 0x3f, 0x86, 0xc9, 0xed, 0x66, 0x7e, 0x30, 0x8c, 0xa8, 0x2f, 0xff, 0x93, + 0x8c, 0x59, 0x55, 0x4e, 0xa3, 0x31, 0x24, 0x44, 0x32, 0xbc, 0x09, 0x40, 0x3c, 0xcf, 0x9a, 0x8c, 0xd1, 0x4c, 0xac, + 0xb6, 0xad, 0xd3, 0x34, 0xfb, 0xf6, 0x92, 0xd3, 0xef, 0x45, 0x85, 0x17, 0x78, 0x5c, 0x75, 0x6e, 0x64, 0xd7, 0x0f, + 0x74, 0x31, 0x87, 0xbe, 0x55, 0xe9, 0xaa, 0xbe, 0x91, 0x1f, 0x6a, 0xf8, 0x4a, 0x0c, 0xea, 0x6e, 0x50, 0xf2, 0x00, + 0x00, 0xfd, 0x71, 0x5e, 0x5e, 0xfd, 0x5f, 0xa3, 0xb9, 0x93, 0x4e, 0xb0, 0xb1, 0x62, 0x69, 0x8e, 0xe3, 0xe5, 0xd0, + 0x5f, 0xa8, 0xe8, 0x39, 0xa1, 0xdf, 0x8d, 0x48, 0xba, 0x44, 0x67, 0x18, 0x4f, 0xcc, 0xd2, 0xe0, 0xb0, 0x86, 0x12, + 0xfa, 0x9b, 0xd1, 0x6f, 0xd7, 0xde, 0x37, 0x90, 0xe2, 0xdf, 0xb8, 0xad, 0x8e, 0x67, 0x47, 0x95, 0x99, 0xd4, 0x32, + 0x0f, 0xdc, 0x16, 0x57, 0x75, 0xd5, 0xcc, 0xa7, 0xed, 0x92, 0x69, 0xda, 0x79, 0xcc, 0x2e, 0xe3, 0x57, 0x38, 0x91, + 0x44, 0x7d, 0xb7, 0x0e, 0x03, 0x34, 0x30, 0xd0, 0x5e, 0x12, 0xa7, 0x17, 0x99, 0xae, 0xde, 0x6a, 0x06, 0x43, 0x73, + 0xa5, 0x4e, 0x3f, 0xb0, 0x7a, 0x41, 0xcb, 0xb0, 0xb3, 0x66, 0xf2, 0xc8, 0x09, 0xb1, 0x8b, 0x9c, 0x9f, 0x98, 0x0f, + 0x39, 0xa1, 0xa6, 0x01, 0xbd, 0x9d, 0x97, 0x57, 0xae, 0x54, 0x91, 0x81, 0x9a, 0x09, 0x29, 0x20, 0xbb, 0xa1, 0xf5, + 0xb1, 0x26, 0xc6, 0x1e, 0xa4, 0x0b, 0xb3, 0xd6, 0x3c, 0x0d, 0x42, 0x53, 0x28, 0x0b, 0x57, 0x66, 0x64, 0xa3, 0xf0, + 0x3d, 0x39, 0x85, 0x86, 0x0b, 0x5a, 0x42, 0x7b, 0xf7, 0x3e, 0x24, 0x74, 0xf7, 0x98, 0x44, 0xd5, 0x74, 0x96, 0x16, + 0xca, 0xcd, 0x42, 0x79, 0x8e, 0xc0, 0x0b, 0x16, 0xb9, 0xe7, 0x55, 0x39, 0x12, 0xb7, 0xee, 0xd6, 0xe9, 0xeb, 0x6e, + 0xb5, 0x86, 0x7d, 0x4c, 0x79, 0x24, 0xbc, 0xa3, 0x85, 0xf9, 0x57, 0xb2, 0xe4, 0x48, 0x87, 0x8d, 0x9a, 0x66, 0xf2, + 0x15, 0x3e, 0xff, 0x47, 0x75, 0x6f, 0xe2, 0x7d, 0xe2, 0x59, 0x81, 0x70, 0x57, 0x14, 0x3a, 0xe3, 0x8e, 0x59, 0x47, + 0xeb, 0x70, 0x4e, 0x9d, 0x98, 0xf1, 0xf0, 0xb8, 0x40, 0x31, 0xfc, 0xf6, 0x8c, 0x06, 0xdc, 0xfd, 0xe9, 0x98, 0xd8, + 0xbd, 0x0e, 0x52, 0xec, 0x32, 0x8b, 0x74, 0x7f, 0xd5, 0x68, 0xaa, 0x0b, 0xb1, 0x6e, 0x95, 0xb9, 0x27, 0xa6, 0xec, + 0x30, 0x9c, 0x51, 0xed, 0xb3, 0x85, 0x9b, 0xbd, 0x91, 0xbb, 0x51, 0xd5, 0x53, 0x6c, 0xe9, 0x92, 0xc3, 0x13, 0x38, + 0x64, 0xd3, 0xa8, 0xdc, 0xfd, 0x5a, 0xcb, 0x57, 0xfb, 0x6a, 0xd1, 0x97, 0x58, 0xc4, 0xf7, 0xf3, 0x21, 0x85, 0x2d, + 0x4f, 0x44, 0xb6, 0x3a, 0x8c, 0x75, 0x80, 0xf1, 0x50, 0xeb, 0xdb, 0xcd, 0x4e, 0x6a, 0x3f, 0xa0, 0xbb, 0x75, 0x96, + 0x96, 0x6f, 0x16, 0xbf, 0xad, 0xff, 0x3c, 0x70, 0x2f, 0x39, 0x14, 0xbf, 0x56, 0x5f, 0x45, 0xa2, 0xc1, 0xfd, 0xb2, + 0x5c, 0x93, 0x49, 0x71, 0xfc, 0x04, 0xc7, 0x54, 0xa6, 0x28, 0x47, 0xd5, 0x6d, 0x37, 0x84, 0x8a, 0x8d, 0x8e, 0xcd, + 0x79, 0xb2, 0x33, 0x75, 0xf5, 0x00, 0x8f, 0x0c, 0x51, 0xfb, 0x9f, 0xca, 0x8b, 0xd3, 0x1e, 0xd9, 0x07, 0xfb, 0xb7, + 0xcc, 0x21, 0xb6, 0x4e, 0xbb, 0x4e, 0xad, 0x9a, 0x70, 0xe0, 0xc3, 0xea, 0x1a, 0xff, 0x17, 0x2f, 0xb8, 0xd1, 0x44, + 0xf4, 0x56, 0xb5, 0x65, 0xa5, 0x04, 0xb6, 0xab, 0x5d, 0x2a, 0x35, 0xbd, 0xd5, 0x4d, 0x8c, 0xcb, 0x9c, 0xd7, 0xd5, + 0xde, 0x90, 0xf5, 0x93, 0xa0, 0x0d, 0xb9, 0x7f, 0xfa, 0x30, 0xe2, 0x10, 0x23, 0x29, 0x6b, 0x17, 0x63, 0xae, 0x0d, + 0xa1, 0x93, 0x2d, 0xca, 0x98, 0xdc, 0xf5, 0x4f, 0x24, 0xaa, 0x2e, 0x9a, 0x20, 0x10, 0xe7, 0xed, 0xb1, 0xf5, 0xea, + 0x6e, 0x71, 0xc9, 0xcd, 0x70, 0x65, 0xaa, 0x12, 0xf0, 0x79, 0x62, 0xf0, 0xc5, 0x82, 0x44, 0x09, 0x3c, 0x0b, 0xd5, + 0x64, 0xdc, 0x35, 0x44, 0x1f, 0x6c, 0xbc, 0xfc, 0xc3, 0xc8, 0x31, 0x3f, 0xf3, 0x4f, 0xca, 0x1b, 0x44, 0x27, 0xc0, + 0x99, 0x00, 0x3c, 0x9e, 0xa5, 0x25, 0xd5, 0x37, 0xa7, 0x7f, 0x6d, 0x92, 0xff, 0x77, 0x6c, 0xf8, 0x56, 0xfb, 0x15, + 0xd0, 0x68, 0x61, 0xd8, 0x21, 0xd0, 0x1a, 0xd4, 0x39, 0x85, 0x71, 0x0f, 0x01, 0xb5, 0xe2, 0x1a, 0xd7, 0x77, 0x69, + 0x84, 0x30, 0x08, 0x49, 0x50, 0xd9, 0x1d, 0x76, 0xb8, 0xb7, 0xbe, 0x2f, 0x90, 0x01, 0xc2, 0x43, 0x19, 0x41, 0x8b, + 0x8c, 0x07, 0xf7, 0x06, 0x7b, 0x10, 0xd6, 0xb9, 0x94, 0x53, 0xae, 0x92, 0xae, 0x43, 0xf6, 0x71, 0xd3, 0xf4, 0x1a, + 0x27, 0xe4, 0x08, 0x52, 0xa9, 0x67, 0x40, 0xd3, 0x74, 0x91, 0x5e, 0xae, 0xb7, 0x74, 0xca, 0xb7, 0x06, 0x62, 0xeb, + 0x5a, 0x58, 0x74, 0x9f, 0x5d, 0xca, 0x43, 0x0f, 0x52, 0x08, 0x0e, 0x89, 0xe5, 0x14, 0xd4, 0x0f, 0x60, 0x52, 0x2e, + 0xff, 0xc3, 0x24, 0x5e, 0xe5, 0xee, 0xfe, 0xd7, 0x6a, 0xb1, 0xaa, 0x1e, 0xcc, 0x6c, 0xfc, 0x40, 0xf7, 0x97, 0xf0, + 0x51, 0xad, 0x3d, 0x5f, 0x39, 0x66, 0x05, 0xa6, 0x0c, 0xfe, 0x93, 0x7f, 0xd0, 0x86, 0x3a, 0x97, 0xf9, 0x6f, 0x71, + 0x25, 0xae, 0x81, 0x14, 0xe7, 0x3d, 0xd4, 0x88, 0x26, 0x69, 0xbc, 0x4c, 0x59, 0x6d, 0x1a, 0x9f, 0x66, 0x8a, 0x40, + 0x88, 0x3a, 0x7a, 0x1d, 0x2e, 0x39, 0x70, 0x91, 0xc3, 0xaa, 0x05, 0xf8, 0x67, 0xc1, 0x0a, 0xe8, 0xf6, 0xb7, 0xe4, + 0x68, 0xcd, 0xfc, 0xed, 0x8e, 0xc6, 0x95, 0x0b, 0x39, 0x34, 0xb1, 0xf6, 0xd5, 0x76, 0x4c, 0xce, 0xd4, 0x9d, 0xa7, + 0x15, 0x8a, 0xae, 0xab, 0x9b, 0x89, 0x2b, 0x02, 0x8e, 0x53, 0xcf, 0x0f, 0x02, 0x0c, 0x66, 0x85, 0x2f, 0xfb, 0x42, + 0x4d, 0xbf, 0xc6, 0x60, 0x0a, 0x32, 0x96, 0x3d, 0x8b, 0x62, 0x78, 0x17, 0xf2, 0x2a, 0x62, 0x2c, 0x97, 0x22, 0x56, + 0x08, 0x65, 0x01, 0x5b, 0x56, 0xae, 0x47, 0xa1, 0x78, 0x78, 0x9c, 0xe2, 0xdd, 0xcc, 0x39, 0x52, 0xee, 0x12, 0xcc, + 0xee, 0x90, 0xf9, 0x49, 0x22, 0xf5, 0xda, 0xb5, 0x60, 0x93, 0x62, 0x8a, 0x5d, 0x51, 0xe4, 0x06, 0x87, 0x30, 0xe1, + 0xa8, 0x7b, 0x7b, 0xa3, 0x69, 0x22, 0xa1, 0x91, 0x28, 0x30, 0x23, 0xa4, 0xbb, 0xfe, 0xe3, 0xee, 0x4d, 0x3f, 0x99, + 0x32, 0x06, 0x11, 0xd0, 0x28, 0x7a, 0x06, 0x10, 0x7a, 0xbe, 0x4a, 0xb9, 0x64, 0x3a, 0xae, 0x60, 0xc4, 0x7d, 0x05, + 0x24, 0x5c, 0x34, 0x6e, 0xcd, 0x2f, 0xd1, 0x49, 0xa6, 0x78, 0x9a, 0x00, 0x45, 0xa3, 0xad, 0xf2, 0x6c, 0x28, 0x1f, + 0x79, 0x16, 0xac, 0x44, 0x3d, 0x69, 0x70, 0x14, 0x0c, 0xba, 0xd9, 0x48, 0xc2, 0x21, 0x35, 0x19, 0xc6, 0xc8, 0x30, + 0x38, 0xfa, 0x97, 0xb6, 0xca, 0x43, 0x6a, 0x5d, 0x2d, 0x14, 0x32, 0xa3, 0x07, 0x33, 0x3f, 0x98, 0xa8, 0x61, 0x55, + 0x0b, 0xf3, 0x41, 0xba, 0x76, 0x5a, 0x65, 0x94, 0x25, 0xc6, 0x69, 0xb0, 0x30, 0x86, 0x1c, 0x6a, 0x1c, 0xb0, 0xd9, + 0x40, 0xee, 0x6a, 0xce, 0xe6, 0x51, 0x33, 0x6e, 0xaf, 0x6b, 0x46, 0x9f, 0xfa, 0xe2, 0x56, 0x7f, 0x2e, 0xd3, 0x0d, + 0x3b, 0x56, 0xf9, 0x4b, 0xbf, 0xa8, 0xa6, 0x0f, 0x3d, 0xe6, 0x4d, 0x39, 0x18, 0x66, 0x78, 0xf5, 0x59, 0x58, 0x3c, + 0x48, 0x1a, 0x94, 0xf9, 0x52, 0xad, 0x1d, 0x6e, 0x7f, 0x3f, 0x30, 0xf4, 0x66, 0x37, 0x31, 0x49, 0x1a, 0x02, 0xe5, + 0x08, 0x89, 0x08, 0x8e, 0x59, 0xf1, 0x1f, 0x57, 0x95, 0xff, 0xbd, 0x53, 0x5f, 0xd0, 0x83, 0xf0, 0xd1, 0x5e, 0xf7, + 0x34, 0x0a, 0x98, 0xb3, 0x96, 0xed, 0xea, 0xd3, 0x84, 0x1a, 0xd2, 0x5f, 0x11, 0x32, 0x6e, 0x1c, 0xab, 0x7f, 0x74, + 0x53, 0xf2, 0x3b, 0x5d, 0x25, 0xf6, 0xd1, 0x5c, 0x9f, 0xd8, 0xa2, 0x4a, 0x3a, 0x3a, 0x36, 0xa7, 0x2d, 0x29, 0xcd, + 0x49, 0xf9, 0x56, 0x7b, 0x78, 0xda, 0x4a, 0x71, 0xc9, 0xe6, 0x3d, 0xb9, 0x9a, 0x27, 0x59, 0x6d, 0xcb, 0x71, 0x84, + 0x3b, 0xc8, 0xd7, 0xe7, 0x8c, 0xd2, 0xd1, 0x07, 0xab, 0x1f, 0xf7, 0x26, 0x81, 0xcc, 0xd3, 0x13, 0x70, 0xa3, 0x6b, + 0x57, 0x7a, 0x7c, 0x2b, 0x4e, 0xcc, 0x93, 0xf7, 0x43, 0xf6, 0x6b, 0x5c, 0xc9, 0x82, 0x8e, 0x7b, 0x5f, 0x35, 0x4c, + 0xb7, 0x19, 0xd3, 0x7e, 0xa4, 0x18, 0x8c, 0xe6, 0xab, 0x2c, 0x89, 0x0a, 0x62, 0xc1, 0x6b, 0xe2, 0x83, 0xd8, 0x00, + 0x40, 0xce, 0x68, 0x8b, 0x5a, 0x7a, 0x8c, 0x25, 0x51, 0xbc, 0xad, 0x40, 0xcd, 0x79, 0x76, 0x96, 0xd1, 0xaa, 0x3a, + 0xd1, 0xab, 0x53, 0xae, 0xd2, 0xec, 0x22, 0x74, 0x3d, 0x7c, 0x65, 0x29, 0x2a, 0x59, 0x56, 0xbd, 0x0b, 0xd3, 0x57, + 0xec, 0x95, 0x17, 0x48, 0x79, 0x57, 0x4a, 0x4d, 0x21, 0x23, 0x1b, 0x83, 0xc6, 0xd6, 0xd9, 0x4b, 0x2c, 0x6e, 0xb2, + 0x3c, 0x4a, 0x28, 0x7c, 0x31, 0xf7, 0x71, 0x7b, 0x2c, 0x55, 0xc5, 0x9c, 0x43, 0x98, 0x93, 0x2a, 0x9d, 0x74, 0x95, + 0x03, 0xf8, 0xd5, 0x65, 0x10, 0xd6, 0x48, 0xa1, 0x3a, 0xc7, 0x3d, 0x6c, 0x49, 0xa6, 0x63, 0x06, 0x19, 0x8b, 0xee, + 0xfa, 0x3b, 0x2a, 0x9d, 0xc7, 0x41, 0x74, 0x1f, 0xba, 0x5a, 0x21, 0xc2, 0x60, 0x7b, 0xd6, 0x92, 0x2b, 0x9e, 0x2b, + 0x8e, 0xb2, 0x2b, 0x31, 0xb5, 0x3c, 0x1b, 0xb2, 0x6d, 0xd1, 0x15, 0x4b, 0x65, 0x4d, 0x77, 0x57, 0x13, 0xa9, 0xe0, + 0xb1, 0x1f, 0x7f, 0xe0, 0xcb, 0x92, 0x91, 0x53, 0x99, 0xc4, 0xb2, 0x0c, 0x61, 0x6e, 0xdc, 0x10, 0x3c, 0xc1, 0x68, + 0xde, 0x92, 0x79, 0xca, 0x29, 0x85, 0xd2, 0xfb, 0x9f, 0x1b, 0x8f, 0x50, 0x36, 0xdb, 0x30, 0xbd, 0x65, 0xea, 0xbb, + 0xc4, 0xf5, 0xfc, 0x87, 0xe8, 0x94, 0x44, 0x0b, 0xde, 0x9f, 0x27, 0x32, 0xda, 0x54, 0xa8, 0xb0, 0x6e, 0x66, 0xbb, + 0xcf, 0xc1, 0xc6, 0x7f, 0x51, 0x49, 0x86, 0x1c, 0x54, 0x98, 0x5e, 0xb5, 0x63, 0xa1, 0x73, 0xc8, 0x5d, 0x6f, 0x28, + 0x88, 0x8d, 0xc0, 0x6e, 0x68, 0x05, 0x89, 0x34, 0x59, 0x88, 0x7d, 0x36, 0xaa, 0xba, 0x9b, 0x55, 0xa1, 0x06, 0xfc, + 0xf2, 0x17, 0xb1, 0x38, 0xbf, 0x40, 0x52, 0x7d, 0xc1, 0x21, 0x21, 0xf4, 0xc9, 0x6f, 0xc4, 0x5e, 0x0d, 0xbe, 0x88, + 0x95, 0x66, 0xdb, 0x31, 0xfa, 0x99, 0x9f, 0x8f, 0xbb, 0xee, 0x0c, 0x53, 0x74, 0xb8, 0x01, 0x8b, 0x11, 0x43, 0x4e, + 0x52, 0x37, 0xf9, 0x4b, 0x4a, 0x7e, 0x32, 0xbd, 0xf9, 0x06, 0xdf, 0x69, 0x6d, 0x6f, 0xa0, 0x50, 0x88, 0x59, 0x67, + 0x68, 0x70, 0xc3, 0x1e, 0x9e, 0xea, 0x98, 0x59, 0x98, 0xe3, 0x90, 0x24, 0xa2, 0x45, 0x0e, 0x67, 0x88, 0xdf, 0x00, + 0x98, 0x40, 0x93, 0x95, 0x08, 0x19, 0x25, 0xb0, 0x47, 0xf0, 0x82, 0x9b, 0x6d, 0xde, 0xef, 0x79, 0x1e, 0x2e, 0xa4, + 0x56, 0xae, 0xe0, 0x0a, 0x30, 0xd5, 0xb3, 0x6b, 0x49, 0xf1, 0xe1, 0x51, 0xb4, 0x86, 0x78, 0xae, 0x25, 0x94, 0xc5, + 0xce, 0x83, 0x60, 0x55, 0x65, 0x57, 0xd9, 0x19, 0xcc, 0x52, 0x0f, 0x0e, 0x54, 0x71, 0x81, 0x24, 0xdd, 0x18, 0x6f, + 0x53, 0xcc, 0xb2, 0x16, 0x7e, 0xaa, 0x62, 0xde, 0xb0, 0xa9, 0xe0, 0xf0, 0x5a, 0x9d, 0x7f, 0x31, 0xd6, 0x30, 0xa1, + 0x4d, 0x0d, 0xac, 0x04, 0x31, 0x69, 0x58, 0xc2, 0xc6, 0xc1, 0x67, 0xd0, 0x9f, 0x07, 0x4c, 0x33, 0x9c, 0xde, 0x8f, + 0x51, 0xbd, 0x65, 0xf5, 0xc9, 0xf7, 0xd2, 0xf3, 0x46, 0x1d, 0x3d, 0xa8, 0xc4, 0xaa, 0xe5, 0xeb, 0x0c, 0x11, 0xdd, + 0xde, 0xfa, 0x8c, 0xe7, 0xd4, 0x32, 0x04, 0x40, 0xe2, 0x49, 0x9d, 0x19, 0x7b, 0x7c, 0xdc, 0x30, 0x46, 0x52, 0xa5, + 0xb7, 0x2c, 0x42, 0xa6, 0x9f, 0x94, 0x55, 0x0d, 0x87, 0x27, 0x9b, 0x76, 0x25, 0x54, 0x0c, 0xd7, 0x6f, 0x96, 0x17, + 0x50, 0x85, 0xc5, 0x0c, 0xc5, 0x1c, 0x9b, 0xca, 0xd9, 0x78, 0x83, 0x79, 0x06, 0xe3, 0x3c, 0xa5, 0x31, 0x37, 0x54, + 0xa0, 0x5f, 0x2a, 0x47, 0x53, 0x67, 0x62, 0xce, 0x58, 0x9e, 0xfb, 0xb0, 0xe3, 0x73, 0xc7, 0x98, 0x5d, 0x78, 0xee, + 0xee, 0xa9, 0xc3, 0xf6, 0x59, 0x74, 0x19, 0xee, 0x6e, 0x61, 0xc9, 0x9e, 0x92, 0x49, 0x8c, 0x03, 0x58, 0xe7, 0xd1, + 0x95, 0xad, 0xf1, 0x52, 0x06, 0xbb, 0x3f, 0x41, 0x0c, 0xe0, 0x68, 0xc1, 0x60, 0x04, 0xec, 0x5a, 0x7e, 0xed, 0x6a, + 0xac, 0x74, 0xf3, 0x71, 0x60, 0x85, 0x17, 0x99, 0xc0, 0xe5, 0x23, 0x26, 0xd2, 0xe0, 0x7f, 0xde, 0xc7, 0xc9, 0x57, + 0x9b, 0x8e, 0x26, 0xb2, 0xd7, 0x42, 0xbe, 0xf0, 0xaf, 0xe1, 0x6e, 0x1e, 0x98, 0xf2, 0xc5, 0x1e, 0x4f, 0x11, 0x05, + 0x4d, 0x62, 0x6c, 0xf5, 0x8c, 0x8b, 0x3d, 0x73, 0xb5, 0xe1, 0x17, 0x22, 0x8a, 0xbb, 0xbb, 0xb8, 0x2c, 0x04, 0x2c, + 0x99, 0xf0, 0x53, 0xce, 0xd4, 0xc8, 0x94, 0x3d, 0xc4, 0xb7, 0xcb, 0xf0, 0xe1, 0x71, 0x23, 0xf6, 0xc9, 0x5d, 0x11, + 0x9e, 0x48, 0xaa, 0xb0, 0xcf, 0xfc, 0xef, 0x90, 0x31, 0x27, 0xa2, 0xe8, 0xb1, 0x4c, 0x37, 0x06, 0xcf, 0x7f, 0x56, + 0xf1, 0x64, 0x55, 0x00, 0xb6, 0x46, 0x05, 0xe9, 0x97, 0x80, 0x73, 0x0a, 0x40, 0x3d, 0x0c, 0x63, 0x20, 0xc5, 0x9c, + 0x42, 0xe0, 0xe8, 0x92, 0x76, 0xc0, 0xf0, 0xb3, 0x59, 0xd0, 0x63, 0xf1, 0x5b, 0xba, 0xdc, 0x6d, 0xce, 0xcf, 0xd6, + 0x28, 0x6f, 0x2a, 0x77, 0xd5, 0xb0, 0xca, 0x4c, 0x4d, 0x61, 0xb2, 0xd8, 0x9b, 0xb9, 0x0e, 0xdf, 0xf9, 0x63, 0x5f, + 0xbb, 0xc4, 0xc1, 0xc8, 0x8d, 0xcc, 0x15, 0x2e, 0x3c, 0x98, 0x76, 0xf2, 0x0a, 0x88, 0x9a, 0xad, 0x04, 0x57, 0x02, + 0xad, 0x07, 0xa7, 0x0e, 0x77, 0x0a, 0x58, 0x41, 0x20, 0xf3, 0xfa, 0xab, 0x3e, 0x39, 0x90, 0xd1, 0xe6, 0x0a, 0x19, + 0xf4, 0xdc, 0xea, 0x05, 0x5a, 0xe5, 0x7d, 0xab, 0xfb, 0x39, 0x79, 0x63, 0xde, 0x75, 0x1f, 0x81, 0xc9, 0xf7, 0x8c, + 0xc4, 0x86, 0x2c, 0xaf, 0x85, 0xc2, 0x24, 0x01, 0x3d, 0x0e, 0xaa, 0x0a, 0x89, 0xd4, 0xa1, 0x6c, 0xd4, 0x0c, 0x15, + 0xc2, 0xf4, 0xfa, 0x07, 0x80, 0x80, 0xa3, 0x94, 0x42, 0x79, 0x22, 0xaa, 0x32, 0x02, 0x08, 0x8c, 0x0d, 0xd0, 0xb0, + 0x0c, 0x4c, 0x61, 0x9b, 0x51, 0xb4, 0xe5, 0x74, 0xe9, 0x6e, 0xbc, 0x2f, 0x47, 0xe6, 0xbc, 0x1b, 0x3c, 0x4b, 0xd0, + 0x6e, 0xec, 0xeb, 0x38, 0x86, 0x7e, 0x2a, 0xfa, 0x47, 0xb0, 0x83, 0x73, 0x58, 0x82, 0x82, 0x53, 0x42, 0x9f, 0x33, + 0xff, 0x83, 0xaf, 0xc4, 0xeb, 0x9e, 0xb6, 0xb8, 0xb7, 0x63, 0xc7, 0xcc, 0xca, 0x8f, 0x4d, 0x96, 0x5c, 0xcb, 0x90, + 0x44, 0x79, 0xcd, 0xa5, 0x63, 0xd0, 0x94, 0xc8, 0xcd, 0x07, 0x81, 0xa4, 0xbd, 0x41, 0xe5, 0x47, 0x9b, 0xd3, 0xfe, + 0x48, 0x08, 0xb1, 0x5a, 0x6a, 0xe6, 0xe2, 0x4b, 0x8a, 0x05, 0x50, 0x70, 0x1f, 0xc0, 0xf9, 0x3b, 0x71, 0xfd, 0xbb, + 0xe8, 0xc0, 0xa1, 0x8f, 0x00, 0x06, 0xe0, 0xad, 0x54, 0x5b, 0x79, 0x13, 0x50, 0x5a, 0x01, 0x90, 0x6b, 0x53, 0x19, + 0xe0, 0x0d, 0xf9, 0x0b, 0x1e, 0xd9, 0x97, 0x29, 0x50, 0x8c, 0xe2, 0xc6, 0xbb, 0x4c, 0xe5, 0xe5, 0xdd, 0x31, 0xe7, + 0x82, 0xdd, 0xdc, 0x30, 0xaf, 0xda, 0x44, 0x99, 0xb4, 0x5d, 0x0c, 0x62, 0x9c, 0x12, 0xa4, 0x28, 0x01, 0xd2, 0xf7, + 0x0f, 0x66, 0x21, 0x7a, 0xfe, 0xee, 0xd1, 0x5d, 0x65, 0xae, 0xc3, 0x30, 0x9a, 0xec, 0x1d, 0x51, 0x0b, 0x72, 0xba, + 0x3a, 0x26, 0xdf, 0x27, 0x07, 0xe1, 0x5f, 0x12, 0xf5, 0x33, 0x55, 0x82, 0xa6, 0xfa, 0xa6, 0x8a, 0x38, 0xa8, 0xf1, + 0x09, 0x88, 0xda, 0x32, 0xa9, 0x09, 0x73, 0x25, 0xea, 0x93, 0xeb, 0x44, 0x60, 0x5a, 0xfd, 0xb3, 0x1f, 0x5f, 0xfd, + 0xc0, 0x60, 0x7b, 0xbf, 0x77, 0xf1, 0x75, 0xa6, 0x0f, 0xe7, 0xef, 0xd8, 0xbd, 0xfb, 0x3c, 0xb8, 0x71, 0x5c, 0x5d, + 0xd6, 0x23, 0x49, 0x23, 0x33, 0xc0, 0x7b, 0xca, 0xa0, 0x61, 0x2f, 0xca, 0xe6, 0xcc, 0x35, 0xbb, 0xcf, 0xba, 0xca, + 0x5d, 0x40, 0x3f, 0x22, 0x69, 0x35, 0xa1, 0xb8, 0x00, 0x6e, 0xe7, 0x31, 0xcd, 0x0a, 0xff, 0x83, 0x42, 0xcd, 0x04, + 0x7b, 0x19, 0x19, 0xa5, 0x2f, 0x23, 0x98, 0xaf, 0x16, 0x41, 0xb6, 0xac, 0x42, 0xbb, 0x8f, 0x1a, 0x6c, 0xd6, 0xae, + 0xcd, 0xb3, 0x7b, 0xcb, 0x01, 0x39, 0x13, 0x44, 0xe3, 0x45, 0xad, 0xe4, 0x59, 0x4f, 0xf5, 0xaa, 0xe7, 0x88, 0x9b, + 0xae, 0xdc, 0xab, 0x47, 0xa6, 0xf9, 0x78, 0x85, 0x77, 0x3f, 0xea, 0x22, 0xda, 0x95, 0xb5, 0x00, 0x56, 0x16, 0x43, + 0xf2, 0x3d, 0xeb, 0xef, 0x99, 0x74, 0x09, 0x16, 0x90, 0xfa, 0xc4, 0xeb, 0xda, 0x75, 0xd0, 0x91, 0x93, 0xba, 0xfd, + 0x28, 0x0a, 0x66, 0xa5, 0x45, 0x26, 0xe5, 0x89, 0xa4, 0x2b, 0xc9, 0x47, 0xae, 0x62, 0x66, 0x98, 0xe6, 0xd2, 0x19, + 0xf6, 0xa4, 0xbf, 0xaa, 0xda, 0xab, 0xed, 0x39, 0x04, 0xc0, 0x35, 0x4f, 0x21, 0x56, 0xef, 0x56, 0xd1, 0xc2, 0x9d, + 0xf1, 0x6e, 0xff, 0xa2, 0xb5, 0xe3, 0x61, 0xec, 0xd6, 0x30, 0x0e, 0x32, 0x67, 0x05, 0xf3, 0x9b, 0x1c, 0xd3, 0x70, + 0xcd, 0x68, 0xa3, 0x0b, 0x3e, 0xc5, 0xbe, 0x5c, 0xbd, 0x8f, 0xba, 0x87, 0x0a, 0x91, 0xde, 0x83, 0x67, 0x84, 0xaf, + 0x37, 0x7f, 0x6d, 0xd4, 0x6b, 0xdf, 0xd1, 0x58, 0xde, 0x58, 0x3a, 0x82, 0xc6, 0x3f, 0x26, 0x38, 0xa3, 0x30, 0xdf, + 0xc0, 0x9b, 0x26, 0x93, 0xb5, 0x09, 0xc7, 0x8e, 0xdc, 0x26, 0xc5, 0xa6, 0xa5, 0x2a, 0xdf, 0x25, 0xb0, 0x92, 0x5b, + 0xa6, 0xfd, 0xe2, 0x9e, 0x52, 0x8f, 0x0d, 0xc1, 0x42, 0xc5, 0x82, 0x00, 0x35, 0xe6, 0xaa, 0xbd, 0x99, 0x48, 0x06, + 0xa7, 0xb4, 0xfd, 0x6c, 0xfb, 0x0c, 0xb3, 0xb3, 0x26, 0x12, 0x82, 0xb3, 0x42, 0xcb, 0xa6, 0x9b, 0xb4, 0x91, 0x00, + 0x8d, 0x54, 0xa3, 0xd0, 0x64, 0x82, 0xc6, 0xce, 0x7b, 0x41, 0xee, 0x86, 0x0e, 0xa9, 0xeb, 0x0c, 0x4a, 0xf0, 0x85, + 0x23, 0x31, 0x4b, 0x77, 0x41, 0x69, 0x0d, 0xdd, 0x63, 0xa8, 0x5e, 0x6f, 0xbf, 0x0b, 0x9e, 0x91, 0xf1, 0xa6, 0x11, + 0x68, 0x8e, 0x41, 0x20, 0xdf, 0x04, 0x63, 0x02, 0x0e, 0xbc, 0xf3, 0xf2, 0xfb, 0x48, 0x44, 0xca, 0x0f, 0xb0, 0x01, + 0xbd, 0xcc, 0x37, 0x5e, 0x04, 0x2b, 0x52, 0xa5, 0x19, 0x16, 0x66, 0x8f, 0xc1, 0xbc, 0xed, 0x8e, 0x7e, 0x32, 0xfc, + 0xcc, 0x04, 0x2f, 0xf9, 0x93, 0xe7, 0xf4, 0xce, 0x10, 0x1e, 0x62, 0xf0, 0xc1, 0x58, 0xf5, 0xc0, 0xe3, 0x94, 0xc6, + 0x0d, 0x01, 0x2e, 0x9f, 0xfd, 0xc8, 0x7c, 0x10, 0xbb, 0x11, 0x80, 0x67, 0x17, 0x57, 0x19, 0x78, 0x9b, 0xa9, 0xab, + 0x93, 0x96, 0x4e, 0xee, 0x17, 0x62, 0xc4, 0x0c, 0x52, 0x31, 0x9f, 0xde, 0xc8, 0x28, 0x0d, 0xe2, 0xa2, 0x64, 0xc6, + 0x25, 0xc5, 0xae, 0x39, 0x6f, 0xe8, 0x96, 0x9f, 0x33, 0x45, 0xed, 0x9d, 0x1d, 0xeb, 0x78, 0xff, 0x8f, 0xf3, 0x27, + 0x5d, 0x30, 0xba, 0xbc, 0xb5, 0xae, 0x66, 0xdb, 0xf3, 0x4d, 0x4d, 0xa9, 0x81, 0xe1, 0x37, 0x26, 0xfc, 0xd1, 0xc7, + 0x4b, 0x4d, 0x41, 0x0d, 0x8d, 0x5d, 0x64, 0x53, 0x8a, 0xac, 0xf0, 0xc6, 0x31, 0x65, 0xbe, 0x04, 0x52, 0x2c, 0xc6, + 0x4f, 0x3e, 0x37, 0x1a, 0x5c, 0x33, 0x52, 0x1c, 0x0e, 0x09, 0xea, 0x45, 0x91, 0xb7, 0x9f, 0x3a, 0xf9, 0x1c, 0x57, + 0x6f, 0xe6, 0x37, 0x43, 0x60, 0xa6, 0xdb, 0x56, 0x5a, 0xac, 0x9b, 0xb6, 0xe2, 0xab, 0xb5, 0x4a, 0xe3, 0x29, 0x5a, + 0xe3, 0xbb, 0x1a, 0x87, 0xe9, 0x95, 0xea, 0xab, 0xe1, 0xd7, 0xbd, 0x8d, 0xcd, 0x8c, 0x66, 0x2e, 0x74, 0x90, 0x38, + 0x24, 0xbd, 0x54, 0x4d, 0xab, 0xa8, 0xc6, 0x9e, 0x1e, 0xab, 0x1a, 0x94, 0x88, 0x77, 0xe8, 0x24, 0xd6, 0x30, 0x5b, + 0x8f, 0x72, 0xfa, 0x61, 0xb5, 0x85, 0x5a, 0xb1, 0x33, 0x31, 0xa9, 0xa9, 0x37, 0x96, 0xe5, 0xd5, 0x56, 0xd5, 0xe7, + 0x74, 0xa0, 0xc3, 0x9f, 0xc3, 0x1d, 0x84, 0xef, 0x6e, 0x05, 0x9a, 0x10, 0x64, 0x2e, 0xb6, 0xf2, 0x75, 0x88, 0xef, + 0xd2, 0x1e, 0x8f, 0x25, 0x56, 0x2b, 0x12, 0x8d, 0x6f, 0xaa, 0x2a, 0x0c, 0xc8, 0x65, 0x46, 0x15, 0x4b, 0x7b, 0x65, + 0x54, 0xc8, 0x8a, 0xec, 0x8d, 0x04, 0x69, 0x55, 0xf0, 0x42, 0x90, 0xd2, 0x2c, 0x9a, 0x98, 0xcc, 0x5f, 0x0d, 0xef, + 0x92, 0x92, 0xcc, 0x26, 0xf8, 0xd3, 0x26, 0xce, 0x66, 0x14, 0x70, 0xb7, 0x52, 0x37, 0xb8, 0xd8, 0x9a, 0x71, 0x55, + 0xae, 0x20, 0xb7, 0x05, 0x07, 0x21, 0xab, 0xa2, 0x68, 0x61, 0x9f, 0xdf, 0xf9, 0xa7, 0x48, 0x53, 0x00, 0x40, 0xe4, + 0x35, 0xa1, 0x29, 0xbb, 0x69, 0x41, 0x66, 0xe9, 0x60, 0x19, 0xc0, 0x07, 0x2c, 0x85, 0xf2, 0xd0, 0x79, 0x1e, 0xd7, + 0xdd, 0xcb, 0x4c, 0x2d, 0x2a, 0x2d, 0x1b, 0x74, 0x4f, 0x07, 0x87, 0x5c, 0xaf, 0x74, 0xfa, 0xef, 0x8f, 0xd4, 0x56, + 0x5e, 0xa0, 0x0e, 0x2a, 0x7c, 0xf7, 0x51, 0xb5, 0x10, 0xe3, 0x50, 0xab, 0xff, 0xad, 0x28, 0xd1, 0x39, 0x05, 0x4f, + 0xa2, 0xd7, 0x3f, 0x56, 0xac, 0xd3, 0x2b, 0x26, 0x91, 0xf8, 0x72, 0x22, 0xa8, 0xdb, 0x66, 0x57, 0x5d, 0x72, 0xda, + 0x5c, 0x65, 0xf6, 0x9f, 0x0e, 0x78, 0xf6, 0xad, 0x77, 0x7e, 0xa7, 0x17, 0x77, 0x90, 0x44, 0xa1, 0xd0, 0xf3, 0x21, + 0x3e, 0xa0, 0xa3, 0xb2, 0xfa, 0x13, 0x91, 0x14, 0xab, 0x96, 0xcb, 0xd0, 0x80, 0x22, 0xc5, 0x4d, 0x19, 0xd5, 0x78, + 0xd0, 0xeb, 0x19, 0xbb, 0x04, 0x69, 0x74, 0xb3, 0x57, 0x2a, 0xc1, 0x49, 0x2b, 0xad, 0x3e, 0x2f, 0x41, 0x90, 0x6d, + 0xbc, 0x93, 0x5c, 0xa5, 0x58, 0x61, 0xf6, 0x58, 0x96, 0x95, 0x51, 0xd7, 0x50, 0x8c, 0xce, 0xb2, 0x8a, 0x5a, 0xe7, + 0xda, 0x6a, 0xa7, 0x08, 0xc5, 0x3a, 0x16, 0x00, 0x9b, 0x16, 0x4e, 0x07, 0x69, 0x61, 0x0b, 0x7a, 0x3a, 0xa9, 0xc6, + 0x25, 0xcd, 0x8e, 0xb1, 0xc8, 0xbb, 0x85, 0x41, 0xd0, 0x44, 0x5f, 0x77, 0x70, 0x53, 0x1e, 0x2b, 0x8a, 0x3a, 0xb8, + 0xde, 0xb1, 0x0c, 0xaf, 0x0e, 0xcd, 0x78, 0x81, 0xae, 0xa4, 0xec, 0x08, 0x95, 0x2b, 0x61, 0x27, 0x6d, 0x4a, 0x07, + 0x6d, 0x15, 0x7e, 0x68, 0x9e, 0x94, 0x8e, 0x05, 0xaf, 0x58, 0xa1, 0xc4, 0x35, 0xdd, 0x79, 0x0f, 0xeb, 0xa5, 0x9b, + 0x18, 0x23, 0xe4, 0x2b, 0xe2, 0xaf, 0x91, 0x22, 0xcc, 0x0d, 0x64, 0x0d, 0x2c, 0x64, 0x34, 0xc5, 0x24, 0x4c, 0x90, + 0xb1, 0xc7, 0x98, 0x78, 0xd1, 0x2d, 0x2e, 0xfd, 0x19, 0xb4, 0x41, 0x9b, 0x4d, 0x2b, 0xe9, 0x3e, 0x70, 0x85, 0xf6, + 0x7b, 0x3c, 0xe9, 0x90, 0x8f, 0x2d, 0x44, 0xcf, 0x14, 0x5c, 0xbe, 0x2e, 0xa1, 0x51, 0x7f, 0x00, 0x65, 0xed, 0x58, + 0x8a, 0xcd, 0x9a, 0x84, 0x1d, 0x98, 0x4e, 0x94, 0xf6, 0x7d, 0xf0, 0xa9, 0xc7, 0x5f, 0xbe, 0xde, 0x23, 0xf8, 0x0c, + 0x65, 0x4f, 0x14, 0x9b, 0x29, 0x86, 0x8a, 0xa6, 0xa6, 0xa0, 0x99, 0x0f, 0xce, 0xc2, 0x6d, 0xf1, 0x7a, 0x42, 0x2f, + 0x57, 0xbb, 0x75, 0x4f, 0xe5, 0xe3, 0x8a, 0x34, 0x40, 0xab, 0xcf, 0x1a, 0x95, 0x0f, 0xe6, 0xc9, 0x3d, 0xaf, 0x74, + 0xcf, 0x0f, 0xe8, 0xa1, 0xd1, 0xee, 0xe2, 0x4d, 0xd7, 0x32, 0x3e, 0xb4, 0x9b, 0xac, 0xcf, 0x60, 0x11, 0x7a, 0xa8, + 0xa1, 0x14, 0xcd, 0xe1, 0x26, 0xab, 0xd9, 0xf0, 0xee, 0x8d, 0x66, 0x6d, 0x29, 0x25, 0x7f, 0x6f, 0xe7, 0xc5, 0x36, + 0x9b, 0x2a, 0x9c, 0xde, 0x0f, 0xa4, 0x77, 0x09, 0x14, 0xcd, 0x91, 0xef, 0x4e, 0xa7, 0xa9, 0x7c, 0xd0, 0x4f, 0x80, + 0xa1, 0x82, 0xef, 0x1e, 0x69, 0x74, 0x67, 0x4d, 0x71, 0xbe, 0x82, 0x4b, 0x0f, 0xa3, 0xc6, 0xb6, 0x4b, 0x4f, 0x04, + 0xe1, 0xd4, 0xe2, 0x1e, 0xe9, 0x25, 0x45, 0x4b, 0x1d, 0x29, 0xe1, 0x9f, 0x22, 0x9c, 0x53, 0x8d, 0x1d, 0xed, 0x6c, + 0x1a, 0x6f, 0xa8, 0x20, 0x3d, 0x6a, 0x72, 0x4d, 0xfb, 0x12, 0x0a, 0xe0, 0xbf, 0x9d, 0xba, 0x12, 0xe1, 0x32, 0x21, + 0x37, 0xab, 0x8a, 0x4a, 0x83, 0x32, 0x00, 0x28, 0xbf, 0x5d, 0xcb, 0xe8, 0x1a, 0x3f, 0x5a, 0xa8, 0xcb, 0x12, 0x73, + 0xa0, 0x83, 0x16, 0x67, 0x77, 0x03, 0x2d, 0x92, 0x6d, 0x73, 0xea, 0xae, 0x51, 0xb5, 0xc5, 0x93, 0xc0, 0x4b, 0x44, + 0x63, 0x39, 0xeb, 0xe7, 0xf0, 0x6d, 0xda, 0x5e, 0x0f, 0xce, 0x3a, 0x40, 0xb7, 0xb0, 0xa0, 0xda, 0x9a, 0xb5, 0xfc, + 0x5e, 0x81, 0x0f, 0xf8, 0x51, 0x6c, 0xec, 0x3c, 0x76, 0x42, 0xcd, 0xc9, 0x0e, 0xbd, 0xb9, 0x49, 0x39, 0x27, 0xca, + 0x9c, 0x4e, 0x62, 0x1c, 0x85, 0x26, 0xea, 0x8c, 0x30, 0xf9, 0xd4, 0x6b, 0x32, 0x03, 0x1e, 0x7d, 0xe3, 0x22, 0x61, + 0x1f, 0x9c, 0x53, 0xc9, 0x96, 0xb5, 0x66, 0x93, 0x9b, 0x9f, 0x49, 0xb1, 0xc9, 0x98, 0x60, 0xbd, 0xa0, 0xf7, 0x37, + 0x44, 0x42, 0x18, 0x37, 0x84, 0x62, 0x68, 0x6a, 0x52, 0xe3, 0x69, 0x73, 0xa5, 0x64, 0x46, 0x97, 0x54, 0xbf, 0xac, + 0x2e, 0x28, 0x24, 0x5a, 0x51, 0xf0, 0xcb, 0x16, 0x8e, 0xbd, 0x46, 0x10, 0x7b, 0x06, 0xa0, 0x0f, 0x1d, 0xa0, 0x89, + 0x91, 0xdc, 0x6a, 0x6a, 0x4f, 0xb6, 0x04, 0x5e, 0x79, 0x19, 0xe2, 0x0d, 0xfb, 0x4d, 0x50, 0x09, 0x3c, 0xc7, 0xbe, + 0x59, 0x0e, 0x79, 0x0e, 0x97, 0xd5, 0x5d, 0x7d, 0x8a, 0xe8, 0xdc, 0xf9, 0xc2, 0xc3, 0x14, 0xbd, 0x43, 0xb5, 0x8f, + 0xd0, 0xd5, 0x2b, 0x79, 0x15, 0x73, 0x90, 0x83, 0x45, 0xdd, 0x98, 0x6c, 0x62, 0x57, 0xa7, 0x9e, 0x6b, 0x40, 0xb7, + 0xc7, 0x6e, 0x06, 0x62, 0x0f, 0x23, 0x26, 0xeb, 0x78, 0x4a, 0x6f, 0x11, 0x31, 0xda, 0x62, 0x22, 0xa9, 0x99, 0x34, + 0x6b, 0x52, 0x03, 0x39, 0x2d, 0xc2, 0x1c, 0xd4, 0x4f, 0x74, 0x8e, 0x3d, 0x12, 0xba, 0xb3, 0x6c, 0xbb, 0x46, 0x25, + 0x93, 0xdc, 0x61, 0xae, 0x50, 0x49, 0x08, 0xa9, 0x28, 0xbb, 0x92, 0x29, 0x30, 0xa5, 0x31, 0xe1, 0x1a, 0x57, 0x83, + 0x22, 0x43, 0x97, 0x0a, 0x6a, 0x6f, 0x9d, 0xa4, 0xd4, 0x39, 0x67, 0x0e, 0xf0, 0x29, 0x94, 0x3f, 0xab, 0x9a, 0x87, + 0x4d, 0xad, 0x40, 0xc5, 0xbd, 0x7a, 0xb9, 0x58, 0xf0, 0x66, 0xfe, 0x04, 0x85, 0x41, 0xa1, 0xaf, 0xa8, 0x29, 0xcf, + 0xe5, 0xa1, 0xac, 0x44, 0xdf, 0x8c, 0xbf, 0xa7, 0xf2, 0x8a, 0xc0, 0x65, 0xbe, 0x42, 0x24, 0xa6, 0xdf, 0x78, 0xa8, + 0x3f, 0x2d, 0x0b, 0x9b, 0x2a, 0x62, 0xfa, 0xf7, 0x93, 0xbf, 0x36, 0x90, 0xe0, 0x87, 0x9e, 0xd8, 0x2f, 0x5a, 0x08, + 0x3a, 0x16, 0x19, 0x54, 0x98, 0x23, 0x72, 0xa2, 0x00, 0x4e, 0xb2, 0x47, 0xd7, 0xcb, 0x4b, 0x6a, 0xe8, 0x08, 0x6e, + 0x81, 0x9c, 0xb0, 0xa2, 0x6a, 0x24, 0xcf, 0xfe, 0xde, 0x76, 0x4b, 0xaa, 0x53, 0x0e, 0x03, 0x36, 0x7f, 0xed, 0x69, + 0x19, 0xba, 0x7b, 0x1b, 0x04, 0xb0, 0x05, 0xbd, 0x1e, 0x87, 0x1f, 0x01, 0x34, 0x82, 0xc9, 0x0c, 0x19, 0xcb, 0xa7, + 0x86, 0xea, 0x55, 0x5f, 0x9e, 0x1c, 0x85, 0xb8, 0x75, 0x8a, 0x8c, 0x28, 0x5b, 0x41, 0x2e, 0x63, 0xcb, 0x6e, 0xbf, + 0x95, 0xfe, 0xc0, 0x0a, 0xaa, 0x6b, 0x15, 0x82, 0x1c, 0x8c, 0x49, 0x78, 0x23, 0x51, 0x61, 0xcd, 0xdf, 0x74, 0x06, + 0x78, 0xcb, 0x53, 0x38, 0x84, 0x7b, 0xbb, 0x03, 0x6a, 0x7d, 0x0d, 0x41, 0xa1, 0xa9, 0x1c, 0x38, 0x9b, 0xa2, 0x35, + 0x47, 0x1c, 0x9c, 0xcf, 0x61, 0xc3, 0xf2, 0x1e, 0x77, 0x33, 0xc4, 0x32, 0x88, 0x14, 0xd1, 0xe1, 0x90, 0xa5, 0xc5, + 0xa2, 0xc6, 0x16, 0xab, 0x90, 0xf6, 0x29, 0xce, 0x08, 0x37, 0x31, 0xa5, 0x38, 0xd1, 0x87, 0x97, 0x60, 0x78, 0x06, + 0xce, 0x30, 0x6b, 0xd7, 0x1e, 0x88, 0xdb, 0x1b, 0x3a, 0x5f, 0x7e, 0xc9, 0x8e, 0x32, 0x7f, 0xae, 0x3a, 0x03, 0x96, + 0xcf, 0x7e, 0xde, 0x13, 0xa0, 0xa7, 0x9f, 0x38, 0x6c, 0xab, 0x9f, 0x4a, 0xcf, 0x46, 0x6a, 0xac, 0xee, 0x99, 0x4e, + 0xce, 0xdc, 0xd6, 0x1b, 0x1e, 0xe8, 0xcc, 0x82, 0x84, 0x0f, 0x5e, 0x63, 0x1f, 0xb4, 0x91, 0x4a, 0xd2, 0x57, 0x3c, + 0x51, 0xde, 0x5b, 0xe0, 0x57, 0x0f, 0x64, 0xe5, 0x91, 0x0b, 0x12, 0xf4, 0x12, 0xda, 0xf3, 0x79, 0x74, 0xc6, 0xc1, + 0xb0, 0x37, 0xca, 0x27, 0xc6, 0x4b, 0x4c, 0xfa, 0xe6, 0xdb, 0x61, 0x88, 0x48, 0xdf, 0x47, 0x54, 0xe0, 0x84, 0x2c, + 0xa9, 0xf2, 0xf0, 0x46, 0xc2, 0x21, 0x9e, 0x4a, 0x74, 0xa6, 0x4e, 0xcd, 0x59, 0xb5, 0x58, 0x6c, 0x59, 0x79, 0xf5, + 0xda, 0x51, 0xe4, 0x77, 0x6c, 0xd5, 0x92, 0x76, 0xb2, 0xaf, 0x94, 0xa1, 0x55, 0x89, 0x33, 0x18, 0x19, 0x5d, 0xb0, + 0xd5, 0x8b, 0x24, 0x1f, 0xb2, 0x26, 0x2a, 0x1a, 0xd6, 0x95, 0xc9, 0xcc, 0x28, 0xb9, 0x95, 0xf5, 0x45, 0xda, 0xb1, + 0x5d, 0x78, 0xad, 0x42, 0x25, 0xe9, 0xa0, 0x9e, 0x93, 0xe4, 0xfc, 0x40, 0xda, 0x3a, 0xca, 0xdf, 0xb6, 0xaa, 0x93, + 0xeb, 0xa1, 0xa7, 0xec, 0x2a, 0x1d, 0x08, 0xf3, 0x55, 0xb2, 0x06, 0x81, 0x81, 0x7c, 0x77, 0x60, 0x3b, 0x06, 0x9d, + 0xbe, 0xdd, 0x1e, 0x99, 0x74, 0x3c, 0x05, 0x3a, 0x65, 0x14, 0x30, 0x4d, 0x14, 0x46, 0x57, 0x6b, 0xcc, 0xfe, 0xee, + 0x78, 0xe9, 0xbb, 0x82, 0x03, 0x55, 0xdc, 0xe9, 0x41, 0xf8, 0xe1, 0xde, 0xd5, 0xc6, 0xe0, 0x87, 0x53, 0x47, 0x4f, + 0xcc, 0xcc, 0x52, 0xcb, 0xbf, 0xaf, 0x77, 0x87, 0xdf, 0xc7, 0x29, 0xc4, 0xf0, 0x81, 0x5e, 0xc7, 0xee, 0x8b, 0xfa, + 0x57, 0xf1, 0x32, 0x97, 0xf8, 0xc2, 0x0f, 0x5d, 0xd1, 0xec, 0xa6, 0xde, 0x95, 0x35, 0xcc, 0xa1, 0x6f, 0xc5, 0xda, + 0x48, 0xe5, 0x16, 0x58, 0xf7, 0x66, 0xb9, 0x13, 0x07, 0x0c, 0x38, 0xd7, 0x73, 0xc4, 0xc4, 0x67, 0xa6, 0x5a, 0xcf, + 0x24, 0x9d, 0x9a, 0xe9, 0x48, 0x86, 0x51, 0x0b, 0x58, 0x89, 0x89, 0x50, 0xdc, 0xe1, 0x01, 0x51, 0xd9, 0x21, 0x8f, + 0x7e, 0x2c, 0xf4, 0x6d, 0x4a, 0x28, 0x1b, 0xbe, 0x82, 0x97, 0x27, 0x97, 0x3f, 0x18, 0xd9, 0x78, 0xdc, 0xfc, 0xb1, + 0xd2, 0x8b, 0x97, 0x33, 0x4f, 0xde, 0xa0, 0x88, 0xd5, 0x79, 0x82, 0xe5, 0x01, 0x12, 0x9b, 0x33, 0x37, 0xd0, 0x49, + 0x30, 0xf5, 0x46, 0x37, 0xbf, 0x14, 0xc1, 0xad, 0xb6, 0x0d, 0x4e, 0xf9, 0x99, 0xe9, 0x1e, 0x85, 0xe2, 0x27, 0x3f, + 0xc3, 0x92, 0x59, 0x36, 0x4e, 0xc0, 0xc9, 0xb2, 0x2b, 0x0f, 0x3e, 0x6d, 0x7d, 0xf1, 0x61, 0x7d, 0x61, 0xfd, 0x79, + 0x8a, 0xb1, 0xfe, 0xfc, 0x92, 0xde, 0xdc, 0x3b, 0xac, 0x83, 0xd8, 0x7a, 0x74, 0x83, 0xfe, 0x29, 0x35, 0xec, 0xfc, + 0xb1, 0x33, 0x84, 0x0a, 0x11, 0x6a, 0xbc, 0x41, 0x58, 0x70, 0xee, 0x8e, 0x94, 0xac, 0x9a, 0x31, 0x4e, 0x2b, 0x49, + 0x6b, 0xc0, 0xbe, 0xd2, 0xa4, 0xb4, 0xca, 0xed, 0x6a, 0x45, 0xcc, 0x2e, 0x4d, 0xed, 0x50, 0x60, 0xd1, 0x77, 0x52, + 0x92, 0x24, 0xe7, 0xa5, 0x67, 0xf7, 0x48, 0x6d, 0x47, 0x40, 0xe5, 0xea, 0xbe, 0x40, 0x30, 0x6e, 0x6f, 0x77, 0x07, + 0xaa, 0xa6, 0xbe, 0x83, 0x41, 0x3d, 0xf2, 0x52, 0xff, 0x1f, 0x6c, 0x44, 0x6f, 0x5e, 0xfa, 0x0e, 0x50, 0x36, 0xa2, + 0xd9, 0xbb, 0x20, 0xd7, 0x01, 0xf2, 0x8e, 0x19, 0x4e, 0x55, 0xe5, 0x30, 0xca, 0xe8, 0xaf, 0x3e, 0x4b, 0xe1, 0xd2, + 0x7b, 0x87, 0x79, 0xb2, 0x49, 0x07, 0x9e, 0x05, 0x5b, 0xba, 0xf7, 0x12, 0x49, 0x60, 0xd9, 0x21, 0x21, 0x57, 0x69, + 0xdf, 0xa0, 0xcb, 0xaa, 0x53, 0x6d, 0x52, 0xf4, 0xd3, 0x8e, 0x1b, 0x3c, 0x62, 0x5a, 0x70, 0x8b, 0x74, 0x84, 0x5a, + 0x25, 0x84, 0x31, 0xe3, 0x29, 0x92, 0xd5, 0x80, 0xf0, 0x5a, 0x4d, 0x3c, 0x25, 0x8d, 0x28, 0x0f, 0x0c, 0x13, 0xdb, + 0x7b, 0xb1, 0xf3, 0x63, 0x17, 0xc4, 0x98, 0xdd, 0xba, 0xfb, 0x55, 0xf1, 0xa1, 0x3f, 0xc3, 0xb3, 0xb6, 0x3b, 0x99, + 0x4b, 0x48, 0x90, 0x38, 0xf4, 0x2f, 0x54, 0xfc, 0x5a, 0x23, 0x3c, 0x01, 0x0d, 0x56, 0x09, 0x86, 0xa9, 0x05, 0xbe, + 0x9d, 0xde, 0x67, 0xbd, 0x5e, 0x3d, 0x61, 0xe2, 0xe8, 0x16, 0xe0, 0xda, 0x23, 0x15, 0x97, 0xd3, 0x87, 0x03, 0xbb, + 0xaf, 0x96, 0x1a, 0xe8, 0xaa, 0x64, 0xb2, 0xf8, 0x4b, 0x6f, 0x9c, 0xff, 0x4d, 0x43, 0xd1, 0x81, 0x25, 0xfa, 0x39, + 0xbd, 0x16, 0x3b, 0xf7, 0xc9, 0x27, 0x12, 0x30, 0x1a, 0xc3, 0x93, 0x9c, 0x1e, 0x58, 0x15, 0x6d, 0x26, 0xdc, 0x9f, + 0x17, 0xa7, 0x09, 0x34, 0x76, 0xa0, 0x17, 0x37, 0x99, 0x6f, 0xe3, 0xdd, 0x95, 0xad, 0xee, 0xfd, 0x61, 0x59, 0xd5, + 0xbc, 0x4d, 0xea, 0xa8, 0xbb, 0x48, 0x10, 0xe2, 0x52, 0x47, 0xa8, 0x49, 0x57, 0x6c, 0xad, 0x39, 0x73, 0x71, 0x9a, + 0xc7, 0x56, 0x7e, 0x96, 0x6d, 0x79, 0x20, 0xeb, 0x85, 0x58, 0xd5, 0x6c, 0xd4, 0x04, 0x29, 0x43, 0x5d, 0x88, 0xee, + 0x32, 0xa2, 0x82, 0x16, 0xab, 0x5f, 0xd7, 0xb1, 0x32, 0xdd, 0xa7, 0xe9, 0x68, 0x42, 0xe0, 0xf7, 0x71, 0x00, 0x46, + 0xb4, 0xfe, 0x15, 0x83, 0xa6, 0xd6, 0x28, 0x39, 0x8d, 0x4e, 0x84, 0x88, 0x43, 0xdb, 0x34, 0x06, 0xbc, 0x74, 0xf9, + 0x99, 0x1c, 0x62, 0xaa, 0x05, 0xc6, 0x1b, 0x18, 0xaf, 0x87, 0xb6, 0x98, 0x72, 0xe7, 0xe9, 0x55, 0x65, 0x94, 0xf1, + 0xc9, 0xfd, 0xcc, 0xeb, 0x9e, 0x7d, 0x40, 0xc9, 0x00, 0x84, 0x0a, 0xaf, 0x55, 0x18, 0x32, 0x58, 0x25, 0xf8, 0xf3, + 0x16, 0x63, 0xf0, 0x73, 0xdb, 0x9c, 0x87, 0xf9, 0x10, 0x2b, 0xec, 0x30, 0x0b, 0xf4, 0xe9, 0x79, 0x01, 0xd3, 0x03, + 0xd9, 0xd9, 0xb3, 0xd2, 0x96, 0x51, 0x5a, 0x22, 0x60, 0x57, 0xb8, 0x4e, 0x01, 0x2c, 0x2a, 0x81, 0xc7, 0x41, 0x94, + 0x40, 0x1b, 0xfb, 0x81, 0x68, 0xca, 0x12, 0x52, 0x00, 0xab, 0xd5, 0x9f, 0x01, 0xec, 0xa0, 0x24, 0xdb, 0xce, 0xae, + 0x09, 0x91, 0xd9, 0x7a, 0xa4, 0xbd, 0xea, 0x30, 0x2d, 0xfa, 0xe7, 0xc4, 0x59, 0x9a, 0x18, 0xfb, 0x28, 0x89, 0x8f, + 0x1a, 0x37, 0x30, 0x25, 0x12, 0x74, 0x23, 0x0f, 0xc0, 0xcd, 0x2d, 0xf7, 0xe4, 0xbc, 0xd3, 0x9b, 0x03, 0x18, 0xe4, + 0xf4, 0x4c, 0x7f, 0xed, 0xc0, 0x82, 0xfb, 0xcf, 0x25, 0xea, 0xe8, 0x69, 0x09, 0xc9, 0x04, 0x2e, 0x7e, 0x34, 0xee, + 0x99, 0x06, 0x02, 0x62, 0xcf, 0x85, 0x51, 0x0b, 0x18, 0xfc, 0xd4, 0x11, 0xaf, 0x69, 0xcf, 0x92, 0x0e, 0xa3, 0xd2, + 0xf7, 0x44, 0x3a, 0xd5, 0xb6, 0x47, 0xa6, 0x7b, 0x9d, 0xc6, 0xd4, 0x87, 0xc8, 0x07, 0x53, 0xb8, 0x52, 0x04, 0xc0, + 0xf9, 0x1f, 0x0f, 0x58, 0xee, 0xdf, 0xc4, 0x8a, 0xae, 0x90, 0xe7, 0xda, 0x98, 0x72, 0x58, 0x25, 0x03, 0x5b, 0xc6, + 0x65, 0x47, 0x4c, 0x98, 0x8d, 0x99, 0x56, 0x46, 0x6f, 0xe6, 0xc8, 0x19, 0xa4, 0xb2, 0x31, 0x5c, 0x44, 0x39, 0xb5, + 0x25, 0x20, 0x21, 0xaa, 0x02, 0x18, 0x3c, 0xbd, 0x85, 0x8d, 0x95, 0xd4, 0xa6, 0x74, 0x26, 0x18, 0xaa, 0x21, 0xca, + 0x57, 0x39, 0xb1, 0x9d, 0xca, 0xd1, 0x7c, 0xa0, 0xc9, 0xea, 0x6f, 0x9f, 0x16, 0xee, 0x63, 0x87, 0xe7, 0xbd, 0x4e, + 0x9e, 0x99, 0x14, 0xe8, 0xd3, 0x96, 0xb9, 0x73, 0xe9, 0xc4, 0x65, 0xf1, 0xd2, 0x74, 0xb1, 0x5f, 0x9c, 0xf5, 0x4d, + 0x0a, 0xb2, 0x6c, 0xed, 0xd7, 0x83, 0xb9, 0xc3, 0xb6, 0x98, 0x3a, 0x8f, 0x45, 0x80, 0xcb, 0x12, 0x51, 0xba, 0x96, + 0x09, 0x81, 0x4d, 0xcb, 0xbc, 0x30, 0x9b, 0xd1, 0xe6, 0x0a, 0x2f, 0xcf, 0x47, 0x35, 0xad, 0xc9, 0x15, 0x7a, 0xdd, + 0xa7, 0xd3, 0x77, 0x42, 0xfe, 0x79, 0x39, 0xea, 0x9e, 0x59, 0xca, 0x40, 0x54, 0xed, 0x94, 0x0e, 0x3c, 0xe9, 0xc0, + 0xce, 0xb6, 0xa6, 0x6f, 0xdf, 0x2f, 0xfe, 0xd1, 0x3e, 0x99, 0x3a, 0xb7, 0xa1, 0xb5, 0xe8, 0xf8, 0xfd, 0x1e, 0x51, + 0xbb, 0x64, 0x85, 0x23, 0x04, 0x2a, 0xcf, 0x18, 0xd8, 0xa4, 0xde, 0xcc, 0x59, 0xdc, 0xf8, 0x48, 0xb5, 0xe8, 0x16, + 0x0e, 0xf0, 0x58, 0xdf, 0xfd, 0xea, 0x4f, 0xaa, 0x6e, 0xcf, 0xff, 0xda, 0x9e, 0x84, 0x76, 0x93, 0x7f, 0x6d, 0x6c, + 0xfd, 0xc7, 0xce, 0xc8, 0x72, 0x05, 0xd1, 0xf3, 0xda, 0x7a, 0xb5, 0x24, 0x1c, 0xbc, 0xc3, 0xdc, 0x3d, 0x01, 0xdf, + 0x8e, 0xbf, 0x35, 0xcd, 0x80, 0xa4, 0x65, 0x16, 0xad, 0x8d, 0x5c, 0xe3, 0x25, 0x0c, 0x28, 0x43, 0xd9, 0x15, 0xce, + 0x54, 0x1b, 0x43, 0xf3, 0xeb, 0x1d, 0xb7, 0x6f, 0x1b, 0x6c, 0xdc, 0xa2, 0x5b, 0x45, 0x37, 0x71, 0x61, 0x75, 0x78, + 0xa6, 0xb8, 0xca, 0xe6, 0x54, 0xb9, 0xca, 0xf8, 0xbb, 0x60, 0xa8, 0x0e, 0x03, 0x6e, 0x33, 0xec, 0xc2, 0x75, 0xe7, + 0xa1, 0x0b, 0x15, 0x45, 0x30, 0x2c, 0x48, 0xa5, 0xe5, 0x04, 0x8a, 0x32, 0xb6, 0xc5, 0x86, 0xa2, 0x04, 0xff, 0xfa, + 0xf7, 0x8c, 0x97, 0x51, 0xc0, 0x37, 0x36, 0xb4, 0xc8, 0x6c, 0x85, 0xa6, 0x29, 0xe1, 0x67, 0x98, 0x92, 0xe3, 0xca, + 0x27, 0x8d, 0xdb, 0xe1, 0x7f, 0x15, 0x4d, 0x04, 0x0a, 0xa8, 0x13, 0x0b, 0x09, 0x99, 0x69, 0x33, 0x45, 0xaf, 0x30, + 0x84, 0xae, 0x48, 0xca, 0x07, 0x97, 0x39, 0xf8, 0xae, 0xcd, 0x3d, 0x77, 0x2d, 0x6f, 0x1e, 0x04, 0x5b, 0x92, 0x4d, + 0x90, 0x96, 0x14, 0x32, 0xc9, 0xc2, 0xda, 0x91, 0x81, 0xeb, 0x6b, 0x7b, 0xa1, 0xa0, 0x24, 0x59, 0x10, 0x89, 0xe7, + 0x6b, 0x6d, 0x91, 0x3a, 0x16, 0x7f, 0x61, 0xba, 0x9f, 0xe2, 0xd3, 0x5e, 0xf8, 0x81, 0xfa, 0x3a, 0xdc, 0x75, 0x5f, + 0x45, 0xb3, 0x21, 0x6e, 0xd5, 0x8f, 0x19, 0x15, 0xd7, 0x23, 0xa5, 0xfd, 0x58, 0xfe, 0xf9, 0xfb, 0x0d, 0x8b, 0xc9, + 0x00, 0xc7, 0xc3, 0x9b, 0x9b, 0xa9, 0xc3, 0x8c, 0xbd, 0x86, 0x54, 0x6d, 0xac, 0xbe, 0x01, 0xb9, 0x45, 0x0e, 0xd5, + 0xae, 0x89, 0xa2, 0x0e, 0xb8, 0x99, 0x08, 0x0e, 0xc4, 0x7d, 0x64, 0xce, 0xa8, 0xd7, 0x9e, 0x54, 0x73, 0xba, 0x94, + 0x69, 0xd1, 0x52, 0xcd, 0x18, 0x66, 0xca, 0x74, 0x54, 0x31, 0xa0, 0xae, 0xdc, 0xd9, 0x95, 0xe7, 0x7b, 0x7e, 0x2a, + 0xcb, 0x8b, 0x0d, 0x9b, 0xa1, 0x4b, 0x2d, 0xcc, 0x51, 0xbe, 0xea, 0xf3, 0xb8, 0xbf, 0xf1, 0x8a, 0xf7, 0x7a, 0x87, + 0xa1, 0x43, 0x2d, 0x3d, 0x66, 0x6f, 0xd6, 0xfa, 0x7a, 0x3e, 0xe3, 0x20, 0x2d, 0x6a, 0xa7, 0x82, 0x71, 0xce, 0xa2, + 0x80, 0x05, 0x78, 0x85, 0xdd, 0x11, 0x8c, 0x8f, 0x67, 0xf1, 0x88, 0x7e, 0x64, 0xec, 0xe6, 0x6d, 0xf3, 0x1a, 0x10, + 0xa4, 0xca, 0x8e, 0x7b, 0x4b, 0x17, 0x2a, 0x16, 0xd1, 0xd2, 0x3b, 0xd3, 0x29, 0x94, 0x8f, 0x15, 0x00, 0xa4, 0xee, + 0xf4, 0x66, 0x30, 0x1e, 0xca, 0xcd, 0x01, 0xc2, 0x8d, 0x9c, 0x19, 0x37, 0x26, 0x0a, 0x47, 0x37, 0x06, 0x84, 0x08, + 0x71, 0x35, 0xf0, 0x95, 0x97, 0xb4, 0x4f, 0x54, 0x2c, 0x0d, 0xf1, 0xbd, 0xf2, 0xe0, 0x3e, 0xdc, 0xda, 0x6f, 0x2f, + 0x4e, 0x55, 0xc9, 0xc1, 0x22, 0x14, 0x1b, 0xc5, 0x7b, 0xe3, 0x77, 0x2f, 0xec, 0x7e, 0x62, 0x31, 0xd7, 0x12, 0x01, + 0xe5, 0x96, 0xef, 0x97, 0x56, 0x4d, 0x9c, 0x3f, 0xfd, 0x87, 0x0f, 0xe5, 0x12, 0x72, 0xe4, 0xab, 0x58, 0x76, 0x40, + 0x66, 0xbe, 0xa2, 0x9f, 0x45, 0x59, 0x4d, 0xe1, 0x53, 0xde, 0xc2, 0xdd, 0x75, 0xc7, 0xb5, 0x0e, 0x00, 0x59, 0x38, + 0x44, 0xb3, 0xd1, 0xd3, 0x2e, 0x89, 0xd0, 0x36, 0xda, 0xf8, 0x96, 0x47, 0x9a, 0x51, 0x51, 0x51, 0x34, 0x2e, 0xcd, + 0x46, 0x3d, 0xa4, 0x20, 0x9e, 0xa0, 0x17, 0xdf, 0x86, 0x80, 0x7c, 0x74, 0xed, 0x9d, 0x12, 0xb3, 0x74, 0x6b, 0xe1, + 0xfb, 0xfe, 0x46, 0xa2, 0x9e, 0x02, 0xfd, 0x28, 0xc2, 0x64, 0x41, 0x33, 0xf2, 0x95, 0xff, 0x2e, 0x00, 0x6f, 0x62, + 0xd1, 0x3f, 0xb1, 0xf0, 0x67, 0xb2, 0xee, 0xe1, 0xab, 0x9d, 0xb8, 0xde, 0x2e, 0x9a, 0xd3, 0x41, 0xfb, 0x10, 0x94, + 0xaa, 0xbe, 0xc7, 0xe1, 0x4d, 0xa1, 0xf5, 0xcb, 0x8e, 0x5d, 0xb0, 0x15, 0x05, 0x03, 0x9e, 0x75, 0x4a, 0x34, 0x31, + 0x5d, 0x97, 0x15, 0x01, 0xc6, 0x12, 0x27, 0x90, 0x5b, 0x9d, 0xb3, 0x74, 0x94, 0x9b, 0xb3, 0x9b, 0x3c, 0x6b, 0x27, + 0xd7, 0xd1, 0xbe, 0x9d, 0xcc, 0x4a, 0x59, 0xe5, 0xba, 0x21, 0x34, 0x7b, 0xf9, 0xe4, 0x2c, 0x94, 0x8b, 0x42, 0x1d, + 0x15, 0x37, 0xb4, 0x6a, 0x4d, 0x56, 0xc0, 0xca, 0xc9, 0x45, 0xab, 0xf2, 0xe6, 0xe9, 0xd0, 0xb8, 0xc9, 0x36, 0xfd, + 0x58, 0xd1, 0x76, 0x07, 0x0a, 0xaf, 0x14, 0xd6, 0xf6, 0x1c, 0x6c, 0xc3, 0x89, 0x06, 0xc7, 0x7d, 0xbb, 0x6d, 0x40, + 0x54, 0x20, 0xbb, 0x98, 0x50, 0x62, 0x6b, 0xf9, 0x2f, 0x0e, 0x28, 0xbe, 0xbd, 0x9a, 0x5e, 0x47, 0x31, 0x32, 0x7c, + 0x53, 0xff, 0x1e, 0x08, 0xf0, 0x2f, 0x7c, 0x60, 0x6a, 0x67, 0x54, 0x45, 0x28, 0x6b, 0x37, 0xab, 0xd4, 0x1f, 0x15, + 0xb9, 0xa5, 0x49, 0x6a, 0xb6, 0x79, 0x7a, 0x82, 0x29, 0x2b, 0xda, 0x9b, 0xc3, 0x06, 0x7b, 0x74, 0x6d, 0x44, 0xd8, + 0x60, 0x42, 0x1c, 0xff, 0x83, 0x9d, 0x00, 0x9d, 0x48, 0xf1, 0x82, 0xcb, 0x71, 0x65, 0x29, 0x9a, 0x12, 0xcd, 0x4b, + 0x51, 0xfb, 0x14, 0xe6, 0x3d, 0xaf, 0x02, 0xae, 0x9b, 0x93, 0xa3, 0x6e, 0x87, 0x3e, 0x71, 0x8a, 0x38, 0xcf, 0x81, + 0x08, 0x7b, 0x2a, 0xa7, 0x5d, 0xbd, 0x59, 0x5b, 0x86, 0xb8, 0x6e, 0x56, 0x88, 0x90, 0x7c, 0x18, 0xa7, 0x62, 0x88, + 0xb1, 0x7d, 0x23, 0x73, 0xde, 0xe3, 0xab, 0x85, 0x28, 0xac, 0x70, 0x11, 0x8f, 0x91, 0xa5, 0x3f, 0x79, 0x05, 0xd1, + 0x9d, 0x51, 0x93, 0x60, 0xd6, 0xea, 0x64, 0xa4, 0x38, 0x53, 0xff, 0x02, 0x02, 0x43, 0x6d, 0x90, 0xd1, 0x41, 0x4d, + 0x95, 0xf9, 0xd1, 0xd3, 0x88, 0x1b, 0x1f, 0x38, 0xaa, 0x46, 0x9b, 0x90, 0x71, 0xa6, 0xd8, 0x16, 0xbf, 0x35, 0x77, + 0x4b, 0x00, 0x66, 0x77, 0x1d, 0xa8, 0x15, 0x71, 0x24, 0xa1, 0x55, 0x7f, 0xae, 0xfe, 0x7a, 0x89, 0x44, 0x71, 0x4e, + 0xe8, 0x63, 0xa0, 0xc5, 0x47, 0x98, 0xae, 0xe6, 0x62, 0x1b, 0x87, 0x1d, 0x47, 0xa2, 0x8a, 0xd3, 0xbb, 0xe8, 0x72, + 0x3f, 0x93, 0x60, 0xf7, 0x13, 0x22, 0x9e, 0xef, 0xad, 0x0b, 0x35, 0x0b, 0x47, 0x1f, 0xb6, 0x3f, 0x09, 0x12, 0x32, + 0xd4, 0x5f, 0x0b, 0x37, 0x47, 0xed, 0xd5, 0x4b, 0x2d, 0xa3, 0x0d, 0x3f, 0x2d, 0xa2, 0xc5, 0xa9, 0x00, 0x94, 0x0c, + 0xa3, 0xf3, 0xcf, 0x5f, 0xec, 0xb0, 0x9f, 0x83, 0x73, 0x0c, 0x26, 0x0f, 0x78, 0xc0, 0xcd, 0xdd, 0x4f, 0xe8, 0xda, + 0x52, 0xce, 0x08, 0x67, 0xac, 0x0d, 0x09, 0x56, 0xc6, 0xb9, 0x66, 0x6b, 0xe3, 0x45, 0xc3, 0x09, 0xe1, 0x08, 0x3a, + 0x68, 0x8c, 0x7a, 0x9e, 0x33, 0x9a, 0xa7, 0x58, 0xfd, 0xca, 0x19, 0x6f, 0xf9, 0x81, 0x9d, 0xcb, 0x15, 0x04, 0x55, + 0x17, 0x55, 0x62, 0x4d, 0x27, 0xda, 0x81, 0xcb, 0xfd, 0x25, 0x7b, 0xca, 0x22, 0x7f, 0xbb, 0xc0, 0xa4, 0xa6, 0x42, + 0x21, 0x67, 0x85, 0x1b, 0xda, 0x15, 0x82, 0xd5, 0x6a, 0xdc, 0xf0, 0x3b, 0x6f, 0x59, 0x96, 0xaa, 0x4e, 0xed, 0x46, + 0x35, 0x34, 0xc3, 0x84, 0x29, 0x6e, 0x68, 0x19, 0xdf, 0x91, 0x94, 0xd8, 0x59, 0xd7, 0x06, 0x73, 0xfa, 0x1f, 0x52, + 0x9c, 0x0a, 0x2d, 0x44, 0xa9, 0xfa, 0x1c, 0x34, 0x3a, 0x31, 0x4d, 0xd5, 0x79, 0x23, 0x77, 0x26, 0x07, 0x83, 0x9a, + 0xb2, 0xb1, 0x53, 0xf3, 0x8e, 0xe9, 0xc8, 0x0c, 0xfe, 0x8e, 0xfc, 0xe4, 0x21, 0xab, 0x65, 0x72, 0x99, 0xe8, 0x67, + 0xbd, 0xf1, 0xaa, 0x00, 0x14, 0xd6, 0x31, 0xa8, 0xd0, 0x3c, 0x6b, 0x2a, 0x6b, 0x55, 0x65, 0xba, 0x09, 0x5f, 0x75, + 0x4b, 0x03, 0x05, 0xcf, 0x94, 0xa7, 0x10, 0x51, 0x53, 0xe2, 0xa4, 0xd5, 0x43, 0xa8, 0x01, 0xe5, 0xe8, 0xbc, 0x88, + 0xb9, 0x8e, 0x95, 0x2a, 0x1b, 0xff, 0x72, 0xef, 0xa3, 0x75, 0xeb, 0x20, 0xef, 0x67, 0x36, 0xea, 0xfd, 0x22, 0x55, + 0x4e, 0xa1, 0xcf, 0x8f, 0x34, 0x8d, 0x35, 0x09, 0xb6, 0x71, 0x32, 0x90, 0x0a, 0x3a, 0xa9, 0xc0, 0xff, 0xcb, 0x94, + 0x33, 0x56, 0x4c, 0x2a, 0x40, 0xc5, 0x62, 0xed, 0x9a, 0x7f, 0xdd, 0x87, 0x05, 0x93, 0xa0, 0xee, 0x1f, 0x80, 0x5e, + 0x0b, 0xb9, 0x90, 0x5f, 0xad, 0xb7, 0xa1, 0xec, 0x7c, 0xc3, 0x49, 0xeb, 0x73, 0xf5, 0x93, 0x23, 0x17, 0xb1, 0x9a, + 0xa2, 0xcd, 0xb4, 0x9e, 0x3a, 0x4b, 0x98, 0xf0, 0xd3, 0x72, 0x6e, 0xba, 0x41, 0xe6, 0x1a, 0x47, 0xca, 0xcb, 0xec, + 0xe3, 0xa8, 0x55, 0x66, 0xe9, 0xd8, 0x86, 0x2a, 0x6a, 0x73, 0x3c, 0x73, 0xc6, 0xf8, 0x62, 0x4f, 0x9a, 0x6a, 0x57, + 0x56, 0xf2, 0xcd, 0xb5, 0x98, 0x37, 0x87, 0x32, 0x45, 0x3d, 0x32, 0xad, 0x93, 0x8b, 0x13, 0x2a, 0xb3, 0x02, 0xc0, + 0xdb, 0x90, 0x6c, 0x84, 0xc0, 0x2e, 0xd8, 0x8f, 0xb5, 0x78, 0xe9, 0x2e, 0xa5, 0x51, 0x82, 0x97, 0x10, 0x2b, 0xfa, + 0x87, 0xd2, 0x42, 0x83, 0x54, 0x57, 0x94, 0x2c, 0x0d, 0xf5, 0xdf, 0x4a, 0x0f, 0x27, 0x39, 0x6a, 0x70, 0x0e, 0xb5, + 0xc7, 0xc2, 0xa8, 0xf7, 0x63, 0xd2, 0xa3, 0x3c, 0xd6, 0x4b, 0x81, 0x4d, 0x96, 0xc0, 0xca, 0x09, 0x76, 0x17, 0x20, + 0xe5, 0xb5, 0x87, 0xbe, 0x56, 0x64, 0xc2, 0xe3, 0xf3, 0xe4, 0xd6, 0xa5, 0x65, 0xe0, 0x15, 0xf4, 0xae, 0xbd, 0xa1, + 0xd2, 0x02, 0x77, 0xbf, 0xc8, 0x95, 0xff, 0xe8, 0x50, 0x24, 0x1d, 0xf1, 0x54, 0x12, 0x78, 0x2b, 0xa9, 0xc1, 0xc0, + 0xad, 0x65, 0xc3, 0xb5, 0x69, 0x1b, 0x7d, 0xa8, 0x8f, 0xe3, 0x3b, 0x46, 0xab, 0xe0, 0x3f, 0x9f, 0x7e, 0xc3, 0x38, + 0xb4, 0xe0, 0xd9, 0xaa, 0x54, 0x59, 0xd7, 0x53, 0x47, 0xb2, 0xfd, 0xd5, 0xce, 0x5b, 0x04, 0xb3, 0x70, 0x25, 0x0b, + 0x4d, 0x02, 0x3a, 0xb6, 0x49, 0x16, 0xb8, 0x4d, 0x81, 0x99, 0x47, 0x3f, 0x45, 0x6f, 0x23, 0xd5, 0x38, 0x52, 0xb5, + 0x68, 0x12, 0xe3, 0x70, 0x41, 0x34, 0x79, 0x73, 0xb7, 0x2a, 0x02, 0x19, 0x1c, 0xc0, 0x2d, 0xbf, 0x33, 0xce, 0x3d, + 0xf5, 0x91, 0xd6, 0x3a, 0xf0, 0xbb, 0x6e, 0xb2, 0x5d, 0xda, 0xa1, 0x51, 0x4b, 0xf4, 0xb6, 0x1d, 0x35, 0x1a, 0x64, + 0xd8, 0x23, 0xc5, 0xd8, 0xbd, 0x8f, 0xcf, 0xea, 0x31, 0x83, 0x2c, 0xd1, 0x01, 0x5f, 0x77, 0x0d, 0x54, 0x2c, 0x32, + 0x90, 0xbb, 0x0b, 0x21, 0x51, 0x87, 0x6d, 0xb4, 0x00, 0x50, 0xfa, 0x04, 0xab, 0xef, 0xc4, 0x2d, 0xf5, 0x06, 0x94, + 0xf9, 0x3e, 0xa4, 0x94, 0x42, 0x7d, 0x51, 0x91, 0x29, 0x67, 0x8b, 0xc5, 0x8c, 0x22, 0x8c, 0x3c, 0x11, 0x19, 0x6a, + 0x13, 0xc4, 0x08, 0x9c, 0xde, 0x32, 0xaa, 0x7e, 0x6c, 0x2f, 0x03, 0x2d, 0xed, 0xb5, 0x88, 0xa9, 0xca, 0x19, 0xcf, + 0x01, 0x94, 0x80, 0xc1, 0x55, 0x00, 0x67, 0xa6, 0x7a, 0x57, 0xc6, 0x9c, 0x58, 0x66, 0x05, 0x0f, 0x94, 0x4e, 0x2e, + 0xc6, 0xd7, 0xc0, 0xf9, 0x8f, 0xad, 0x89, 0xab, 0xf8, 0xeb, 0xd7, 0x2d, 0x9f, 0x67, 0xff, 0x97, 0x89, 0x76, 0x75, + 0x06, 0xac, 0x9c, 0xb0, 0xcf, 0x13, 0xc4, 0xeb, 0x06, 0xdb, 0xcb, 0xd6, 0x62, 0xc5, 0x93, 0x5e, 0x7f, 0x6c, 0xb5, + 0xa4, 0x2c, 0xab, 0xe4, 0x57, 0x1b, 0x08, 0xa4, 0xf1, 0x9d, 0x49, 0x64, 0x90, 0x0a, 0x92, 0x62, 0xba, 0x11, 0xfc, + 0xee, 0x5b, 0xef, 0x61, 0x47, 0x1a, 0x78, 0xd9, 0xea, 0xc2, 0xf0, 0x99, 0xba, 0x5d, 0xd3, 0x49, 0xce, 0xe0, 0xcc, + 0x9b, 0x09, 0x47, 0x5b, 0xef, 0xf2, 0xe5, 0x0a, 0x2d, 0xfa, 0x3c, 0xf4, 0x2b, 0xba, 0x4d, 0x5a, 0x96, 0xc7, 0x3d, + 0xcc, 0xa0, 0xfe, 0xaf, 0x62, 0xcd, 0x69, 0xf4, 0x55, 0x51, 0x5f, 0x7a, 0x41, 0x2b, 0xcd, 0x6d, 0xad, 0x2d, 0xe4, + 0x74, 0x6e, 0x91, 0x7b, 0x30, 0x34, 0xed, 0xfb, 0x8f, 0x2a, 0xc2, 0x92, 0x3d, 0xa5, 0xad, 0xf7, 0xc9, 0x45, 0x2f, + 0xd5, 0xb9, 0x11, 0xff, 0x96, 0x53, 0x79, 0xd3, 0x3a, 0x6a, 0x64, 0x27, 0xfe, 0x0f, 0xd6, 0x4d, 0x94, 0x71, 0xb9, + 0x4e, 0xee, 0xb4, 0x83, 0xe2, 0xa8, 0x4b, 0x8e, 0x87, 0x38, 0xd7, 0x8c, 0x46, 0x7a, 0x25, 0xcc, 0x33, 0xa7, 0x55, + 0x85, 0x1e, 0x8b, 0x06, 0xc9, 0x1a, 0x1a, 0x90, 0x04, 0xa8, 0xc9, 0x09, 0x71, 0xea, 0x4e, 0x70, 0x6b, 0x40, 0x72, + 0x72, 0x89, 0x90, 0x9c, 0x16, 0xde, 0xe5, 0xe7, 0x0d, 0x19, 0xa2, 0x9c, 0xd7, 0x37, 0xb1, 0x23, 0x2a, 0x3e, 0x8b, + 0x6e, 0xb9, 0x6f, 0x11, 0x1a, 0x6d, 0x1f, 0x34, 0x9a, 0x8e, 0x39, 0xb0, 0xcb, 0x9b, 0x35, 0x68, 0x39, 0x33, 0x08, + 0xf9, 0xe9, 0x19, 0x34, 0x61, 0xc0, 0x6c, 0x85, 0x10, 0x73, 0x94, 0x6c, 0x95, 0x9a, 0x34, 0x06, 0xf5, 0xc4, 0x4e, + 0x1c, 0xa8, 0xcf, 0xcf, 0xba, 0x59, 0x29, 0xd9, 0x9c, 0x9a, 0xda, 0xf4, 0x03, 0xd8, 0xe2, 0x89, 0x36, 0x1f, 0x28, + 0xc3, 0x20, 0x0d, 0x57, 0x25, 0xc2, 0xdf, 0xa8, 0xa8, 0xf3, 0x65, 0x3e, 0x6f, 0xd8, 0x46, 0xd0, 0x88, 0x21, 0x03, + 0xb3, 0x13, 0xac, 0x81, 0x20, 0x58, 0x16, 0x67, 0x72, 0x96, 0xd2, 0xd9, 0x38, 0x96, 0x58, 0x0b, 0x05, 0xb4, 0xbc, + 0x4d, 0xce, 0x1d, 0x04, 0x50, 0x46, 0xa2, 0xc4, 0xb2, 0x8d, 0x88, 0x3e, 0x30, 0x09, 0xde, 0x10, 0x2b, 0xf8, 0x05, + 0xdf, 0x50, 0x3a, 0xe9, 0x40, 0x6d, 0x92, 0x3b, 0x85, 0xaa, 0x0c, 0x0e, 0xc6, 0xe1, 0x15, 0xff, 0xed, 0xca, 0x0f, + 0x0e, 0x6d, 0x22, 0xee, 0x2a, 0xe0, 0x92, 0xd1, 0x73, 0x08, 0xea, 0xe4, 0xda, 0xb2, 0x89, 0xef, 0x34, 0xda, 0xde, + 0x55, 0xb5, 0x2b, 0x8e, 0xf8, 0xfc, 0x51, 0x60, 0x14, 0xa4, 0x5c, 0xce, 0xfe, 0x8d, 0x27, 0x69, 0xa2, 0x39, 0xb7, + 0xef, 0x1d, 0x2e, 0x16, 0x69, 0xa6, 0x3a, 0x35, 0xbd, 0xb9, 0x58, 0xb5, 0x0f, 0x46, 0xae, 0xb5, 0x67, 0x67, 0x1c, + 0x47, 0x20, 0x05, 0xe5, 0x03, 0xfe, 0x0b, 0xa9, 0x1a, 0xf2, 0xf9, 0xd0, 0xcf, 0x01, 0xd5, 0x4c, 0xf1, 0x69, 0xd5, + 0xd6, 0xbe, 0x49, 0xb5, 0xe0, 0x7f, 0x73, 0x58, 0xa4, 0x75, 0xfd, 0xfd, 0xf9, 0x8b, 0xde, 0x36, 0xd2, 0xf1, 0x23, + 0xb1, 0x92, 0xfa, 0x13, 0xa0, 0xac, 0xbd, 0x19, 0xb3, 0x76, 0x10, 0x4e, 0x63, 0x4a, 0x21, 0xfa, 0x4f, 0xe2, 0x53, + 0x4f, 0x65, 0xf0, 0x0d, 0xd4, 0x0f, 0xde, 0xc4, 0x68, 0xe9, 0x67, 0x6d, 0x6a, 0x21, 0xfc, 0x2d, 0xe6, 0x8b, 0x5a, + 0x3d, 0xe8, 0x38, 0x0f, 0xb9, 0x78, 0xc5, 0xea, 0x3f, 0x7d, 0xf1, 0xe5, 0x6c, 0x61, 0xb0, 0x96, 0xb5, 0x07, 0xff, + 0x8f, 0xf3, 0x00, 0x20, 0x5b, 0x61, 0x58, 0x4b, 0x34, 0xd3, 0x2f, 0xad, 0xa7, 0x00, 0xdf, 0x9d, 0xa7, 0x52, 0x6c, + 0x4d, 0x8b, 0x95, 0xa9, 0x67, 0x3a, 0xa8, 0x57, 0x6a, 0x6b, 0xdc, 0x37, 0xd6, 0x87, 0xd1, 0xd0, 0x77, 0x30, 0x57, + 0xbc, 0x7e, 0x8c, 0xe9, 0xee, 0x9f, 0x26, 0x26, 0x86, 0xfd, 0x4e, 0x75, 0x4a, 0x9a, 0xa5, 0xbe, 0x6d, 0xc8, 0x99, + 0xdd, 0x26, 0x70, 0xdf, 0x64, 0x8a, 0x90, 0xe3, 0x7d, 0x72, 0x94, 0xa2, 0xa6, 0x7d, 0x4b, 0x25, 0xab, 0x5b, 0x0f, + 0x69, 0xc4, 0x2c, 0x35, 0xd0, 0xfb, 0xe2, 0x55, 0x81, 0x81, 0x87, 0xea, 0xbc, 0x7e, 0x8b, 0x02, 0x9e, 0xc1, 0x47, + 0xcb, 0xac, 0xda, 0xba, 0x04, 0x8e, 0x51, 0xeb, 0xc0, 0xfd, 0xf2, 0xc0, 0x1f, 0x29, 0xfa, 0xe2, 0x6d, 0xe4, 0x60, + 0x83, 0xd7, 0x53, 0x83, 0x53, 0x1e, 0x9e, 0x8d, 0xf5, 0x31, 0xe3, 0xa7, 0x95, 0xa3, 0xb0, 0x67, 0x5c, 0x3c, 0x99, + 0x5d, 0x8c, 0xc3, 0xa6, 0xdb, 0x2a, 0x27, 0x4a, 0xa6, 0x4c, 0xd7, 0x64, 0x7e, 0xc6, 0x85, 0x9e, 0x37, 0x6b, 0xb5, + 0x84, 0xcd, 0x0f, 0xfe, 0x70, 0x53, 0x5c, 0x19, 0x27, 0xa3, 0xd0, 0xfe, 0x1f, 0xd9, 0x99, 0xa6, 0x77, 0xa1, 0x46, + 0xe0, 0x52, 0x70, 0xb5, 0x54, 0x96, 0x46, 0xda, 0xcf, 0xf6, 0xe9, 0xfb, 0x24, 0x5f, 0x41, 0x9e, 0xfe, 0x92, 0x15, + 0x1b, 0x73, 0x92, 0x64, 0xff, 0xa8, 0x14, 0x32, 0x87, 0xaa, 0x45, 0x3b, 0x46, 0x5b, 0xf9, 0x09, 0x41, 0x7d, 0xd9, + 0x21, 0xea, 0x00, 0xcc, 0xb6, 0x4a, 0x79, 0xbf, 0x18, 0x68, 0x46, 0x51, 0xb6, 0x1c, 0xf4, 0xb5, 0x61, 0x06, 0x07, + 0xaf, 0x1a, 0xd6, 0xef, 0xbd, 0xac, 0x55, 0x32, 0x52, 0x69, 0xb3, 0xcc, 0x51, 0x6a, 0xf2, 0x74, 0xbf, 0xd4, 0xb9, + 0xe8, 0x9a, 0x38, 0xf8, 0xd9, 0xda, 0xf7, 0x60, 0xd7, 0x4e, 0xcb, 0xae, 0x14, 0xe6, 0x06, 0xc7, 0x79, 0xcc, 0x71, + 0x65, 0x03, 0x11, 0x6b, 0x16, 0x5a, 0xde, 0x14, 0x2d, 0x52, 0x77, 0xea, 0xbb, 0xb3, 0xec, 0x26, 0x80, 0xad, 0x62, + 0xef, 0xa1, 0xe5, 0xdb, 0x67, 0xe9, 0x8d, 0x0e, 0x6c, 0x6b, 0xe3, 0x5e, 0xc7, 0x37, 0x16, 0x84, 0x9e, 0x2c, 0xaf, + 0xce, 0xa8, 0x8e, 0x3b, 0xa7, 0xf9, 0xfc, 0x50, 0x31, 0x96, 0x6e, 0x93, 0xe8, 0x9c, 0x8f, 0xe4, 0x09, 0xb2, 0x0c, + 0x15, 0xcb, 0x69, 0x60, 0x2d, 0x23, 0x68, 0xec, 0x24, 0x7d, 0xe5, 0x91, 0xac, 0xc6, 0x8a, 0xf9, 0x47, 0xa0, 0x76, + 0xae, 0xec, 0xb8, 0x6d, 0x86, 0xa4, 0x5a, 0xae, 0xb4, 0x46, 0x30, 0x0c, 0x8d, 0x7f, 0x2d, 0x44, 0xa2, 0xda, 0x4a, + 0x40, 0x02, 0x0e, 0x67, 0x29, 0xa8, 0xdd, 0x6d, 0x79, 0xf3, 0x6e, 0x94, 0x1e, 0x51, 0xa4, 0xa2, 0x56, 0x54, 0x4e, + 0xf1, 0x86, 0xb2, 0xf5, 0x4c, 0x34, 0x01, 0x13, 0x8d, 0x62, 0x23, 0x33, 0x28, 0x6f, 0xb7, 0x2a, 0xe4, 0x5e, 0xae, + 0xfb, 0xb7, 0x57, 0xef, 0x28, 0x0d, 0x9b, 0xbe, 0x12, 0x92, 0x06, 0xad, 0x50, 0x44, 0x7c, 0xc0, 0x8e, 0x31, 0x8e, + 0xae, 0xc9, 0xf4, 0x99, 0x3a, 0x30, 0x46, 0x75, 0x89, 0x94, 0x2f, 0xcd, 0x9f, 0xbd, 0xf1, 0xea, 0x25, 0xb0, 0xf5, + 0x3b, 0x5d, 0x6b, 0x4d, 0x66, 0xde, 0x96, 0x52, 0x2b, 0x91, 0x6e, 0x32, 0x22, 0x8d, 0xff, 0x4c, 0xb3, 0x6f, 0x26, + 0xf2, 0x87, 0x1d, 0xed, 0xc0, 0x40, 0x86, 0xf4, 0x66, 0xb3, 0x39, 0xa7, 0x6a, 0x16, 0x00, 0x0a, 0xff, 0xd5, 0xba, + 0x0f, 0x66, 0x6b, 0xa6, 0xa9, 0x88, 0xe0, 0xb3, 0x30, 0x34, 0x6f, 0xe1, 0x90, 0xd5, 0x69, 0x04, 0xe0, 0x20, 0x09, + 0x81, 0xcc, 0xd9, 0x5c, 0x6f, 0x08, 0xaa, 0xd8, 0xdb, 0xb0, 0x46, 0x9f, 0x42, 0xe8, 0x7f, 0xe4, 0xd3, 0xcf, 0xf9, + 0x5e, 0x45, 0x51, 0x0c, 0x5d, 0x1d, 0x0a, 0x87, 0xd6, 0xdf, 0x64, 0xd2, 0x78, 0x97, 0x2c, 0x14, 0x83, 0xfa, 0x8b, + 0xbd, 0x43, 0xcb, 0xdc, 0x74, 0x67, 0x03, 0x0b, 0x97, 0x0a, 0x06, 0x52, 0x2c, 0x42, 0x48, 0x73, 0x83, 0xb3, 0x7e, + 0xeb, 0xb1, 0x7c, 0xe9, 0x02, 0x4d, 0xdf, 0xca, 0xe3, 0x31, 0x3e, 0xfb, 0x76, 0xbc, 0xe3, 0x13, 0x66, 0x5a, 0x66, + 0x89, 0x4a, 0x0a, 0xe9, 0x93, 0xff, 0x0e, 0xa3, 0x96, 0xc7, 0x84, 0x05, 0xd3, 0xea, 0xee, 0xa9, 0x14, 0xc5, 0xce, + 0x73, 0x58, 0x53, 0x2f, 0xa0, 0x0e, 0x85, 0x9b, 0xea, 0x03, 0xbb, 0x12, 0x41, 0x6a, 0x53, 0x00, 0x30, 0xfe, 0x08, + 0x80, 0x88, 0x07, 0x99, 0x57, 0xaa, 0x25, 0x64, 0xb8, 0x59, 0x4e, 0xa4, 0xbb, 0x8b, 0x51, 0xe2, 0x9b, 0x23, 0x02, + 0xb4, 0xa5, 0x66, 0x18, 0x9e, 0xc9, 0x6f, 0x73, 0x79, 0x13, 0x2e, 0x81, 0xed, 0x1a, 0xc1, 0x1b, 0x21, 0x6d, 0xd6, + 0x7e, 0x38, 0x02, 0xaa, 0xb6, 0x01, 0x51, 0xfa, 0x4d, 0x79, 0x63, 0xde, 0x88, 0x14, 0xaa, 0xd5, 0xce, 0xee, 0x4d, + 0x5a, 0xa7, 0x0d, 0xab, 0xe1, 0x29, 0xdc, 0x54, 0xa9, 0x6d, 0x23, 0xd7, 0xf6, 0x7f, 0x92, 0x82, 0x9c, 0x4d, 0xdd, + 0xd5, 0x6d, 0xf7, 0xfb, 0xa7, 0x09, 0x38, 0xfc, 0x24, 0x31, 0xbe, 0xfb, 0xd5, 0x32, 0xfb, 0x3f, 0xb6, 0xf2, 0xa0, + 0x04, 0x0f, 0xa7, 0x20, 0x9f, 0x62, 0x0d, 0xd7, 0x90, 0x7a, 0xf2, 0xae, 0xaf, 0xbb, 0x80, 0xc0, 0xfa, 0x2d, 0xb9, + 0x13, 0xef, 0x32, 0x82, 0x53, 0x00, 0xdb, 0xd6, 0x11, 0x58, 0xeb, 0xe6, 0x3b, 0x90, 0x82, 0x18, 0xf9, 0x2d, 0x92, + 0xff, 0xb3, 0x32, 0x37, 0xfc, 0x48, 0x51, 0xdc, 0x9c, 0x4b, 0x17, 0xd1, 0x93, 0x55, 0xd8, 0x0e, 0x1b, 0x55, 0x80, + 0x23, 0xb0, 0xf0, 0x7e, 0x6e, 0x26, 0xff, 0x0c, 0xa1, 0x9d, 0xab, 0x33, 0xc5, 0xa1, 0x18, 0xd5, 0x4f, 0x75, 0x01, + 0xca, 0xc3, 0x64, 0xc4, 0xa6, 0x26, 0xb4, 0x18, 0x0b, 0x4b, 0x97, 0x24, 0x80, 0x40, 0x7b, 0xa8, 0x25, 0x32, 0x97, + 0x6b, 0x91, 0x5d, 0x32, 0xee, 0xd9, 0x56, 0x2c, 0x5d, 0xfb, 0x98, 0xd7, 0xd9, 0x33, 0x70, 0xe3, 0x3c, 0x06, 0x5f, + 0xdc, 0xd9, 0x52, 0x58, 0xe9, 0x19, 0xb2, 0x3a, 0x3b, 0x57, 0xe2, 0xb0, 0x4d, 0xb6, 0x1f, 0x15, 0xec, 0xee, 0xdb, + 0x5b, 0x22, 0x0b, 0xc4, 0xe0, 0x3f, 0xad, 0x35, 0x59, 0xeb, 0x6f, 0xe4, 0x00, 0xbe, 0x85, 0x95, 0x7c, 0x41, 0x33, + 0xe0, 0x72, 0x77, 0x73, 0x40, 0xea, 0x81, 0x4f, 0x26, 0xac, 0xaa, 0x72, 0xcd, 0xcd, 0x46, 0xa6, 0x09, 0x9a, 0x10, + 0xff, 0xbf, 0xb2, 0xd5, 0x10, 0x1b, 0x80, 0x27, 0x63, 0xdf, 0x7c, 0xd9, 0x85, 0xc1, 0x66, 0xa1, 0xc5, 0x16, 0xf6, + 0xe1, 0x2d, 0xa7, 0xe2, 0x75, 0x73, 0x03, 0x35, 0xfc, 0x20, 0x81, 0x95, 0xef, 0x12, 0xaa, 0xf9, 0x9e, 0x38, 0xf6, + 0xbd, 0x57, 0xbe, 0x7a, 0x4e, 0x8f, 0x40, 0xd3, 0xe8, 0xac, 0x99, 0xf4, 0xe4, 0x70, 0x6e, 0x0c, 0x55, 0x23, 0xaf, + 0x95, 0xb7, 0x07, 0x57, 0xab, 0xbf, 0x3e, 0x9b, 0xf3, 0x36, 0x3f, 0xa2, 0x1f, 0x5d, 0x63, 0x23, 0x66, 0x71, 0xc2, + 0x57, 0xd7, 0x47, 0x91, 0x50, 0x51, 0xc4, 0xc5, 0x87, 0x75, 0x9f, 0x36, 0xae, 0xb7, 0x8e, 0x6e, 0xf1, 0x2e, 0xc0, + 0x9c, 0x92, 0x54, 0x9d, 0x6d, 0x67, 0xe8, 0x0a, 0xbe, 0x97, 0xb5, 0xc5, 0xf1, 0xa5, 0xb5, 0x6e, 0xcb, 0xcb, 0xae, + 0xbc, 0x37, 0x46, 0x5d, 0xb4, 0x60, 0xd7, 0x77, 0x9c, 0xbc, 0xd5, 0xc8, 0xfd, 0xea, 0xa9, 0x2d, 0x96, 0x50, 0x40, + 0x1b, 0x5a, 0xbe, 0x20, 0x3b, 0xc6, 0x9e, 0x8d, 0x4e, 0xa5, 0xc9, 0x53, 0xf4, 0xba, 0xfb, 0xcc, 0x23, 0x1e, 0xd6, + 0x81, 0xae, 0x9c, 0x06, 0x1d, 0xff, 0xc2, 0x7f, 0x79, 0x59, 0xaa, 0xb7, 0x2a, 0xae, 0xbd, 0x12, 0x00, 0x93, 0x2a, + 0x9f, 0xf4, 0xf2, 0xf7, 0x41, 0x10, 0x19, 0xd9, 0x08, 0xf1, 0x4c, 0x54, 0x96, 0x00, 0x3a, 0xae, 0x72, 0xf1, 0xce, + 0x74, 0xd0, 0x2f, 0x67, 0x22, 0x11, 0x39, 0x03, 0x6d, 0x1b, 0x14, 0x0a, 0x91, 0x7a, 0xbb, 0x08, 0xe2, 0x1e, 0x45, + 0x4c, 0x34, 0xd7, 0x5d, 0xdf, 0xaf, 0xd1, 0x71, 0x34, 0x36, 0xa3, 0x76, 0xfb, 0x5b, 0xc1, 0x14, 0x48, 0x89, 0x83, + 0x81, 0xba, 0xa2, 0x22, 0x1e, 0xff, 0xf1, 0x40, 0xfb, 0x25, 0x35, 0x9c, 0xb2, 0xc3, 0x78, 0x15, 0x5f, 0x59, 0x55, + 0xb5, 0xe2, 0x97, 0x88, 0x99, 0x21, 0x88, 0x37, 0x1a, 0xe9, 0x95, 0xcd, 0x5e, 0xcd, 0x64, 0xa2, 0x38, 0x29, 0x2c, + 0x8f, 0x6b, 0xd7, 0x84, 0x75, 0x00, 0x6b, 0xf5, 0xd1, 0xa1, 0xa5, 0xf8, 0xfb, 0xec, 0x8f, 0x4b, 0x8e, 0x99, 0xe7, + 0xcf, 0xf0, 0xbf, 0xcd, 0x2e, 0x97, 0xfc, 0xd1, 0x3d, 0xc9, 0xf6, 0x3d, 0x76, 0x00, 0xcd, 0x32, 0xa5, 0x8e, 0x32, + 0x86, 0x00, 0xc0, 0x41, 0xe2, 0x7b, 0x8b, 0xdb, 0xff, 0xee, 0x18, 0x44, 0xce, 0xf2, 0xa6, 0xc5, 0x83, 0xff, 0x18, + 0x51, 0x5a, 0x1a, 0x6b, 0xe1, 0x08, 0x82, 0x71, 0x6d, 0xac, 0x1b, 0xc9, 0x3c, 0xd0, 0x75, 0x04, 0xb2, 0x96, 0x9c, + 0x60, 0xa2, 0x44, 0xee, 0x55, 0xcd, 0xeb, 0x10, 0x6a, 0x25, 0x96, 0xa9, 0xcd, 0x23, 0xea, 0xa8, 0xb1, 0xef, 0x40, + 0xf0, 0x32, 0x3b, 0x44, 0x6d, 0xfe, 0x63, 0x4b, 0x81, 0x5f, 0x4a, 0x79, 0x32, 0x70, 0x78, 0x23, 0x14, 0x15, 0x1f, + 0x05, 0x30, 0x9c, 0x11, 0xbc, 0xa8, 0xd5, 0x57, 0x8e, 0x63, 0xa0, 0x1f, 0x4a, 0x2a, 0x5e, 0xec, 0x3e, 0x6f, 0xbc, + 0x01, 0x77, 0xa1, 0xfc, 0x03, 0xe5, 0x3a, 0x52, 0x2d, 0x7b, 0xf9, 0xc8, 0x4e, 0x6d, 0xc7, 0xd9, 0x50, 0x15, 0x54, + 0x45, 0xef, 0xd0, 0x2f, 0x85, 0x70, 0x60, 0x79, 0xb2, 0xda, 0x1b, 0xee, 0x0c, 0x7c, 0x6c, 0xc4, 0x47, 0x7d, 0x25, + 0x7b, 0x43, 0xa2, 0x8c, 0x85, 0xe4, 0x38, 0x2a, 0x40, 0xf4, 0xe4, 0xd3, 0x75, 0x36, 0x0d, 0x7b, 0x75, 0xb6, 0x14, + 0x48, 0x23, 0x46, 0x3a, 0x97, 0x4a, 0x67, 0xf6, 0xf4, 0x48, 0x19, 0x3f, 0xef, 0xfc, 0x6a, 0xd9, 0xa0, 0xcc, 0x36, + 0xa4, 0xf2, 0xa7, 0xbc, 0x2f, 0x25, 0x65, 0xb2, 0xad, 0xd8, 0xf4, 0xc6, 0xe6, 0x14, 0xc0, 0x64, 0x05, 0x61, 0xee, + 0xbe, 0x41, 0x39, 0x18, 0x63, 0x5d, 0xa9, 0x22, 0xdf, 0xf8, 0x3c, 0x76, 0x7a, 0x7a, 0xc1, 0x33, 0x8a, 0x2c, 0xfa, + 0x53, 0x04, 0x36, 0xcb, 0x6b, 0x85, 0x09, 0xdf, 0xe7, 0xb8, 0x46, 0xbf, 0xd0, 0x14, 0x4d, 0x42, 0xf4, 0xe3, 0x8d, + 0x48, 0x35, 0x2b, 0xe0, 0xcd, 0xfb, 0xa6, 0x1b, 0xc1, 0xb3, 0x32, 0xda, 0x48, 0x24, 0xda, 0xba, 0x29, 0xf0, 0xef, + 0x11, 0x7d, 0x23, 0x66, 0xfa, 0x83, 0x34, 0x5f, 0xfd, 0x20, 0xcc, 0x37, 0xdb, 0x03, 0xaa, 0xda, 0x87, 0xdc, 0xf8, + 0xe4, 0x42, 0x01, 0x16, 0x10, 0x46, 0x2f, 0x95, 0x36, 0xd6, 0x04, 0xa5, 0x84, 0x4b, 0x51, 0x93, 0x51, 0x5e, 0x4f, + 0xf5, 0x09, 0xad, 0xeb, 0x25, 0x19, 0x60, 0x12, 0xba, 0xb1, 0x8d, 0xbe, 0x8d, 0xb9, 0x4d, 0x97, 0xfd, 0x87, 0x0a, + 0xed, 0x81, 0x2b, 0x1b, 0x2c, 0xe0, 0x73, 0xb5, 0xe7, 0xce, 0x45, 0x04, 0x5a, 0x83, 0xf8, 0x8f, 0xe3, 0x7a, 0xb1, + 0x77, 0x4b, 0x25, 0x25, 0x56, 0x59, 0x08, 0x19, 0x2a, 0x17, 0x76, 0x73, 0xc3, 0x3c, 0xeb, 0x71, 0xf0, 0x8c, 0x04, + 0x01, 0xc1, 0xa9, 0x82, 0x49, 0x5c, 0x4d, 0x69, 0x58, 0xd9, 0x73, 0x74, 0xc3, 0x69, 0xf9, 0x35, 0x53, 0x65, 0xbb, + 0x40, 0xa7, 0x6f, 0x5c, 0x31, 0x98, 0x9f, 0xd8, 0x17, 0x8e, 0x1e, 0x5a, 0x46, 0xd7, 0x67, 0x07, 0x46, 0x80, 0x1c, + 0x56, 0x96, 0x81, 0x84, 0x2d, 0x49, 0xab, 0x37, 0x79, 0x78, 0xcf, 0x14, 0x22, 0xc9, 0x02, 0x55, 0x8e, 0x5f, 0x60, + 0x6b, 0x69, 0x49, 0x39, 0x2b, 0xd1, 0x5a, 0x85, 0x32, 0x44, 0x6b, 0xbd, 0x6f, 0x57, 0x9d, 0xde, 0x7b, 0x5f, 0xd0, + 0x79, 0x69, 0x24, 0x87, 0x18, 0x02, 0x43, 0x2c, 0x8d, 0xef, 0x14, 0x36, 0x5a, 0x6f, 0x96, 0xd9, 0x7d, 0x35, 0xb6, + 0x5f, 0xc3, 0x75, 0x3d, 0xf1, 0xa6, 0xfc, 0xb6, 0xce, 0x1e, 0xe6, 0xbc, 0x72, 0xa2, 0x1b, 0xba, 0x86, 0xcd, 0xda, + 0x4e, 0x7f, 0x55, 0xdf, 0x32, 0x19, 0x16, 0x1f, 0x7b, 0x08, 0x21, 0x17, 0xaa, 0x54, 0x88, 0xf4, 0x76, 0x27, 0x90, + 0x2a, 0xf7, 0x94, 0x2b, 0x9d, 0xe3, 0x44, 0xd6, 0xb1, 0x9d, 0x1c, 0x2e, 0x4d, 0x2a, 0x88, 0x63, 0x7b, 0xf7, 0x9d, + 0x58, 0xf0, 0xc9, 0x17, 0xd2, 0x9c, 0xa7, 0xeb, 0x97, 0x7e, 0x78, 0x65, 0xac, 0x94, 0x9c, 0x6e, 0x66, 0x51, 0xd3, + 0xdd, 0x2c, 0xb2, 0xf3, 0xaf, 0x71, 0xeb, 0x92, 0xf0, 0x3a, 0x69, 0xff, 0x6a, 0x84, 0x97, 0x5c, 0xeb, 0x52, 0x44, + 0x53, 0x94, 0xba, 0x7f, 0x9d, 0xa0, 0x20, 0x12, 0xfc, 0xb9, 0x68, 0x18, 0x6b, 0x9f, 0x56, 0xcd, 0x47, 0x63, 0xc5, + 0xd6, 0xde, 0xb7, 0x92, 0x1a, 0x17, 0x05, 0xd7, 0x8c, 0x5c, 0x69, 0xa5, 0xc4, 0xe0, 0x38, 0xd0, 0x94, 0x3f, 0x50, + 0xe5, 0x0f, 0x53, 0xd2, 0x79, 0x8b, 0xd9, 0xea, 0xfb, 0xd4, 0x6e, 0x1d, 0x53, 0x45, 0x23, 0x9d, 0x19, 0xb3, 0x51, + 0x2b, 0x38, 0xda, 0xe3, 0x7a, 0x59, 0x48, 0xe7, 0xb4, 0xcd, 0xe0, 0x93, 0xf4, 0xf1, 0xad, 0x7c, 0xb6, 0xca, 0x5f, + 0xea, 0xfd, 0x5e, 0xda, 0xdb, 0xe4, 0xc5, 0x06, 0xde, 0x0a, 0x13, 0x60, 0x20, 0xa2, 0x52, 0x05, 0xb5, 0x84, 0x24, + 0xec, 0xb4, 0xd3, 0x39, 0x43, 0x55, 0x5a, 0x4c, 0x81, 0x1f, 0x97, 0xf5, 0xf1, 0xf8, 0x5a, 0x34, 0xa6, 0xd6, 0x51, + 0x23, 0x3e, 0x2e, 0xe7, 0x19, 0x20, 0x2f, 0x54, 0x3c, 0x53, 0x11, 0x7d, 0x46, 0xce, 0xf0, 0xa0, 0xcc, 0x82, 0x91, + 0x76, 0x18, 0x8a, 0x2d, 0x37, 0xa6, 0x3a, 0x03, 0xba, 0xf0, 0x67, 0x8d, 0x94, 0x69, 0x84, 0x52, 0xc8, 0xb5, 0x49, + 0xbb, 0xcc, 0x37, 0x08, 0xd3, 0x0b, 0x1a, 0x7f, 0x3d, 0xf9, 0x5e, 0x0a, 0x99, 0x02, 0xee, 0x23, 0x85, 0xd7, 0xf4, + 0x42, 0x66, 0xc0, 0x9b, 0x1a, 0x20, 0x09, 0x40, 0x9a, 0x55, 0x27, 0xbc, 0x0d, 0x0f, 0x49, 0xb3, 0xdf, 0xca, 0x52, + 0xb9, 0x27, 0x57, 0x5a, 0xf2, 0xad, 0x6e, 0x2b, 0xe6, 0x4b, 0xd6, 0x36, 0xad, 0x9d, 0x9d, 0xd0, 0xeb, 0x34, 0x4d, + 0xba, 0x44, 0x38, 0xa8, 0x24, 0xbd, 0xdf, 0x01, 0x06, 0x53, 0x5f, 0xbe, 0x45, 0xcd, 0xfc, 0x5e, 0x82, 0x9d, 0x0c, + 0xd8, 0x50, 0x65, 0xe5, 0x32, 0x0b, 0x00, 0x01, 0xba, 0x6d, 0xa3, 0x9b, 0x26, 0x8b, 0x37, 0x22, 0xf7, 0x80, 0xce, + 0x05, 0x77, 0x64, 0x6f, 0x29, 0xdd, 0x99, 0x8e, 0x95, 0x6c, 0xbc, 0x2b, 0x6b, 0xb2, 0x0b, 0x95, 0xf8, 0x26, 0x06, + 0x66, 0x3b, 0x2b, 0x09, 0x80, 0xeb, 0xc6, 0x2e, 0xf3, 0x42, 0x9d, 0xc9, 0x6c, 0xcd, 0xaa, 0x3c, 0x55, 0xc3, 0x54, + 0x3a, 0x74, 0xd5, 0x44, 0x0d, 0x41, 0x36, 0x20, 0x6c, 0x5e, 0xdb, 0x5c, 0xc7, 0x67, 0x01, 0x20, 0xe8, 0x41, 0x29, + 0x4b, 0xc6, 0x8e, 0x1b, 0x69, 0x77, 0xbd, 0xac, 0x00, 0x61, 0xbc, 0xb3, 0x26, 0x39, 0x39, 0x2d, 0xfd, 0xc9, 0x78, + 0xdb, 0x6a, 0xa6, 0xdd, 0xf1, 0x43, 0x42, 0xdb, 0xe2, 0xd0, 0x82, 0x1f, 0xa9, 0xdd, 0xb9, 0x5a, 0xc4, 0xaa, 0xbd, + 0x2c, 0x60, 0xb0, 0x8d, 0xd6, 0xba, 0x6d, 0xee, 0xe6, 0x98, 0x08, 0x27, 0xcb, 0xc6, 0x74, 0x27, 0x96, 0x17, 0x89, + 0x35, 0x06, 0x6a, 0x6b, 0xde, 0xf8, 0xa5, 0xc0, 0xd4, 0x04, 0xdf, 0xa8, 0x5c, 0x2c, 0x8d, 0xfe, 0xf4, 0x03, 0x11, + 0xa1, 0x59, 0x6c, 0xae, 0xd6, 0x4d, 0x68, 0xbc, 0xc6, 0xf5, 0x06, 0xdc, 0x0d, 0x2c, 0x1a, 0x4e, 0xf4, 0x60, 0xce, + 0xee, 0x48, 0xcd, 0x8a, 0x65, 0xf8, 0xd1, 0xa3, 0xa3, 0x02, 0xbb, 0xb3, 0x39, 0x96, 0x14, 0x48, 0x30, 0xe2, 0xd7, + 0xd7, 0x58, 0x2c, 0x6a, 0x97, 0x46, 0x07, 0x63, 0x2e, 0xf9, 0x0f, 0xaa, 0x9b, 0x69, 0x5f, 0x01, 0x9b, 0x7b, 0x86, + 0x23, 0x49, 0x99, 0x19, 0xbd, 0xbc, 0x36, 0x0d, 0xec, 0x55, 0x1e, 0x75, 0x1c, 0x46, 0x4f, 0x4a, 0xc2, 0xde, 0x6c, + 0x4d, 0xa9, 0x5c, 0x8a, 0x51, 0xe8, 0x79, 0x83, 0x58, 0xf4, 0xb8, 0x07, 0x38, 0xc9, 0x98, 0x22, 0x7d, 0xb5, 0x51, + 0x90, 0xb7, 0xda, 0x5f, 0xba, 0xec, 0xa0, 0x49, 0x87, 0xce, 0x02, 0x9c, 0x8c, 0x92, 0x82, 0x10, 0xa0, 0x0d, 0xa1, + 0xd7, 0x06, 0x2f, 0xa5, 0x08, 0x4d, 0x4d, 0x66, 0xd4, 0x85, 0xf9, 0x9c, 0x71, 0x46, 0xa1, 0xa0, 0xa7, 0x5d, 0x9a, + 0x77, 0xab, 0xdb, 0x5c, 0x38, 0xde, 0x5d, 0x54, 0x2b, 0x02, 0x29, 0x5a, 0xf1, 0xd3, 0x43, 0xe1, 0x22, 0xb7, 0x20, + 0xa2, 0xd6, 0x1c, 0xde, 0x1a, 0x9c, 0x5c, 0x4c, 0x68, 0x95, 0xea, 0xae, 0xf7, 0xf0, 0x85, 0x88, 0xef, 0xda, 0x3c, + 0x21, 0x8e, 0x5a, 0x6f, 0xe8, 0xe6, 0x2c, 0xcd, 0x53, 0x09, 0xf5, 0xcc, 0x16, 0x02, 0x97, 0x8d, 0x8c, 0x2a, 0x7c, + 0x33, 0x3e, 0xc7, 0xc8, 0x92, 0x80, 0x32, 0x38, 0x9d, 0xc5, 0x08, 0x2c, 0x32, 0xe6, 0xe3, 0xd8, 0x1f, 0xcf, 0x6c, + 0x82, 0x7c, 0xd7, 0x98, 0x11, 0x89, 0xb7, 0xbd, 0x37, 0xd4, 0x28, 0x94, 0x8c, 0x44, 0x5c, 0x1e, 0x39, 0xb4, 0x7b, + 0x50, 0x7d, 0x37, 0x20, 0x36, 0x8c, 0x29, 0xd3, 0x09, 0xa1, 0x4f, 0x1e, 0xc4, 0x9a, 0x5c, 0x98, 0xb0, 0xd2, 0x49, + 0x0c, 0xc4, 0xe8, 0x6c, 0x40, 0xf5, 0x8d, 0xd0, 0x22, 0x91, 0x05, 0x25, 0x12, 0xf9, 0x6c, 0x4e, 0x88, 0xc3, 0x56, + 0x64, 0xfc, 0x60, 0xb5, 0x77, 0x11, 0x95, 0x3e, 0xe3, 0xa4, 0xb0, 0x2e, 0x0b, 0xa3, 0x3f, 0x46, 0x09, 0x61, 0xc0, + 0xd9, 0xed, 0x49, 0x51, 0xde, 0x0d, 0x8b, 0x47, 0x17, 0xa8, 0xca, 0xb7, 0x5c, 0x01, 0xec, 0xd1, 0x42, 0x0d, 0x54, + 0x59, 0xb2, 0x9c, 0xeb, 0x47, 0x21, 0xc2, 0x53, 0x66, 0x8e, 0xaa, 0x10, 0x06, 0x84, 0x88, 0x4a, 0xed, 0xc2, 0xae, + 0x95, 0x02, 0x74, 0x30, 0xa6, 0x8d, 0x46, 0x88, 0x0b, 0x78, 0x9e, 0xb7, 0x3f, 0x0e, 0x98, 0xe6, 0x89, 0x3f, 0xa8, + 0x06, 0xfd, 0x77, 0x24, 0x9b, 0xac, 0x9f, 0xdc, 0xf7, 0xc3, 0x27, 0x7d, 0x87, 0xce, 0xde, 0xef, 0xab, 0xbf, 0x7f, + 0xec, 0xd1, 0x40, 0x16, 0xf2, 0x0b, 0xdd, 0x84, 0x56, 0xcf, 0xde, 0x18, 0xee, 0x88, 0x56, 0xcf, 0x4e, 0x2f, 0x0a, + 0xd4, 0x3b, 0xd7, 0x4e, 0x6d, 0x1b, 0x36, 0x32, 0x89, 0xc7, 0x9a, 0x27, 0x63, 0xb0, 0x22, 0x83, 0x6a, 0x05, 0x2b, + 0x9b, 0x2c, 0xd1, 0x5d, 0x9f, 0x99, 0x83, 0x7b, 0xe2, 0x46, 0xbe, 0x93, 0x67, 0x1f, 0x80, 0x9b, 0x10, 0xf9, 0x4b, + 0x0e, 0xab, 0xfa, 0x1d, 0xd5, 0xa6, 0x3b, 0x28, 0x18, 0x4a, 0x2d, 0x31, 0x5b, 0x15, 0x8d, 0x25, 0xd8, 0x1b, 0x04, + 0x5a, 0x53, 0xab, 0x0f, 0xeb, 0x70, 0xc8, 0x1f, 0x5b, 0xfb, 0x07, 0x95, 0x89, 0xba, 0x68, 0x40, 0x9e, 0x86, 0x5f, + 0xba, 0x44, 0xb8, 0x6c, 0x53, 0xff, 0xaf, 0x6e, 0x2f, 0x76, 0x46, 0xc1, 0x24, 0xe4, 0x6d, 0xc8, 0xc3, 0xdd, 0xc1, + 0x00, 0x05, 0x4a, 0xe7, 0x1b, 0x6d, 0x78, 0x12, 0x3d, 0xc9, 0xc3, 0xf6, 0x79, 0x69, 0xaf, 0x46, 0x7d, 0xae, 0x63, + 0x9b, 0xda, 0xb6, 0x49, 0x4d, 0x49, 0x73, 0x70, 0x05, 0x96, 0x18, 0x17, 0x34, 0xad, 0xe4, 0x11, 0x4b, 0x2c, 0xc7, + 0xa4, 0xca, 0xad, 0xe4, 0x29, 0xa7, 0x8c, 0xff, 0x10, 0xb4, 0x97, 0x59, 0x1e, 0x0d, 0x97, 0xe5, 0xd1, 0x65, 0xb0, + 0x36, 0xa1, 0xba, 0xb7, 0xa1, 0xfa, 0x62, 0xd6, 0xb4, 0xd4, 0x6a, 0x93, 0x24, 0x91, 0xc6, 0x7b, 0xba, 0x58, 0xd7, + 0x03, 0xe8, 0xce, 0xd4, 0x2e, 0x65, 0x12, 0xc7, 0x38, 0xd9, 0x86, 0xb9, 0xfa, 0xd8, 0x2a, 0xad, 0xcf, 0x5f, 0xd0, + 0xf8, 0xdc, 0x7d, 0x2b, 0x8f, 0x18, 0xb5, 0x18, 0x78, 0x7f, 0x78, 0x2a, 0xc1, 0xc5, 0xa1, 0xb1, 0xb3, 0x3d, 0x4c, + 0x1c, 0x76, 0xec, 0xec, 0xd7, 0x14, 0x4c, 0xcf, 0x81, 0x36, 0xf4, 0xd5, 0xe0, 0xf8, 0xda, 0x3d, 0x77, 0xf0, 0x62, + 0x40, 0x4b, 0xa4, 0xbc, 0x53, 0xe4, 0x88, 0x01, 0x26, 0x5a, 0xf9, 0x9b, 0x5f, 0xe7, 0xf5, 0x87, 0xf8, 0x7a, 0x3c, + 0x10, 0x3b, 0x51, 0x1e, 0x3d, 0x2b, 0x14, 0xa5, 0x44, 0x45, 0x4f, 0xe1, 0x2f, 0x6e, 0xa1, 0x0c, 0xa7, 0x89, 0x4e, + 0x47, 0x45, 0xb7, 0x77, 0x4f, 0x7c, 0x67, 0xff, 0xa6, 0x3a, 0x97, 0xf3, 0x0a, 0x03, 0x5d, 0x08, 0x6c, 0xa0, 0x8c, + 0x8c, 0x05, 0x4a, 0xf1, 0x63, 0xcc, 0x2e, 0x43, 0x94, 0xdc, 0xea, 0x13, 0x3e, 0x70, 0x11, 0x98, 0x3b, 0xa4, 0x49, + 0xc2, 0xe8, 0x51, 0x7b, 0x6e, 0x5a, 0x9e, 0x84, 0x99, 0x9d, 0x27, 0x99, 0x9d, 0x53, 0xc5, 0x85, 0x09, 0x53, 0x35, + 0x28, 0x16, 0x8f, 0xe5, 0xa4, 0xb6, 0x5a, 0x4d, 0x33, 0x27, 0x9a, 0xe9, 0x91, 0x3b, 0x0c, 0x81, 0x6e, 0xd2, 0x0d, + 0x35, 0xfa, 0x4d, 0x54, 0xf1, 0xd1, 0x7a, 0x11, 0x0c, 0xd1, 0xfa, 0x74, 0xd6, 0x46, 0xb9, 0x63, 0x14, 0x25, 0xdf, + 0x17, 0x80, 0xb8, 0xb7, 0xae, 0x28, 0x5d, 0x7d, 0xf2, 0xc7, 0x3f, 0x4c, 0xb5, 0x9e, 0x07, 0x10, 0x23, 0xbe, 0x66, + 0x93, 0x33, 0xa3, 0xf2, 0x48, 0xfc, 0x43, 0x98, 0xb4, 0x80, 0x3b, 0x42, 0x58, 0xb5, 0x71, 0x30, 0x49, 0x4e, 0xe7, + 0x62, 0xa8, 0xef, 0xa2, 0x91, 0x24, 0x94, 0x49, 0x7d, 0x0a, 0x9e, 0x4d, 0x4e, 0xad, 0x8f, 0x0e, 0x09, 0x77, 0xeb, + 0x20, 0x14, 0x62, 0xa6, 0x5a, 0x03, 0xdc, 0x3d, 0xa5, 0xfb, 0x7a, 0xed, 0x8d, 0xd7, 0x2a, 0xe2, 0xfe, 0xfb, 0xc5, + 0xc1, 0xfb, 0xef, 0x78, 0xa9, 0xa9, 0xdf, 0x6f, 0x9c, 0x0d, 0xdb, 0xb7, 0x3c, 0x00, 0x2f, 0x06, 0x78, 0x08, 0x70, + 0x11, 0xf5, 0x56, 0xa7, 0xfd, 0x61, 0x74, 0xe3, 0xeb, 0xca, 0xec, 0x59, 0xd1, 0xf5, 0x3b, 0x3f, 0x78, 0xb7, 0x6f, + 0x21, 0x60, 0x17, 0xdd, 0xff, 0x1f, 0x81, 0x0a, 0x08, 0x86, 0x82, 0xbf, 0x3f, 0x6e, 0x87, 0xb3, 0x23, 0x78, 0x0e, + 0xbd, 0x3e, 0x8e, 0x62, 0xa5, 0x7b, 0x27, 0x4d, 0xb1, 0x57, 0x11, 0x54, 0x99, 0x57, 0xc4, 0xa6, 0x8c, 0xcd, 0x2e, + 0xeb, 0x52, 0xaf, 0xcd, 0x37, 0x18, 0xd0, 0x97, 0x00, 0xc8, 0x48, 0xf5, 0xa6, 0x0c, 0x20, 0xfc, 0xfa, 0x52, 0x2c, + 0x46, 0xf3, 0x7c, 0xa7, 0xb5, 0x6b, 0xf7, 0x29, 0xf4, 0xc3, 0x76, 0x1d, 0x1e, 0x0c, 0xed, 0x09, 0x79, 0x9e, 0x37, + 0xbc, 0xcb, 0xf0, 0x6d, 0x5e, 0x14, 0x9c, 0x06, 0x2f, 0xa3, 0x5a, 0x1a, 0xf2, 0x49, 0x34, 0x06, 0xfa, 0xb4, 0x6f, + 0x29, 0x01, 0xb7, 0x21, 0x31, 0xd8, 0x41, 0x56, 0x7a, 0x7d, 0x24, 0xed, 0x9d, 0xeb, 0x31, 0xbc, 0xd9, 0x6e, 0x71, + 0x91, 0x32, 0x22, 0xb1, 0x63, 0xa0, 0xc9, 0x8d, 0x50, 0xed, 0xed, 0xce, 0x9e, 0x0f, 0xdf, 0xdc, 0x5c, 0xde, 0xdc, + 0xae, 0x8f, 0x43, 0xaa, 0xb1, 0x4e, 0xa7, 0xd6, 0x6a, 0x6c, 0x27, 0x6d, 0x91, 0xef, 0x2d, 0x0b, 0x9b, 0x84, 0x16, + 0xe9, 0x06, 0x96, 0x96, 0x0f, 0x93, 0xaa, 0x55, 0x06, 0x38, 0x91, 0x9a, 0xba, 0x9f, 0x9e, 0x9e, 0x33, 0x25, 0xcb, + 0x03, 0x7a, 0x71, 0xd0, 0x55, 0x21, 0xb6, 0x4b, 0xd7, 0x6f, 0x2f, 0x97, 0x9e, 0xeb, 0x06, 0x60, 0x13, 0x39, 0x30, + 0x90, 0xf2, 0x7f, 0xc7, 0xa2, 0x5e, 0x0e, 0xcb, 0x93, 0x25, 0x82, 0xc2, 0x25, 0xde, 0x75, 0x49, 0x4a, 0xb4, 0x29, + 0x45, 0x68, 0x2e, 0x5e, 0x1f, 0x15, 0xe3, 0x49, 0xdd, 0x59, 0xf3, 0xec, 0x20, 0x12, 0x19, 0x5b, 0x19, 0x1b, 0xcc, + 0x4d, 0x5a, 0x86, 0x00, 0x07, 0x85, 0x64, 0xcb, 0xf5, 0xa6, 0x0b, 0xb0, 0x5d, 0xf2, 0x57, 0xa3, 0x71, 0x9e, 0x2c, + 0xd1, 0x1d, 0x1a, 0xf6, 0xe5, 0x40, 0xc1, 0xe4, 0xe6, 0xca, 0xe9, 0x91, 0x1f, 0xc5, 0x6c, 0xb1, 0x46, 0x86, 0xc1, + 0x82, 0xe9, 0x04, 0x1c, 0x08, 0xb9, 0x57, 0x0e, 0x10, 0x5b, 0x16, 0xf8, 0x30, 0x98, 0x5b, 0x22, 0x9b, 0x3c, 0xda, + 0xd9, 0x3d, 0x55, 0x28, 0xf8, 0xe4, 0xd6, 0x6d, 0x59, 0xf2, 0xca, 0x0f, 0x82, 0x5e, 0xc5, 0xe5, 0x69, 0xbb, 0x68, + 0x8e, 0xc9, 0xd1, 0x77, 0xd9, 0x94, 0xfd, 0x30, 0x8d, 0xc8, 0xc3, 0x43, 0x92, 0xe1, 0x30, 0x0b, 0x82, 0xc5, 0x4e, + 0x78, 0x29, 0x6c, 0x60, 0xac, 0x6d, 0xc2, 0x8e, 0xd4, 0x10, 0xde, 0x21, 0x26, 0xac, 0x99, 0xb3, 0x16, 0x2c, 0x10, + 0x71, 0x39, 0xe8, 0x3e, 0x72, 0xa0, 0x5f, 0xd9, 0x0a, 0x1d, 0xed, 0xe2, 0x6e, 0xf6, 0x23, 0x16, 0xc8, 0xd8, 0x92, + 0x39, 0xe9, 0x1a, 0x7e, 0xcb, 0x50, 0xad, 0xad, 0x67, 0xa3, 0xb3, 0x7b, 0xc3, 0x34, 0xd1, 0x96, 0x25, 0x3b, 0xa2, + 0x64, 0xfd, 0x42, 0x02, 0xb7, 0x50, 0xe5, 0x46, 0xee, 0xad, 0x44, 0x11, 0xc4, 0x14, 0xba, 0x78, 0xdd, 0x2d, 0x8c, + 0x88, 0x37, 0xd3, 0xa9, 0x39, 0x2a, 0x7c, 0x22, 0x63, 0x50, 0x52, 0x92, 0x82, 0xff, 0x67, 0xbd, 0x5f, 0x80, 0x82, + 0xf8, 0xc4, 0xaf, 0x7f, 0x17, 0x44, 0x38, 0xb0, 0xdb, 0x5e, 0xb6, 0xaa, 0x1d, 0x4b, 0x50, 0x1e, 0x15, 0xe6, 0xdc, + 0x40, 0x6a, 0xfd, 0x7b, 0x6e, 0xe3, 0xcd, 0x9f, 0xbf, 0xcb, 0x5c, 0xad, 0xdb, 0xe5, 0xe6, 0xb5, 0x3b, 0xe4, 0x9a, + 0xb1, 0x03, 0xf6, 0xe5, 0xe0, 0xc3, 0x6a, 0x26, 0xdd, 0x02, 0x92, 0x86, 0x4c, 0x2f, 0xdc, 0xae, 0xe8, 0x86, 0x13, + 0x72, 0x07, 0xe4, 0x10, 0x20, 0xd0, 0x66, 0x50, 0xd6, 0xe8, 0x58, 0xef, 0xc3, 0x79, 0x7b, 0x7d, 0xf9, 0xf7, 0xba, + 0x5e, 0xa2, 0x43, 0x9a, 0x9d, 0xc5, 0xa0, 0xff, 0x7e, 0x2b, 0x19, 0xc9, 0xf6, 0xcd, 0xf6, 0xfe, 0x5d, 0x0b, 0x8a, + 0x6b, 0x9a, 0xf6, 0x0f, 0x7e, 0xf9, 0xa2, 0xb7, 0xf0, 0x7a, 0xe7, 0x23, 0xa9, 0x49, 0x53, 0x6e, 0xf8, 0x71, 0xb5, + 0x95, 0xef, 0x4a, 0x66, 0x7c, 0x40, 0x60, 0xc4, 0xc9, 0xea, 0xe2, 0xe9, 0x61, 0xc4, 0x64, 0x3d, 0x6a, 0x18, 0x4e, + 0x6e, 0x6d, 0xc6, 0xb4, 0x6a, 0x21, 0x32, 0xc0, 0x25, 0x1a, 0x95, 0x28, 0x12, 0x25, 0x31, 0x40, 0x70, 0x6f, 0x7d, + 0x9e, 0xa0, 0x2d, 0x6a, 0xd6, 0x0e, 0xd4, 0x76, 0x56, 0x36, 0x27, 0x01, 0xa3, 0xcd, 0x1c, 0xd3, 0x6a, 0x2e, 0x42, + 0xe7, 0xee, 0x34, 0x88, 0x0e, 0xbd, 0x25, 0xba, 0x94, 0xb9, 0x62, 0xdf, 0xb4, 0xac, 0x2d, 0x03, 0xf2, 0x49, 0xd4, + 0x46, 0x1d, 0x24, 0x58, 0xe5, 0x54, 0x6c, 0x26, 0xf6, 0x8d, 0xa1, 0x2d, 0xdc, 0x81, 0xbe, 0x81, 0x1e, 0xac, 0xf1, + 0x92, 0xdd, 0xe4, 0xed, 0x53, 0xca, 0x0b, 0x8b, 0x49, 0xf7, 0x3b, 0xa9, 0x1e, 0xdb, 0x5b, 0x03, 0xa2, 0x50, 0x8c, + 0x77, 0x0f, 0x09, 0x56, 0x1e, 0xbd, 0x0d, 0x38, 0xb6, 0x4a, 0xaf, 0x71, 0x55, 0x3d, 0x31, 0x26, 0x78, 0x58, 0xca, + 0x27, 0xdf, 0x3f, 0x79, 0x35, 0xee, 0x1a, 0xc6, 0x4b, 0x8b, 0x5b, 0x10, 0x54, 0x30, 0x7b, 0x8b, 0x59, 0xfc, 0xd2, + 0xfc, 0xbe, 0x7b, 0xe0, 0xc6, 0xce, 0x21, 0x37, 0x6f, 0x70, 0xf7, 0x5a, 0xdc, 0xa7, 0xce, 0x67, 0xf5, 0xec, 0xd3, + 0xe9, 0x6a, 0x6b, 0x14, 0x7d, 0x3b, 0x03, 0xed, 0x11, 0xe9, 0xac, 0x01, 0x98, 0x04, 0x28, 0x4b, 0x32, 0xa0, 0x86, + 0x05, 0x5e, 0x2e, 0xad, 0xba, 0x13, 0xd4, 0x54, 0x7b, 0xb6, 0x29, 0x9f, 0x0b, 0x6b, 0x2c, 0xbe, 0x58, 0xba, 0x4e, + 0x53, 0xc3, 0x14, 0xb5, 0xae, 0x5d, 0xf3, 0xf7, 0x6f, 0x65, 0x09, 0x34, 0x4c, 0xe5, 0x8a, 0xfd, 0x1a, 0x55, 0x43, + 0xf0, 0x29, 0x2c, 0xa2, 0x84, 0x00, 0xcf, 0x62, 0x12, 0xa8, 0x5a, 0x3f, 0xb4, 0xbd, 0xdf, 0xbb, 0x63, 0xeb, 0x64, + 0x3a, 0xb8, 0x6b, 0x40, 0x96, 0x99, 0xf3, 0xce, 0x99, 0x96, 0xa1, 0x9b, 0xc6, 0x45, 0x48, 0xd9, 0x4f, 0x5f, 0xa0, + 0x4e, 0x96, 0xdb, 0xec, 0x51, 0xd0, 0x58, 0x0e, 0x91, 0x14, 0xb9, 0x20, 0xc5, 0xbf, 0x0b, 0x47, 0x3c, 0x46, 0x6a, + 0x9d, 0xa9, 0x65, 0x8c, 0xa6, 0xff, 0x16, 0xd6, 0x82, 0xa5, 0xdd, 0x7b, 0x96, 0xc1, 0x8f, 0x93, 0x01, 0xd5, 0x3a, + 0x77, 0x52, 0x26, 0x9b, 0x25, 0x8c, 0x0c, 0xed, 0x8e, 0x5a, 0xfd, 0xf4, 0x6b, 0xbd, 0x5d, 0x9a, 0xbd, 0x34, 0xcd, + 0x2f, 0xa2, 0x85, 0x81, 0x2c, 0x01, 0x17, 0x0b, 0x4a, 0x3b, 0x27, 0xd5, 0xbf, 0xf7, 0xcd, 0xf7, 0xc4, 0xf7, 0xc2, + 0x5f, 0x66, 0x3e, 0x8f, 0x7c, 0xca, 0x2b, 0x3f, 0x40, 0x9e, 0x4f, 0xee, 0xad, 0x16, 0x0c, 0x23, 0x98, 0x88, 0xac, + 0x5c, 0x81, 0x80, 0x45, 0x91, 0x3c, 0x50, 0x01, 0x89, 0x88, 0x2b, 0xdb, 0x21, 0xad, 0x66, 0xbd, 0x9b, 0x01, 0x85, + 0x01, 0xd7, 0xfe, 0x42, 0xe3, 0x9c, 0x2e, 0xf6, 0xd6, 0x51, 0x51, 0xe9, 0x58, 0x1a, 0xfd, 0x11, 0x98, 0x18, 0x51, + 0xc9, 0xe9, 0xa8, 0x38, 0xb3, 0x18, 0xed, 0x2b, 0x3a, 0x8b, 0x19, 0xc8, 0x58, 0xa9, 0x29, 0x5b, 0xf9, 0x0d, 0x30, + 0xbb, 0x3d, 0x97, 0x34, 0xf5, 0x18, 0x0e, 0xe4, 0x05, 0x44, 0x0d, 0xac, 0x68, 0x03, 0x9d, 0xda, 0x6f, 0x08, 0xcf, + 0x1b, 0x96, 0x47, 0x80, 0x20, 0x28, 0xdf, 0x41, 0xd8, 0x9f, 0xd8, 0xbe, 0x72, 0x35, 0xc3, 0x29, 0xc3, 0xf4, 0x19, + 0x87, 0x86, 0xfa, 0x14, 0xfc, 0x04, 0x6c, 0xa2, 0xab, 0x11, 0x20, 0xdf, 0x24, 0x84, 0x1e, 0x04, 0xfd, 0x2b, 0x8f, + 0x48, 0x7f, 0xdd, 0xd4, 0xea, 0x2b, 0x98, 0xe2, 0xa8, 0x4c, 0xd6, 0x6d, 0x6a, 0x5b, 0xbd, 0xb2, 0x65, 0x5c, 0xd7, + 0x80, 0x3a, 0x2d, 0x9d, 0xe3, 0x0c, 0x27, 0x0d, 0xf1, 0xbf, 0x06, 0x86, 0x3f, 0xa8, 0xdd, 0x0e, 0xa3, 0x0f, 0xfd, + 0xc6, 0x8c, 0x79, 0x87, 0x70, 0x78, 0x3c, 0x31, 0x8d, 0xdc, 0x9f, 0x0b, 0x4c, 0x87, 0x96, 0xf8, 0x23, 0x8d, 0x38, + 0xe9, 0x83, 0xd2, 0x8b, 0xd5, 0xa1, 0x32, 0xfe, 0xdb, 0xb8, 0x1f, 0xbe, 0x6d, 0xb3, 0x8a, 0xe1, 0xc9, 0x88, 0x02, + 0xb6, 0x1a, 0xb3, 0x8e, 0x4f, 0x8e, 0xd6, 0xe3, 0x98, 0xdb, 0x80, 0xa8, 0x71, 0xbd, 0xa9, 0xda, 0x2c, 0x52, 0xb1, + 0xe5, 0x96, 0x3d, 0x1f, 0xcc, 0xa8, 0x7c, 0xfc, 0xf3, 0x32, 0x15, 0x82, 0x00, 0x55, 0xe2, 0x43, 0x34, 0xd0, 0xc5, + 0x6e, 0x27, 0x68, 0xe1, 0xb7, 0x96, 0xd2, 0x4a, 0xe6, 0xc1, 0x6a, 0xee, 0x90, 0x80, 0x8e, 0xaa, 0x01, 0xc3, 0xa7, + 0x68, 0xb2, 0xab, 0xc9, 0x31, 0x42, 0x01, 0x4d, 0xce, 0x92, 0x86, 0x93, 0x61, 0xbf, 0x2d, 0x4e, 0x7f, 0x9d, 0xf3, + 0x51, 0xb3, 0x21, 0x52, 0xdf, 0x8e, 0x89, 0x98, 0x7e, 0xc7, 0x57, 0x59, 0x19, 0x1b, 0xa1, 0x78, 0x33, 0x88, 0x8d, + 0x21, 0xc9, 0x1b, 0x05, 0x25, 0x42, 0x24, 0xbb, 0x38, 0x11, 0x66, 0xf3, 0x7e, 0xa5, 0xf0, 0xf4, 0x15, 0xa1, 0xd4, + 0x1c, 0x23, 0x8d, 0x0e, 0xb6, 0x74, 0xc2, 0xda, 0xb4, 0x7d, 0x5c, 0x7d, 0x81, 0x41, 0x87, 0xcf, 0x1c, 0xf0, 0x02, + 0xe0, 0xc6, 0xb0, 0x0a, 0x60, 0xad, 0x31, 0x77, 0x0c, 0xb7, 0x65, 0x7c, 0x62, 0x2d, 0x73, 0x40, 0xff, 0x98, 0xc8, + 0x72, 0x43, 0x7b, 0x0e, 0x41, 0xc1, 0xb4, 0x1d, 0x58, 0xa2, 0xf2, 0xef, 0xb4, 0x29, 0x76, 0x55, 0x31, 0x31, 0x0f, + 0x84, 0xcb, 0x12, 0x09, 0x95, 0xaf, 0x7b, 0xd7, 0x63, 0x06, 0xf8, 0x88, 0xa8, 0x19, 0x54, 0xbc, 0xce, 0x4d, 0x7e, + 0x55, 0x3f, 0xbf, 0x04, 0xec, 0x75, 0xf6, 0xba, 0xfe, 0xf0, 0xba, 0x7a, 0xfa, 0x93, 0x52, 0x00, 0xf4, 0x5c, 0xd8, + 0x95, 0x61, 0x26, 0x0b, 0x9b, 0xc8, 0xf0, 0x73, 0xbd, 0x84, 0xf2, 0xb4, 0x99, 0x03, 0x42, 0x38, 0xc7, 0xf9, 0xe4, + 0xfa, 0x74, 0x95, 0xb9, 0x09, 0xa4, 0x08, 0xb8, 0x09, 0x20, 0xf3, 0xfe, 0x08, 0x67, 0xce, 0x07, 0x04, 0xe2, 0x5d, + 0x5c, 0x9b, 0x1c, 0x3d, 0x0e, 0x92, 0x98, 0xdd, 0x4f, 0x3d, 0x2a, 0x88, 0xcb, 0x68, 0x01, 0x0d, 0x5b, 0x53, 0x76, + 0x2d, 0x58, 0xee, 0x08, 0x1d, 0x36, 0x84, 0x99, 0x42, 0x57, 0x89, 0xfc, 0x87, 0x47, 0x4b, 0xaa, 0xe8, 0xb1, 0x3b, + 0x7a, 0xb6, 0x22, 0xca, 0x70, 0x52, 0x47, 0x42, 0x82, 0xf0, 0x85, 0xa8, 0x81, 0x7e, 0xc0, 0xc6, 0xa8, 0x52, 0xe2, + 0x12, 0xdb, 0x12, 0xe8, 0x3b, 0x09, 0xc2, 0xb2, 0x53, 0x1a, 0x86, 0xe6, 0x90, 0xc3, 0x48, 0x14, 0x41, 0x29, 0xfc, + 0x02, 0x25, 0xcf, 0x34, 0x94, 0x80, 0x32, 0x75, 0x60, 0x47, 0x0d, 0x55, 0x89, 0x09, 0x75, 0x7a, 0x7a, 0x10, 0xdd, + 0xbb, 0x0c, 0x34, 0x4d, 0x07, 0xa7, 0x1d, 0x2a, 0xc6, 0xd2, 0x98, 0xea, 0x60, 0x3b, 0x2a, 0x04, 0x47, 0x3a, 0x1e, + 0x32, 0x0a, 0x4e, 0x6e, 0xdf, 0xe1, 0xb2, 0xe1, 0xd3, 0xed, 0xa7, 0x4a, 0x8c, 0x8e, 0x9e, 0xac, 0xce, 0xa5, 0xd5, + 0xf3, 0x6c, 0xcc, 0x24, 0x48, 0x9f, 0xc0, 0xa1, 0x52, 0xf8, 0x32, 0x03, 0xd3, 0x22, 0x8f, 0xb7, 0x65, 0xb4, 0x38, + 0x85, 0x92, 0xab, 0x6e, 0x1f, 0xe9, 0x36, 0xdf, 0xce, 0xa4, 0xdb, 0x6f, 0xa7, 0xc1, 0x51, 0xd6, 0xcc, 0xfa, 0x42, + 0xf9, 0xbc, 0x52, 0xaa, 0xed, 0x5b, 0xf9, 0x49, 0xa2, 0x83, 0x63, 0x0d, 0xd5, 0x2a, 0x2c, 0xf1, 0x93, 0x81, 0xd5, + 0x6b, 0x48, 0xb5, 0x91, 0x8a, 0x61, 0x07, 0x9e, 0x8f, 0x3c, 0x9e, 0xbb, 0xae, 0x34, 0xe3, 0xca, 0x30, 0xb3, 0x49, + 0x25, 0xc6, 0xf7, 0xc3, 0x63, 0x0f, 0xed, 0x99, 0xf6, 0xf9, 0x74, 0xf8, 0x12, 0xe8, 0x74, 0x20, 0x9a, 0x80, 0x81, + 0x39, 0x84, 0x32, 0x16, 0x68, 0x6c, 0x2c, 0x66, 0x51, 0x1e, 0x95, 0x29, 0x4d, 0x95, 0xc6, 0x30, 0x86, 0xda, 0x00, + 0xae, 0x6e, 0xd7, 0x4c, 0x4a, 0x46, 0x49, 0x77, 0x29, 0x0d, 0x14, 0xd3, 0x31, 0x8c, 0x15, 0x9e, 0x29, 0x19, 0x2e, + 0x0a, 0x71, 0x1a, 0xe0, 0xcb, 0x8b, 0xff, 0xf7, 0xaf, 0xc0, 0xa8, 0xb9, 0xed, 0x91, 0xac, 0xd9, 0xec, 0x68, 0x4b, + 0x2b, 0x3c, 0x4f, 0xe7, 0xcb, 0x17, 0x29, 0xeb, 0x52, 0x2d, 0x8a, 0xd3, 0xe8, 0x28, 0x23, 0x4a, 0xfb, 0x76, 0xf7, + 0x97, 0xba, 0x33, 0x8c, 0x98, 0x2b, 0xdf, 0xf8, 0x3d, 0xe5, 0x5a, 0xf2, 0x6e, 0xb7, 0x8c, 0xac, 0x4a, 0x31, 0xe1, + 0x43, 0xe5, 0x1a, 0x5e, 0x69, 0xfd, 0x07, 0xf9, 0x4f, 0xb9, 0xaa, 0x6d, 0x7f, 0x0c, 0xeb, 0x95, 0x6c, 0x4e, 0xb4, + 0xde, 0x3c, 0xe3, 0x88, 0xb7, 0x3d, 0xc6, 0xfd, 0x25, 0x85, 0x63, 0x69, 0xfc, 0xae, 0xea, 0x64, 0x37, 0x3f, 0xb9, + 0x5c, 0x90, 0xb4, 0x98, 0x74, 0xeb, 0xad, 0xca, 0x7e, 0xe6, 0xab, 0xf7, 0xfb, 0xb3, 0x87, 0x3b, 0x26, 0x41, 0xc2, + 0x6d, 0x43, 0x3e, 0x0d, 0x22, 0xbd, 0x6d, 0x46, 0x47, 0x69, 0xf2, 0xca, 0x99, 0x4d, 0x08, 0x84, 0xe3, 0x8d, 0xe9, + 0x01, 0x26, 0x3b, 0x93, 0xd2, 0xcb, 0xfe, 0x67, 0x76, 0xe5, 0xda, 0xd4, 0xc5, 0x5d, 0xb1, 0xc5, 0x83, 0xe4, 0xd7, + 0x43, 0x7c, 0x38, 0x86, 0x37, 0x9f, 0xe3, 0x77, 0xc8, 0x3f, 0xea, 0xb8, 0x0c, 0x0c, 0x4c, 0xac, 0x1c, 0xfb, 0x4e, + 0x78, 0xd9, 0xdf, 0x12, 0x6b, 0x50, 0x56, 0x69, 0x8a, 0x21, 0x18, 0xc4, 0x79, 0x1d, 0x00, 0xc8, 0x95, 0x0d, 0x62, + 0x9b, 0x27, 0xb2, 0xe5, 0xab, 0x60, 0xf1, 0xce, 0xf1, 0xd1, 0x0b, 0x6e, 0x4a, 0x7c, 0xaa, 0xbc, 0x3d, 0x63, 0x0c, + 0x70, 0x0b, 0xca, 0xd3, 0xb1, 0x83, 0x19, 0x31, 0x47, 0x42, 0xed, 0x8a, 0x4a, 0x2c, 0x49, 0x1d, 0x2a, 0x14, 0xcd, + 0xea, 0x82, 0x91, 0x89, 0xe4, 0xb3, 0x35, 0x55, 0x82, 0x81, 0xd4, 0x41, 0x7b, 0xf6, 0x2c, 0x4a, 0x9a, 0x7d, 0x1e, + 0x9a, 0x6c, 0x92, 0x3b, 0x7e, 0x09, 0xa6, 0x3f, 0xf8, 0x59, 0x28, 0xe9, 0x73, 0x6f, 0x62, 0x21, 0x7f, 0xb7, 0x95, + 0xf5, 0x27, 0xec, 0x1d, 0xfe, 0x26, 0x21, 0x7c, 0x39, 0x85, 0xd5, 0x24, 0x61, 0x59, 0xb8, 0xf0, 0x76, 0x49, 0x80, + 0x3c, 0x65, 0x69, 0x57, 0x83, 0x03, 0x85, 0x3e, 0x14, 0x94, 0x2c, 0x96, 0xb1, 0x12, 0x33, 0xc3, 0x22, 0xa6, 0xe4, + 0x5e, 0xf4, 0x35, 0xf3, 0xbe, 0xf9, 0x3a, 0x85, 0x47, 0x06, 0x4f, 0xe5, 0xa6, 0x6d, 0x5b, 0x88, 0x0e, 0x18, 0x9a, + 0xe9, 0x4f, 0x70, 0x40, 0xbb, 0x7f, 0xdd, 0xa5, 0xa7, 0x1c, 0xf8, 0xec, 0x39, 0x0e, 0xd6, 0x56, 0x9e, 0xa5, 0x9c, + 0x35, 0x54, 0xf7, 0x39, 0x05, 0x3f, 0x17, 0xef, 0x10, 0x57, 0x26, 0xc1, 0xd3, 0x5d, 0x4c, 0x12, 0x54, 0x9f, 0x82, + 0x21, 0xe9, 0x04, 0x74, 0xb1, 0xc2, 0xea, 0x5a, 0xb3, 0xe5, 0x09, 0xba, 0x98, 0x60, 0x05, 0x63, 0x38, 0x14, 0xf4, + 0xf2, 0x30, 0xb3, 0x1e, 0x56, 0xd3, 0xd3, 0x22, 0x48, 0x22, 0x9d, 0xec, 0xf6, 0x53, 0x92, 0xbd, 0x26, 0x12, 0x40, + 0x3f, 0x37, 0x2b, 0x69, 0x03, 0xe0, 0x41, 0xad, 0x10, 0xb1, 0xef, 0x45, 0xcc, 0x49, 0x2a, 0x55, 0x73, 0x46, 0xb7, + 0x15, 0x02, 0x62, 0x5d, 0xf8, 0x5b, 0x5e, 0xdd, 0x94, 0xfa, 0x53, 0xb0, 0x80, 0xbe, 0xe1, 0x42, 0x02, 0xaf, 0x8d, + 0x8d, 0xf7, 0x8a, 0xc6, 0x1a, 0x5f, 0x02, 0x58, 0x1c, 0x0c, 0xf0, 0xa4, 0xc6, 0x32, 0x2c, 0x01, 0x69, 0x15, 0x0f, + 0x9d, 0x98, 0xb0, 0xf2, 0xb4, 0xe0, 0x98, 0xe5, 0xbb, 0x7f, 0x98, 0xdf, 0xe9, 0xb4, 0x4e, 0x20, 0x31, 0xd3, 0xa9, + 0x76, 0x4b, 0x2f, 0x1f, 0x58, 0xbf, 0xd6, 0x98, 0x25, 0xe2, 0x9e, 0xe4, 0x65, 0xb7, 0x63, 0x15, 0xda, 0x58, 0xc4, + 0x32, 0x9e, 0x29, 0x87, 0x57, 0x53, 0x6f, 0xf3, 0xf0, 0x00, 0x0e, 0xcf, 0xa7, 0x96, 0xfb, 0xeb, 0x00, 0x13, 0x87, + 0x9b, 0x52, 0x28, 0x15, 0xf1, 0x7a, 0x10, 0x20, 0x12, 0xc4, 0x44, 0xbb, 0xc8, 0x50, 0x7a, 0xca, 0x0d, 0x62, 0xb3, + 0x01, 0x25, 0x62, 0x87, 0xb6, 0x8e, 0xd2, 0x1f, 0xc2, 0x57, 0x47, 0xf9, 0x54, 0x99, 0xea, 0xa4, 0xb7, 0x30, 0xcb, + 0xe5, 0x48, 0x35, 0x34, 0x60, 0xd9, 0x71, 0xfb, 0xc9, 0x63, 0x5b, 0x61, 0x78, 0x6e, 0xab, 0xfe, 0x6e, 0x1b, 0xfe, + 0xfe, 0x02, 0x5e, 0x3c, 0xfd, 0xbe, 0xee, 0x6b, 0x6e, 0xd9, 0x90, 0x43, 0x5d, 0xda, 0x8d, 0x88, 0xb8, 0x17, 0x2f, + 0xaf, 0x52, 0x48, 0x01, 0xd2, 0xfc, 0x01, 0x3c, 0x3b, 0xbe, 0x3d, 0xd2, 0x7d, 0x2a, 0x32, 0x41, 0x24, 0xe4, 0xed, + 0x82, 0xb0, 0xe2, 0xb1, 0xa7, 0xb0, 0x69, 0x64, 0x41, 0x9f, 0x4a, 0xe8, 0x12, 0x7e, 0x8a, 0x7c, 0x79, 0x39, 0x17, + 0xfc, 0x18, 0xd2, 0x09, 0x68, 0xb0, 0x3b, 0xeb, 0x45, 0x50, 0x06, 0x39, 0xed, 0x2d, 0xa5, 0x79, 0x27, 0x97, 0x8d, + 0x02, 0xd3, 0x96, 0x85, 0xf6, 0x4b, 0xa3, 0x6e, 0xba, 0x78, 0x6a, 0xa2, 0x10, 0xf0, 0xf0, 0xb0, 0xd9, 0xed, 0xa4, + 0xa1, 0x9c, 0x55, 0x73, 0xef, 0xab, 0x55, 0xe3, 0x8a, 0xe4, 0xe3, 0x61, 0x86, 0x20, 0xa4, 0xdd, 0x8e, 0x9c, 0x1a, + 0xc3, 0x51, 0xd1, 0xbe, 0x48, 0xd6, 0x79, 0xe2, 0x70, 0xdc, 0xcb, 0x27, 0x71, 0xb2, 0x71, 0xac, 0x8b, 0x93, 0x48, + 0x05, 0xbe, 0x58, 0x7d, 0xd5, 0x10, 0x6d, 0xa6, 0xc5, 0xe9, 0x5d, 0x55, 0xa5, 0x6a, 0x0a, 0xb4, 0x93, 0x22, 0x47, + 0x76, 0x33, 0xbb, 0x2b, 0xb6, 0xa1, 0xd0, 0x0c, 0x38, 0x7f, 0xd6, 0x5e, 0xac, 0x47, 0x78, 0xa8, 0xbc, 0xf8, 0x47, + 0xd1, 0x3f, 0x56, 0x3d, 0x91, 0x65, 0x2b, 0xfc, 0xd5, 0x78, 0xbd, 0xb4, 0xf8, 0x37, 0x0f, 0xdc, 0x67, 0xd7, 0xd9, + 0x91, 0xb7, 0xde, 0x9c, 0x8f, 0x57, 0x15, 0x4f, 0x17, 0x89, 0x6f, 0x18, 0x06, 0x70, 0x39, 0xa4, 0x79, 0xb9, 0xdb, + 0x7b, 0x0c, 0x9f, 0x86, 0x80, 0x90, 0x6c, 0xe7, 0xdc, 0x3e, 0x9f, 0x3f, 0x1c, 0x69, 0x33, 0x9c, 0xc9, 0x4b, 0x21, + 0xd9, 0x57, 0x08, 0x00, 0x64, 0xd5, 0x66, 0xa4, 0x63, 0x5d, 0x4d, 0x02, 0x69, 0x32, 0x49, 0xdd, 0x6e, 0x03, 0x5c, + 0x80, 0x54, 0x94, 0x2f, 0xd7, 0x83, 0x15, 0x35, 0xf5, 0xc2, 0x14, 0x5f, 0xee, 0xe5, 0x0b, 0x34, 0xad, 0x69, 0xda, + 0xcb, 0xb9, 0x0c, 0x05, 0xd6, 0xcb, 0x0e, 0x11, 0x1e, 0x64, 0x2b, 0xc6, 0xe3, 0x71, 0xe4, 0xbb, 0xc9, 0x07, 0x94, + 0x1b, 0x2c, 0x2e, 0xf7, 0xea, 0xcb, 0xa9, 0xdd, 0x14, 0xb6, 0x42, 0x9f, 0x61, 0x15, 0x05, 0x73, 0xc0, 0x9b, 0x6b, + 0x7a, 0x3b, 0x9b, 0x0b, 0xb2, 0xd9, 0xc5, 0x67, 0x0b, 0xdb, 0x20, 0x81, 0x78, 0x1c, 0x06, 0x6b, 0x72, 0x88, 0x94, + 0x78, 0x74, 0x4a, 0x53, 0x42, 0x01, 0xc8, 0x00, 0x5e, 0x4c, 0xe2, 0x2d, 0x24, 0xfd, 0xf7, 0xe0, 0x13, 0xec, 0xad, + 0x71, 0xc5, 0xc8, 0x79, 0xfe, 0xe1, 0x74, 0xc0, 0xe9, 0xcf, 0xed, 0x9d, 0xcf, 0x3d, 0x23, 0xa0, 0x46, 0xa9, 0x0f, + 0xe5, 0xc1, 0x7f, 0xd2, 0x15, 0x9d, 0xd6, 0x62, 0xbe, 0x13, 0xb1, 0x4a, 0x85, 0x2d, 0xf7, 0x32, 0xd8, 0xdf, 0xef, + 0x87, 0xe9, 0xff, 0xab, 0x6b, 0x43, 0x55, 0x7e, 0xfe, 0xb7, 0x35, 0xfc, 0x27, 0xbd, 0x0e, 0x4b, 0xcd, 0xfd, 0x6f, + 0x0d, 0x36, 0xfd, 0xf6, 0x1a, 0xea, 0xa1, 0x6d, 0xff, 0xd6, 0x03, 0x88, 0x3a, 0x28, 0x72, 0xb3, 0x27, 0xb2, 0xd2, + 0xaa, 0x73, 0x0f, 0x06, 0xda, 0xc2, 0xff, 0x9f, 0xe5, 0x3d, 0xcb, 0x9e, 0xad, 0x30, 0xb5, 0xf0, 0xf1, 0xfd, 0x8c, + 0x49, 0x00, 0xcb, 0x49, 0x84, 0x36, 0x0e, 0x39, 0xad, 0xfc, 0xb4, 0x46, 0xae, 0x43, 0x5a, 0xb1, 0x56, 0x01, 0xfd, + 0xb2, 0xa4, 0x4f, 0x10, 0xcf, 0x3d, 0x8c, 0xbd, 0x86, 0x92, 0xe0, 0x81, 0x7a, 0xbe, 0x75, 0x94, 0x1f, 0x49, 0xd3, + 0x62, 0x57, 0x4a, 0x7e, 0xe9, 0x9f, 0x3f, 0x66, 0xd9, 0x57, 0x96, 0x1f, 0x88, 0xa1, 0x26, 0xb7, 0xff, 0xc1, 0x42, + 0xda, 0x17, 0x24, 0x31, 0x16, 0xa6, 0x6e, 0x5d, 0x38, 0x9e, 0x38, 0xbd, 0x63, 0x5b, 0xb5, 0x19, 0x84, 0x17, 0x55, + 0x2d, 0x14, 0x67, 0xd7, 0x82, 0x32, 0xa6, 0xf7, 0xe9, 0x4c, 0x13, 0x0c, 0xa8, 0xa5, 0xe4, 0x9d, 0xdf, 0xf0, 0xef, + 0x6c, 0x85, 0x79, 0x57, 0x63, 0xee, 0xde, 0xc0, 0x3e, 0x1a, 0x39, 0x8c, 0xe3, 0x3e, 0x42, 0xa1, 0x6e, 0x70, 0x83, + 0x2f, 0x35, 0x12, 0xdd, 0xb3, 0x65, 0x1a, 0x46, 0x54, 0xf6, 0xbc, 0x05, 0x47, 0xe2, 0x9c, 0x71, 0x09, 0x32, 0xf4, + 0x08, 0x0d, 0xcb, 0x69, 0x78, 0x8b, 0x29, 0x6c, 0x2f, 0xef, 0x18, 0x77, 0x96, 0xad, 0xed, 0x55, 0x9a, 0x21, 0x90, + 0xce, 0x8b, 0xe0, 0xad, 0xe2, 0x49, 0xb8, 0x31, 0x6d, 0xcf, 0xd4, 0x83, 0x5d, 0x7b, 0x49, 0x2f, 0x6a, 0xf3, 0x37, + 0xb2, 0xdb, 0x7b, 0xe9, 0x98, 0x29, 0xcd, 0xeb, 0x9a, 0x2d, 0x5e, 0xbc, 0x20, 0x13, 0x7e, 0x1c, 0x5c, 0x1b, 0xb3, + 0x6e, 0xb7, 0x12, 0x80, 0xcc, 0x89, 0xc6, 0xd5, 0x5c, 0xec, 0x7f, 0xda, 0x1f, 0xa4, 0xf5, 0x60, 0xde, 0x3d, 0xb8, + 0x92, 0x11, 0x9b, 0xbf, 0x33, 0x37, 0x92, 0x7d, 0x93, 0x49, 0x0e, 0xb5, 0xa8, 0xaa, 0xe2, 0xc1, 0xbb, 0x17, 0xc9, + 0xdd, 0xd5, 0xa5, 0x25, 0xa3, 0xde, 0x20, 0x9f, 0xef, 0xd0, 0xcd, 0x3e, 0xac, 0xdb, 0x5a, 0xe3, 0xd4, 0xe2, 0x24, + 0x36, 0x4d, 0xac, 0xc2, 0xac, 0xa6, 0x13, 0xc1, 0xf6, 0xbf, 0xd6, 0xe0, 0x9a, 0x89, 0x3a, 0x14, 0xd6, 0x56, 0x28, + 0x94, 0x82, 0x1f, 0x25, 0x20, 0x61, 0xc6, 0x98, 0x13, 0x70, 0x82, 0x64, 0x4c, 0x27, 0x53, 0xa2, 0x69, 0x28, 0x37, + 0x3f, 0x88, 0x19, 0xbe, 0xcd, 0x28, 0x46, 0x40, 0x72, 0x3f, 0x32, 0x72, 0xc3, 0xc9, 0x92, 0x50, 0x23, 0xee, 0xf6, + 0xc9, 0x2f, 0x70, 0xcc, 0x78, 0x8e, 0xa5, 0xd4, 0xf8, 0x69, 0x7d, 0x7e, 0xcc, 0x7a, 0x3f, 0x5d, 0xff, 0xb0, 0xba, + 0xe7, 0xce, 0x3f, 0x28, 0xe9, 0xd4, 0x5c, 0x43, 0x66, 0x55, 0x00, 0xc8, 0x9b, 0xf2, 0xce, 0xb8, 0x8e, 0xd3, 0x7b, + 0xab, 0x44, 0x04, 0x2e, 0x55, 0xb4, 0xaa, 0x31, 0x82, 0xf9, 0x5e, 0x88, 0x18, 0x27, 0x2b, 0x07, 0xbe, 0xf7, 0x2b, + 0x54, 0x24, 0xe7, 0xe1, 0x73, 0xf6, 0x46, 0x9a, 0x3e, 0x16, 0x4d, 0x26, 0xcf, 0x1d, 0xf1, 0x55, 0x7c, 0x7e, 0x37, + 0x4b, 0x17, 0x9b, 0x6c, 0x0e, 0x52, 0xc1, 0x92, 0x86, 0xba, 0x80, 0xda, 0xd6, 0x62, 0x28, 0xd1, 0x8e, 0xd4, 0xea, + 0x84, 0x2f, 0xa5, 0x80, 0xa5, 0x32, 0x22, 0x67, 0xa8, 0xad, 0xc1, 0xa9, 0xa3, 0x34, 0x71, 0xdd, 0xab, 0x0a, 0xbe, + 0x28, 0xf3, 0xc8, 0x9d, 0x31, 0xfc, 0xd2, 0xc7, 0xeb, 0x90, 0x8c, 0x91, 0x69, 0x36, 0x70, 0x7e, 0x9a, 0x15, 0xeb, + 0x1d, 0x7c, 0x21, 0x74, 0xea, 0xd4, 0x4c, 0xbe, 0x40, 0xdd, 0x0a, 0x4a, 0x32, 0x1c, 0x7c, 0xad, 0x8a, 0x5b, 0xb4, + 0x12, 0xf7, 0x1f, 0x90, 0xf5, 0x49, 0x2b, 0x69, 0xd1, 0x9e, 0x56, 0x56, 0x04, 0xa5, 0x65, 0x52, 0xb5, 0x29, 0x4c, + 0xbf, 0x14, 0x1d, 0xd5, 0xd3, 0xba, 0x7b, 0x3f, 0xe4, 0x76, 0xc9, 0x25, 0xdb, 0x7d, 0x8b, 0x34, 0x34, 0xba, 0xda, + 0x15, 0x80, 0xb4, 0xeb, 0x4d, 0x5f, 0x85, 0xcc, 0x53, 0xd2, 0x94, 0x92, 0x1e, 0x1c, 0xb2, 0x23, 0x34, 0xbf, 0xef, + 0xc6, 0x56, 0x1d, 0xe9, 0x4e, 0x05, 0xfb, 0xce, 0x2f, 0x73, 0xbb, 0x19, 0x9c, 0xc4, 0xe7, 0x36, 0x7e, 0xed, 0x11, + 0x40, 0xb6, 0xa8, 0x84, 0xaf, 0x4d, 0x39, 0x68, 0x97, 0x5f, 0xe2, 0x99, 0x9a, 0x1d, 0x0a, 0xef, 0xf3, 0xd6, 0x69, + 0xba, 0x75, 0x6c, 0x94, 0x4a, 0x1e, 0x7e, 0xa3, 0x42, 0xb6, 0x62, 0x77, 0x56, 0xb8, 0x00, 0x73, 0xfe, 0xaa, 0x20, + 0xea, 0x4a, 0x56, 0xdb, 0x45, 0x8d, 0xc1, 0x06, 0xda, 0x38, 0xd4, 0x2b, 0x44, 0xcc, 0x3b, 0x46, 0x39, 0x42, 0x87, + 0xa4, 0x43, 0x49, 0x27, 0xd3, 0x40, 0x4e, 0xac, 0x3a, 0x24, 0xd8, 0x9f, 0x8e, 0x94, 0x03, 0xf8, 0x9f, 0x4c, 0x91, + 0xe5, 0x9f, 0xea, 0x55, 0xce, 0xd4, 0x29, 0xfe, 0x5c, 0xb2, 0x6b, 0x76, 0x94, 0x5a, 0x4d, 0x35, 0xee, 0x17, 0x4d, + 0x01, 0xa3, 0x52, 0x5e, 0xcb, 0x8e, 0xdc, 0xcc, 0x91, 0x14, 0xff, 0x60, 0xb2, 0xf4, 0xa4, 0x7f, 0x7c, 0xc8, 0xa5, + 0xaf, 0x9c, 0x7b, 0xf5, 0xce, 0x22, 0xa7, 0x2a, 0xdd, 0xfd, 0x34, 0x77, 0x9e, 0xfe, 0xfe, 0x92, 0x9d, 0x1f, 0xfd, + 0xc5, 0x43, 0x74, 0x86, 0xbf, 0x60, 0x43, 0xec, 0xc1, 0xda, 0x65, 0xe1, 0xc9, 0xeb, 0xf3, 0x43, 0xa3, 0x4f, 0x19, + 0x58, 0xf2, 0xee, 0x82, 0x96, 0x40, 0x99, 0xd7, 0x94, 0xa5, 0x5a, 0xdf, 0x17, 0xd3, 0xa7, 0x2b, 0x76, 0xbe, 0x98, + 0x55, 0x5b, 0x6d, 0xdf, 0x97, 0xd5, 0x6d, 0x75, 0xff, 0x72, 0xf6, 0xe1, 0xaf, 0xdb, 0x7b, 0x3e, 0x31, 0x01, 0x08, + 0xec, 0xf4, 0x50, 0xf5, 0x8b, 0x9f, 0xab, 0xb2, 0x98, 0xaa, 0xba, 0x38, 0xab, 0xc6, 0xc5, 0x79, 0x35, 0x3d, 0xfc, + 0x74, 0xc4, 0x0f, 0x3c, 0x12, 0x86, 0xd5, 0x89, 0x06, 0x59, 0x5b, 0xfc, 0xd2, 0xd4, 0x32, 0xcb, 0x27, 0x8a, 0xdd, + 0x4a, 0xad, 0x3f, 0xed, 0xd2, 0xf8, 0xd3, 0x64, 0x79, 0x23, 0x05, 0xbd, 0x52, 0xd1, 0x2e, 0x27, 0xb6, 0xd3, 0x4c, + 0x2c, 0x48, 0x2c, 0x65, 0xa7, 0xbd, 0xb5, 0x0e, 0x39, 0x83, 0x41, 0x6f, 0xbf, 0xe4, 0x1a, 0xcf, 0x22, 0x8c, 0x99, + 0xbc, 0xa1, 0xb7, 0x4c, 0x05, 0x5f, 0xa1, 0x1a, 0x33, 0xeb, 0x3b, 0x51, 0x47, 0x12, 0x0b, 0x82, 0x18, 0xba, 0xd4, + 0x49, 0xed, 0xed, 0xd2, 0xd5, 0xad, 0xab, 0xbe, 0x04, 0x70, 0x2d, 0xd6, 0x94, 0x9e, 0xfa, 0xa2, 0x46, 0x31, 0x3a, + 0x2a, 0x4b, 0x66, 0xaa, 0x84, 0x8a, 0x1e, 0x62, 0x7d, 0xcb, 0xbc, 0xce, 0xca, 0x73, 0x33, 0x4c, 0xd3, 0x2d, 0xcd, + 0x00, 0x5f, 0xd1, 0x85, 0xac, 0xcc, 0x05, 0x6f, 0x29, 0x99, 0xd6, 0x23, 0xe3, 0x54, 0xd3, 0xba, 0x7a, 0x44, 0xf6, + 0xf2, 0x97, 0xb7, 0x40, 0x64, 0x1f, 0xfa, 0xa2, 0xf6, 0x59, 0x94, 0xad, 0x30, 0x89, 0x41, 0xa6, 0x21, 0xe4, 0x28, + 0x0d, 0xd1, 0x88, 0xb3, 0x78, 0xb4, 0xab, 0x20, 0xb1, 0xf1, 0x59, 0x7e, 0xcd, 0x8c, 0xbd, 0x0e, 0x20, 0x16, 0xa8, + 0xb8, 0x2c, 0xbd, 0xe0, 0xff, 0x41, 0x0d, 0xe5, 0xbe, 0xe9, 0x7f, 0xa0, 0x98, 0x14, 0xca, 0xcd, 0xd0, 0x8f, 0x4b, + 0xae, 0x60, 0x13, 0x62, 0xd0, 0x83, 0x15, 0x51, 0x9d, 0xc5, 0xbe, 0x45, 0x9d, 0x40, 0x0a, 0x38, 0x50, 0x9c, 0x41, + 0xe3, 0x44, 0x01, 0x8e, 0x06, 0xad, 0xb5, 0x48, 0x85, 0x50, 0x78, 0x3f, 0xea, 0xaa, 0x75, 0x39, 0xd2, 0xd0, 0x4d, + 0xa4, 0xdf, 0xea, 0xd7, 0x56, 0x94, 0xc1, 0x9c, 0x5f, 0xae, 0xbc, 0xf9, 0xa0, 0xe4, 0xef, 0xdb, 0x3f, 0xa9, 0x0b, + 0x54, 0xf4, 0x0e, 0x1c, 0x46, 0xb4, 0x39, 0x62, 0x6c, 0x61, 0x71, 0x18, 0x5b, 0xea, 0x09, 0xb1, 0xfe, 0x0e, 0x3d, + 0xc2, 0xd9, 0x37, 0x49, 0xad, 0x79, 0x39, 0x99, 0xe5, 0x76, 0x3b, 0xba, 0xdd, 0xf9, 0x99, 0x29, 0xfc, 0xa4, 0xe6, + 0x60, 0x51, 0xef, 0x49, 0xa4, 0x01, 0xba, 0x5e, 0x38, 0x8f, 0xc0, 0xf5, 0x28, 0x49, 0xc1, 0x64, 0x40, 0x13, 0x1a, + 0x3b, 0x62, 0x65, 0xc5, 0x59, 0x1a, 0x8d, 0xce, 0x85, 0xab, 0xa2, 0xfa, 0xfb, 0xcb, 0x62, 0x2e, 0x00, 0x8c, 0x20, + 0xf4, 0xc1, 0x1b, 0xbb, 0x9d, 0x36, 0xbd, 0xda, 0x96, 0x34, 0xc4, 0x11, 0x44, 0x65, 0x41, 0xc5, 0x2e, 0xa8, 0x3a, + 0xda, 0x2f, 0xa8, 0x1c, 0x27, 0xd5, 0x90, 0x9f, 0x7a, 0x65, 0xb9, 0x0b, 0xfe, 0xdc, 0xa3, 0x5a, 0xfd, 0xf3, 0x43, + 0xc3, 0x53, 0xfd, 0x43, 0x98, 0xf7, 0x95, 0xf2, 0x3c, 0x97, 0x7c, 0x6c, 0x12, 0xc9, 0xd5, 0x56, 0x05, 0x1f, 0x1e, + 0x4a, 0x7a, 0x2b, 0x6a, 0x16, 0x58, 0x6f, 0x0f, 0xcf, 0x6b, 0xcf, 0x61, 0xc6, 0x8e, 0xfa, 0x25, 0x51, 0x37, 0x67, + 0xff, 0x0d, 0x06, 0xf6, 0x9b, 0x56, 0x72, 0xae, 0x9b, 0xf5, 0x9e, 0x27, 0xc5, 0x7a, 0x3d, 0xbf, 0xa2, 0x81, 0x8d, + 0x7d, 0xf6, 0x99, 0x3f, 0xa0, 0x61, 0x90, 0x3d, 0x5d, 0x37, 0xe7, 0xb4, 0xce, 0xce, 0xb9, 0x72, 0xd8, 0x69, 0x33, + 0x7e, 0xd2, 0xbd, 0xe5, 0xa0, 0xda, 0x02, 0xf9, 0x9d, 0xfd, 0x84, 0x38, 0x69, 0xf9, 0xf9, 0x69, 0xb4, 0x33, 0x0b, + 0x21, 0x0f, 0xce, 0x76, 0x2b, 0x20, 0xe5, 0x65, 0x76, 0x01, 0x49, 0x73, 0xa1, 0xe7, 0x38, 0x2a, 0x45, 0x82, 0x2f, + 0x03, 0x66, 0xdd, 0x35, 0x02, 0xd3, 0xf5, 0x6e, 0x65, 0xde, 0xc5, 0xaa, 0x06, 0x9d, 0xd7, 0x36, 0x6d, 0xdf, 0x7c, + 0xa5, 0x3b, 0x9e, 0xbe, 0x28, 0x16, 0x3b, 0xac, 0xdc, 0xe5, 0x20, 0x7f, 0xaf, 0x04, 0x1e, 0x05, 0xf0, 0x5e, 0x4c, + 0xd2, 0x4f, 0xf0, 0x74, 0x27, 0x13, 0x98, 0xa8, 0x86, 0xa4, 0x6c, 0x75, 0x77, 0x23, 0x9b, 0x51, 0x35, 0xd0, 0x29, + 0x47, 0x8e, 0x78, 0xf5, 0xb3, 0xf6, 0x98, 0x07, 0x3b, 0xf7, 0xad, 0x17, 0x7e, 0x94, 0x0d, 0x15, 0x96, 0x67, 0x0c, + 0x0d, 0x38, 0x65, 0x58, 0x5c, 0xc6, 0x60, 0x40, 0x6e, 0xae, 0xe3, 0x46, 0x0a, 0xcd, 0x3f, 0x47, 0x3f, 0xa6, 0xa0, + 0x06, 0xea, 0x8d, 0xeb, 0xf1, 0xa1, 0x19, 0xec, 0x97, 0xbf, 0x01, 0x8f, 0x0f, 0x32, 0xa0, 0x9a, 0x85, 0xce, 0x68, + 0xe3, 0x69, 0x9e, 0x7f, 0xd2, 0xb7, 0xb9, 0xe4, 0xfc, 0x47, 0xff, 0x34, 0x1b, 0xa7, 0xce, 0xc9, 0x99, 0x26, 0xc1, + 0x79, 0x0a, 0x5d, 0x9d, 0xfd, 0x7f, 0x97, 0x6c, 0x64, 0x15, 0x2f, 0x9a, 0x47, 0x71, 0x75, 0x81, 0x28, 0xaa, 0xf5, + 0x91, 0x67, 0xed, 0xce, 0x5e, 0xec, 0x7b, 0x38, 0x0c, 0x7a, 0x83, 0x0f, 0x7e, 0xaa, 0xf2, 0x24, 0x66, 0xfd, 0xca, + 0x44, 0xca, 0x25, 0x7e, 0x4a, 0x5d, 0xd9, 0xd7, 0x49, 0xb3, 0x0f, 0x97, 0xa6, 0x34, 0x1c, 0xd8, 0x94, 0x62, 0x8d, + 0x0a, 0xb0, 0x5f, 0x89, 0xd2, 0xb7, 0x76, 0xce, 0xd0, 0x07, 0xff, 0xac, 0x0a, 0x2c, 0x4e, 0xeb, 0x32, 0x40, 0x52, + 0xd7, 0xe3, 0xca, 0x7e, 0x3d, 0x09, 0x88, 0x8b, 0x7c, 0x85, 0x36, 0x47, 0x8c, 0x51, 0x91, 0x0b, 0xd1, 0x41, 0xe6, + 0xaa, 0x62, 0xa2, 0xd6, 0xa7, 0x17, 0xb4, 0xfb, 0x6e, 0x22, 0x2e, 0xd4, 0xd0, 0xf9, 0x57, 0x27, 0x16, 0x94, 0x36, + 0xc7, 0xf6, 0x8e, 0xd0, 0x23, 0x97, 0xf1, 0x11, 0x41, 0x12, 0x5f, 0x4f, 0x61, 0xde, 0x7e, 0xc7, 0x8f, 0xab, 0x08, + 0x20, 0x81, 0x77, 0x8b, 0xb8, 0x19, 0x18, 0x4a, 0x12, 0xa8, 0x9a, 0x5a, 0xeb, 0x01, 0x13, 0xf3, 0x4e, 0x47, 0xe1, + 0x56, 0x54, 0x20, 0xf0, 0x10, 0x99, 0xd8, 0x83, 0x44, 0x56, 0x8f, 0xa2, 0x87, 0x3b, 0xda, 0xe9, 0x4a, 0xa6, 0x68, + 0x04, 0x25, 0xda, 0xf4, 0x90, 0xa4, 0x87, 0x2f, 0x9b, 0x89, 0xde, 0x89, 0x73, 0xd3, 0x1f, 0xf5, 0x5e, 0xcb, 0xfe, + 0x77, 0x5d, 0x47, 0xf6, 0x2e, 0x63, 0x44, 0xcc, 0xe1, 0x51, 0xb6, 0x9e, 0xac, 0x8e, 0xdb, 0x3e, 0xe4, 0xdc, 0x0b, + 0x8a, 0x01, 0x68, 0x6f, 0x0e, 0xdd, 0x77, 0xa5, 0x44, 0xad, 0xeb, 0xd6, 0x43, 0xca, 0x35, 0x12, 0xfd, 0xc5, 0xf7, + 0xe7, 0x77, 0xb5, 0xc9, 0xc9, 0x26, 0x0a, 0x15, 0x4d, 0xf2, 0x18, 0x44, 0x87, 0x97, 0xc6, 0x30, 0xea, 0xc5, 0xc5, + 0x18, 0xb1, 0xa7, 0xd3, 0x28, 0x6e, 0x61, 0x31, 0x5a, 0x65, 0x6f, 0x11, 0x62, 0x5d, 0x3a, 0x35, 0x4c, 0x51, 0xf5, + 0xdf, 0x9f, 0x46, 0xb5, 0x3b, 0x05, 0x11, 0xf8, 0x7a, 0xee, 0x58, 0xb2, 0x0b, 0xa8, 0x97, 0xf3, 0x77, 0xac, 0x68, + 0xd3, 0x69, 0x1f, 0x84, 0x71, 0x8c, 0xcc, 0x7b, 0xf9, 0xb6, 0x08, 0x31, 0x94, 0x12, 0xa4, 0xe0, 0x6b, 0xc7, 0x30, + 0x08, 0x0e, 0xf3, 0xf2, 0x31, 0xb4, 0xff, 0x10, 0xee, 0xc8, 0x8c, 0x31, 0x99, 0xe2, 0xde, 0x00, 0xeb, 0x0d, 0x77, + 0xd8, 0x47, 0x47, 0xbd, 0xd2, 0xe4, 0x4e, 0x12, 0x7b, 0x9a, 0x49, 0x8e, 0xde, 0xed, 0xd2, 0x28, 0x53, 0x3a, 0x7c, + 0x33, 0x89, 0xf8, 0x56, 0x9c, 0x10, 0xa9, 0xba, 0xac, 0xad, 0xae, 0xfd, 0xbe, 0x74, 0x1c, 0xdd, 0xb3, 0x6b, 0xbd, + 0x8f, 0x62, 0x6c, 0xd5, 0x9b, 0x9a, 0x6d, 0xea, 0xa7, 0xa1, 0x40, 0x8e, 0x0e, 0x77, 0xba, 0x95, 0x4c, 0xc7, 0xea, + 0xf2, 0x17, 0x6d, 0x5b, 0xe4, 0x0b, 0x03, 0x98, 0x9e, 0xba, 0xb7, 0x59, 0xed, 0x27, 0x44, 0x89, 0xf4, 0x81, 0x98, + 0x25, 0x3e, 0x4a, 0x01, 0xe3, 0x2b, 0xa7, 0x89, 0x6c, 0xf0, 0xb3, 0xfc, 0x5c, 0xc4, 0xed, 0xae, 0xf1, 0x9c, 0x4f, + 0x00, 0xbd, 0x1f, 0x8f, 0xb3, 0x33, 0x68, 0xe7, 0xdb, 0x74, 0xa6, 0x53, 0x79, 0x31, 0xfd, 0xb3, 0xff, 0xcf, 0xf4, + 0x40, 0xfd, 0x01, 0x24, 0x1a, 0xff, 0xf7, 0x22, 0x93, 0xd7, 0x6a, 0x24, 0x26, 0x07, 0x31, 0xea, 0x1e, 0x14, 0x8b, + 0x68, 0x08, 0xe0, 0x2b, 0x2f, 0x88, 0x1b, 0x1c, 0x1e, 0x15, 0x3e, 0x4d, 0xef, 0x0e, 0xe4, 0x70, 0xa7, 0xe3, 0x49, + 0x5b, 0xdc, 0x57, 0xc9, 0xcd, 0x8c, 0xfd, 0x3e, 0x83, 0x68, 0x18, 0x14, 0x7d, 0x81, 0x41, 0x29, 0xe4, 0xe7, 0x4b, + 0xf1, 0xa5, 0x99, 0xab, 0x2b, 0xa3, 0xa4, 0xb5, 0x82, 0xf5, 0x2a, 0xa4, 0x06, 0x12, 0xef, 0xa5, 0xf0, 0x19, 0xf4, + 0x14, 0x8a, 0xfd, 0xfe, 0xd4, 0x29, 0x27, 0x68, 0x2f, 0xab, 0xd2, 0xa4, 0x57, 0x92, 0xdb, 0x7b, 0x67, 0x1d, 0xfd, + 0x04, 0x28, 0xc7, 0x0f, 0xa2, 0xc5, 0xd7, 0x0e, 0x8b, 0x72, 0xbb, 0x54, 0x75, 0x1c, 0x43, 0xf0, 0xfc, 0xc9, 0xb3, + 0xb0, 0x5d, 0x91, 0x9e, 0xfe, 0x6d, 0xb1, 0xe9, 0xbb, 0x73, 0xab, 0xe1, 0xff, 0xe4, 0xb3, 0x3f, 0xf0, 0x36, 0x3d, + 0xeb, 0xcf, 0xd8, 0x48, 0xe5, 0x5d, 0xc2, 0xe5, 0x36, 0xb1, 0xf9, 0x02, 0x86, 0xe1, 0x71, 0x7b, 0x9e, 0x08, 0x89, + 0xfd, 0xa6, 0x30, 0xb3, 0xc7, 0xb1, 0x68, 0x25, 0xc2, 0xdf, 0xee, 0x46, 0xde, 0xf9, 0x4f, 0x87, 0x25, 0x08, 0xc3, + 0xb9, 0x71, 0xa6, 0xdf, 0x33, 0xda, 0x7f, 0x9a, 0xa7, 0x4f, 0x7f, 0x77, 0xc9, 0xe9, 0x8f, 0xfe, 0x69, 0xf6, 0xbd, + 0x7d, 0x55, 0xa2, 0x77, 0xc0, 0x66, 0xdf, 0x44, 0x8c, 0x9a, 0xbc, 0x9e, 0x53, 0x0e, 0x7a, 0x44, 0x57, 0x33, 0xe1, + 0xe5, 0x09, 0x5c, 0xa0, 0x61, 0x54, 0xe7, 0x3d, 0xcf, 0xc1, 0x0b, 0x65, 0xbb, 0xa3, 0x58, 0x92, 0x68, 0xb3, 0x90, + 0x3b, 0xf4, 0x53, 0x83, 0x28, 0xc1, 0xac, 0xfb, 0x49, 0xb2, 0x47, 0x6d, 0x35, 0x4c, 0xac, 0x52, 0x5d, 0x7c, 0xe7, + 0x5a, 0x26, 0x29, 0xe5, 0x55, 0xbc, 0x53, 0x89, 0xbc, 0xf9, 0x21, 0xcc, 0x98, 0x0d, 0x46, 0x2f, 0x84, 0xb0, 0xdf, + 0x29, 0x02, 0x23, 0x47, 0x15, 0x2c, 0x24, 0x7e, 0xbb, 0x03, 0x24, 0xde, 0xbe, 0x0b, 0xd2, 0x57, 0x12, 0x20, 0x5f, + 0xcb, 0x96, 0x53, 0x9b, 0x9d, 0x1b, 0xe1, 0xb0, 0x47, 0xe9, 0x1b, 0xef, 0x91, 0x6f, 0x64, 0xd2, 0x56, 0xa9, 0x1f, + 0x03, 0xcc, 0xce, 0xd6, 0x61, 0x64, 0xc4, 0x0e, 0xe4, 0x10, 0x53, 0xb1, 0x03, 0x04, 0xb3, 0x0e, 0xfd, 0x1c, 0xf8, + 0x63, 0xd7, 0x0d, 0x40, 0x34, 0x6b, 0x2e, 0x7d, 0x92, 0xb1, 0x9d, 0x1c, 0x8e, 0x4d, 0x04, 0xe3, 0x7d, 0xa9, 0xfb, + 0xac, 0x79, 0x8a, 0x94, 0x6a, 0x89, 0x14, 0x34, 0x20, 0xbd, 0x8a, 0x3b, 0xf7, 0x6c, 0x0e, 0x46, 0x9c, 0xec, 0xef, + 0x4a, 0xa9, 0x3e, 0xdc, 0xb8, 0xcb, 0xa1, 0x71, 0x5e, 0x1e, 0xb0, 0x8b, 0xcd, 0xa0, 0x04, 0xda, 0xe9, 0x34, 0x4f, + 0xd6, 0x1a, 0xcc, 0xb9, 0x26, 0x25, 0x29, 0x0b, 0x9f, 0x90, 0x19, 0xb9, 0xf9, 0xbe, 0xbc, 0xbe, 0xe5, 0xc3, 0x68, + 0x4e, 0x29, 0xd8, 0x2b, 0x7d, 0xd3, 0xa7, 0xfb, 0xba, 0xfc, 0xdc, 0x05, 0xdd, 0xda, 0x41, 0x2b, 0x17, 0x0f, 0xfb, + 0x93, 0x47, 0x02, 0xc8, 0x04, 0xf1, 0xc3, 0x0d, 0xcb, 0xee, 0xbe, 0x4f, 0x60, 0xf6, 0x8d, 0x5f, 0xec, 0xa7, 0x0c, + 0x83, 0x6f, 0xec, 0x66, 0x95, 0x60, 0x39, 0xfc, 0x3f, 0xf7, 0xcf, 0xb6, 0x5e, 0xec, 0x26, 0x87, 0xab, 0xfd, 0xba, + 0x7d, 0x06, 0x18, 0x7b, 0xbf, 0x5c, 0x27, 0x54, 0xc2, 0x48, 0x6d, 0xd1, 0xe4, 0xab, 0xc2, 0x99, 0x3d, 0x9c, 0x4c, + 0xd9, 0x4e, 0xa1, 0x16, 0x69, 0x1c, 0xd7, 0x39, 0x47, 0x5a, 0xa0, 0x8d, 0x65, 0xb1, 0x68, 0x14, 0x09, 0x9d, 0x60, + 0x8b, 0x8d, 0x1c, 0xf7, 0xc3, 0xfa, 0x6c, 0x98, 0xf1, 0x96, 0x28, 0xb4, 0xe0, 0x6c, 0xc4, 0x44, 0x90, 0x51, 0x35, + 0x06, 0xa1, 0x1d, 0x72, 0xb0, 0x00, 0xd5, 0xd0, 0x29, 0x82, 0xe7, 0xc6, 0x9f, 0x16, 0x3f, 0x2e, 0x0c, 0x5e, 0x42, + 0x32, 0x0c, 0x12, 0x40, 0x8a, 0xc9, 0x4a, 0xba, 0x71, 0x6f, 0xb7, 0x70, 0xbc, 0x2f, 0x98, 0x6a, 0xec, 0xa7, 0xdd, + 0xa3, 0x9b, 0x0e, 0xd4, 0x8b, 0x8f, 0x06, 0x86, 0xed, 0x8e, 0x21, 0xf3, 0xca, 0x88, 0xce, 0x44, 0xcf, 0xfb, 0x38, + 0xe9, 0xb1, 0x55, 0x98, 0x23, 0xcc, 0x08, 0xbe, 0x31, 0x99, 0x8d, 0x3c, 0xc2, 0xdd, 0x6e, 0x3f, 0x9a, 0xe3, 0xd8, + 0x1a, 0x7b, 0x85, 0x50, 0xa8, 0x78, 0xcb, 0x74, 0x37, 0xa1, 0x59, 0x87, 0xcd, 0x3d, 0xd4, 0xd9, 0x55, 0x06, 0xfa, + 0x2c, 0xab, 0x04, 0x27, 0xf2, 0xf6, 0xdb, 0xe8, 0x42, 0x03, 0x27, 0x68, 0x6b, 0xa3, 0x87, 0x7f, 0x88, 0xd0, 0xb7, + 0xa0, 0x4e, 0x38, 0x29, 0xdf, 0x19, 0x8f, 0x89, 0x41, 0xd4, 0x38, 0x4e, 0x95, 0x59, 0x4e, 0x4f, 0x76, 0x23, 0x57, + 0x4a, 0xae, 0xb0, 0x9c, 0x59, 0x5a, 0x36, 0x4b, 0x05, 0x78, 0xff, 0x51, 0x17, 0xc7, 0x84, 0x94, 0xab, 0x46, 0x6d, + 0xea, 0x81, 0x86, 0x4f, 0xa3, 0x95, 0x54, 0x56, 0x36, 0xf1, 0x87, 0x1e, 0xee, 0xf4, 0x07, 0xd1, 0xdd, 0x8a, 0x6a, + 0x93, 0xdb, 0xd0, 0x78, 0x42, 0x8f, 0x29, 0xec, 0x83, 0x45, 0xa0, 0xce, 0xa3, 0xf0, 0xf0, 0xf8, 0x3b, 0x26, 0x6f, + 0x24, 0xd1, 0xad, 0xc0, 0xcd, 0xe2, 0x07, 0x2e, 0x58, 0x24, 0x39, 0x5a, 0xc5, 0xd2, 0xbb, 0xd3, 0xb2, 0x35, 0xa9, + 0xfc, 0x84, 0xb6, 0xaf, 0xaf, 0xe5, 0x55, 0x0b, 0xac, 0xc4, 0xec, 0x55, 0x23, 0xf9, 0x45, 0x29, 0x0e, 0xec, 0x80, + 0x69, 0x91, 0x6b, 0x34, 0xcc, 0xd4, 0xb2, 0x79, 0x30, 0xee, 0xe9, 0x36, 0x1c, 0x4a, 0x67, 0x77, 0x7f, 0xa1, 0x09, + 0x0e, 0xa1, 0x29, 0xa9, 0x09, 0x93, 0x7c, 0x3c, 0xb5, 0x71, 0x62, 0x15, 0xb5, 0x60, 0xb2, 0xe5, 0xb8, 0xe5, 0xb5, + 0x3a, 0xa6, 0xea, 0xa5, 0xf7, 0x31, 0x90, 0x24, 0xd3, 0x38, 0xa1, 0x72, 0x70, 0x43, 0xbc, 0x42, 0xc1, 0x69, 0x7b, + 0x1a, 0x27, 0x76, 0x28, 0x6f, 0xff, 0x2a, 0xde, 0x56, 0x68, 0xfe, 0x15, 0x4e, 0xde, 0xcb, 0xf5, 0xbb, 0x6e, 0xb8, + 0x99, 0xd8, 0x0d, 0xbb, 0xfd, 0xab, 0x69, 0xab, 0x54, 0xec, 0xe9, 0xa4, 0xe7, 0x23, 0x1f, 0x00, 0xf8, 0xf3, 0xca, + 0x04, 0xf9, 0x64, 0x98, 0x11, 0xb5, 0x09, 0xc2, 0x4c, 0x65, 0xc4, 0xf8, 0xa6, 0x2a, 0x37, 0xb5, 0x68, 0x45, 0x62, + 0x49, 0x69, 0x1a, 0x67, 0xe7, 0x8e, 0x34, 0x3b, 0xee, 0x8e, 0xd8, 0x6d, 0x89, 0xb9, 0x7e, 0x9a, 0xf4, 0x34, 0x58, + 0x85, 0x22, 0x54, 0x9e, 0x50, 0xae, 0x29, 0x47, 0x7b, 0xd0, 0x8d, 0xba, 0x86, 0x0c, 0x86, 0x54, 0xa1, 0x8c, 0x5e, + 0xec, 0x3c, 0x22, 0x70, 0x54, 0xa1, 0x87, 0x0c, 0xa4, 0xa8, 0x88, 0x66, 0x33, 0x7e, 0x7c, 0xfe, 0x95, 0xa2, 0x2d, + 0xea, 0x06, 0xe1, 0x10, 0x80, 0xac, 0x77, 0x87, 0x43, 0x08, 0x5c, 0xff, 0x0e, 0xcb, 0xd6, 0xa8, 0x51, 0x46, 0x06, + 0x36, 0x64, 0x3d, 0x45, 0xfa, 0x8f, 0x51, 0x5d, 0x91, 0x49, 0xdd, 0xac, 0x50, 0x46, 0x90, 0x41, 0xcc, 0x3b, 0x4a, + 0x9b, 0x6f, 0x86, 0xd1, 0x91, 0x35, 0x8a, 0x30, 0x15, 0xbb, 0x41, 0xe1, 0xaa, 0x3f, 0x48, 0x91, 0x5d, 0x88, 0x38, + 0x05, 0x78, 0x77, 0x6a, 0x48, 0xd4, 0xac, 0xa9, 0x68, 0xf8, 0x18, 0x7a, 0xee, 0xcc, 0xbb, 0x0d, 0x07, 0x12, 0xc2, + 0x22, 0x35, 0xd8, 0x81, 0x68, 0x0b, 0x32, 0x16, 0xe1, 0x8d, 0x48, 0x34, 0xd4, 0x7b, 0x02, 0xf0, 0x6e, 0xdd, 0xa7, + 0xbc, 0x03, 0x80, 0x3e, 0x59, 0x39, 0x91, 0xee, 0x8f, 0x07, 0x72, 0x88, 0xb9, 0xd9, 0x91, 0xba, 0x43, 0x5c, 0x8a, + 0xf3, 0x89, 0x62, 0xbd, 0x20, 0x07, 0x91, 0xa0, 0x15, 0xaf, 0xc9, 0x45, 0x99, 0xb4, 0xf3, 0xae, 0x33, 0xd7, 0xb9, + 0x26, 0x9e, 0xe4, 0xa8, 0x33, 0x51, 0x4c, 0xee, 0x99, 0x7c, 0xad, 0xdb, 0xb0, 0xda, 0x41, 0x9f, 0x10, 0xe3, 0xc9, + 0x58, 0xa6, 0x1e, 0xd9, 0xd9, 0x78, 0x36, 0xe2, 0x50, 0x01, 0x2d, 0x1d, 0xdc, 0x72, 0xd9, 0xac, 0xf9, 0x19, 0x77, + 0xfc, 0xb0, 0x09, 0x1f, 0xad, 0xe2, 0xda, 0xf4, 0xe9, 0x65, 0x90, 0x06, 0xf3, 0xa1, 0xa4, 0xe0, 0x4a, 0xaa, 0xb1, + 0xef, 0x4d, 0x25, 0xb5, 0x7f, 0xb7, 0x99, 0x9a, 0xb5, 0x58, 0xf1, 0x64, 0x5c, 0x04, 0x91, 0xf9, 0xfa, 0xdd, 0xd4, + 0x8c, 0xa3, 0xdd, 0xb4, 0x20, 0x42, 0x5f, 0xe5, 0x62, 0x64, 0x39, 0xfd, 0xa6, 0x89, 0x37, 0x37, 0x84, 0x3e, 0x62, + 0xfa, 0xb3, 0x8d, 0x39, 0x3e, 0x3b, 0xbc, 0x50, 0x43, 0x0f, 0xda, 0x20, 0x22, 0x35, 0x4e, 0x77, 0xb0, 0x48, 0x64, + 0x4b, 0x78, 0x45, 0xd1, 0x8a, 0xb9, 0xfa, 0xe1, 0x90, 0xb1, 0x44, 0x26, 0x88, 0x34, 0xfa, 0xf1, 0xc3, 0x2e, 0x1d, + 0xb6, 0x1e, 0x86, 0xb1, 0x02, 0x5c, 0xe6, 0x25, 0x25, 0x6f, 0xac, 0xe0, 0xb7, 0x9f, 0x03, 0xd3, 0xbc, 0xdf, 0xde, + 0x35, 0xbd, 0x11, 0x2f, 0xd5, 0x8d, 0xd3, 0x3b, 0x14, 0x4a, 0x42, 0x94, 0xd3, 0xc6, 0xc5, 0xc5, 0x9c, 0x3d, 0x0d, + 0x2c, 0xf2, 0x72, 0xc5, 0xd2, 0x2e, 0x7e, 0x0d, 0xa2, 0x61, 0xc5, 0x3b, 0x08, 0xe9, 0x22, 0xbb, 0xce, 0xf0, 0x00, + 0x8d, 0xea, 0xe1, 0x1e, 0x6d, 0xd1, 0x05, 0x04, 0x99, 0x63, 0xf4, 0x68, 0xa0, 0x04, 0x14, 0x7c, 0xc5, 0x09, 0x74, + 0x95, 0xd6, 0xcc, 0xb3, 0x35, 0x32, 0x63, 0x02, 0x84, 0xd3, 0xfa, 0x93, 0x08, 0x2e, 0x21, 0x73, 0xb8, 0x54, 0xd8, + 0x82, 0x8c, 0x5a, 0x29, 0x4e, 0x46, 0x01, 0x4d, 0x9f, 0x88, 0xe3, 0x17, 0xbd, 0x4b, 0x01, 0x38, 0x7a, 0x2c, 0xac, + 0x24, 0xf0, 0x99, 0xc6, 0x15, 0xb3, 0xcb, 0xa0, 0x39, 0xd0, 0xb8, 0xf6, 0xb5, 0xd5, 0x18, 0x8b, 0x8d, 0xd7, 0xdf, + 0x43, 0x84, 0x0d, 0xf6, 0x94, 0x42, 0xac, 0x48, 0x74, 0x80, 0xac, 0x5c, 0x43, 0x27, 0xef, 0xd9, 0xd3, 0xb1, 0xb5, + 0x5c, 0x41, 0x17, 0x3a, 0x92, 0x70, 0xad, 0xc1, 0x66, 0xff, 0x11, 0xe0, 0x4c, 0x43, 0x5a, 0xcf, 0x0c, 0x2b, 0x72, + 0x99, 0x82, 0x1a, 0xf1, 0xaf, 0x53, 0x07, 0x8b, 0x7a, 0x48, 0x17, 0x71, 0x2a, 0xea, 0x99, 0x56, 0x16, 0xe8, 0x84, + 0x3a, 0x52, 0x43, 0x6c, 0x00, 0x05, 0x6f, 0x94, 0x9e, 0x70, 0xfa, 0xdd, 0xa5, 0xe7, 0xa8, 0x2c, 0xb8, 0x0e, 0xcd, + 0xe2, 0x0f, 0x51, 0x6d, 0x3c, 0xfd, 0xf8, 0x60, 0x06, 0x0f, 0xe2, 0xed, 0x59, 0xc0, 0x87, 0x89, 0xb7, 0x63, 0xe7, + 0x79, 0x67, 0x37, 0x01, 0xc1, 0xac, 0x34, 0x11, 0x92, 0x11, 0xe6, 0xce, 0xbd, 0xc3, 0xd6, 0xf8, 0x2b, 0x76, 0x7f, + 0x29, 0x14, 0x06, 0xdb, 0x91, 0x08, 0xf3, 0xb1, 0x18, 0x45, 0xa8, 0xed, 0xe5, 0xd7, 0x2c, 0x19, 0xc9, 0xef, 0xce, + 0x9b, 0x8b, 0xb8, 0x1d, 0xd8, 0xaa, 0x54, 0xa9, 0x1f, 0x10, 0x55, 0xed, 0xf7, 0xb2, 0x61, 0x9b, 0x85, 0x8f, 0x17, + 0x3d, 0x3b, 0xf1, 0xc1, 0x72, 0x3d, 0xc7, 0x92, 0xdf, 0x3f, 0x43, 0x40, 0xcd, 0x66, 0xfb, 0xd5, 0xe2, 0xa0, 0xcf, + 0xb5, 0xf5, 0x1b, 0xb5, 0x81, 0x7e, 0x42, 0x58, 0xe0, 0xfb, 0x79, 0x8d, 0x5c, 0x3c, 0xca, 0xe6, 0xfa, 0x81, 0xdf, + 0x78, 0xb5, 0xc0, 0x3e, 0xbb, 0x33, 0x37, 0x9c, 0x1b, 0xc2, 0xd0, 0xf6, 0x44, 0xe3, 0xfe, 0x89, 0x49, 0x08, 0xaf, + 0xb3, 0x8a, 0x29, 0x9d, 0xc8, 0xac, 0xf2, 0x4f, 0xfa, 0x9d, 0xbb, 0x9b, 0xf9, 0x08, 0x25, 0xda, 0xdf, 0x80, 0xf3, + 0x72, 0xd5, 0x7e, 0x4d, 0xf2, 0x8c, 0x96, 0x1e, 0xb0, 0xa9, 0xa5, 0x9f, 0xeb, 0x95, 0xea, 0x40, 0xe9, 0xbe, 0x03, + 0x09, 0x30, 0x50, 0x87, 0x19, 0xbf, 0x8f, 0xcd, 0x10, 0x6e, 0x4a, 0x30, 0x06, 0x9e, 0xe9, 0x3f, 0x7c, 0x81, 0x83, + 0xb3, 0x92, 0x81, 0x39, 0xa2, 0xe6, 0x15, 0x41, 0xc0, 0xe7, 0x12, 0x54, 0xc8, 0x6e, 0x05, 0xf2, 0xf3, 0xbc, 0x72, + 0xe4, 0x06, 0x90, 0x5b, 0x21, 0xa8, 0xb8, 0x27, 0xcf, 0x5c, 0x1a, 0xd0, 0x03, 0x50, 0xfe, 0xe1, 0x9c, 0x93, 0x84, + 0xfe, 0x26, 0xa0, 0xa8, 0xd1, 0x49, 0x7f, 0xfe, 0xb5, 0x66, 0x64, 0xf2, 0xe7, 0xb1, 0x5f, 0x79, 0xbc, 0xec, 0xe6, + 0x2d, 0xc8, 0x48, 0x1b, 0xdf, 0x86, 0x19, 0x99, 0x81, 0x8e, 0x55, 0x50, 0x5b, 0xf8, 0x42, 0xaa, 0x55, 0x40, 0xae, + 0x2e, 0x42, 0x8b, 0x14, 0xb7, 0x90, 0xd3, 0x9f, 0xb6, 0xb3, 0x90, 0x7f, 0x9a, 0x01, 0x8e, 0x59, 0xf9, 0xcf, 0xc6, + 0x15, 0x45, 0xf6, 0x10, 0x18, 0xcd, 0x8f, 0x2e, 0x15, 0xd4, 0xb4, 0x72, 0x12, 0x7f, 0x02, 0x72, 0x09, 0x12, 0x30, + 0x3e, 0xbf, 0x51, 0x7b, 0xff, 0x9d, 0xce, 0x52, 0x8b, 0xaa, 0x63, 0xa4, 0x9f, 0xfc, 0x1a, 0xf2, 0x1f, 0xe0, 0x47, + 0x1f, 0x91, 0xd2, 0xd9, 0x3c, 0x5b, 0xfd, 0x89, 0xab, 0xd8, 0x65, 0x41, 0x75, 0x02, 0x2a, 0x48, 0x58, 0x05, 0xb5, + 0x06, 0x23, 0xfb, 0x1f, 0x16, 0xae, 0x46, 0x4c, 0xf3, 0xa7, 0x5b, 0xb4, 0x1a, 0xba, 0x57, 0xa0, 0xea, 0x70, 0x03, + 0x44, 0x0e, 0xdd, 0xa3, 0xea, 0x62, 0xc7, 0x99, 0xfe, 0x5b, 0x09, 0xd8, 0x38, 0x73, 0x82, 0xd3, 0xfd, 0x87, 0x97, + 0x2f, 0xd6, 0xf6, 0xa4, 0x5f, 0x32, 0xc3, 0xf8, 0x92, 0xba, 0x78, 0x70, 0x5f, 0xd3, 0xe2, 0x5b, 0xc2, 0xe4, 0xd3, + 0xfc, 0xf3, 0x49, 0xff, 0xea, 0x4b, 0xfe, 0xfc, 0xe8, 0x17, 0xbe, 0x95, 0xaf, 0x79, 0xf6, 0x4d, 0x5a, 0xa3, 0x1d, + 0xf6, 0x7a, 0x88, 0xbb, 0x37, 0xfd, 0xa1, 0x0e, 0xf9, 0x5a, 0xc5, 0xf8, 0xaf, 0x9e, 0xe9, 0xd3, 0x1f, 0x1e, 0x1f, + 0xdc, 0xa4, 0x77, 0x09, 0x39, 0xcd, 0x94, 0x57, 0xe7, 0xd6, 0xbe, 0xc1, 0x12, 0xb6, 0xf5, 0x26, 0xc1, 0xde, 0xa0, + 0x20, 0xd2, 0x48, 0xbb, 0x13, 0x21, 0x02, 0x95, 0x41, 0xae, 0x60, 0xc8, 0xcd, 0x71, 0xd4, 0xf0, 0x3f, 0x71, 0xc0, + 0x28, 0x97, 0x11, 0x55, 0xa5, 0x8a, 0xd3, 0xd1, 0xc1, 0x4c, 0xc0, 0x29, 0x44, 0x18, 0x21, 0xf9, 0x5e, 0xcd, 0x62, + 0x81, 0xce, 0x24, 0x0d, 0x3e, 0x7e, 0x27, 0x1d, 0x4b, 0x56, 0x5c, 0x5b, 0xe6, 0xeb, 0xfd, 0x27, 0xd9, 0x58, 0xf9, + 0x28, 0x90, 0x59, 0x79, 0x87, 0x02, 0xd5, 0x21, 0x05, 0x93, 0x8b, 0xd4, 0xf9, 0x88, 0x99, 0xf3, 0x91, 0x4a, 0x2f, + 0xd8, 0xaf, 0xe6, 0x06, 0xda, 0x8d, 0x3d, 0x1c, 0xec, 0x5b, 0x65, 0x6c, 0xc2, 0x90, 0xe4, 0x26, 0xbf, 0x46, 0x06, + 0xe5, 0xe4, 0xa6, 0x0d, 0x5b, 0xe0, 0x9b, 0x5f, 0x9f, 0xa1, 0x49, 0x0a, 0x9d, 0x8d, 0x7c, 0xcf, 0xc8, 0x83, 0xeb, + 0xfb, 0xb3, 0xd7, 0xfe, 0xd1, 0x94, 0x45, 0x13, 0xd6, 0x6e, 0xa9, 0x7d, 0x42, 0x28, 0x05, 0x2a, 0x08, 0x10, 0xa6, + 0xc2, 0x1a, 0x58, 0xd6, 0x21, 0x35, 0x87, 0x9a, 0xae, 0x3f, 0x67, 0x90, 0x23, 0xb5, 0xc3, 0xc4, 0xbe, 0x0d, 0x03, + 0x5f, 0x2b, 0xa5, 0xb7, 0x37, 0x50, 0xa5, 0x16, 0xf6, 0x59, 0x64, 0xa8, 0x33, 0x39, 0x57, 0x1c, 0x81, 0xd7, 0x2d, + 0x35, 0x33, 0x51, 0xe8, 0x2c, 0x1b, 0x69, 0x7e, 0x4a, 0x78, 0x45, 0x7f, 0x55, 0x04, 0x4c, 0x74, 0xd0, 0x99, 0xdc, + 0x9a, 0x8a, 0x02, 0x93, 0x90, 0xaa, 0xba, 0x62, 0xeb, 0x78, 0x0a, 0x84, 0x9f, 0xa7, 0x88, 0xed, 0x1a, 0x9f, 0x87, + 0xa2, 0x3c, 0xc9, 0xfb, 0x34, 0x77, 0x7d, 0xe8, 0x9c, 0x6b, 0x03, 0x91, 0x6c, 0x46, 0x74, 0xe1, 0x87, 0xd7, 0x54, + 0xa7, 0xc5, 0x6d, 0x4b, 0xf7, 0x69, 0x5e, 0x7c, 0xd2, 0xac, 0x4b, 0x2e, 0x7e, 0xf4, 0x97, 0x6c, 0x9b, 0x65, 0x08, + 0x45, 0x2a, 0x53, 0xf0, 0x6a, 0x9f, 0x2f, 0x8a, 0xc9, 0xf6, 0x7b, 0x58, 0xf2, 0xc4, 0x97, 0x41, 0x83, 0x89, 0x7e, + 0x71, 0xe7, 0x11, 0x1c, 0xaf, 0xba, 0xc8, 0x6a, 0x0e, 0x9c, 0xeb, 0x7a, 0x36, 0xe6, 0xb2, 0x35, 0x2e, 0x34, 0x42, + 0xa2, 0xae, 0x1a, 0x79, 0xd9, 0xbb, 0x80, 0x0c, 0x23, 0x29, 0x7b, 0x20, 0xc0, 0x9c, 0x5f, 0x5b, 0x46, 0xc3, 0xb3, + 0x90, 0x7c, 0xdd, 0x74, 0xba, 0xa0, 0x21, 0x54, 0x40, 0x83, 0x9f, 0xbf, 0x97, 0xd0, 0x9e, 0x0a, 0x7b, 0x7d, 0xfa, + 0x0b, 0xcf, 0x4c, 0x5a, 0x51, 0xc6, 0x33, 0x7d, 0x16, 0x4b, 0x9a, 0x27, 0x9d, 0xb1, 0x25, 0xcf, 0xfb, 0x58, 0xbe, + 0x4f, 0xe5, 0x58, 0xee, 0xee, 0x69, 0xba, 0xe4, 0x24, 0x35, 0xc7, 0x4a, 0x67, 0x42, 0x6d, 0x7c, 0x99, 0x4f, 0x22, + 0x12, 0x37, 0x78, 0x8a, 0x81, 0x58, 0xcf, 0x7d, 0x3a, 0x18, 0x4e, 0x15, 0xcd, 0xb7, 0xa7, 0xbb, 0x55, 0xe9, 0x9b, + 0x4d, 0xb5, 0x08, 0x71, 0x79, 0xc8, 0x62, 0xe2, 0xc3, 0x40, 0xd9, 0xd9, 0xa6, 0x8d, 0x9b, 0x04, 0x0f, 0xa4, 0xce, + 0xe5, 0xf4, 0x60, 0xb8, 0x88, 0xbd, 0xce, 0x3c, 0xa4, 0x57, 0x5c, 0xdc, 0x05, 0xe2, 0xbc, 0x42, 0x38, 0xa8, 0x57, + 0x8c, 0x6b, 0xf9, 0xa6, 0xd9, 0xbf, 0x9c, 0x4a, 0xe2, 0x92, 0x87, 0x6b, 0xd0, 0x4a, 0x35, 0x6b, 0x9d, 0x62, 0xab, + 0xa3, 0xf5, 0xf0, 0xdf, 0x37, 0x88, 0xac, 0xd8, 0x7c, 0xe1, 0x5b, 0xf9, 0xca, 0x76, 0x41, 0xc8, 0xec, 0x2f, 0xc7, + 0x17, 0x68, 0x3f, 0xcb, 0xd6, 0xda, 0x8b, 0xd3, 0xee, 0x74, 0xe3, 0x2e, 0xaf, 0x0f, 0xdb, 0x60, 0x7c, 0x85, 0x0e, + 0xdb, 0x05, 0x99, 0x7e, 0x62, 0xbd, 0xbe, 0xa7, 0x12, 0xfe, 0xe1, 0xfa, 0x87, 0xdf, 0xf4, 0xb9, 0x3f, 0xe6, 0x2a, + 0xe2, 0x00, 0x99, 0x97, 0xd4, 0x86, 0x71, 0xcd, 0x62, 0x2f, 0xe8, 0x56, 0x42, 0x7d, 0x6e, 0x9f, 0x01, 0x07, 0x37, + 0x37, 0xbd, 0xa7, 0x56, 0x03, 0x80, 0x45, 0x1c, 0x5d, 0xc3, 0x8e, 0x27, 0xe0, 0x13, 0x4a, 0x05, 0x61, 0x8f, 0x63, + 0x54, 0x29, 0x5d, 0xaa, 0x47, 0x1d, 0x3f, 0x0f, 0xa3, 0x3a, 0x10, 0x20, 0xe0, 0xf1, 0x98, 0xc7, 0x82, 0x44, 0x0d, + 0xea, 0x3c, 0x9a, 0xf2, 0x0a, 0x3e, 0x44, 0x02, 0xf6, 0x5d, 0xaf, 0xef, 0xc6, 0x37, 0xc3, 0x2b, 0x02, 0x5b, 0xf8, + 0x25, 0x8d, 0x6c, 0x23, 0x34, 0x8a, 0x47, 0xb9, 0x75, 0x4d, 0xf4, 0x45, 0x6d, 0xc7, 0xcc, 0x0b, 0x41, 0x56, 0x4f, + 0x78, 0x06, 0x0b, 0xe5, 0x82, 0xe0, 0x0b, 0xab, 0x80, 0xfb, 0x73, 0xa2, 0x1f, 0x83, 0x94, 0x1e, 0x8a, 0xe8, 0x88, + 0xd6, 0x91, 0xa9, 0xc1, 0x71, 0x8f, 0x65, 0x89, 0xe1, 0x3c, 0x42, 0xb0, 0xdb, 0x96, 0x35, 0x22, 0xab, 0xd5, 0x08, + 0x7e, 0xf3, 0x52, 0xd1, 0x3a, 0xa4, 0x24, 0x85, 0x0a, 0xd6, 0xd4, 0xf4, 0x5a, 0x10, 0xa9, 0x45, 0xe7, 0x7f, 0x02, + 0xc4, 0x69, 0x4f, 0x34, 0xad, 0xf6, 0x9c, 0x5a, 0x54, 0x1c, 0xda, 0x46, 0xc2, 0xdc, 0xa5, 0xc0, 0x95, 0x38, 0x70, + 0x00, 0xb1, 0xf4, 0xae, 0x48, 0xe4, 0x3d, 0xb4, 0x3f, 0xb8, 0x42, 0x9a, 0x4e, 0x8d, 0x77, 0x72, 0xca, 0x0d, 0x52, + 0x75, 0x61, 0xe4, 0x34, 0x12, 0x93, 0x2a, 0x27, 0x8c, 0x50, 0xc5, 0xed, 0x5a, 0x2d, 0xe1, 0xd4, 0x1b, 0xb7, 0x03, + 0x4f, 0x01, 0xef, 0x92, 0x21, 0x6c, 0xaf, 0x35, 0xe2, 0xcc, 0x18, 0xba, 0x7c, 0xf3, 0x9f, 0xba, 0x9d, 0x53, 0xfb, + 0x65, 0x70, 0x45, 0x87, 0x81, 0xaf, 0xc6, 0xab, 0x30, 0x79, 0x4a, 0x61, 0x5a, 0xfd, 0xa5, 0xeb, 0x33, 0x18, 0xf2, + 0x27, 0xf9, 0x4c, 0x43, 0x22, 0x48, 0xf1, 0x36, 0x7c, 0x78, 0x3f, 0xda, 0x06, 0xe4, 0x21, 0x70, 0x98, 0x8f, 0xc1, + 0xef, 0x44, 0xf6, 0x41, 0x6b, 0x44, 0x77, 0x8a, 0xb0, 0x20, 0x35, 0x77, 0xf8, 0xe8, 0x90, 0x6f, 0x1e, 0xea, 0x91, + 0x5c, 0x5e, 0x83, 0x00, 0x0a, 0x56, 0xd3, 0xc2, 0x9e, 0x3e, 0xb7, 0x79, 0xc6, 0x7b, 0xd0, 0x44, 0x47, 0xe1, 0x10, + 0x13, 0x3c, 0xe7, 0x0c, 0xed, 0x68, 0x27, 0x87, 0xe1, 0x31, 0xf4, 0x4a, 0x61, 0xee, 0x3f, 0x23, 0x72, 0xc3, 0xf9, + 0xb9, 0x9e, 0x31, 0x8d, 0x72, 0x9e, 0xb2, 0xaf, 0x57, 0x8d, 0x1e, 0xff, 0xb1, 0x03, 0x70, 0xff, 0xf4, 0xd7, 0x84, + 0xe4, 0x4f, 0x75, 0x0a, 0xdf, 0x57, 0x96, 0x84, 0xb7, 0x02, 0xff, 0x06, 0xaf, 0x59, 0x62, 0x70, 0x98, 0x82, 0x42, + 0xf9, 0x6b, 0x0b, 0x42, 0x6e, 0x73, 0x72, 0x6d, 0x0e, 0x97, 0xcf, 0x99, 0xe4, 0x0b, 0xb6, 0x09, 0xb5, 0x3e, 0x2b, + 0x70, 0xf0, 0xa6, 0xc9, 0x72, 0x3a, 0x8e, 0x9c, 0xf9, 0xad, 0xd8, 0x5c, 0x37, 0x26, 0x79, 0x14, 0x29, 0xfa, 0xcd, + 0xf4, 0x46, 0xde, 0x78, 0xb3, 0x10, 0x6d, 0x87, 0x5e, 0x9a, 0xd6, 0x8f, 0x2f, 0x08, 0x3f, 0x0d, 0xcb, 0x89, 0xd9, + 0x1f, 0x7c, 0x2f, 0xb0, 0xba, 0xc4, 0xc5, 0x80, 0x0c, 0xc3, 0xee, 0x58, 0xb0, 0x0e, 0x57, 0xd7, 0x68, 0xca, 0xb8, + 0x1c, 0xa4, 0x8a, 0x96, 0xee, 0x08, 0xa1, 0x9b, 0xb8, 0x28, 0xed, 0x4c, 0xd9, 0x7b, 0xf9, 0x3b, 0xb4, 0xfa, 0xb5, + 0x2a, 0xde, 0x5d, 0x12, 0x3e, 0xf8, 0xee, 0x5d, 0xd0, 0xdf, 0x74, 0xc8, 0xc6, 0xba, 0x5f, 0x3e, 0xbe, 0x54, 0x4d, + 0x16, 0x46, 0x83, 0x99, 0x4f, 0x79, 0x73, 0x76, 0x57, 0x65, 0x94, 0xd4, 0x35, 0x14, 0x46, 0x62, 0x8f, 0x1c, 0xe7, + 0xbd, 0x33, 0x59, 0xd7, 0xbb, 0x8e, 0x55, 0xe9, 0xf2, 0xb3, 0x04, 0x8b, 0xd6, 0x72, 0xef, 0xfe, 0x2c, 0xd5, 0xa7, + 0x50, 0x03, 0x69, 0xb3, 0x81, 0x0e, 0xdd, 0x46, 0x9b, 0x68, 0x9c, 0x49, 0xa0, 0xb4, 0x87, 0x2b, 0x2f, 0x6a, 0xfa, + 0x2c, 0x26, 0xd0, 0xba, 0x9d, 0x2d, 0x74, 0xb6, 0x0b, 0x4a, 0x83, 0xdb, 0x3f, 0xee, 0x76, 0xe9, 0xcc, 0xe0, 0xe3, + 0xfd, 0x83, 0x0c, 0xcb, 0xff, 0x1b, 0x55, 0xec, 0x9e, 0x1c, 0x80, 0x86, 0x35, 0x6f, 0x9b, 0x44, 0x44, 0x48, 0x58, + 0xdc, 0x7c, 0x72, 0xec, 0xfb, 0xc6, 0x97, 0xe8, 0xb9, 0xa1, 0x27, 0xe3, 0xc4, 0xf5, 0x52, 0x9d, 0xb2, 0x1e, 0x89, + 0x01, 0x7f, 0xd2, 0x39, 0x90, 0x68, 0x6b, 0x9a, 0xdd, 0x0e, 0xca, 0x81, 0xdd, 0x9b, 0x03, 0xeb, 0x8f, 0xf9, 0x06, + 0x23, 0x07, 0x2b, 0x9b, 0x3f, 0xb5, 0xb9, 0xed, 0xb4, 0x0e, 0x9f, 0x4d, 0xc6, 0xd2, 0xe3, 0xe1, 0x2b, 0xab, 0x23, + 0xb4, 0x35, 0x92, 0x15, 0x83, 0x6a, 0x6f, 0xf7, 0x63, 0x0f, 0x22, 0x7e, 0xa6, 0xee, 0xde, 0x45, 0xdd, 0xa1, 0xa5, + 0x67, 0xf6, 0xf6, 0xe0, 0xb1, 0x7f, 0x60, 0x8d, 0x43, 0xdd, 0xcb, 0x05, 0x08, 0x4b, 0xdc, 0x51, 0xd6, 0x56, 0x71, + 0x71, 0xfb, 0xe7, 0xd7, 0x0f, 0x9a, 0x83, 0x40, 0xe5, 0x70, 0x30, 0xd1, 0x8b, 0x11, 0xeb, 0xc8, 0xb1, 0x63, 0x18, + 0x23, 0x76, 0x73, 0x80, 0x94, 0x11, 0x23, 0xcd, 0x29, 0xdf, 0x07, 0x63, 0x5c, 0xf4, 0x46, 0xed, 0xc2, 0x86, 0x79, + 0x80, 0x15, 0xee, 0xa4, 0xaa, 0xc3, 0xc2, 0xc4, 0xfc, 0xba, 0xb5, 0x49, 0x72, 0xde, 0x91, 0xf5, 0xa9, 0xd9, 0xbb, + 0x12, 0x84, 0x3e, 0x1f, 0xfe, 0x8d, 0x8a, 0x78, 0xae, 0xb3, 0x87, 0x60, 0x02, 0x7e, 0xac, 0x3a, 0xec, 0x6f, 0xc1, + 0xa7, 0x0d, 0x27, 0xea, 0xe8, 0x93, 0xd1, 0x59, 0xe1, 0x80, 0x5d, 0x6b, 0xfa, 0x50, 0xc6, 0x43, 0x8f, 0x59, 0x18, + 0x2b, 0xd3, 0x5b, 0x15, 0x94, 0x0d, 0x9b, 0xa9, 0x2e, 0xa9, 0x06, 0xaa, 0x4c, 0x26, 0x99, 0x4c, 0xd9, 0x42, 0xce, + 0x00, 0xb6, 0xf7, 0x41, 0x72, 0x85, 0x88, 0x7a, 0x5f, 0x5a, 0x8f, 0xcc, 0x22, 0xae, 0x91, 0x23, 0xda, 0x63, 0x50, + 0x8b, 0x88, 0x77, 0x6a, 0x75, 0x94, 0xe4, 0xa3, 0x2f, 0x1f, 0x82, 0xd0, 0xb5, 0xa4, 0x3f, 0x9b, 0xa1, 0x84, 0x65, + 0x46, 0x2e, 0xdb, 0x4f, 0xdc, 0xbd, 0x3f, 0x8d, 0x7f, 0x9a, 0x08, 0x6d, 0x97, 0x67, 0xeb, 0xc1, 0xc8, 0xb5, 0x34, + 0x95, 0xd7, 0xb8, 0xa5, 0xc6, 0xb8, 0xe0, 0xa7, 0x38, 0xd2, 0xe6, 0x6b, 0xcd, 0xd3, 0x43, 0xdd, 0x7a, 0x1e, 0xb5, + 0x0f, 0xb2, 0xb6, 0x0e, 0xec, 0xc5, 0x42, 0x7b, 0x0a, 0x7b, 0xe7, 0xf8, 0xd0, 0xfd, 0xc5, 0xad, 0xcb, 0x4d, 0x95, + 0x8f, 0xce, 0x5c, 0x48, 0x64, 0x8e, 0x8a, 0xb7, 0x38, 0xc8, 0x07, 0xa0, 0x22, 0x92, 0xe1, 0xbd, 0x5b, 0x1e, 0x36, + 0xcf, 0xba, 0x47, 0x3d, 0xf6, 0xa0, 0x8c, 0x84, 0x8f, 0x77, 0x08, 0x89, 0x52, 0x21, 0xf6, 0xfc, 0x67, 0x92, 0x72, + 0x16, 0x0d, 0x95, 0xb7, 0x65, 0xe5, 0xf4, 0xf5, 0x3c, 0x92, 0x6a, 0x19, 0x0f, 0x78, 0x4f, 0x6e, 0xb6, 0x96, 0x13, + 0xc5, 0xad, 0xbe, 0xda, 0x5c, 0x82, 0xa0, 0x6c, 0xf4, 0x86, 0xdb, 0xb7, 0x11, 0x3b, 0x4e, 0xa0, 0x6d, 0xdb, 0x9f, + 0x5c, 0x2c, 0x45, 0xa9, 0x70, 0xc2, 0x58, 0x37, 0x39, 0x8a, 0xe6, 0x10, 0x86, 0x37, 0x6b, 0xab, 0x09, 0x1f, 0x70, + 0xc3, 0x31, 0x6f, 0x6f, 0x29, 0x87, 0x55, 0x2d, 0x9c, 0xa3, 0x48, 0xc6, 0xc4, 0xde, 0x2e, 0xa3, 0xdb, 0x5b, 0x85, + 0xfe, 0x13, 0xb2, 0xeb, 0xac, 0x56, 0xde, 0x04, 0x5f, 0x29, 0x88, 0x6c, 0xee, 0xc7, 0x67, 0xc6, 0x01, 0xd2, 0x0d, + 0xf0, 0xd7, 0x0a, 0x92, 0x55, 0x9e, 0xa8, 0xbc, 0x0a, 0x4c, 0xd3, 0x90, 0x82, 0xe1, 0x53, 0x7a, 0x0f, 0x96, 0xbc, + 0xe6, 0xcb, 0x66, 0xd7, 0x37, 0x17, 0x3f, 0xac, 0xf5, 0x10, 0x2f, 0x3b, 0xbd, 0xb5, 0x2a, 0x9c, 0xe0, 0x31, 0x49, + 0xfc, 0xba, 0xf4, 0xb3, 0xfd, 0x60, 0xe3, 0x96, 0x42, 0xed, 0x07, 0x9c, 0xd9, 0xba, 0xe7, 0x30, 0xb3, 0x49, 0x9f, + 0x01, 0x12, 0x16, 0x68, 0xdd, 0xc7, 0x22, 0x53, 0x60, 0xab, 0x01, 0x6e, 0x00, 0x23, 0xb6, 0x7d, 0xc8, 0x1e, 0xbd, + 0x29, 0x92, 0x2d, 0xe4, 0x7b, 0x3a, 0x72, 0xfb, 0x53, 0x4c, 0xef, 0x17, 0x75, 0x20, 0x9a, 0xaf, 0x03, 0x6e, 0xeb, + 0x81, 0x77, 0x1c, 0xa4, 0x48, 0x5c, 0x21, 0xa6, 0x49, 0xf7, 0x15, 0x5a, 0xb5, 0xba, 0x9b, 0x5c, 0xf6, 0xe7, 0x8e, + 0x93, 0xb5, 0xde, 0x86, 0xbb, 0xd8, 0xcf, 0xaa, 0x1d, 0xd2, 0x51, 0x03, 0xf8, 0xd2, 0xaf, 0x0c, 0x74, 0x7a, 0x9a, + 0xc2, 0x77, 0x25, 0x96, 0x4d, 0x08, 0x98, 0x3b, 0x28, 0xec, 0x2c, 0x90, 0x04, 0x2b, 0x9c, 0x38, 0x96, 0x77, 0x58, + 0x93, 0x17, 0xfa, 0x7a, 0x1c, 0x19, 0x18, 0x98, 0xb2, 0x27, 0x11, 0x61, 0xef, 0x2c, 0x52, 0x34, 0x6b, 0x19, 0xde, + 0x32, 0xd1, 0x93, 0x0f}; // Backwards compatibility alias #define INDEX_GZ INDEX_BR From 37a0cec53dc0e4e9d9c0c5848e220b6d8480964d Mon Sep 17 00:00:00 2001 From: Szpadel Date: Wed, 25 Feb 2026 17:12:03 +0100 Subject: [PATCH 196/609] [ac_dimmer] Use a shared ESP32 GPTimer for multiple dimmers (#13523) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/ac_dimmer/ac_dimmer.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp index 1e850a18fe..f731a8c753 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.cpp +++ b/esphome/components/ac_dimmer/ac_dimmer.cpp @@ -199,12 +199,19 @@ void AcDimmer::setup() { setTimer1Callback(&timer_interrupt); #endif #ifdef USE_ESP32 - dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ); - timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr); - // For ESP32, we can't use dynamic interval calculation because the timerX functions - // are not callable from ISR (placed in flash storage). - // Here we just use an interrupt firing every 50 µs. - timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0); + if (dimmer_timer == nullptr) { + dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ); + if (dimmer_timer == nullptr) { + ESP_LOGE(TAG, "Failed to create GPTimer for AC dimmer"); + this->mark_failed(); + return; + } + timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr); + // For ESP32, we can't use dynamic interval calculation because the timerX functions + // are not callable from ISR (placed in flash storage). + // Here we just use an interrupt firing every 50 µs. + timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0); + } #endif } From ede8235aaee8467be33dd33fd8c25c39957063e3 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Wed, 25 Feb 2026 17:46:28 +0100 Subject: [PATCH 197/609] [core] more accurate check for leap year and valid day_of_month (#14197) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/core/time.cpp | 2 +- esphome/core/time.h | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 554431c631..1aea18ac8d 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -8,7 +8,7 @@ namespace esphome { uint8_t days_in_month(uint8_t month, uint16_t year) { static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - if (month == 2 && (year % 4 == 0)) + if (month == 2 && (year % 4 == 0) && (year % 100 != 0 || year % 400 == 0)) return 29; return DAYS_IN_MONTH[month]; } diff --git a/esphome/core/time.h b/esphome/core/time.h index 87ebb5c221..d9ce86131c 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -70,14 +70,14 @@ struct ESPTime { /// @copydoc strftime(const std::string &format) std::string strftime(const char *format); - /// Check if this ESPTime is valid (all fields in range and year is greater than 2018) + /// Check if this ESPTime is valid (all fields in range and year is greater than or equal to 2019) bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); } /// Check if all time fields of this ESPTime are in range. bool fields_in_range() const { return this->second < 61 && this->minute < 60 && this->hour < 24 && this->day_of_week > 0 && - this->day_of_week < 8 && this->day_of_month > 0 && this->day_of_month < 32 && this->day_of_year > 0 && - this->day_of_year < 367 && this->month > 0 && this->month < 13; + this->day_of_week < 8 && this->day_of_year > 0 && this->day_of_year < 367 && this->month > 0 && + this->month < 13 && this->day_of_month > 0 && this->day_of_month <= days_in_month(this->month, this->year); } /** Convert a string to ESPTime struct as specified by the format argument. From 8bb577de644243c3822305d3720ddaec8b9864c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Feb 2026 12:23:13 -0600 Subject: [PATCH 198/609] [api] Split ProtoVarInt::parse into 32-bit and 64-bit phases (#14039) --- esphome/components/api/api_pb2.h | 1 - esphome/components/api/api_pb2_defines.h | 12 ++ esphome/components/api/proto.cpp | 17 +++ esphome/components/api/proto.h | 70 ++++++---- esphome/core/defines.h | 1 + script/api_protobuf/api_protobuf.py | 65 +++++++++- .../fixtures/varint_five_byte_device_id.yaml | 47 +++++++ .../test_varint_five_byte_device_id.py | 120 ++++++++++++++++++ 8 files changed, 301 insertions(+), 32 deletions(-) create mode 100644 esphome/components/api/api_pb2_defines.h create mode 100644 tests/integration/fixtures/varint_five_byte_device_id.yaml create mode 100644 tests/integration/test_varint_five_byte_device_id.py diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index c90873d993..c2675cefe4 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -2,7 +2,6 @@ // See script/api_protobuf/api_protobuf.py #pragma once -#include "esphome/core/defines.h" #include "esphome/core/string_ref.h" #include "proto.h" diff --git a/esphome/components/api/api_pb2_defines.h b/esphome/components/api/api_pb2_defines.h new file mode 100644 index 0000000000..8ebd60fb5d --- /dev/null +++ b/esphome/components/api/api_pb2_defines.h @@ -0,0 +1,12 @@ +// This file was automatically generated with a tool. +// See script/api_protobuf/api_protobuf.py +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_BLUETOOTH_PROXY +#ifndef USE_API_VARINT64 +#define USE_API_VARINT64 +#endif +#endif + +namespace esphome::api {} // namespace esphome::api diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 73a3bab12a..a252907fd7 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -7,6 +7,23 @@ namespace esphome::api { static const char *const TAG = "api.proto"; +#ifdef USE_API_VARINT64 +optional ProtoVarInt::parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed, + uint32_t result32) { + uint64_t result64 = result32; + uint32_t limit = std::min(len, uint32_t(10)); + for (uint32_t i = 4; i < limit; i++) { + uint8_t val = buffer[i]; + result64 |= uint64_t(val & 0x7F) << (i * 7); + if ((val & 0x80) == 0) { + *consumed = i + 1; + return ProtoVarInt(result64); + } + } + return {}; +} +#endif + uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id) { uint32_t count = 0; const uint8_t *ptr = buffer; diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 4522fc9665..c34f7744e6 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -1,5 +1,6 @@ #pragma once +#include "api_pb2_defines.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -110,59 +111,78 @@ class ProtoVarInt { #endif if (len == 0) return {}; - - // Most common case: single-byte varint (values 0-127) + // Fast path: single-byte varints (0-127) are the most common case + // (booleans, small enums, field tags). Avoid loop overhead entirely. if ((buffer[0] & 0x80) == 0) { *consumed = 1; return ProtoVarInt(buffer[0]); } - - // General case for multi-byte varints - // Since we know buffer[0]'s high bit is set, initialize with its value - uint64_t result = buffer[0] & 0x7F; - uint8_t bitpos = 7; - - // A 64-bit varint is at most 10 bytes (ceil(64/7)). Reject overlong encodings - // to avoid undefined behavior from shifting uint64_t by >= 64 bits. - uint32_t max_len = std::min(len, uint32_t(10)); - - // Start from the second byte since we've already processed the first - for (uint32_t i = 1; i < max_len; i++) { + // 32-bit phase: process remaining bytes with native 32-bit shifts. + // Without USE_API_VARINT64: cover bytes 1-4 (shifts 7, 14, 21, 28) — the uint32_t + // shift at byte 4 (shift by 28) may lose bits 32-34, but those are always zero for valid uint32 values. + // With USE_API_VARINT64: cover bytes 1-3 (shifts 7, 14, 21) so parse_wide handles + // byte 4+ with full 64-bit arithmetic (avoids truncating values > UINT32_MAX). + uint32_t result32 = buffer[0] & 0x7F; +#ifdef USE_API_VARINT64 + uint32_t limit = std::min(len, uint32_t(4)); +#else + uint32_t limit = std::min(len, uint32_t(5)); +#endif + for (uint32_t i = 1; i < limit; i++) { uint8_t val = buffer[i]; - result |= uint64_t(val & 0x7F) << uint64_t(bitpos); - bitpos += 7; + result32 |= uint32_t(val & 0x7F) << (i * 7); if ((val & 0x80) == 0) { *consumed = i + 1; - return ProtoVarInt(result); + return ProtoVarInt(result32); } } - - return {}; // Incomplete or invalid varint + // 64-bit phase for remaining bytes (BLE addresses etc.) +#ifdef USE_API_VARINT64 + return parse_wide(buffer, len, consumed, result32); +#else + return {}; +#endif } +#ifdef USE_API_VARINT64 + protected: + /// Continue parsing varint bytes 4-9 with 64-bit arithmetic. + /// Separated to keep 64-bit shift code (__ashldi3 on 32-bit platforms) out of the common path. + static optional parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed, uint32_t result32) + __attribute__((noinline)); + + public: +#endif + constexpr uint16_t as_uint16() const { return this->value_; } constexpr uint32_t as_uint32() const { return this->value_; } - constexpr uint64_t as_uint64() const { return this->value_; } constexpr bool as_bool() const { return this->value_; } constexpr int32_t as_int32() const { // Not ZigZag encoded - return static_cast(this->as_int64()); - } - constexpr int64_t as_int64() const { - // Not ZigZag encoded - return static_cast(this->value_); + return static_cast(this->value_); } constexpr int32_t as_sint32() const { // with ZigZag encoding return decode_zigzag32(static_cast(this->value_)); } +#ifdef USE_API_VARINT64 + constexpr uint64_t as_uint64() const { return this->value_; } + constexpr int64_t as_int64() const { + // Not ZigZag encoded + return static_cast(this->value_); + } constexpr int64_t as_sint64() const { // with ZigZag encoding return decode_zigzag64(this->value_); } +#endif protected: +#ifdef USE_API_VARINT64 uint64_t value_; +#else + uint32_t value_; +#endif }; // Forward declarations for decode_to_message, encode_message and encode_packed_sint32 diff --git a/esphome/core/defines.h b/esphome/core/defines.h index a1e3d1707f..673aa246fe 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -144,6 +144,7 @@ #define USE_API_HOMEASSISTANT_SERVICES #define USE_API_HOMEASSISTANT_STATES #define USE_API_NOISE +#define USE_API_VARINT64 #define USE_API_PLAINTEXT #define USE_API_USER_DEFINED_ACTIONS #define USE_API_CUSTOM_SERVICES diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index cc881caa5c..350947a8d6 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1913,6 +1913,37 @@ def build_type_usage_map( ) +def get_varint64_ifdef( + file_desc: descriptor.FileDescriptorProto, + message_ifdef_map: dict[str, str | None], +) -> tuple[bool, str | None]: + """Check if 64-bit varint fields exist and get their common ifdef guard. + + Returns: + (has_varint64, ifdef_guard) - has_varint64 is True if any fields exist, + ifdef_guard is the common guard or None if unconditional. + """ + varint64_types = { + FieldDescriptorProto.TYPE_INT64, + FieldDescriptorProto.TYPE_UINT64, + FieldDescriptorProto.TYPE_SINT64, + } + ifdefs: set[str | None] = { + message_ifdef_map.get(msg.name) + for msg in file_desc.message_type + if not msg.options.deprecated + for field in msg.field + if not field.options.deprecated and field.type in varint64_types + } + if not ifdefs: + return False, None + if None in ifdefs: + # At least one 64-bit varint field is unconditional, so the guard must be unconditional. + return True, None + ifdefs.discard(None) + return True, ifdefs.pop() if len(ifdefs) == 1 else None + + def build_enum_type(desc, enum_ifdef_map) -> tuple[str, str, str]: """Builds the enum type. @@ -2567,11 +2598,38 @@ def main() -> None: file = d.file[0] + # Build dynamic ifdef mappings early so we can emit USE_API_VARINT64 before includes + enum_ifdef_map, message_ifdef_map, message_source_map, used_messages = ( + build_type_usage_map(file) + ) + + # Find the ifdef guard for 64-bit varint fields (int64/uint64/sint64). + # Generated into api_pb2_defines.h so proto.h can include it, ensuring + # consistent ProtoVarInt layout across all translation units. + has_varint64, varint64_guard = get_varint64_ifdef(file, message_ifdef_map) + + # Generate api_pb2_defines.h — included by proto.h to ensure all translation + # units see USE_API_VARINT64 consistently (avoids ODR violations in ProtoVarInt). + defines_content = FILE_HEADER + defines_content += "#pragma once\n\n" + defines_content += '#include "esphome/core/defines.h"\n' + if has_varint64: + lines = [ + "#ifndef USE_API_VARINT64", + "#define USE_API_VARINT64", + "#endif", + ] + defines_content += "\n".join(wrap_with_ifdef(lines, varint64_guard)) + defines_content += "\n" + defines_content += "\nnamespace esphome::api {} // namespace esphome::api\n" + + with open(root / "api_pb2_defines.h", "w", encoding="utf-8") as f: + f.write(defines_content) + content = FILE_HEADER content += """\ #pragma once -#include "esphome/core/defines.h" #include "esphome/core/string_ref.h" #include "proto.h" @@ -2702,11 +2760,6 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint content += "namespace enums {\n\n" - # Build dynamic ifdef mappings for both enums and messages - enum_ifdef_map, message_ifdef_map, message_source_map, used_messages = ( - build_type_usage_map(file) - ) - # Simple grouping of enums by ifdef current_ifdef = None diff --git a/tests/integration/fixtures/varint_five_byte_device_id.yaml b/tests/integration/fixtures/varint_five_byte_device_id.yaml new file mode 100644 index 0000000000..08259869ca --- /dev/null +++ b/tests/integration/fixtures/varint_five_byte_device_id.yaml @@ -0,0 +1,47 @@ +esphome: + name: varint-5byte-test + # Define areas and devices - device_ids will be FNV hashes > 2^28, + # requiring 5-byte varint encoding that exercises the 32-bit parse boundary. + areas: + - id: test_area + name: Test Area + devices: + - id: sub_device_one + name: Sub Device One + area_id: test_area + - id: sub_device_two + name: Sub Device Two + area_id: test_area + +host: +api: +logger: + +# Switches on sub-devices so we can send commands with large device_id varints +switch: + - platform: template + name: Device Switch + device_id: sub_device_one + id: device_switch_one + optimistic: true + turn_on_action: + - logger.log: "Switch one on" + turn_off_action: + - logger.log: "Switch one off" + + - platform: template + name: Device Switch + device_id: sub_device_two + id: device_switch_two + optimistic: true + turn_on_action: + - logger.log: "Switch two on" + turn_off_action: + - logger.log: "Switch two off" + +sensor: + - platform: template + name: Device Sensor + device_id: sub_device_one + lambda: return 42.0; + update_interval: 0.1s diff --git a/tests/integration/test_varint_five_byte_device_id.py b/tests/integration/test_varint_five_byte_device_id.py new file mode 100644 index 0000000000..d34c2f03d6 --- /dev/null +++ b/tests/integration/test_varint_five_byte_device_id.py @@ -0,0 +1,120 @@ +"""Integration test for 5-byte varint parsing of device_id fields. + +Device IDs are FNV hashes (uint32) that frequently exceed 2^28 (268435456), +requiring 5 varint bytes. This test verifies that: +1. The firmware correctly decodes 5-byte varint device_id in incoming commands +2. The firmware correctly encodes large device_id values in state responses +3. Switch commands with large device_id reach the correct entity +""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import EntityState, SwitchInfo, SwitchState +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_varint_five_byte_device_id( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that device_id values requiring 5-byte varints parse correctly.""" + async with run_compiled(yaml_config), api_client_connected() as client: + device_info = await client.device_info() + devices = device_info.devices + assert len(devices) >= 2, f"Expected at least 2 devices, got {len(devices)}" + + # Verify at least one device_id exceeds the 4-byte varint boundary (2^28) + large_ids = [d for d in devices if d.device_id >= (1 << 28)] + assert len(large_ids) > 0, ( + "Expected at least one device_id >= 2^28 to exercise 5-byte varint path. " + f"Got device_ids: {[d.device_id for d in devices]}" + ) + + # Get entities + all_entities, _ = await client.list_entities_services() + switch_entities = [e for e in all_entities if isinstance(e, SwitchInfo)] + + # Find switches named "Device Switch" — one per sub-device + device_switches = [e for e in switch_entities if e.name == "Device Switch"] + assert len(device_switches) == 2, ( + f"Expected 2 'Device Switch' entities, got {len(device_switches)}" + ) + + # Verify switches have different device_ids matching the sub-devices + switch_device_ids = {s.device_id for s in device_switches} + assert len(switch_device_ids) == 2, "Switches should have different device_ids" + + # Subscribe to states and wait for initial states + loop = asyncio.get_running_loop() + states: dict[tuple[int, int], EntityState] = {} + switch_futures: dict[tuple[int, int], asyncio.Future[EntityState]] = {} + initial_done: asyncio.Future[bool] = loop.create_future() + + def on_state(state: EntityState) -> None: + key = (state.device_id, state.key) + states[key] = state + + if len(states) >= 3 and not initial_done.done(): + initial_done.set_result(True) + + if initial_done.done() and key in switch_futures: + fut = switch_futures[key] + if not fut.done() and isinstance(state, SwitchState): + fut.set_result(state) + + client.subscribe_states(on_state) + + try: + await asyncio.wait_for(initial_done, timeout=10.0) + except TimeoutError: + pytest.fail( + f"Timed out waiting for initial states. Got {len(states)} states" + ) + + # Verify state responses contain correct large device_id values + for device in devices: + device_states = [ + s for (did, _), s in states.items() if did == device.device_id + ] + assert len(device_states) > 0, ( + f"No states received for device '{device.name}' " + f"(device_id={device.device_id})" + ) + + # Test switch commands with large device_id varints — + # this is the critical path: the client encodes device_id as a varint + # in the SwitchCommandRequest, and the firmware must decode it correctly. + for switch in device_switches: + state_key = (switch.device_id, switch.key) + + # Turn on + switch_futures[state_key] = loop.create_future() + client.switch_command(switch.key, True, device_id=switch.device_id) + try: + await asyncio.wait_for(switch_futures[state_key], timeout=2.0) + except TimeoutError: + pytest.fail( + f"Timed out waiting for switch ON state " + f"(device_id={switch.device_id}, key={switch.key}). " + f"This likely means the firmware failed to decode the " + f"5-byte varint device_id in SwitchCommandRequest." + ) + assert states[state_key].state is True + + # Turn off + switch_futures[state_key] = loop.create_future() + client.switch_command(switch.key, False, device_id=switch.device_id) + try: + await asyncio.wait_for(switch_futures[state_key], timeout=2.0) + except TimeoutError: + pytest.fail( + f"Timed out waiting for switch OFF state " + f"(device_id={switch.device_id}, key={switch.key})" + ) + assert states[state_key].state is False From 62da60df477d3efcf9fcb80de6978b7ff44749f9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:19:20 -0500 Subject: [PATCH 199/609] [ld2420] Fix sizeof vs value bug in register memcpy (#14286) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/ld2420/ld2420.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index cf78a1a460..b653f4ae88 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -590,7 +590,7 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { for (uint16_t index = 0; index < (CMD_REG_DATA_REPLY_SIZE * // NOLINT ((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_REG_DATA_REPLY_SIZE)); index += CMD_REG_DATA_REPLY_SIZE) { - memcpy(&this->cmd_reply_.data[reg_element], &buffer[data_pos + index], sizeof(CMD_REG_DATA_REPLY_SIZE)); + memcpy(&this->cmd_reply_.data[reg_element], &buffer[data_pos + index], CMD_REG_DATA_REPLY_SIZE); byteswap(this->cmd_reply_.data[reg_element]); reg_element++; } @@ -729,9 +729,9 @@ void LD2420Component::set_reg_value(uint16_t reg, uint16_t value) { cmd_frame.data_length = 0; cmd_frame.header = CMD_FRAME_HEADER; cmd_frame.command = CMD_WRITE_REGISTER; - memcpy(&cmd_frame.data[cmd_frame.data_length], ®, sizeof(CMD_REG_DATA_REPLY_SIZE)); + memcpy(&cmd_frame.data[cmd_frame.data_length], ®, CMD_REG_DATA_REPLY_SIZE); cmd_frame.data_length += 2; - memcpy(&cmd_frame.data[cmd_frame.data_length], &value, sizeof(CMD_REG_DATA_REPLY_SIZE)); + memcpy(&cmd_frame.data[cmd_frame.data_length], &value, CMD_REG_DATA_REPLY_SIZE); cmd_frame.data_length += 2; cmd_frame.footer = CMD_FRAME_FOOTER; ESP_LOGV(TAG, "Sending write register %4X command: %2X data = %4X", reg, cmd_frame.command, value); From e601162cdd0500581cb838b8fd588fca3e3bba8e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:21:00 -0500 Subject: [PATCH 200/609] [lcd_base] Fix millis() truncation to uint8_t (#14289) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/lcd_base/lcd_display.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/lcd_base/lcd_display.cpp b/esphome/components/lcd_base/lcd_display.cpp index d3434cce10..cd08a739eb 100644 --- a/esphome/components/lcd_base/lcd_display.cpp +++ b/esphome/components/lcd_base/lcd_display.cpp @@ -45,7 +45,7 @@ void LCDDisplay::setup() { // TODO dotsize // Commands can only be sent 40ms after boot-up, so let's wait if we're close - const uint8_t now = millis(); + const uint32_t now = millis(); if (now < 40) delay(40u - now); From df77213f2cb21a31e62ff0a9570d7af73e8f431b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:27:00 -0500 Subject: [PATCH 201/609] [shelly_dimmer] Fix millis overflow in ACK timeout check (#14288) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/shelly_dimmer/stm32flash.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/shelly_dimmer/stm32flash.cpp b/esphome/components/shelly_dimmer/stm32flash.cpp index b052c0cee9..a1a933bcab 100644 --- a/esphome/components/shelly_dimmer/stm32flash.cpp +++ b/esphome/components/shelly_dimmer/stm32flash.cpp @@ -149,7 +149,7 @@ stm32_err_t stm32_get_ack_timeout(const stm32_unique_ptr &stm, uint32_t timeout) do { yield(); if (!stream->available()) { - if (millis() < start_time + timeout) + if (millis() - start_time < timeout) continue; ESP_LOGD(TAG, "Failed to read ACK timeout=%i", timeout); return STM32_ERR_UNKNOWN; @@ -212,7 +212,7 @@ stm32_err_t stm32_resync(const stm32_unique_ptr &stm) { static_assert(sizeof(buf) == BUFFER_SIZE, "Buf expected to be 2 bytes"); uint8_t ack; - while (t1 < t0 + STM32_RESYNC_TIMEOUT) { + while (t1 - t0 < STM32_RESYNC_TIMEOUT) { stream->write_array(buf, BUFFER_SIZE); stream->flush(); if (!stream->read_array(&ack, 1)) { From 3f558f63d8fec863d51e05e44a8dcee406d278c1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:28:47 -0500 Subject: [PATCH 202/609] [bl0942] Fix millis overflow in packet timeout check (#14285) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/bl0942/bl0942.cpp | 8 ++++---- esphome/components/bl0942/bl0942.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/bl0942/bl0942.cpp b/esphome/components/bl0942/bl0942.cpp index b408c5549c..16ad33141d 100644 --- a/esphome/components/bl0942/bl0942.cpp +++ b/esphome/components/bl0942/bl0942.cpp @@ -52,12 +52,12 @@ void BL0942::loop() { return; } if (avail < sizeof(buffer)) { - if (!this->rx_start_) { + if (!this->rx_start_.has_value()) { this->rx_start_ = millis(); - } else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) { + } else if (millis() - *this->rx_start_ > PKT_TIMEOUT_MS) { ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%zu bytes)", avail); this->read_array((uint8_t *) &buffer, avail); - this->rx_start_ = 0; + this->rx_start_.reset(); } return; } @@ -67,7 +67,7 @@ void BL0942::loop() { this->received_package_(&buffer); } } - this->rx_start_ = 0; + this->rx_start_.reset(); } bool BL0942::validate_checksum_(DataPacket *data) { diff --git a/esphome/components/bl0942/bl0942.h b/esphome/components/bl0942/bl0942.h index 10b29a72c6..3c013f86e7 100644 --- a/esphome/components/bl0942/bl0942.h +++ b/esphome/components/bl0942/bl0942.h @@ -140,7 +140,7 @@ class BL0942 : public PollingComponent, public uart::UARTDevice { uint8_t address_ = 0; bool reset_ = false; LineFrequency line_freq_ = LINE_FREQUENCY_50HZ; - uint32_t rx_start_ = 0; + optional rx_start_{}; uint32_t prev_cf_cnt_ = 0; bool validate_checksum_(DataPacket *data); From d1a636a5c3227d08eed01f704e26f172394eba64 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:34:38 -0500 Subject: [PATCH 203/609] [rtttl] Fix speaker playback bugs (#14280) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/rtttl/rtttl.cpp | 21 +++++++++++---------- esphome/components/rtttl/rtttl.h | 6 +++--- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 6e86405b74..ab95067f45 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -139,9 +139,10 @@ void Rtttl::loop() { x++; } if (x > 0) { - int send = this->speaker_->play((uint8_t *) (&sample), x * 2); - if (send != x * 4) { - this->samples_sent_ -= (x - (send / 2)); + size_t bytes_to_send = x * sizeof(SpeakerSample); + size_t send = this->speaker_->play((uint8_t *) (&sample), bytes_to_send); + if (send != bytes_to_send) { + this->samples_sent_ -= (x - (send / sizeof(SpeakerSample))); } return; } @@ -201,9 +202,9 @@ void Rtttl::loop() { bool need_note_gap = false; if (note) { auto note_index = (scale - 4) * 12 + note; - if (note_index < 0 || note_index >= (int) sizeof(NOTES)) { + if (note_index < 0 || note_index >= (int) (sizeof(NOTES) / sizeof(NOTES[0]))) { ESP_LOGE(TAG, "Note out of range (note: %d, scale: %d, index: %d, max: %d)", note, scale, note_index, - (int) sizeof(NOTES)); + (int) (sizeof(NOTES) / sizeof(NOTES[0]))); this->finish_(); return; } @@ -221,7 +222,7 @@ void Rtttl::loop() { #ifdef USE_OUTPUT if (this->output_ != nullptr) { - if (need_note_gap) { + if (need_note_gap && this->note_duration_ > DOUBLE_NOTE_GAP_MS) { this->output_->set_level(0.0); delay(DOUBLE_NOTE_GAP_MS); this->note_duration_ -= DOUBLE_NOTE_GAP_MS; @@ -240,9 +241,9 @@ void Rtttl::loop() { this->samples_sent_ = 0; this->samples_gap_ = 0; this->samples_per_wave_ = 0; - this->samples_count_ = (this->sample_rate_ * this->note_duration_) / 1600; //(ms); + this->samples_count_ = (this->sample_rate_ * this->note_duration_) / 1000; if (need_note_gap) { - this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1600; //(ms); + this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1000; } if (this->output_freq_ != 0) { // make sure there is enough samples to add a full last sinus. @@ -279,7 +280,7 @@ void Rtttl::play(std::string rtttl) { this->note_duration_ = 0; int bpm = 63; - uint8_t num; + uint16_t num; // Get name this->position_ = this->rtttl_.find(':'); @@ -395,7 +396,7 @@ void Rtttl::finish_() { sample[0].right = 0; sample[1].left = 0; sample[1].right = 0; - this->speaker_->play((uint8_t *) (&sample), 8); + this->speaker_->play((uint8_t *) (&sample), sizeof(sample)); this->speaker_->finish(); this->set_state_(State::STOPPING); } diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index 6f5df07766..4d4a652c51 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -46,8 +46,8 @@ class Rtttl : public Component { } protected: - inline uint8_t get_integer_() { - uint8_t ret = 0; + inline uint16_t get_integer_() { + uint16_t ret = 0; while (isdigit(this->rtttl_[this->position_])) { ret = (ret * 10) + (this->rtttl_[this->position_++] - '0'); } @@ -87,7 +87,7 @@ class Rtttl : public Component { #ifdef USE_OUTPUT /// The output to write the sound to. - output::FloatOutput *output_; + output::FloatOutput *output_{nullptr}; #endif // USE_OUTPUT #ifdef USE_SPEAKER From 5dffceda59542cf5abca89415bc6944b94b97793 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:35:27 -0500 Subject: [PATCH 204/609] [hmc5883l] Fix wrong gain for 88uT range (#14281) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/hmc5883l/hmc5883l.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/hmc5883l/hmc5883l.cpp b/esphome/components/hmc5883l/hmc5883l.cpp index b62381a287..bee5282125 100644 --- a/esphome/components/hmc5883l/hmc5883l.cpp +++ b/esphome/components/hmc5883l/hmc5883l.cpp @@ -95,7 +95,7 @@ void HMC5883LComponent::update() { float mg_per_bit; switch (this->range_) { case HMC5883L_RANGE_88_UT: - mg_per_bit = 0.073f; + mg_per_bit = 0.73f; break; case HMC5883L_RANGE_130_UT: mg_per_bit = 0.92f; From d61e2f9c29543e19c79b6accaf94a47b11cf87ab Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:06:13 -0500 Subject: [PATCH 205/609] [light] Fix millis overflow in transition progress and flash timing (#14292) Co-authored-by: J. Nick Koston Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/light/light_transformer.h | 7 +++---- esphome/components/light/transformers.h | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/esphome/components/light/light_transformer.h b/esphome/components/light/light_transformer.h index 079c2d2ae0..b9535c834c 100644 --- a/esphome/components/light/light_transformer.h +++ b/esphome/components/light/light_transformer.h @@ -44,12 +44,11 @@ class LightTransformer { /// The progress of this transition, on a scale of 0 to 1. float get_progress_() { uint32_t now = esphome::millis(); - if (now < this->start_time_) - return 0.0f; - if (now >= this->start_time_ + this->length_) + uint32_t elapsed = now - this->start_time_; + if (elapsed >= this->length_) return 1.0f; - return clamp((now - this->start_time_) / float(this->length_), 0.0f, 1.0f); + return clamp(elapsed / float(this->length_), 0.0f, 1.0f); } uint32_t start_time_; diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h index a26713b723..b6e5e08f2b 100644 --- a/esphome/components/light/transformers.h +++ b/esphome/components/light/transformers.h @@ -78,7 +78,7 @@ class LightFlashTransformer : public LightTransformer { optional apply() override { optional result = {}; - if (this->transformer_ == nullptr && millis() > this->start_time_ + this->length_ - this->transition_length_) { + if (this->transformer_ == nullptr && millis() - this->start_time_ > this->length_ - this->transition_length_) { // second transition back to start value this->transformer_ = this->state_.get_output()->create_default_transition(); this->transformer_->setup(this->state_.current_values, this->get_start_values(), this->transition_length_); From 3dcc9ab76537dc884dbc93924e5190fd12c14bef Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:08:04 -0500 Subject: [PATCH 206/609] [ble_presence] Fix millis overflow in presence timeout check (#14293) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/ble_presence/ble_presence_device.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index 70ecc67c32..f2f0a3ed19 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -101,7 +101,7 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, } void loop() override { - if (this->found_ && this->last_seen_ + this->timeout_ < millis()) + if (this->found_ && millis() - this->last_seen_ > this->timeout_) this->set_found_(false); } void dump_config() override; From a60e5c5c4f5b3871ac30633ed007f21bbbc24333 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:11:52 -0500 Subject: [PATCH 207/609] [lightwaverf] Fix millis overflow in send timeout check (#14294) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/lightwaverf/lightwaverf.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/lightwaverf/lightwaverf.cpp b/esphome/components/lightwaverf/lightwaverf.cpp index 2b44195c97..2c6a1ecf5b 100644 --- a/esphome/components/lightwaverf/lightwaverf.cpp +++ b/esphome/components/lightwaverf/lightwaverf.cpp @@ -31,13 +31,12 @@ void LightWaveRF::read_tx() { void LightWaveRF::send_rx(const std::vector &msg, uint8_t repeats, bool inverted, int u_sec) { this->lwtx_.lwtx_setup(pin_tx_, repeats, inverted, u_sec); - uint32_t timeout = 0; + uint32_t timeout = millis(); if (this->lwtx_.lwtx_free()) { this->lwtx_.lwtx_send(msg); - timeout = millis(); ESP_LOGD(TAG, "[%i] msg start", timeout); } - while (!this->lwtx_.lwtx_free() && millis() < (timeout + 1000)) { + while (!this->lwtx_.lwtx_free() && millis() - timeout < 1000) { delay(10); } timeout = millis() - timeout; From 2e167835ead5cc85d3507d0f262695ed112d5729 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:15:49 -0500 Subject: [PATCH 208/609] [pn532] Replace millis zero sentinel with optional (#14295) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/pn532/pn532.cpp | 8 ++++---- esphome/components/pn532/pn532.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 1ab0da3df7..199a44dacc 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -308,13 +308,13 @@ void PN532::send_nack_() { enum PN532ReadReady PN532::read_ready_(bool block) { if (this->rd_ready_ == READY) { if (block) { - this->rd_start_time_ = 0; + this->rd_start_time_.reset(); this->rd_ready_ = WOULDBLOCK; } return READY; } - if (!this->rd_start_time_) { + if (!this->rd_start_time_.has_value()) { this->rd_start_time_ = millis(); } @@ -324,7 +324,7 @@ enum PN532ReadReady PN532::read_ready_(bool block) { break; } - if (millis() - this->rd_start_time_ > 100) { + if (millis() - *this->rd_start_time_ > 100) { ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!"); this->rd_ready_ = TIMEOUT; break; @@ -340,7 +340,7 @@ enum PN532ReadReady PN532::read_ready_(bool block) { auto rdy = this->rd_ready_; if (block || rdy == TIMEOUT) { - this->rd_start_time_ = 0; + this->rd_start_time_.reset(); this->rd_ready_ = WOULDBLOCK; } return rdy; diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index f98c0f9322..e57ecd8104 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -99,7 +99,7 @@ class PN532 : public PollingComponent { std::vector triggers_ontagremoved_; nfc::NfcTagUid current_uid_; nfc::NdefMessage *next_task_message_to_write_; - uint32_t rd_start_time_{0}; + optional rd_start_time_{}; enum PN532ReadReady rd_ready_ { WOULDBLOCK }; enum NfcTask { READ = 0, From 24fb74f78bce8d618f1a4e134c61aeded194956d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:21:33 -0500 Subject: [PATCH 209/609] [ld2420] Fix buffer overflows in command response parsing (#14297) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/ld2420/ld2420.cpp | 39 ++++++++++++++-------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index b653f4ae88..f14400d15a 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -560,8 +560,6 @@ void LD2420Component::read_batch_(std::span buffer) { void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { this->cmd_reply_.command = buffer[CMD_FRAME_COMMAND]; this->cmd_reply_.length = buffer[CMD_FRAME_DATA_LENGTH]; - uint8_t reg_element = 0; - uint8_t data_element = 0; uint16_t data_pos = 0; if (this->cmd_reply_.length > CMD_MAX_BYTES) { ESP_LOGW(TAG, "Reply frame too long"); @@ -583,43 +581,44 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { case (CMD_DISABLE_CONF): ESP_LOGV(TAG, "Set config disable: CMD = %2X %s", CMD_DISABLE_CONF, result); break; - case (CMD_READ_REGISTER): + case (CMD_READ_REGISTER): { ESP_LOGV(TAG, "Read register: CMD = %2X %s", CMD_READ_REGISTER, result); // TODO Read/Write register is not implemented yet, this will get flushed out to a proper header file data_pos = 0x0A; - for (uint16_t index = 0; index < (CMD_REG_DATA_REPLY_SIZE * // NOLINT - ((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_REG_DATA_REPLY_SIZE)); - index += CMD_REG_DATA_REPLY_SIZE) { - memcpy(&this->cmd_reply_.data[reg_element], &buffer[data_pos + index], CMD_REG_DATA_REPLY_SIZE); - byteswap(this->cmd_reply_.data[reg_element]); - reg_element++; + uint16_t reg_count = std::min((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_REG_DATA_REPLY_SIZE, + sizeof(this->cmd_reply_.data) / sizeof(this->cmd_reply_.data[0])); + for (uint16_t i = 0; i < reg_count; i++) { + memcpy(&this->cmd_reply_.data[i], &buffer[data_pos + i * CMD_REG_DATA_REPLY_SIZE], CMD_REG_DATA_REPLY_SIZE); } break; + } case (CMD_WRITE_REGISTER): ESP_LOGV(TAG, "Write register: CMD = %2X %s", CMD_WRITE_REGISTER, result); break; case (CMD_WRITE_ABD_PARAM): ESP_LOGV(TAG, "Write gate parameter(s): %2X %s", CMD_WRITE_ABD_PARAM, result); break; - case (CMD_READ_ABD_PARAM): + case (CMD_READ_ABD_PARAM): { ESP_LOGV(TAG, "Read gate parameter(s): %2X %s", CMD_READ_ABD_PARAM, result); data_pos = CMD_ABD_DATA_REPLY_START; - for (uint16_t index = 0; index < (CMD_ABD_DATA_REPLY_SIZE * // NOLINT - ((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_ABD_DATA_REPLY_SIZE)); - index += CMD_ABD_DATA_REPLY_SIZE) { - memcpy(&this->cmd_reply_.data[data_element], &buffer[data_pos + index], - sizeof(this->cmd_reply_.data[data_element])); - byteswap(this->cmd_reply_.data[data_element]); - data_element++; + uint16_t abd_count = std::min((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_ABD_DATA_REPLY_SIZE, + sizeof(this->cmd_reply_.data) / sizeof(this->cmd_reply_.data[0])); + for (uint16_t i = 0; i < abd_count; i++) { + memcpy(&this->cmd_reply_.data[i], &buffer[data_pos + i * CMD_ABD_DATA_REPLY_SIZE], + sizeof(this->cmd_reply_.data[i])); } break; + } case (CMD_WRITE_SYS_PARAM): ESP_LOGV(TAG, "Set system parameter(s): %2X %s", CMD_WRITE_SYS_PARAM, result); break; - case (CMD_READ_VERSION): - memcpy(this->firmware_ver_, &buffer[12], buffer[10]); - ESP_LOGV(TAG, "Firmware version: %7s %s", this->firmware_ver_, result); + case (CMD_READ_VERSION): { + uint8_t ver_len = std::min(buffer[10], sizeof(this->firmware_ver_) - 1); + memcpy(this->firmware_ver_, &buffer[12], ver_len); + this->firmware_ver_[ver_len] = '\0'; + ESP_LOGV(TAG, "Firmware version: %s %s", this->firmware_ver_, result); break; + } default: break; } From 23ef233b60de8bef361eb062cfa732321c9a4ceb Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:21:50 -0500 Subject: [PATCH 210/609] [gp8403] Fix enum size mismatch in voltage register write (#14296) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/gp8403/gp8403.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/gp8403/gp8403.h b/esphome/components/gp8403/gp8403.h index 972f2ce60c..a19df15515 100644 --- a/esphome/components/gp8403/gp8403.h +++ b/esphome/components/gp8403/gp8403.h @@ -6,12 +6,12 @@ namespace esphome { namespace gp8403 { -enum GP8403Voltage { +enum GP8403Voltage : uint8_t { GP8403_VOLTAGE_5V = 0x00, GP8403_VOLTAGE_10V = 0x11, }; -enum GP8403Model { +enum GP8403Model : uint8_t { GP8403, GP8413, }; From 0a81a7a50bc4b73f4075a20fb8c5a74b4ddc82f4 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:01:32 -0500 Subject: [PATCH 211/609] [mcp2515] Fix millis overflow in set_mode_ timeout (#14298) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/mcp2515/mcp2515.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/mcp2515/mcp2515.cpp b/esphome/components/mcp2515/mcp2515.cpp index 1a17715315..77bfaf9224 100644 --- a/esphome/components/mcp2515/mcp2515.cpp +++ b/esphome/components/mcp2515/mcp2515.cpp @@ -133,8 +133,8 @@ uint8_t MCP2515::get_status_() { canbus::Error MCP2515::set_mode_(const CanctrlReqopMode mode) { modify_register_(MCP_CANCTRL, CANCTRL_REQOP, mode); - uint32_t end_time = millis() + 10; - while (millis() < end_time) { + uint32_t start_time = millis(); + while (millis() - start_time < 10) { if ((read_register_(MCP_CANSTAT) & CANSTAT_OPMOD) == mode) return canbus::ERROR_OK; } From 534857db9c16b7ebfc62693291d4b7a56b91eb36 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:01:49 -0500 Subject: [PATCH 212/609] [wled] Fix millis overflow in blank timeout (#14300) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/wled/wled_light_effect.cpp | 21 +++++++++++-------- esphome/components/wled/wled_light_effect.h | 3 ++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp index d26b7a1750..87bae5b1da 100644 --- a/esphome/components/wled/wled_light_effect.cpp +++ b/esphome/components/wled/wled_light_effect.cpp @@ -24,7 +24,7 @@ namespace wled { // https://github.com/Aircoookie/WLED/wiki/UDP-Realtime-Control enum Protocol { WLED_NOTIFIER = 0, WARLS = 1, DRGB = 2, DRGBW = 3, DNRGB = 4 }; -const int DEFAULT_BLANK_TIME = 1000; +constexpr uint32_t DEFAULT_BLANK_TIME = 1000; static const char *const TAG = "wled_light_effect"; @@ -34,9 +34,10 @@ void WLEDLightEffect::start() { AddressableLightEffect::start(); if (this->blank_on_start_) { - this->blank_at_ = 0; + this->blank_start_ = millis(); + this->blank_timeout_ = 0; } else { - this->blank_at_ = UINT32_MAX; + this->blank_start_.reset(); } } @@ -81,10 +82,10 @@ void WLEDLightEffect::apply(light::AddressableLight &it, const Color ¤t_co } } - // FIXME: Use roll-over safe arithmetic - if (blank_at_ < millis()) { + if (this->blank_start_.has_value() && millis() - *this->blank_start_ >= this->blank_timeout_) { blank_all_leds_(it); - blank_at_ = millis() + DEFAULT_BLANK_TIME; + this->blank_start_ = millis(); + this->blank_timeout_ = DEFAULT_BLANK_TIME; } } @@ -142,11 +143,13 @@ bool WLEDLightEffect::parse_frame_(light::AddressableLight &it, const uint8_t *p } if (timeout == UINT8_MAX) { - blank_at_ = UINT32_MAX; + this->blank_start_.reset(); } else if (timeout > 0) { - blank_at_ = millis() + timeout * 1000; + this->blank_start_ = millis(); + this->blank_timeout_ = timeout * 1000; } else { - blank_at_ = millis() + DEFAULT_BLANK_TIME; + this->blank_start_ = millis(); + this->blank_timeout_ = DEFAULT_BLANK_TIME; } it.schedule_show(); diff --git a/esphome/components/wled/wled_light_effect.h b/esphome/components/wled/wled_light_effect.h index 6da5f4e9f9..3f3b710611 100644 --- a/esphome/components/wled/wled_light_effect.h +++ b/esphome/components/wled/wled_light_effect.h @@ -35,7 +35,8 @@ class WLEDLightEffect : public light::AddressableLightEffect { uint16_t port_{0}; std::unique_ptr udp_; - uint32_t blank_at_{0}; + optional blank_start_{}; + uint32_t blank_timeout_{0}; uint32_t dropped_{0}; uint8_t sync_group_mask_{0}; bool blank_on_start_{true}; From 0d5b7df77d11b48ad58029adea4dfb88743e2932 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:32:02 -0500 Subject: [PATCH 213/609] [sensor] Fix delta filter percentage mode regression (#14302) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/sensor/__init__.py | 2 +- .../fixtures/sensor_filters_delta.yaml | 40 +++++++++++++++++++ .../integration/test_sensor_filters_delta.py | 27 ++++++++++++- 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index b0e0c28bda..770c044efb 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -603,7 +603,7 @@ DELTA_SCHEMA = cv.Any( def _get_delta(value): if isinstance(value, str): assert value.endswith("%") - return 0.0, float(value[:-1]) + return 0.0, float(value[:-1]) / 100.0 return value, 0.0 diff --git a/tests/integration/fixtures/sensor_filters_delta.yaml b/tests/integration/fixtures/sensor_filters_delta.yaml index 19bd2d5ca4..2494a430da 100644 --- a/tests/integration/fixtures/sensor_filters_delta.yaml +++ b/tests/integration/fixtures/sensor_filters_delta.yaml @@ -28,6 +28,11 @@ sensor: id: source_sensor_4 accuracy_decimals: 1 + - platform: template + name: "Source Sensor 5" + id: source_sensor_5 + accuracy_decimals: 1 + - platform: copy source_id: source_sensor_1 name: "Filter Min" @@ -69,6 +74,13 @@ sensor: filters: - delta: 0 + - platform: copy + source_id: source_sensor_5 + name: "Filter Percentage" + id: filter_percentage + filters: + - delta: 50% + script: - id: test_filter_min then: @@ -154,6 +166,28 @@ script: id: source_sensor_4 state: 2.0 + - id: test_filter_percentage + then: + - sensor.template.publish: + id: source_sensor_5 + state: 100.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor_5 + state: 120.0 # Filtered out (delta=20, need >50) + - delay: 20ms + - sensor.template.publish: + id: source_sensor_5 + state: 160.0 # Passes (delta=60 > 50% of 100=50) + - delay: 20ms + - sensor.template.publish: + id: source_sensor_5 + state: 200.0 # Filtered out (delta=40, need >50% of 160=80) + - delay: 20ms + - sensor.template.publish: + id: source_sensor_5 + state: 250.0 # Passes (delta=90 > 80) + button: - platform: template name: "Test Filter Min" @@ -178,3 +212,9 @@ button: id: btn_filter_zero_delta on_press: - script.execute: test_filter_zero_delta + + - platform: template + name: "Test Filter Percentage" + id: btn_filter_percentage + on_press: + - script.execute: test_filter_percentage diff --git a/tests/integration/test_sensor_filters_delta.py b/tests/integration/test_sensor_filters_delta.py index c7a26bf9d1..9d0114e0c4 100644 --- a/tests/integration/test_sensor_filters_delta.py +++ b/tests/integration/test_sensor_filters_delta.py @@ -24,12 +24,14 @@ async def test_sensor_filters_delta( "filter_max": [], "filter_baseline_max": [], "filter_zero_delta": [], + "filter_percentage": [], } filter_min_done = loop.create_future() filter_max_done = loop.create_future() filter_baseline_max_done = loop.create_future() filter_zero_delta_done = loop.create_future() + filter_percentage_done = loop.create_future() def on_state(state: EntityState) -> None: if not isinstance(state, SensorState) or state.missing_state: @@ -66,6 +68,12 @@ async def test_sensor_filters_delta( and not filter_zero_delta_done.done() ): filter_zero_delta_done.set_result(True) + elif ( + sensor_name == "filter_percentage" + and len(sensor_values[sensor_name]) == 3 + and not filter_percentage_done.done() + ): + filter_percentage_done.set_result(True) async with ( run_compiled(yaml_config), @@ -80,6 +88,7 @@ async def test_sensor_filters_delta( "filter_max": "Filter Max", "filter_baseline_max": "Filter Baseline Max", "filter_zero_delta": "Filter Zero Delta", + "filter_percentage": "Filter Percentage", }, ) @@ -98,13 +107,14 @@ async def test_sensor_filters_delta( "Test Filter Max": "filter_max", "Test Filter Baseline Max": "filter_baseline_max", "Test Filter Zero Delta": "filter_zero_delta", + "Test Filter Percentage": "filter_percentage", } buttons = {} for entity in entities: if isinstance(entity, ButtonInfo) and entity.name in button_name_map: buttons[button_name_map[entity.name]] = entity.key - assert len(buttons) == 4, f"Expected 3 buttons, found {len(buttons)}" + assert len(buttons) == 5, f"Expected 5 buttons, found {len(buttons)}" # Test 1: Min sensor_values["filter_min"].clear() @@ -161,3 +171,18 @@ async def test_sensor_filters_delta( assert sensor_values["filter_zero_delta"] == pytest.approx(expected), ( f"Test 4 failed: expected {expected}, got {sensor_values['filter_zero_delta']}" ) + + # Test 5: Percentage (delta: 50%) + sensor_values["filter_percentage"].clear() + client.button_command(buttons["filter_percentage"]) + try: + await asyncio.wait_for(filter_percentage_done, timeout=2.0) + except TimeoutError: + pytest.fail( + f"Test 5 timed out. Values: {sensor_values['filter_percentage']}" + ) + + expected = [100.0, 160.0, 250.0] + assert sensor_values["filter_percentage"] == pytest.approx(expected), ( + f"Test 5 failed: expected {expected}, got {sensor_values['filter_percentage']}" + ) From 789da5fdf822a7332adb7e670ff2de9353907bc3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Feb 2026 20:13:44 -0700 Subject: [PATCH 214/609] [logger] Mark Logger and LoggerMessageTrigger as final (#14291) --- esphome/components/logger/logger.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 90722ee79c..263d12b444 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -141,7 +141,7 @@ enum UARTSelection : uint8_t { * 2. Works with ESP-IDF's pthread implementation that uses a linked list for TLS variables * 3. Avoids the limitations of the fixed FreeRTOS task local storage slots */ -class Logger : public Component { +class Logger final : public Component { public: explicit Logger(uint32_t baud_rate); #ifdef USE_ESPHOME_TASK_LOG_BUFFER @@ -481,7 +481,7 @@ class Logger : public Component { }; extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -class LoggerMessageTrigger : public Trigger { +class LoggerMessageTrigger final : public Trigger { public: explicit LoggerMessageTrigger(Logger *parent, uint8_t level) : level_(level) { parent->add_log_callback(this, From 478a876b0156763e27605ed22e034fb2ed442a3c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Feb 2026 20:13:51 -0700 Subject: [PATCH 215/609] [mdns] Mark MDNSComponent as final (#14290) --- esphome/components/mdns/mdns_component.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 32f8f16ec1..13c8ccf288 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -40,7 +40,7 @@ struct MDNSService { FixedVector txt_records; }; -class MDNSComponent : public Component { +class MDNSComponent final : public Component { public: void setup() override; void dump_config() override; From cced0a82b55d17ae402425c86dfd9654ed4f7d86 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Feb 2026 20:14:04 -0700 Subject: [PATCH 216/609] [ota] Mark OTA backend and component leaf classes as final (#14287) --- esphome/components/esphome/ota/ota_esphome.h | 2 +- esphome/components/http_request/ota/ota_http_request.h | 2 +- esphome/components/ota/ota_backend_arduino_libretiny.h | 2 +- esphome/components/ota/ota_backend_arduino_rp2040.h | 2 +- esphome/components/ota/ota_backend_esp8266.h | 2 +- esphome/components/ota/ota_backend_esp_idf.h | 2 +- esphome/components/ota/ota_backend_host.h | 2 +- esphome/components/web_server/ota/ota_web_server.h | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index c9e89c82ba..53715cfe6a 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -12,7 +12,7 @@ namespace esphome { /// ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA. -class ESPHomeOTAComponent : public ota::OTAComponent { +class ESPHomeOTAComponent final : public ota::OTAComponent { public: enum class OTAState : uint8_t { IDLE, diff --git a/esphome/components/http_request/ota/ota_http_request.h b/esphome/components/http_request/ota/ota_http_request.h index e3f1a4aa90..6d39b0d466 100644 --- a/esphome/components/http_request/ota/ota_http_request.h +++ b/esphome/components/http_request/ota/ota_http_request.h @@ -22,7 +22,7 @@ enum OtaHttpRequestError : uint8_t { OTA_CONNECTION_ERROR = 0x12, }; -class OtaHttpRequestComponent : public ota::OTAComponent, public Parented { +class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented { public: void dump_config() override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.h b/esphome/components/ota/ota_backend_arduino_libretiny.h index 6d9b7a96d5..8f9d268eec 100644 --- a/esphome/components/ota/ota_backend_arduino_libretiny.h +++ b/esphome/components/ota/ota_backend_arduino_libretiny.h @@ -7,7 +7,7 @@ namespace esphome { namespace ota { -class ArduinoLibreTinyOTABackend : public OTABackend { +class ArduinoLibreTinyOTABackend final : public OTABackend { public: OTAResponseTypes begin(size_t image_size) override; void set_update_md5(const char *md5) override; diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.h b/esphome/components/ota/ota_backend_arduino_rp2040.h index b9e10d506c..6a708f9c57 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.h +++ b/esphome/components/ota/ota_backend_arduino_rp2040.h @@ -9,7 +9,7 @@ namespace esphome { namespace ota { -class ArduinoRP2040OTABackend : public OTABackend { +class ArduinoRP2040OTABackend final : public OTABackend { public: OTAResponseTypes begin(size_t image_size) override; void set_update_md5(const char *md5) override; diff --git a/esphome/components/ota/ota_backend_esp8266.h b/esphome/components/ota/ota_backend_esp8266.h index a9d6dd2ccc..52f657f006 100644 --- a/esphome/components/ota/ota_backend_esp8266.h +++ b/esphome/components/ota/ota_backend_esp8266.h @@ -12,7 +12,7 @@ namespace esphome::ota { /// OTA backend for ESP8266 using native SDK functions. /// This implementation bypasses the Arduino Updater library to save ~228 bytes of RAM /// by not having a global Update object in .bss. -class ESP8266OTABackend : public OTABackend { +class ESP8266OTABackend final : public OTABackend { public: OTAResponseTypes begin(size_t image_size) override; void set_update_md5(const char *md5) override; diff --git a/esphome/components/ota/ota_backend_esp_idf.h b/esphome/components/ota/ota_backend_esp_idf.h index 764010e614..7f7f6115c5 100644 --- a/esphome/components/ota/ota_backend_esp_idf.h +++ b/esphome/components/ota/ota_backend_esp_idf.h @@ -10,7 +10,7 @@ namespace esphome { namespace ota { -class IDFOTABackend : public OTABackend { +class IDFOTABackend final : public OTABackend { public: OTAResponseTypes begin(size_t image_size) override; void set_update_md5(const char *md5) override; diff --git a/esphome/components/ota/ota_backend_host.h b/esphome/components/ota/ota_backend_host.h index ae7d0cb0b3..5a2dcfcf39 100644 --- a/esphome/components/ota/ota_backend_host.h +++ b/esphome/components/ota/ota_backend_host.h @@ -7,7 +7,7 @@ namespace esphome::ota { /// Stub OTA backend for host platform - allows compilation but does not implement OTA. /// All operations return error codes immediately. This enables configurations with /// OTA triggers to compile for host platform during development. -class HostOTABackend : public OTABackend { +class HostOTABackend final : public OTABackend { public: OTAResponseTypes begin(size_t image_size) override; void set_update_md5(const char *md5) override; diff --git a/esphome/components/web_server/ota/ota_web_server.h b/esphome/components/web_server/ota/ota_web_server.h index 53ff99899c..0857c31c5d 100644 --- a/esphome/components/web_server/ota/ota_web_server.h +++ b/esphome/components/web_server/ota/ota_web_server.h @@ -9,7 +9,7 @@ namespace esphome::web_server { -class WebServerOTAComponent : public ota::OTAComponent { +class WebServerOTAComponent final : public ota::OTAComponent { public: void setup() override; void dump_config() override; From ee4d67930f11a4af1997190f9dc576db8a0d05c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Feb 2026 20:14:16 -0700 Subject: [PATCH 217/609] [api] Mark ListEntitiesIterator and InitialStateIterator as final (#14284) --- esphome/components/api/list_entities.h | 2 +- esphome/components/api/subscribe_state.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 90769f9a81..7d0eb5bb13 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -15,7 +15,7 @@ class APIConnection; return this->client_->schedule_message_(entity, ResponseType::MESSAGE_TYPE, ResponseType::ESTIMATED_SIZE); \ } -class ListEntitiesIterator : public ComponentIterator { +class ListEntitiesIterator final : public ComponentIterator { public: ListEntitiesIterator(APIConnection *client); #ifdef USE_BINARY_SENSOR diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 6f8577ca7b..9edf0f0f0c 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -16,7 +16,7 @@ class APIConnection; return this->client_->send_##entity_type##_state(entity); \ } -class InitialStateIterator : public ComponentIterator { +class InitialStateIterator final : public ComponentIterator { public: InitialStateIterator(APIConnection *client); #ifdef USE_BINARY_SENSOR From d52f8c9c6f250024f8ce4a353100919cb2b25215 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Feb 2026 20:14:33 -0700 Subject: [PATCH 218/609] [web_server] Mark classes as final (#14283) --- esphome/components/web_server/list_entities.cpp | 2 -- esphome/components/web_server/list_entities.h | 3 +-- esphome/components/web_server/web_server.h | 6 +++--- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index 8458298062..ebe7bf4450 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -14,8 +14,6 @@ ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, AsyncEventSource ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es) : web_server_(ws), events_(es) {} #endif -ListEntitiesIterator::~ListEntitiesIterator() {} - #ifdef USE_BINARY_SENSOR bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *obj) { this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::binary_sensor_all_json_generator); diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index d0a4fa2725..6a84066109 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -17,14 +17,13 @@ class DeferredUpdateEventSource; #endif class WebServer; -class ListEntitiesIterator : public ComponentIterator { +class ListEntitiesIterator final : public ComponentIterator { public: #ifdef USE_ESP32 ListEntitiesIterator(const WebServer *ws, esphome::web_server_idf::AsyncEventSource *es); #elif defined(USE_ARDUINO) ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es); #endif - virtual ~ListEntitiesIterator(); #ifdef USE_BINARY_SENSOR bool on_binary_sensor(binary_sensor::BinarySensor *obj) override; #endif diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 76c1c8b0bd..64c492f82b 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -107,7 +107,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; using message_generator_t = json::SerializationBuffer<>(WebServer *, void *); class DeferredUpdateEventSourceList; -class DeferredUpdateEventSource : public AsyncEventSource { +class DeferredUpdateEventSource final : public AsyncEventSource { friend class DeferredUpdateEventSourceList; /* @@ -163,7 +163,7 @@ class DeferredUpdateEventSource : public AsyncEventSource { void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); }; -class DeferredUpdateEventSourceList : public std::list { +class DeferredUpdateEventSourceList final : public std::list { protected: void on_client_connect_(DeferredUpdateEventSource *source); void on_client_disconnect_(DeferredUpdateEventSource *source); @@ -187,7 +187,7 @@ class DeferredUpdateEventSourceList : public std::list Date: Wed, 25 Feb 2026 20:14:53 -0700 Subject: [PATCH 219/609] [safe_mode] Mark SafeModeComponent and SafeModeTrigger as final (#14282) --- esphome/components/safe_mode/automation.h | 2 +- esphome/components/safe_mode/safe_mode.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/safe_mode/automation.h b/esphome/components/safe_mode/automation.h index 952ed4da33..1d82ac45f1 100644 --- a/esphome/components/safe_mode/automation.h +++ b/esphome/components/safe_mode/automation.h @@ -8,7 +8,7 @@ namespace esphome::safe_mode { -class SafeModeTrigger : public Trigger<> { +class SafeModeTrigger final : public Trigger<> { public: explicit SafeModeTrigger(SafeModeComponent *parent) { parent->add_on_safe_mode_callback([this]() { trigger(); }); diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index a4d27c15da..902b8c415d 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -15,7 +15,7 @@ namespace esphome::safe_mode { constexpr uint32_t RTC_KEY = 233825507UL; /// SafeModeComponent provides a safe way to recover from repeated boot failures -class SafeModeComponent : public Component { +class SafeModeComponent final : public Component { public: bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, uint32_t boot_is_good_after); From 6c253f0c71958a182cf1d9c4ad64a10b2b62957f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 26 Feb 2026 08:40:43 -0500 Subject: [PATCH 220/609] [sprinkler] Fix millis overflow and underflow bugs (#14299) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/sprinkler/sprinkler.cpp | 81 +++++++++++----------- esphome/components/sprinkler/sprinkler.h | 4 +- 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 9e423c1760..d82d7baaf6 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -84,32 +84,30 @@ SprinklerValveOperator::SprinklerValveOperator(SprinklerValve *valve, Sprinkler : controller_(controller), valve_(valve) {} void SprinklerValveOperator::loop() { + // Use wrapping subtraction so 32-bit millis() rollover is handled correctly: + // (now - start) yields the true elapsed time even across the 49.7-day boundary. uint32_t now = App.get_loop_component_start_time(); - if (now >= this->start_millis_) { // dummy check - switch (this->state_) { - case STARTING: - if (now > (this->start_millis_ + this->start_delay_)) { - this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state - } - break; + switch (this->state_) { + case STARTING: + if ((now - *this->start_millis_) > this->start_delay_) { + this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state + } + break; - case ACTIVE: - if (now > (this->start_millis_ + this->start_delay_ + this->run_duration_)) { - this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down - } - break; + case ACTIVE: + if ((now - *this->start_millis_) > (this->start_delay_ + this->run_duration_)) { + this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down + } + break; - case STOPPING: - if (now > (this->stop_millis_ + this->stop_delay_)) { - this->kill_(); // stop_delay_has been exceeded, ensure all valves are off - } - break; + case STOPPING: + if ((now - *this->stop_millis_) > this->stop_delay_) { + this->kill_(); // stop_delay_has been exceeded, ensure all valves are off + } + break; - default: - break; - } - } else { // perhaps millis() rolled over...or something else is horribly wrong! - this->stop(); // bail out (TODO: handle this highly unlikely situation better...) + default: + break; } } @@ -124,11 +122,11 @@ void SprinklerValveOperator::set_valve(SprinklerValve *valve) { if (this->state_ != IDLE) { // Only kill if not already idle this->kill_(); // ensure everything is off before we let go! } - this->state_ = IDLE; // reset state - this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it - this->start_millis_ = 0; // reset because (new) valve has not been started yet - this->stop_millis_ = 0; // reset because (new) valve has not been started yet - this->valve_ = valve; // finally, set the pointer to the new valve + this->state_ = IDLE; // reset state + this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it + this->start_millis_.reset(); // reset because (new) valve has not been started yet + this->stop_millis_.reset(); // reset because (new) valve has not been started yet + this->valve_ = valve; // finally, set the pointer to the new valve } } @@ -162,7 +160,7 @@ void SprinklerValveOperator::start() { } else { this->run_(); // there is no start_delay_, so just start the pump and valve } - this->stop_millis_ = 0; + this->stop_millis_.reset(); this->start_millis_ = millis(); // save the time the start request was made } @@ -189,22 +187,25 @@ void SprinklerValveOperator::stop() { uint32_t SprinklerValveOperator::run_duration() { return this->run_duration_ / 1000; } uint32_t SprinklerValveOperator::time_remaining() { - if (this->start_millis_ == 0) { + if (!this->start_millis_.has_value()) { return this->run_duration(); // hasn't been started yet } - if (this->stop_millis_) { - if (this->stop_millis_ - this->start_millis_ >= this->start_delay_ + this->run_duration_) { + if (this->stop_millis_.has_value()) { + uint32_t elapsed = *this->stop_millis_ - *this->start_millis_; + if (elapsed >= this->start_delay_ + this->run_duration_) { return 0; // valve was active for more than its configured duration, so we are done - } else { - // we're stopped; return time remaining - return (this->run_duration_ - (this->stop_millis_ - this->start_millis_)) / 1000; } + if (elapsed <= this->start_delay_) { + return this->run_duration_ / 1000; // stopped during start delay, full run duration remains + } + return (this->run_duration_ - (elapsed - this->start_delay_)) / 1000; } - auto completed_millis = this->start_millis_ + this->start_delay_ + this->run_duration_; - if (completed_millis > millis()) { - return (completed_millis - millis()) / 1000; // running now + uint32_t elapsed = millis() - *this->start_millis_; + uint32_t total_duration = this->start_delay_ + this->run_duration_; + if (elapsed < total_duration) { + return (total_duration - elapsed) / 1000; // running now } return 0; // run completed } @@ -593,7 +594,7 @@ void Sprinkler::set_repeat(optional repeat) { if (this->repeat_number_ == nullptr) { return; } - if (this->repeat_number_->state == repeat.value()) { + if (this->repeat_number_->state == repeat.value_or(0)) { return; } auto call = this->repeat_number_->make_call(); @@ -793,7 +794,7 @@ void Sprinkler::start_single_valve(const optional valve_number, optional void Sprinkler::queue_valve(optional valve_number, optional run_duration) { if (valve_number.has_value()) { if (this->is_a_valid_valve(valve_number.value()) && (this->queued_valves_.size() < this->max_queue_size_)) { - SprinklerQueueItem item{valve_number.value(), run_duration.value()}; + SprinklerQueueItem item{valve_number.value(), run_duration.value_or(0)}; this->queued_valves_.insert(this->queued_valves_.begin(), item); ESP_LOGD(TAG, "Valve %zu placed into queue with run duration of %" PRIu32 " seconds", valve_number.value_or(0), run_duration.value_or(0)); @@ -1080,7 +1081,7 @@ uint32_t Sprinkler::total_cycle_time_enabled_incomplete_valves() { } } - if (incomplete_valve_count >= enabled_valve_count) { + if (incomplete_valve_count > 0 && incomplete_valve_count >= enabled_valve_count) { incomplete_valve_count--; } if (incomplete_valve_count) { diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h index a3cdef5b1a..2598a5606a 100644 --- a/esphome/components/sprinkler/sprinkler.h +++ b/esphome/components/sprinkler/sprinkler.h @@ -141,8 +141,8 @@ class SprinklerValveOperator { uint32_t start_delay_{0}; uint32_t stop_delay_{0}; uint32_t run_duration_{0}; - uint64_t start_millis_{0}; - uint64_t stop_millis_{0}; + optional start_millis_{}; + optional stop_millis_{}; Sprinkler *controller_{nullptr}; SprinklerValve *valve_{nullptr}; SprinklerState state_{IDLE}; From a05d0202e696b5498787b3b895acd71915844ac5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 09:21:27 -0700 Subject: [PATCH 221/609] [core] ESP32: massively reduce main loop socket polling overhead by replacing select() (#14249) --- esphome/components/socket/__init__.py | 8 +- esphome/components/socket/socket.h | 2 +- esphome/core/application.cpp | 102 ++++++--- esphome/core/application.h | 47 ++-- esphome/core/lwip_fast_select.c | 216 ++++++++++++++++++ esphome/core/lwip_fast_select.h | 33 +++ .../socket/test_wake_loop_threadsafe.py | 39 ++++ 7 files changed, 397 insertions(+), 50 deletions(-) create mode 100644 esphome/core/lwip_fast_select.c create mode 100644 esphome/core/lwip_fast_select.h diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index d82f0c7aba..5f4d04eb44 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -134,6 +134,8 @@ def require_wake_loop_threadsafe() -> None: IMPORTANT: This is for background thread context only, NOT ISR context. Socket operations are not safe to call from ISR handlers. + On ESP32, FreeRTOS task notifications are used instead (no socket needed). + Example: from esphome.components import socket @@ -147,8 +149,10 @@ def require_wake_loop_threadsafe() -> None: ): CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True cg.add_define("USE_WAKE_LOOP_THREADSAFE") - # Consume 1 socket for the shared wake notification socket - consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({}) + if not CORE.is_esp32: + # Only non-ESP32 platforms need a UDP socket for wake notifications. + # ESP32 uses FreeRTOS task notifications instead (no socket needed). + consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({}) CONFIG_SCHEMA = cv.Schema( diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index c0098d689a..a771e2fe1a 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -71,7 +71,7 @@ class Socket { int get_fd() const { return -1; } #endif - /// Check if socket has data ready to read + /// Check if socket has data ready to read. Must only be called from the main loop thread. /// For select()-based sockets: non-virtual, checks Application's select() results /// For LWIP raw TCP sockets: virtual, checks internal buffer state #ifdef USE_SOCKET_SELECT_SUPPORT diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 6a7683a987..fd6a14b50f 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -9,6 +9,9 @@ #endif #ifdef USE_ESP32 #include +#include "esphome/core/lwip_fast_select.h" +#include +#include #endif #include "esphome/core/version.h" #include "esphome/core/hal.h" @@ -144,8 +147,14 @@ void Application::setup() { clear_setup_priority_overrides(); #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) - // Set up wake socket for waking main loop from tasks +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_ESP32) + // Initialize fast select: saves main loop task handle for xTaskNotifyGive wake. + // Always init on ESP32 — the fast path (rcvevent reads + ulTaskNotifyTake) is used + // unconditionally when USE_SOCKET_SELECT_SUPPORT is enabled. + esphome_lwip_fast_select_init(); +#endif +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) + // Set up wake socket for waking main loop from tasks (non-ESP32 only) this->setup_wake_loop_threadsafe_(); #endif @@ -523,7 +532,7 @@ void Application::enable_pending_loops_() { } void Application::before_loop_tasks_(uint32_t loop_start_time) { -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) // Drain wake notifications first to clear socket for next wake this->drain_wake_notifications_(); #endif @@ -576,11 +585,15 @@ bool Application::register_socket_fd(int fd) { #endif this->socket_fds_.push_back(fd); +#ifdef USE_ESP32 + // Hook the socket's netconn callback for instant wake on receive events + esphome_lwip_hook_socket(fd); +#else this->socket_fds_changed_ = true; - if (fd > this->max_fd_) { this->max_fd_ = fd; } +#endif return true; } @@ -595,12 +608,14 @@ void Application::unregister_socket_fd(int fd) { if (this->socket_fds_[i] != fd) continue; - // Swap with last element and pop - O(1) removal since order doesn't matter + // Swap with last element and pop - O(1) removal since order doesn't matter. + // No need to unhook the netconn callback on ESP32 — all LwIP sockets share + // the same static event_callback, and the socket will be closed by the caller. if (i < this->socket_fds_.size() - 1) this->socket_fds_[i] = this->socket_fds_.back(); this->socket_fds_.pop_back(); +#ifndef USE_ESP32 this->socket_fds_changed_ = true; - // Only recalculate max_fd if we removed the current max if (fd == this->max_fd_) { this->max_fd_ = -1; @@ -609,6 +624,7 @@ void Application::unregister_socket_fd(int fd) { this->max_fd_ = sock_fd; } } +#endif return; } } @@ -616,16 +632,41 @@ void Application::unregister_socket_fd(int fd) { #endif void Application::yield_with_select_(uint32_t delay_ms) { - // Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run - // since select() with 0 timeout only polls without yielding. -#ifdef USE_SOCKET_SELECT_SUPPORT - if (!this->socket_fds_.empty()) { + // Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run. +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_ESP32) + // ESP32 fast path: reads rcvevent directly via lwip_socket_dbg_get_socket() (~215 ns per socket). + // Safe because this runs on the main loop which owns socket lifetime (create, read, close). + if (delay_ms == 0) [[unlikely]] { + yield(); + return; + } + + // Check if any socket already has pending data before sleeping. + // If a socket still has unread data (rcvevent > 0) but the task notification was already + // consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency. + // This scan preserves select() semantics: return immediately when any fd is ready. + for (int fd : this->socket_fds_) { + if (esphome_lwip_socket_has_data(fd)) { + yield(); + return; + } + } + + // Sleep with instant wake via FreeRTOS task notification. + // Woken by: callback wrapper (socket data arrives), wake_loop_threadsafe() (other tasks), or timeout. + // Without USE_WAKE_LOOP_THREADSAFE, only hooked socket callbacks wake the task — + // background tasks won't call wake, so this degrades to a pure timeout (same as old select path). + ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms)); + +#elif defined(USE_SOCKET_SELECT_SUPPORT) + // Non-ESP32 select() path (LibreTiny bk72xx/rtl87xx, host platform). + // ESP32 is excluded by the #if above — both BSD_SOCKETS and LWIP_SOCKETS on ESP32 + // use LwIP under the hood, so the fast path handles all ESP32 socket implementations. + if (!this->socket_fds_.empty()) [[likely]] { // Update fd_set if socket list has changed - if (this->socket_fds_changed_) { + if (this->socket_fds_changed_) [[unlikely]] { FD_ZERO(&this->base_read_fds_); - // fd bounds are already validated in register_socket_fd() or guaranteed by platform design: - // - ESP32: LwIP guarantees fd < FD_SETSIZE by design (LWIP_SOCKET_OFFSET = FD_SETSIZE - CONFIG_LWIP_MAX_SOCKETS) - // - Other platforms: register_socket_fd() validates fd < FD_SETSIZE + // fd bounds are validated in register_socket_fd() for (int fd : this->socket_fds_) { FD_SET(fd, &this->base_read_fds_); } @@ -641,7 +682,7 @@ void Application::yield_with_select_(uint32_t delay_ms) { tv.tv_usec = (delay_ms - tv.tv_sec * 1000) * 1000; // Call select with timeout -#if defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || (defined(USE_ESP32) && defined(USE_SOCKET_IMPL_BSD_SOCKETS)) +#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS int ret = lwip_select(this->max_fd_ + 1, &this->read_fds_, nullptr, nullptr, &tv); #else int ret = ::select(this->max_fd_ + 1, &this->read_fds_, nullptr, nullptr, &tv); @@ -651,19 +692,18 @@ void Application::yield_with_select_(uint32_t delay_ms) { // ret < 0: error (except EINTR which is normal) // ret > 0: socket(s) have data ready - normal and expected // ret == 0: timeout occurred - normal and expected - if (ret < 0 && errno != EINTR) { - // Actual error - log and fall back to delay - ESP_LOGW(TAG, "select() failed with errno %d", errno); - delay(delay_ms); + if (ret >= 0 || errno == EINTR) [[likely]] { + // Yield if zero timeout since select(0) only polls without yielding + if (delay_ms == 0) [[unlikely]] { + yield(); + } + return; } - // When delay_ms is 0, we need to yield since select(0) doesn't yield - if (delay_ms == 0) { - yield(); - } - } else { - // No sockets registered, use regular delay - delay(delay_ms); + // select() error - log and fall through to delay() + ESP_LOGW(TAG, "select() failed with errno %d", errno); } + // No sockets registered or select() failed - use regular delay + delay(delay_ms); #elif defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) // No select support but can wake on socket activity via esp_schedule() socket::socket_delay(delay_ms); @@ -676,6 +716,14 @@ void Application::yield_with_select_(uint32_t delay_ms) { Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + +#ifdef USE_ESP32 +void Application::wake_loop_threadsafe() { + // Direct FreeRTOS task notification — <1 us, task context only (NOT ISR-safe) + esphome_lwip_wake_main_loop(); +} +#else // !USE_ESP32 + void Application::setup_wake_loop_threadsafe_() { // Create UDP socket for wake notifications this->wake_socket_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); @@ -742,6 +790,8 @@ void Application::wake_loop_threadsafe() { lwip_send(this->wake_socket_fd_, &dummy, 1, 0); } } +#endif // USE_ESP32 + #endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) void Application::get_build_time_string(std::span buffer) { diff --git a/esphome/core/application.h b/esphome/core/application.h index cd275bb97f..f5df5e7bdf 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -24,10 +24,14 @@ #endif #ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_ESP32 +#include "esphome/core/lwip_fast_select.h" +#else #include #ifdef USE_WAKE_LOOP_THREADSAFE #include #endif +#endif #endif // USE_SOCKET_SELECT_SUPPORT #ifdef USE_BINARY_SENSOR @@ -491,15 +495,12 @@ class Application { /// @return true if registration was successful, false if fd exceeds limits bool register_socket_fd(int fd); void unregister_socket_fd(int fd); - /// Check if there's data available on a socket without blocking - /// This function is thread-safe for reading, but should be called after select() has run - /// The read_fds_ is only modified by select() in the main loop - bool is_socket_ready(int fd) const { return fd >= 0 && this->is_socket_ready_(fd); } #ifdef USE_WAKE_LOOP_THREADSAFE - /// Wake the main event loop from a FreeRTOS task - /// Thread-safe, can be called from task context to immediately wake select() - /// IMPORTANT: NOT safe to call from ISR context (socket operations not ISR-safe) + /// Wake the main event loop from another FreeRTOS task. + /// Thread-safe, but must only be called from task context (NOT ISR-safe). + /// On ESP32: uses xTaskNotifyGive (<1 us) + /// On other platforms: uses UDP loopback socket void wake_loop_threadsafe(); #endif #endif @@ -510,10 +511,14 @@ class Application { #ifdef USE_SOCKET_SELECT_SUPPORT /// Fast path for Socket::ready() via friendship - skips negative fd check. - /// Safe because: fd was validated in register_socket_fd() at registration time, - /// and Socket::ready() only calls this when loop_monitored_ is true (registration succeeded). - /// FD_ISSET may include its own upper bounds check depending on platform. + /// Main loop only — on ESP32, reads rcvevent via lwip_socket_dbg_get_socket() + /// which has no refcount; safe only because the main loop owns socket lifetime + /// (creates, reads, and closes sockets on the same thread). +#ifdef USE_ESP32 + bool is_socket_ready_(int fd) const { return esphome_lwip_socket_has_data(fd); } +#else bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); } +#endif #endif void register_component_(Component *comp); @@ -541,7 +546,7 @@ class Application { /// Perform a delay while also monitoring socket file descriptors for readiness void yield_with_select_(uint32_t delay_ms); -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) void setup_wake_loop_threadsafe_(); // Create wake notification socket inline void drain_wake_notifications_(); // Read pending wake notifications in main loop (hot path - inlined) #endif @@ -571,7 +576,7 @@ class Application { FixedVector looping_components_{}; #ifdef USE_SOCKET_SELECT_SUPPORT std::vector socket_fds_; // Vector of all monitored socket file descriptors -#ifdef USE_WAKE_LOOP_THREADSAFE +#if defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks #endif #endif @@ -584,7 +589,7 @@ class Application { uint32_t last_loop_{0}; uint32_t loop_component_start_time_{0}; -#ifdef USE_SOCKET_SELECT_SUPPORT +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_ESP32) int max_fd_{-1}; // Highest file descriptor number for select() #endif @@ -600,14 +605,14 @@ class Application { bool in_loop_{false}; volatile bool has_pending_enable_loop_requests_{false}; -#ifdef USE_SOCKET_SELECT_SUPPORT +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_ESP32) bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes #endif -#ifdef USE_SOCKET_SELECT_SUPPORT - // Variable-sized members +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_ESP32) + // Variable-sized members (not needed on ESP32 — is_socket_ready_ reads rcvevent directly) + fd_set read_fds_{}; // Working fd_set: populated by select() fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes - fd_set read_fds_{}; // Working fd_set for select(), copied from base_read_fds_ #endif // StaticVectors (largest members - contain actual array data inline) @@ -694,7 +699,7 @@ class Application { /// Global storage of Application pointer - only one Application can exist. extern Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) // Inline implementations for hot-path functions // drain_wake_notifications_() is called on every loop iteration @@ -704,8 +709,8 @@ static constexpr size_t WAKE_NOTIFY_DRAIN_BUFFER_SIZE = 16; inline void Application::drain_wake_notifications_() { // Called from main loop to drain any pending wake notifications - // Must check is_socket_ready() to avoid blocking on empty socket - if (this->wake_socket_fd_ >= 0 && this->is_socket_ready(this->wake_socket_fd_)) { + // Must check is_socket_ready_() to avoid blocking on empty socket + if (this->wake_socket_fd_ >= 0 && this->is_socket_ready_(this->wake_socket_fd_)) { char buffer[WAKE_NOTIFY_DRAIN_BUFFER_SIZE]; // Drain all pending notifications with non-blocking reads // Multiple wake events may have triggered multiple writes, so drain until EWOULDBLOCK @@ -716,6 +721,6 @@ inline void Application::drain_wake_notifications_() { } } } -#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) +#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) } // namespace esphome diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c new file mode 100644 index 0000000000..70a6482d48 --- /dev/null +++ b/esphome/core/lwip_fast_select.c @@ -0,0 +1,216 @@ +// Fast socket monitoring for ESP32 (ESP-IDF LwIP) +// Replaces lwip_select() with direct rcvevent reads and FreeRTOS task notifications. +// +// This must be a .c file (not .cpp) because: +// 1. lwip/priv/sockets_priv.h conflicts with C++ compilation units that include bootloader headers +// 2. The netconn callback is a C function pointer +// +// defines.h is force-included by the build system (-include flag), providing USE_ESP32 etc. +// +// Thread safety analysis +// ====================== +// Three threads interact with this code: +// 1. Main loop task — calls init, has_data, hook +// 2. LwIP TCP/IP task — calls event_callback (reads s_original_callback; writes rcvevent +// via the original callback under SYS_ARCH_PROTECT/UNPROTECT mutex) +// 3. Background tasks — call wake_main_loop +// +// LwIP source references (ESP-IDF v5.5.2, commit 30aaf64524): +// sockets.c: https://github.com/espressif/esp-idf/blob/30aaf64524/components/lwip/lwip/src/api/sockets.c +// - event_callback (static, same for all sockets): L327 +// - DEFAULT_SOCKET_EVENTCB = event_callback: L328 +// - tryget_socket_unconn_nouse (direct array lookup): L450 +// - lwip_socket_dbg_get_socket (thin wrapper): L461 +// - All socket types use DEFAULT_SOCKET_EVENTCB: L1741, L1748, L1759 +// - event_callback definition: L2538 +// - SYS_ARCH_PROTECT before rcvevent switch: L2578 +// - sock->rcvevent++ (NETCONN_EVT_RCVPLUS case): L2582 +// - SYS_ARCH_UNPROTECT after switch: L2615 +// sys.h: https://github.com/espressif/esp-idf/blob/30aaf64524/components/lwip/lwip/src/include/lwip/sys.h +// - SYS_ARCH_PROTECT calls sys_arch_protect(): L495 +// - SYS_ARCH_UNPROTECT calls sys_arch_unprotect(): L506 +// (ESP-IDF implements sys_arch_protect/unprotect as FreeRTOS mutex lock/unlock) +// +// Socket slot lifetime +// ==================== +// This code reads struct lwip_sock fields without SYS_ARCH_PROTECT. The safety +// argument requires that the slot cannot be freed while we read it. +// +// In LwIP, the socket table is a static array and slots are only freed via: +// lwip_close() -> lwip_close_internal() -> free_socket_free_elements() -> free_socket() +// The TCP/IP thread does NOT call free_socket(). On link loss, RST, or timeout +// it frees the TCP PCB and signals the netconn (rcvevent++ to indicate EOF), but +// the netconn and lwip_sock slot remain allocated until the application calls +// lwip_close(). ESPHome removes the fd from the monitored set before calling +// lwip_close(). +// +// Therefore lwip_socket_dbg_get_socket(fd) plus a volatile read of rcvevent +// (to prevent compiler reordering or caching) is safe as long as the application +// is single-writer for close. ESPHome guarantees this by design: all socket +// create/read/close happens on the main loop. fd numbers are not reused while +// the slot remains allocated, and the slot remains allocated until lwip_close(). +// Any change in LwIP that allows free_socket() to be called outside lwip_close() +// would invalidate this assumption. +// +// LwIP source references for slot lifetime: +// sockets.c (same commit as above): +// - alloc_socket (slot allocation): L419 +// - free_socket (slot deallocation): L384 +// - free_socket_free_elements (called from lwip_close_internal): L393 +// - lwip_close_internal (only caller of free_socket_free_elements): L2355 +// - lwip_close (only caller of lwip_close_internal): L2450 +// +// Shared state and safety rationale: +// +// s_main_loop_task (TaskHandle_t, 4 bytes): +// Written once by main loop in init(). Read by TCP/IP thread (in callback) +// and background tasks (in wake). +// Safe: write-once-then-read pattern. Socket hooks may run before init(), +// but the NULL check on s_main_loop_task in the callback provides correct +// degraded behavior — notifications are simply skipped until init() completes. +// +// s_original_callback (netconn_callback, 4-byte function pointer): +// Written by main loop in hook_socket() (only when NULL — set once). +// Read by TCP/IP thread in esphome_socket_event_callback(). +// Safe: set-once pattern. The first hook_socket() captures the original callback. +// All subsequent hooks see it already set and skip the write. The TCP/IP thread +// only reads this after the callback pointer has been swapped (which happens after +// the write), so it always sees the initialized value. +// +// sock->conn->callback (netconn_callback, 4-byte function pointer): +// Written by main loop in hook_socket(). Never restored — all LwIP sockets share +// the same static event_callback (DEFAULT_SOCKET_EVENTCB), so the wrapper stays permanently. +// Read by TCP/IP thread when invoking the callback. +// Safe: 32-bit aligned pointer writes are atomic on Xtensa and RISC-V (ESP32). +// The TCP/IP thread will see either the old or new pointer atomically — never a +// torn value. Both the wrapper and original callbacks are valid at all times +// (the wrapper itself calls the original), so either value is correct. +// +// sock->rcvevent (s16_t, 2 bytes): +// Written by TCP/IP thread in event_callback under SYS_ARCH_PROTECT. +// Read by main loop in has_data() via volatile cast. +// Safe: SYS_ARCH_UNPROTECT releases a FreeRTOS mutex, which internally +// uses a critical section with memory barrier (rsync on dual-core Xtensa; on +// single-core builds the spinlock is compiled out, but cross-core visibility is +// not an issue). The volatile cast prevents the compiler +// from caching the read. Aligned 16-bit reads are single-instruction loads on +// Xtensa (L16SI) and RISC-V (LH), which cannot produce torn values. +// +// FreeRTOS task notification value: +// Written by TCP/IP thread (xTaskNotifyGive in callback) and background tasks +// (xTaskNotifyGive in wake_main_loop). Read by main loop (ulTaskNotifyTake). +// Safe: FreeRTOS notification APIs are thread-safe by design (use internal +// critical sections). Multiple concurrent xTaskNotifyGive calls are safe — +// the notification count simply increments. + +#ifdef USE_ESP32 + +// LwIP headers must come first — they define netconn_callback, struct lwip_sock, etc. +#include +#include +#include +#include + +#include "esphome/core/lwip_fast_select.h" + +#include + +// Compile-time verification of thread safety assumptions. +// On ESP32 (Xtensa/RISC-V), naturally-aligned reads/writes up to 32 bits are atomic. +// These asserts ensure our cross-thread shared state meets those requirements. + +// Pointer types must fit in a single 32-bit store (atomic write) +_Static_assert(sizeof(TaskHandle_t) <= 4, "TaskHandle_t must be <= 4 bytes for atomic access"); +_Static_assert(sizeof(netconn_callback) <= 4, "netconn_callback must be <= 4 bytes for atomic access"); + +// rcvevent must fit in a single atomic read +_Static_assert(sizeof(((struct lwip_sock *) 0)->rcvevent) <= 4, "rcvevent must be <= 4 bytes for atomic access"); + +// Struct member alignment — natural alignment guarantees atomicity on Xtensa/RISC-V. +// Misaligned access would not be atomic even if the size is <= 4 bytes. +_Static_assert(offsetof(struct netconn, callback) % sizeof(netconn_callback) == 0, + "netconn.callback must be naturally aligned for atomic access"); +_Static_assert(offsetof(struct lwip_sock, rcvevent) % sizeof(((struct lwip_sock *) 0)->rcvevent) == 0, + "lwip_sock.rcvevent must be naturally aligned for atomic access"); + +// Task handle for the main loop — written once in init(), read from TCP/IP and background tasks. +static TaskHandle_t s_main_loop_task = NULL; + +// Saved original event_callback pointer — written once in first hook_socket(), read from TCP/IP task. +static netconn_callback s_original_callback = NULL; + +// Wrapper callback: calls original event_callback + notifies main loop task. +// Called from LwIP's TCP/IP thread when socket events occur (task context, not ISR). +static void esphome_socket_event_callback(struct netconn *conn, enum netconn_evt evt, u16_t len) { + // Call original LwIP event_callback first — updates rcvevent/sendevent/errevent, + // signals any select() waiters. This preserves all LwIP behavior. + // s_original_callback is always valid here: hook_socket() sets it before swapping + // the callback pointer, so this wrapper cannot run until it's initialized. + s_original_callback(conn, evt, len); + // Wake the main loop task if sleeping in ulTaskNotifyTake(). + // Only notify on receive events to avoid spurious wakeups from send-ready events. + // NETCONN_EVT_ERROR is deliberately omitted: LwIP signals errors via RCVPLUS + // (rcvevent++ with a NULL pbuf or error in recvmbox), so error conditions + // already wake the main loop through the RCVPLUS path. + if (evt == NETCONN_EVT_RCVPLUS) { + TaskHandle_t task = s_main_loop_task; + if (task != NULL) { + xTaskNotifyGive(task); + } + } +} + +void esphome_lwip_fast_select_init(void) { s_main_loop_task = xTaskGetCurrentTaskHandle(); } + +// lwip_socket_dbg_get_socket() is a thin wrapper around the static +// tryget_socket_unconn_nouse() — a direct array lookup without the refcount +// that get_socket()/done_socket() uses. This is safe because: +// 1. The only path to free_socket() is lwip_close(), called exclusively from the main loop +// 2. The TCP/IP thread never frees socket slots (see "Socket slot lifetime" above) +// 3. Both has_data() reads and lwip_close() run on the main loop — no concurrent free +// If lwip_socket_dbg_get_socket() were ever removed, we could fall back to lwip_select(). +// Returns the sock only if both the sock and its netconn are valid, NULL otherwise. +static inline struct lwip_sock *get_sock(int fd) { + struct lwip_sock *sock = lwip_socket_dbg_get_socket(fd); + if (sock == NULL || sock->conn == NULL) + return NULL; + return sock; +} + +bool esphome_lwip_socket_has_data(int fd) { + struct lwip_sock *sock = get_sock(fd); + if (sock == NULL) + return false; + // volatile prevents the compiler from caching/reordering this cross-thread read. + // The write side (TCP/IP thread) commits via SYS_ARCH_UNPROTECT which releases a + // FreeRTOS mutex with a memory barrier (rsync on Xtensa), ensuring the value is + // visible. Aligned 16-bit reads are single-instruction loads (L16SI/LH) on + // Xtensa/RISC-V and cannot produce torn values. + return *(volatile s16_t *) &sock->rcvevent > 0; +} + +void esphome_lwip_hook_socket(int fd) { + struct lwip_sock *sock = get_sock(fd); + if (sock == NULL) + return; + + // Save original callback once — all LwIP sockets share the same static event_callback + // (DEFAULT_SOCKET_EVENTCB in sockets.c, used for SOCK_RAW, SOCK_DGRAM, and SOCK_STREAM). + if (s_original_callback == NULL) { + s_original_callback = sock->conn->callback; + } + + // Replace with our wrapper. Atomic on ESP32 (32-bit aligned pointer write). + // TCP/IP thread sees either old or new pointer — both are valid. + sock->conn->callback = esphome_socket_event_callback; +} + +// Wake the main loop from another FreeRTOS task. NOT ISR-safe. +void esphome_lwip_wake_main_loop(void) { + TaskHandle_t task = s_main_loop_task; + if (task != NULL) { + xTaskNotifyGive(task); + } +} + +#endif // USE_ESP32 diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h new file mode 100644 index 0000000000..73a89fdc3d --- /dev/null +++ b/esphome/core/lwip_fast_select.h @@ -0,0 +1,33 @@ +#pragma once + +// Fast socket monitoring for ESP32 (ESP-IDF LwIP) +// Replaces lwip_select() with direct rcvevent reads and FreeRTOS task notifications. + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/// Initialize fast select — must be called from the main loop task during setup(). +/// Saves the current task handle for xTaskNotifyGive() wake notifications. +void esphome_lwip_fast_select_init(void); + +/// Check if a LwIP socket has data ready via direct rcvevent read (~215 ns per socket). +/// Uses lwip_socket_dbg_get_socket() — a direct array lookup without the refcount that +/// get_socket()/done_socket() uses. Safe because the caller owns the socket lifetime: +/// both has_data reads and socket close/unregister happen on the main loop thread. +bool esphome_lwip_socket_has_data(int fd); + +/// Hook a socket's netconn callback to notify the main loop task on receive events. +/// Wraps the original event_callback with one that also calls xTaskNotifyGive(). +/// Must be called from the main loop after socket creation. +void esphome_lwip_hook_socket(int fd); + +/// Wake the main loop task from another FreeRTOS task — costs <1 us. +/// NOT ISR-safe — must only be called from task context. +void esphome_lwip_wake_main_loop(void); + +#ifdef __cplusplus +} +#endif diff --git a/tests/components/socket/test_wake_loop_threadsafe.py b/tests/components/socket/test_wake_loop_threadsafe.py index a40b6068a8..28b4ee564f 100644 --- a/tests/components/socket/test_wake_loop_threadsafe.py +++ b/tests/components/socket/test_wake_loop_threadsafe.py @@ -1,9 +1,21 @@ from esphome.components import socket +from esphome.const import ( + KEY_CORE, + KEY_TARGET_PLATFORM, + PLATFORM_ESP32, + PLATFORM_ESP8266, +) from esphome.core import CORE +def _setup_platform(platform=PLATFORM_ESP8266) -> None: + """Set up CORE.data with a platform for testing.""" + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform} + + def test_require_wake_loop_threadsafe__first_call() -> None: """Test that first call sets up define and consumes socket.""" + _setup_platform() CORE.config = {"wifi": True} socket.require_wake_loop_threadsafe() @@ -32,6 +44,7 @@ def test_require_wake_loop_threadsafe__idempotent() -> None: def test_require_wake_loop_threadsafe__multiple_calls() -> None: """Test that multiple calls only set up once.""" + _setup_platform() # Call three times CORE.config = {"openthread": True} socket.require_wake_loop_threadsafe() @@ -75,3 +88,29 @@ def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() - udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) assert "socket.wake_loop_threadsafe" not in udp_consumers assert udp_consumers == initial_udp + + +def test_require_wake_loop_threadsafe__esp32_no_udp_socket() -> None: + """Test that ESP32 uses task notifications instead of UDP socket.""" + _setup_platform(PLATFORM_ESP32) + CORE.config = {"wifi": True} + socket.require_wake_loop_threadsafe() + + # Verify the define was added + assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True + assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) + + # Verify no UDP socket was consumed (ESP32 uses FreeRTOS task notifications) + udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) + assert "socket.wake_loop_threadsafe" not in udp_consumers + + +def test_require_wake_loop_threadsafe__non_esp32_consumes_udp_socket() -> None: + """Test that non-ESP32 platforms consume a UDP socket for wake notifications.""" + _setup_platform(PLATFORM_ESP8266) + CORE.config = {"wifi": True} + socket.require_wake_loop_threadsafe() + + # Verify UDP socket was consumed + udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) + assert udp_consumers.get("socket.wake_loop_threadsafe") == 1 From be000eab4e0cb4f9eaa3807b069c143c4eeee5b6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 11:02:52 -0700 Subject: [PATCH 222/609] [ci] Add undocumented C++ API change checkbox and auto-label (#14317) --- .github/PULL_REQUEST_TEMPLATE.md | 5 +++-- .github/scripts/auto-label-pr/constants.js | 1 + .github/scripts/auto-label-pr/detectors.js | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d1ef3bd822..965b186c31 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,8 +6,9 @@ - [ ] Bugfix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) -- [ ] Developer breaking change (an API change that could break external components) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) — [policy](https://developers.esphome.io/contributing/code/#what-constitutes-a-c-breaking-change) +- [ ] Developer breaking change (an API change that could break external components) — [policy](https://developers.esphome.io/contributing/code/#what-is-considered-public-c-api) +- [ ] Undocumented C++ API change (removal or change of undocumented public methods that lambda users may depend on) — [policy](https://developers.esphome.io/contributing/code/#c-user-expectations) - [ ] Code quality improvements to existing code or addition of tests - [ ] Other diff --git a/.github/scripts/auto-label-pr/constants.js b/.github/scripts/auto-label-pr/constants.js index bd60d8c766..8c3a62cf19 100644 --- a/.github/scripts/auto-label-pr/constants.js +++ b/.github/scripts/auto-label-pr/constants.js @@ -27,6 +27,7 @@ module.exports = { 'new-feature', 'breaking-change', 'developer-breaking-change', + 'undocumented-api-change', 'code-quality', 'deprecated-component' ], diff --git a/.github/scripts/auto-label-pr/detectors.js b/.github/scripts/auto-label-pr/detectors.js index f502a85666..a45a84f219 100644 --- a/.github/scripts/auto-label-pr/detectors.js +++ b/.github/scripts/auto-label-pr/detectors.js @@ -238,6 +238,7 @@ async function detectPRTemplateCheckboxes(context) { { pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' }, { pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' }, { pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' }, + { pattern: /- \[x\] Undocumented C\+\+ API change \(removal or change of undocumented public methods that lambda users may depend on\)/i, label: 'undocumented-api-change' }, { pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' } ]; From ae16c3bae7450db7bba0b1c06755022e5b2696e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 11:25:36 -0700 Subject: [PATCH 223/609] Add socket compile tests for libretiny platforms (#14314) --- tests/components/socket/test.bk72xx-ard.yaml | 1 + tests/components/socket/test.ln882x-ard.yaml | 1 + tests/components/socket/test.rtl87xx-ard.yaml | 1 + 3 files changed, 3 insertions(+) create mode 100644 tests/components/socket/test.bk72xx-ard.yaml create mode 100644 tests/components/socket/test.ln882x-ard.yaml create mode 100644 tests/components/socket/test.rtl87xx-ard.yaml diff --git a/tests/components/socket/test.bk72xx-ard.yaml b/tests/components/socket/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/socket/test.bk72xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/socket/test.ln882x-ard.yaml b/tests/components/socket/test.ln882x-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/socket/test.ln882x-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/socket/test.rtl87xx-ard.yaml b/tests/components/socket/test.rtl87xx-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/socket/test.rtl87xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 1912dcf03da6f0d1590f3708cf8ddad9b404ef04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 12:07:42 -0700 Subject: [PATCH 224/609] [core] Use placement new for global Application instance (#14052) --- esphome/core/application.cpp | 10 +++++++++- esphome/core/config.py | 3 +++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index fd6a14b50f..a9753da1b5 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -713,7 +713,15 @@ void Application::yield_with_select_(uint32_t delay_ms) { #endif } -Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +// App storage — asm label shares the linker symbol with "extern Application App". +// char[] is trivially destructible, so no __cxa_atexit or destructor chain is emitted. +// Constructed via placement new in the generated setup(). +#ifndef __GXX_ABI_VERSION +#error "Application placement new requires Itanium C++ ABI (GCC/Clang)" +#endif +static_assert(std::is_default_constructible::value, "Application must be default-constructible"); +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +alignas(Application) char app_storage[sizeof(Application)] asm("_ZN7esphome3AppE"); #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) diff --git a/esphome/core/config.py b/esphome/core/config.py index 21ed8ced1a..215432835a 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -512,6 +512,9 @@ async def to_code(config: ConfigType) -> None: cg.add_global(cg.RawExpression("using std::min")) cg.add_global(cg.RawExpression("using std::max")) + # Construct App via placement new — see application.cpp for storage details + cg.add_global(cg.RawStatement("#include ")) + cg.add(cg.RawExpression("new (&App) Application()")) cg.add( cg.App.pre_setup( config[CONF_NAME], From 4c3bb1596e876259f496cafc682dfb75e34d3089 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 12:14:46 -0700 Subject: [PATCH 225/609] [wifi] Use memcpy-based insertion sort for scan results (#13960) --- esphome/components/wifi/wifi_component.cpp | 50 ++++++++++++++++++++-- esphome/components/wifi/wifi_component.h | 9 ++++ 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d5d0419395..1e6961b8bd 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #ifdef USE_ESP32 #if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1) @@ -1334,20 +1335,61 @@ void WiFiComponent::start_scanning() { // Using insertion sort instead of std::stable_sort saves flash memory // by avoiding template instantiations (std::rotate, std::stable_sort, lambdas) // IMPORTANT: This sort is stable (preserves relative order of equal elements) +// +// Uses raw memcpy instead of copy assignment to avoid CompactString's +// destructor/constructor overhead (heap delete[]/new[] for long SSIDs). +// Copy assignment calls ~CompactString() then placement-new for every shift, +// which means delete[]/new[] per shift for heap-allocated SSIDs. With 70+ +// networks (e.g., captive portal showing full scan results), this caused +// event loop blocking from hundreds of heap operations in a tight loop. +// +// This is safe because we're permuting elements within the same array — +// each slot is overwritten exactly once, so no ownership duplication occurs. +// All members of WiFiScanResult are either trivially copyable (bssid, channel, +// rssi, priority, flags) or CompactString, which stores either inline data or +// a heap pointer — never a self-referential pointer (unlike std::string's SSO +// on some implementations). This was not possible before PR#13472 replaced +// std::string with CompactString, since std::string's internal layout is +// implementation-defined and may use self-referential pointers. +// +// TODO: If C++ standardizes std::trivially_relocatable, add the assertion for +// WiFiScanResult/CompactString here to formally express the memcpy safety guarantee. template static void insertion_sort_scan_results(VectorType &results) { + // memcpy-based sort requires no self-referential pointers or virtual dispatch. + // These static_asserts guard the assumptions. If any fire, the memcpy sort + // must be reviewed for safety before updating the expected values. + // + // No vtable pointers (memcpy would corrupt vptr) + static_assert(!std::is_polymorphic::value, "WiFiScanResult must not have vtable"); + static_assert(!std::is_polymorphic::value, "CompactString must not have vtable"); + // Standard layout ensures predictable memory layout with no virtual bases + // and no mixed-access-specifier reordering + static_assert(std::is_standard_layout::value, "WiFiScanResult must be standard layout"); + static_assert(std::is_standard_layout::value, "CompactString must be standard layout"); + // Size checks catch added/removed fields that may need safety review + static_assert(sizeof(WiFiScanResult) == 32, "WiFiScanResult size changed - verify memcpy sort is still safe"); + static_assert(sizeof(CompactString) == 20, "CompactString size changed - verify memcpy sort is still safe"); + // Alignment must match for reinterpret_cast of key_buf to be valid + static_assert(alignof(WiFiScanResult) <= alignof(std::max_align_t), "WiFiScanResult alignment exceeds max_align_t"); const size_t size = results.size(); + constexpr size_t elem_size = sizeof(WiFiScanResult); + // Suppress warnings for intentional memcpy on non-trivially-copyable type. + // Safety is guaranteed by the static_asserts above and the permutation invariant. + // NOLINTNEXTLINE(bugprone-undefined-memory-manipulation) + auto *memcpy_fn = &memcpy; for (size_t i = 1; i < size; i++) { - // Make a copy to avoid issues with move semantics during comparison - WiFiScanResult key = results[i]; + alignas(WiFiScanResult) uint8_t key_buf[elem_size]; + memcpy_fn(key_buf, &results[i], elem_size); + const auto &key = *reinterpret_cast(key_buf); int32_t j = i - 1; // Move elements that are worse than key to the right // For stability, we only move if key is strictly better than results[j] while (j >= 0 && wifi_scan_result_is_better(key, results[j])) { - results[j + 1] = results[j]; + memcpy_fn(&results[j + 1], &results[j], elem_size); j--; } - results[j + 1] = key; + memcpy_fn(&results[j + 1], key_buf, elem_size); } } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 984930c80c..63c7039f21 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -10,6 +10,7 @@ #include #include +#include #include #ifdef USE_LIBRETINY @@ -223,6 +224,14 @@ class CompactString { }; static_assert(sizeof(CompactString) == 20, "CompactString must be exactly 20 bytes"); +// CompactString is not trivially copyable (non-trivial destructor/copy for heap case). +// However, its layout has no self-referential pointers: storage_[] contains either inline +// data or an external heap pointer — never a pointer to itself. This is unlike libstdc++ +// std::string SSO where _M_p points to _M_local_buf within the same object. +// This property allows memcpy-based permutation sorting where each element ends up in +// exactly one slot (no ownership duplication). These asserts document that layout property. +static_assert(std::is_standard_layout::value, "CompactString must be standard layout"); +static_assert(!std::is_polymorphic::value, "CompactString must not have vtable"); class WiFiAP { friend class WiFiComponent; From c149be20fcf8d2eecc8a4c518673f2bb8cc1bc42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 19:31:47 +0000 Subject: [PATCH 226/609] Bump aioesphomeapi from 44.1.0 to 44.2.0 (#14324) 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 d22097b3ca..95e3710f9e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.1.7 esphome-dashboard==20260210.0 -aioesphomeapi==44.1.0 +aioesphomeapi==44.2.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 8da1e3ce21de2e720294c1ff05d3021a07b0ddb7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 19:32:53 +0000 Subject: [PATCH 227/609] Bump ruff from 0.15.2 to 0.15.3 (#14323) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 07d02e0e3c..d70dd9d0e1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.2 + rev: v0.15.3 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 3e5dc8a90c..88a38ffa99 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.2 # also change in .pre-commit-config.yaml when updating +ruff==0.15.3 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From d325890148091a682c31185ccf1c5d3864e8c303 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:48:05 -0500 Subject: [PATCH 228/609] [cc1101] Transition through IDLE in begin_tx/begin_rx for reliable state changes (#14321) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/cc1101/cc1101.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/cc1101/cc1101.cpp b/esphome/components/cc1101/cc1101.cpp index b6973da78d..51aa88b8f7 100644 --- a/esphome/components/cc1101/cc1101.cpp +++ b/esphome/components/cc1101/cc1101.cpp @@ -242,6 +242,9 @@ void CC1101Component::begin_tx() { if (this->gdo0_pin_ != nullptr) { this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT); } + // Transition through IDLE to bypass CCA (Clear Channel Assessment) which can + // block TX entry when strobing from RX, and to ensure FS_AUTOCAL calibration + this->enter_idle_(); if (!this->enter_tx_()) { ESP_LOGW(TAG, "Failed to enter TX state!"); } @@ -252,6 +255,8 @@ void CC1101Component::begin_rx() { if (this->gdo0_pin_ != nullptr) { this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); } + // Transition through IDLE to ensure FS_AUTOCAL calibration occurs + this->enter_idle_(); if (!this->enter_rx_()) { ESP_LOGW(TAG, "Failed to enter RX state!"); } From e8b45e53fd4020ecc65500e9df8c0c7145b4f1fe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 13:02:25 -0700 Subject: [PATCH 229/609] [libretiny] Use -Os optimization for ESPHome source on BK72xx (SDK remains at -O1) (#14322) --- esphome/components/libretiny/__init__.py | 10 +++++++++- esphome/components/libretiny/lt_component.cpp | 3 +++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 2291114d9a..8f99124604 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -27,6 +27,7 @@ from esphome.storage_json import StorageJSON from . import gpio # noqa from .const import ( + COMPONENT_BK72XX, CONF_GPIO_RECOVER, CONF_LOGLEVEL, CONF_SDK_SILENT, @@ -453,7 +454,14 @@ async def component_to_code(config): cg.add_platformio_option("lib_ldf_mode", "off") cg.add_platformio_option("lib_compat_mode", "soft") # include in every file - cg.add_platformio_option("build_src_flags", "-include Arduino.h") + build_src_flags = "-include Arduino.h" + if FAMILY_COMPONENT[config[CONF_FAMILY]] == COMPONENT_BK72XX: + # LibreTiny forces -O1 globally for BK72xx because the Beken SDK + # has issues with higher optimization levels. However, ESPHome code + # works fine with -Os (used on every other platform), so override + # it for project source files only. GCC uses the last -O flag. + build_src_flags += " -Os" + cg.add_platformio_option("build_src_flags", build_src_flags) # dummy version code cg.add_define("USE_ARDUINO_VERSION_CODE", cg.RawExpression("VERSION_CODE(0, 0, 0)")) # decrease web server stack size (16k words -> 4k words) diff --git a/esphome/components/libretiny/lt_component.cpp b/esphome/components/libretiny/lt_component.cpp index ffccd0ad7a..834245c147 100644 --- a/esphome/components/libretiny/lt_component.cpp +++ b/esphome/components/libretiny/lt_component.cpp @@ -15,6 +15,9 @@ void LTComponent::dump_config() { " Version: %s\n" " Loglevel: %u", LT_BANNER_STR + 10, LT_LOGLEVEL); +#if defined(__OPTIMIZE_SIZE__) && __OPTIMIZE_LEVEL__ > 0 && __OPTIMIZE_LEVEL__ <= 3 + ESP_LOGCONFIG(TAG, " Optimization: -Os, SDK: -O" STRINGIFY_MACRO(__OPTIMIZE_LEVEL__)); +#endif #ifdef USE_TEXT_SENSOR if (this->version_ != nullptr) { From 08035261b85617b6b978336c8ca56629a86bb5cc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 13:02:36 -0700 Subject: [PATCH 230/609] [libretiny] Use C++17 nested namespace syntax (#14325) --- esphome/components/libretiny/core.h | 4 +--- esphome/components/libretiny/gpio_arduino.cpp | 7 ++++--- esphome/components/libretiny/gpio_arduino.h | 6 ++---- esphome/components/libretiny/lt_component.cpp | 6 ++---- esphome/components/libretiny/lt_component.h | 6 ++---- esphome/components/libretiny/preferences.cpp | 7 ++++--- esphome/components/libretiny/preferences.h | 6 ++---- 7 files changed, 17 insertions(+), 25 deletions(-) diff --git a/esphome/components/libretiny/core.h b/esphome/components/libretiny/core.h index 9458df1f16..f909db4f0f 100644 --- a/esphome/components/libretiny/core.h +++ b/esphome/components/libretiny/core.h @@ -4,8 +4,6 @@ #include -namespace esphome { -namespace libretiny {} // namespace libretiny -} // namespace esphome +namespace esphome::libretiny {} // namespace esphome::libretiny #endif // USE_LIBRETINY diff --git a/esphome/components/libretiny/gpio_arduino.cpp b/esphome/components/libretiny/gpio_arduino.cpp index 0b14c77cf2..1af0dce16d 100644 --- a/esphome/components/libretiny/gpio_arduino.cpp +++ b/esphome/components/libretiny/gpio_arduino.cpp @@ -3,8 +3,7 @@ #include "gpio_arduino.h" #include "esphome/core/log.h" -namespace esphome { -namespace libretiny { +namespace esphome::libretiny { static const char *const TAG = "lt.gpio"; @@ -77,7 +76,9 @@ void ArduinoInternalGPIOPin::detach_interrupt() const { detachInterrupt(pin_); // NOLINT } -} // namespace libretiny +} // namespace esphome::libretiny + +namespace esphome { using namespace libretiny; diff --git a/esphome/components/libretiny/gpio_arduino.h b/esphome/components/libretiny/gpio_arduino.h index 30c7c33869..5f1fa3fec7 100644 --- a/esphome/components/libretiny/gpio_arduino.h +++ b/esphome/components/libretiny/gpio_arduino.h @@ -3,8 +3,7 @@ #ifdef USE_LIBRETINY #include "esphome/core/hal.h" -namespace esphome { -namespace libretiny { +namespace esphome::libretiny { class ArduinoInternalGPIOPin : public InternalGPIOPin { public: @@ -31,7 +30,6 @@ class ArduinoInternalGPIOPin : public InternalGPIOPin { gpio::Flags flags_{}; }; -} // namespace libretiny -} // namespace esphome +} // namespace esphome::libretiny #endif // USE_LIBRETINY diff --git a/esphome/components/libretiny/lt_component.cpp b/esphome/components/libretiny/lt_component.cpp index 834245c147..c01661b3a6 100644 --- a/esphome/components/libretiny/lt_component.cpp +++ b/esphome/components/libretiny/lt_component.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace libretiny { +namespace esphome::libretiny { static const char *const TAG = "lt.component"; @@ -28,7 +27,6 @@ void LTComponent::dump_config() { float LTComponent::get_setup_priority() const { return setup_priority::LATE; } -} // namespace libretiny -} // namespace esphome +} // namespace esphome::libretiny #endif // USE_LIBRETINY diff --git a/esphome/components/libretiny/lt_component.h b/esphome/components/libretiny/lt_component.h index 3d4483ab5d..896f1901e3 100644 --- a/esphome/components/libretiny/lt_component.h +++ b/esphome/components/libretiny/lt_component.h @@ -12,8 +12,7 @@ #include "esphome/components/text_sensor/text_sensor.h" #endif -namespace esphome { -namespace libretiny { +namespace esphome::libretiny { class LTComponent : public Component { public: @@ -30,7 +29,6 @@ class LTComponent : public Component { #endif // USE_TEXT_SENSOR }; -} // namespace libretiny -} // namespace esphome +} // namespace esphome::libretiny #endif // USE_LIBRETINY diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index 8549631e46..740c1a233a 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -8,8 +8,7 @@ #include #include -namespace esphome { -namespace libretiny { +namespace esphome::libretiny { static const char *const TAG = "lt.preferences"; @@ -194,7 +193,9 @@ void setup_preferences() { global_preferences = &s_preferences; } -} // namespace libretiny +} // namespace esphome::libretiny + +namespace esphome { ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/libretiny/preferences.h b/esphome/components/libretiny/preferences.h index 8ec3cd31b1..68f377bd3e 100644 --- a/esphome/components/libretiny/preferences.h +++ b/esphome/components/libretiny/preferences.h @@ -2,12 +2,10 @@ #ifdef USE_LIBRETINY -namespace esphome { -namespace libretiny { +namespace esphome::libretiny { void setup_preferences(); -} // namespace libretiny -} // namespace esphome +} // namespace esphome::libretiny #endif // USE_LIBRETINY From 54edc46c7f95ee35ec3c681276aa6a789fdeafdc Mon Sep 17 00:00:00 2001 From: Oliver Kleinecke Date: Thu, 26 Feb 2026 21:12:52 +0100 Subject: [PATCH 231/609] [esp_ldo] Add channels 1&2 support and passthrough mode (#14177) --- esphome/components/esp_ldo/__init__.py | 67 ++++++++++++++++--- esphome/components/esp_ldo/esp_ldo.cpp | 20 +++--- esphome/components/esp_ldo/esp_ldo.h | 4 +- .../components/esp_ldo/test.esp32-p4-idf.yaml | 9 ++- 4 files changed, 75 insertions(+), 25 deletions(-) diff --git a/esphome/components/esp_ldo/__init__.py b/esphome/components/esp_ldo/__init__.py index f136dd149b..5235a9411e 100644 --- a/esphome/components/esp_ldo/__init__.py +++ b/esphome/components/esp_ldo/__init__.py @@ -13,22 +13,63 @@ esp_ldo_ns = cg.esphome_ns.namespace("esp_ldo") EspLdo = esp_ldo_ns.class_("EspLdo", cg.Component) AdjustAction = esp_ldo_ns.class_("AdjustAction", Action) -CHANNELS = (3, 4) +CHANNELS = (1, 2, 3, 4) +CHANNELS_INTERNAL = (1, 2) CONF_ADJUSTABLE = "adjustable" +CONF_ALLOW_INTERNAL_CHANNEL = "allow_internal_channel" +CONF_PASSTHROUGH = "passthrough" adjusted_ids = set() + +def validate_ldo_voltage(value): + if isinstance(value, str) and value.lower() == CONF_PASSTHROUGH: + return CONF_PASSTHROUGH + value = cv.voltage(value) + if 0.5 <= value <= 2.7: + return value + raise cv.Invalid( + f"LDO voltage must be in range 0.5V-2.7V or 'passthrough' (bypass mode), got {value}V" + ) + + +def validate_ldo_config(config): + channel = config[CONF_CHANNEL] + allow_internal = config[CONF_ALLOW_INTERNAL_CHANNEL] + if allow_internal and channel not in CHANNELS_INTERNAL: + raise cv.Invalid( + f"'{CONF_ALLOW_INTERNAL_CHANNEL}' is only valid for internal channels (1, 2). " + f"Channel {channel} is a user-configurable channel — its usage depends on your board schematic.", + path=[CONF_ALLOW_INTERNAL_CHANNEL], + ) + if channel in CHANNELS_INTERNAL and not allow_internal: + raise cv.Invalid( + f"LDO channel {channel} is normally used internally by the chip (flash/PSRAM). " + f"Set '{CONF_ALLOW_INTERNAL_CHANNEL}: true' to confirm you know what you are doing.", + path=[CONF_CHANNEL], + ) + if config[CONF_VOLTAGE] == CONF_PASSTHROUGH and config[CONF_ADJUSTABLE]: + raise cv.Invalid( + "Passthrough mode passes the supply voltage directly to the output and does not support " + "runtime voltage adjustment.", + path=[CONF_ADJUSTABLE], + ) + return config + + CONFIG_SCHEMA = cv.All( cv.ensure_list( - cv.COMPONENT_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(EspLdo), - cv.Required(CONF_VOLTAGE): cv.All( - cv.voltage, cv.float_range(min=0.5, max=2.7) - ), - cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True), - cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean, - } + cv.All( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(EspLdo), + cv.Required(CONF_VOLTAGE): validate_ldo_voltage, + cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True), + cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean, + cv.Optional(CONF_ALLOW_INTERNAL_CHANNEL, default=False): cv.boolean, + } + ), + validate_ldo_config, ) ), cv.only_on_esp32, @@ -40,7 +81,11 @@ async def to_code(configs): for config in configs: var = cg.new_Pvariable(config[CONF_ID], config[CONF_CHANNEL]) await cg.register_component(var, config) - cg.add(var.set_voltage(config[CONF_VOLTAGE])) + voltage = config[CONF_VOLTAGE] + if voltage == CONF_PASSTHROUGH: + cg.add(var.set_voltage(3300)) + else: + cg.add(var.set_voltage(int(round(voltage * 1000)))) cg.add(var.set_adjustable(config[CONF_ADJUSTABLE])) diff --git a/esphome/components/esp_ldo/esp_ldo.cpp b/esphome/components/esp_ldo/esp_ldo.cpp index 2eee855b46..f8ebec1903 100644 --- a/esphome/components/esp_ldo/esp_ldo.cpp +++ b/esphome/components/esp_ldo/esp_ldo.cpp @@ -10,32 +10,34 @@ static const char *const TAG = "esp_ldo"; void EspLdo::setup() { esp_ldo_channel_config_t config{}; config.chan_id = this->channel_; - config.voltage_mv = (int) (this->voltage_ * 1000.0f); + config.voltage_mv = this->voltage_mv_; config.flags.adjustable = this->adjustable_; auto err = esp_ldo_acquire_channel(&config, &this->handle_); if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_); + ESP_LOGE(TAG, "Failed to acquire LDO channel %d with voltage %dmV", this->channel_, this->voltage_mv_); this->mark_failed(LOG_STR("Failed to acquire LDO channel")); } else { - ESP_LOGD(TAG, "Acquired LDO channel %d with voltage %fV", this->channel_, this->voltage_); + ESP_LOGD(TAG, "Acquired LDO channel %d with voltage %dmV", this->channel_, this->voltage_mv_); } } void EspLdo::dump_config() { ESP_LOGCONFIG(TAG, "ESP LDO Channel %d:\n" - " Voltage: %fV\n" + " Voltage: %dmV\n" " Adjustable: %s", - this->channel_, this->voltage_, YESNO(this->adjustable_)); + this->channel_, this->voltage_mv_, YESNO(this->adjustable_)); } void EspLdo::adjust_voltage(float voltage) { if (!std::isfinite(voltage) || voltage < 0.5f || voltage > 2.7f) { - ESP_LOGE(TAG, "Invalid voltage %fV for LDO channel %d", voltage, this->channel_); + ESP_LOGE(TAG, "Invalid voltage %fV for LDO channel %d (must be 0.5V-2.7V)", voltage, this->channel_); return; } - auto erro = esp_ldo_channel_adjust_voltage(this->handle_, (int) (voltage * 1000.0f)); - if (erro != ESP_OK) { - ESP_LOGE(TAG, "Failed to adjust LDO channel %d to voltage %fV: %s", this->channel_, voltage, esp_err_to_name(erro)); + int voltage_mv = (int) roundf(voltage * 1000.0f); + auto err = esp_ldo_channel_adjust_voltage(this->handle_, voltage_mv); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to adjust LDO channel %d to voltage %dmV: %s", this->channel_, voltage_mv, + esp_err_to_name(err)); } } diff --git a/esphome/components/esp_ldo/esp_ldo.h b/esphome/components/esp_ldo/esp_ldo.h index 9edd303e16..1a20f1d08a 100644 --- a/esphome/components/esp_ldo/esp_ldo.h +++ b/esphome/components/esp_ldo/esp_ldo.h @@ -15,7 +15,7 @@ class EspLdo : public Component { void dump_config() override; void set_adjustable(bool adjustable) { this->adjustable_ = adjustable; } - void set_voltage(float voltage) { this->voltage_ = voltage; } + void set_voltage(int voltage_mv) { this->voltage_mv_ = voltage_mv; } void adjust_voltage(float voltage); float get_setup_priority() const override { return setup_priority::BUS; // LDO setup should be done early @@ -23,7 +23,7 @@ class EspLdo : public Component { protected: int channel_; - float voltage_{2.7}; + int voltage_mv_{2700}; bool adjustable_{false}; esp_ldo_channel_handle_t handle_{}; }; diff --git a/tests/components/esp_ldo/test.esp32-p4-idf.yaml b/tests/components/esp_ldo/test.esp32-p4-idf.yaml index 38bd6ac411..31d2b8cf7a 100644 --- a/tests/components/esp_ldo/test.esp32-p4-idf.yaml +++ b/tests/components/esp_ldo/test.esp32-p4-idf.yaml @@ -3,10 +3,13 @@ esp_ldo: channel: 3 voltage: 2.5V adjustable: true - - id: ldo_4 + - id: ldo_4_passthrough channel: 4 - voltage: 2.0V - setup_priority: 900 + voltage: passthrough + - id: ldo_1_internal + channel: 1 + voltage: 1.8V + allow_internal_channel: true esphome: on_boot: From 8bd474fd017d31f4f8504f4c96db5a8e1a7f2f80 Mon Sep 17 00:00:00 2001 From: lyubomirtraykov Date: Thu, 26 Feb 2026 22:27:18 +0200 Subject: [PATCH 232/609] [api] Add DEFROSTING to ClimateAction (#13976) Co-authored-by: J. Nick Koston --- esphome/components/api/api.proto | 1 + esphome/components/api/api_pb2.h | 1 + esphome/components/api/api_pb2_dump.cpp | 2 ++ esphome/components/climate/climate_mode.cpp | 6 ++++-- esphome/components/climate/climate_mode.h | 2 ++ esphome/components/mqtt/mqtt_climate.cpp | 5 +++-- 6 files changed, 13 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 18dac6a2d1..d7f32cd8d1 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -989,6 +989,7 @@ enum ClimateAction { CLIMATE_ACTION_IDLE = 4; CLIMATE_ACTION_DRYING = 5; CLIMATE_ACTION_FAN = 6; + CLIMATE_ACTION_DEFROSTING = 7; } enum ClimatePreset { CLIMATE_PRESET_NONE = 0; diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index c2675cefe4..22dc3de995 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -116,6 +116,7 @@ enum ClimateAction : uint32_t { CLIMATE_ACTION_IDLE = 4, CLIMATE_ACTION_DRYING = 5, CLIMATE_ACTION_FAN = 6, + CLIMATE_ACTION_DEFROSTING = 7, }; enum ClimatePreset : uint32_t { CLIMATE_PRESET_NONE = 0, diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 73690610ed..52d2486410 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -321,6 +321,8 @@ template<> const char *proto_enum_to_string(enums::Climate return "CLIMATE_ACTION_DRYING"; case enums::CLIMATE_ACTION_FAN: return "CLIMATE_ACTION_FAN"; + case enums::CLIMATE_ACTION_DEFROSTING: + return "CLIMATE_ACTION_DEFROSTING"; default: return "UNKNOWN"; } diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index c4dd19d503..8e443f4146 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -10,8 +10,10 @@ const LogString *climate_mode_to_string(ClimateMode mode) { return ClimateModeStrings::get_log_str(static_cast(mode), ClimateModeStrings::LAST_INDEX); } -// Climate action strings indexed by ClimateAction enum (0,2-6): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN -PROGMEM_STRING_TABLE(ClimateActionStrings, "OFF", "UNKNOWN", "COOLING", "HEATING", "IDLE", "DRYING", "FAN", "UNKNOWN"); +// Climate action strings indexed by ClimateAction enum (0,2-7): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN, +// DEFROSTING +PROGMEM_STRING_TABLE(ClimateActionStrings, "OFF", "UNKNOWN", "COOLING", "HEATING", "IDLE", "DRYING", "FAN", + "DEFROSTING", "UNKNOWN"); const LogString *climate_action_to_string(ClimateAction action) { return ClimateActionStrings::get_log_str(static_cast(action), ClimateActionStrings::LAST_INDEX); diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index c961c44248..014b1a9e64 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -41,6 +41,8 @@ enum ClimateAction : uint8_t { CLIMATE_ACTION_DRYING = 5, /// The climate device is in fan only mode CLIMATE_ACTION_FAN = 6, + /// The climate device is defrosting + CLIMATE_ACTION_DEFROSTING = 7, }; /// NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 81b2e0e8db..443c983efe 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -20,9 +20,10 @@ static ProgmemStr climate_mode_to_mqtt_str(ClimateMode mode) { return ClimateMqttModeStrings::get_progmem_str(static_cast(mode), ClimateMqttModeStrings::LAST_INDEX); } -// Climate action MQTT strings indexed by ClimateAction enum (0,2-6): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN +// Climate action MQTT strings indexed by ClimateAction enum (0,2-7): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN, +// DEFROSTING PROGMEM_STRING_TABLE(ClimateMqttActionStrings, "off", "unknown", "cooling", "heating", "idle", "drying", "fan", - "unknown"); + "defrosting", "unknown"); static ProgmemStr climate_action_to_mqtt_str(ClimateAction action) { return ClimateMqttActionStrings::get_progmem_str(static_cast(action), ClimateMqttActionStrings::LAST_INDEX); From 67ba68a1a09adb4cf3cb8178d625645a59a33c29 Mon Sep 17 00:00:00 2001 From: esphomebot Date: Fri, 27 Feb 2026 11:21:40 +1300 Subject: [PATCH 233/609] Update webserver local assets to 20260226-220330 (#14330) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/captive_portal/captive_index.h | 4 ++-- esphome/components/web_server/server_index_v2.h | 4 ++-- esphome/components/web_server/server_index_v3.h | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/captive_portal/captive_index.h b/esphome/components/captive_portal/captive_index.h index 645ebb7a2f..a81edc1900 100644 --- a/esphome/components/captive_portal/captive_index.h +++ b/esphome/components/captive_portal/captive_index.h @@ -6,7 +6,7 @@ namespace esphome::captive_portal { #ifdef USE_CAPTIVE_PORTAL_GZIP -const uint8_t INDEX_GZ[] PROGMEM = { +constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7e, 0x05, 0x8f, 0x49, 0xbb, 0x52, 0xb3, 0x7a, 0x7a, 0xed, 0x6c, 0x24, 0x51, 0x45, 0x9a, 0xbb, 0xa2, 0x05, 0x9a, 0x36, 0xc0, 0x6e, 0x73, 0x1f, 0x82, 0x00, 0x4b, 0x53, 0x23, 0x8b, 0x31, 0x45, 0xea, 0x48, 0xca, 0x8f, 0x18, 0xbe, 0xdf, @@ -86,7 +86,7 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0xfc, 0xda, 0xd1, 0xf8, 0xe9, 0xa3, 0xe1, 0xa6, 0xfb, 0x1f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00}; #else // Brotli (default, smaller) -const uint8_t INDEX_BR[] PROGMEM = { +constexpr uint8_t INDEX_BR[] PROGMEM = { 0x1b, 0xf8, 0x0a, 0x00, 0x64, 0x5a, 0xd3, 0xfa, 0xe7, 0xf3, 0x62, 0xd8, 0x06, 0x1b, 0xe9, 0x6a, 0x8a, 0x81, 0x2b, 0xb5, 0x49, 0x14, 0x37, 0xdc, 0x9e, 0x1a, 0xcb, 0x56, 0x87, 0xfb, 0xff, 0xf7, 0x73, 0x75, 0x12, 0x0a, 0xd6, 0x48, 0x84, 0xc6, 0x21, 0xa4, 0x6d, 0xb5, 0x71, 0xef, 0x13, 0xbe, 0x4e, 0x54, 0xf1, 0x64, 0x8f, 0x3f, 0xcc, 0x9a, 0x78, diff --git a/esphome/components/web_server/server_index_v2.h b/esphome/components/web_server/server_index_v2.h index ffa9c87b3a..ac2195f387 100644 --- a/esphome/components/web_server/server_index_v2.h +++ b/esphome/components/web_server/server_index_v2.h @@ -9,7 +9,7 @@ namespace esphome::web_server { #ifdef USE_WEBSERVER_GZIP -const uint8_t INDEX_GZ[] PROGMEM = { +constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xed, 0x7d, 0xd9, 0x72, 0xdb, 0x48, 0xb6, 0xe0, 0xf3, 0xd4, 0x57, 0x40, 0x28, 0xb5, 0x8c, 0x2c, 0x26, 0xc1, 0x45, 0x92, 0x2d, 0x83, 0x4a, 0xb2, 0x65, 0xd9, 0xd5, 0x76, 0x97, 0xb7, 0xb6, 0xec, 0xda, 0x58, 0x6c, 0x09, 0x02, 0x92, 0x44, 0x96, 0x41, 0x80, 0x05, 0x24, 0xb5, 0x14, 0x89, @@ -698,7 +698,7 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0x01, 0x65, 0x21, 0x07, 0x4b, 0xe3, 0x97, 0x00, 0x00}; #else // Brotli (default, smaller) -const uint8_t INDEX_BR[] PROGMEM = { +constexpr uint8_t INDEX_BR[] PROGMEM = { 0x1b, 0xe2, 0x97, 0xa3, 0x90, 0xa2, 0x95, 0x55, 0x51, 0x04, 0x1b, 0x07, 0x80, 0x20, 0x79, 0x0e, 0x50, 0xab, 0x02, 0xdb, 0x98, 0x16, 0xf4, 0x7b, 0x22, 0xa3, 0x4d, 0xd3, 0x86, 0xc1, 0x26, 0x48, 0x49, 0x60, 0xbe, 0xb3, 0xc9, 0xa1, 0x8c, 0x96, 0x10, 0x1b, 0x21, 0xcf, 0x48, 0x68, 0xce, 0x10, 0x34, 0x32, 0x7c, 0xbf, 0x71, 0x7b, 0x03, 0x8f, 0xdd, diff --git a/esphome/components/web_server/server_index_v3.h b/esphome/components/web_server/server_index_v3.h index b7c15df32b..a1cafe8707 100644 --- a/esphome/components/web_server/server_index_v3.h +++ b/esphome/components/web_server/server_index_v3.h @@ -9,7 +9,7 @@ namespace esphome::web_server { #ifdef USE_WEBSERVER_GZIP -const uint8_t INDEX_GZ[] PROGMEM = { +constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xcc, 0xbd, 0x7b, 0x7f, 0x1a, 0xb9, 0xb2, 0x28, 0xfa, 0xf7, 0x3d, 0x9f, 0xc2, 0xee, 0x9d, 0xf1, 0xb4, 0x8c, 0x68, 0x03, 0x36, 0x8e, 0xd3, 0x58, 0xe6, 0xe4, 0x39, 0xc9, 0x3c, 0x92, 0x4c, 0x9c, 0x64, 0x26, 0xc3, 0xb0, 0x33, 0xa2, 0x11, 0xa0, 0xa4, 0x91, 0x98, 0x96, 0x88, 0xed, 0x01, @@ -4107,7 +4107,7 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0xe8, 0xcd, 0xfe, 0x2c, 0x9d, 0x07, 0xfd, 0xff, 0x05, 0x64, 0x23, 0xa6, 0xdb, 0x06, 0x7b, 0x03, 0x00}; #else // Brotli (default, smaller) -const uint8_t INDEX_BR[] PROGMEM = { +constexpr uint8_t INDEX_BR[] PROGMEM = { 0x5b, 0x05, 0x7b, 0x53, 0xc1, 0xb6, 0x69, 0x3d, 0x41, 0xeb, 0x04, 0x30, 0xf6, 0xd6, 0x77, 0x35, 0xdb, 0xa3, 0x08, 0x36, 0x0e, 0x04, 0x80, 0x90, 0x4f, 0xf1, 0xb2, 0x21, 0xa4, 0x82, 0xee, 0x00, 0xaa, 0x20, 0x7f, 0x3b, 0xff, 0x00, 0xaa, 0x9a, 0x73, 0x74, 0x8c, 0xe1, 0xa6, 0x1f, 0xa0, 0xa2, 0x59, 0xf5, 0xaa, 0x92, 0x79, 0x50, 0x43, 0x1f, 0xe8, From 527d4964f61b77439c469d41207527c5677b813b Mon Sep 17 00:00:00 2001 From: George Joseph Date: Thu, 26 Feb 2026 17:38:07 -0700 Subject: [PATCH 234/609] [mipi_dsi] Add more Waveshare panels and comments (#14023) --- .../components/mipi_dsi/models/waveshare.py | 139 +++++++++++++++++- 1 file changed, 136 insertions(+), 3 deletions(-) diff --git a/esphome/components/mipi_dsi/models/waveshare.py b/esphome/components/mipi_dsi/models/waveshare.py index bf4f9063bb..69414065f1 100644 --- a/esphome/components/mipi_dsi/models/waveshare.py +++ b/esphome/components/mipi_dsi/models/waveshare.py @@ -2,7 +2,11 @@ from esphome.components.mipi import DriverChip import esphome.config_validation as cv # fmt: off -DriverChip( + +# Source for parameters and initsequence: +# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365_10_1 +# Product page: https://www.waveshare.com/wiki/ESP32-P4-Nano-StartPage +JD9365_10_1_DSI_TOUCH_A = DriverChip( "WAVESHARE-P4-NANO-10.1", height=1280, width=800, @@ -52,6 +56,15 @@ DriverChip( ], ) +# Standalone display +# Product page: https://www.waveshare.com/wiki/10.1-DSI-TOUCH-A +JD9365_10_1_DSI_TOUCH_A.extend( + "WAVESHARE-10.1-DSI-TOUCH-A", +) + +# Source for parameters and initsequence: +# https://github.com/espressif/esp-iot-solution/tree/master/components/display/lcd/esp_lcd_st7703 +# Product page: https://www.waveshare.com/wiki/ESP32-P4-86-Panel-ETH-2RO DriverChip( "WAVESHARE-P4-86-PANEL", height=720, @@ -95,6 +108,9 @@ DriverChip( ], ) +# Source for parameters and initsequence: +# https://github.com/espressif/esp-iot-solution/tree/master/components/display/lcd/esp_lcd_ek79007 +# Product page: https://www.waveshare.com/wiki/ESP32-P4-WIFI6-Touch-LCD-7B DriverChip( "WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-7B", height=600, @@ -121,7 +137,10 @@ DriverChip( ], ) -DriverChip( +# Source for parameters and initsequence: +# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365 +# Product page: https://www.waveshare.com/wiki/ESP32-P4-WIFI6-Touch-LCD-3.4C +JD9365_3_4_DSI_TOUCH_C = DriverChip( "WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-3.4C", height=800, width=800, @@ -170,7 +189,16 @@ DriverChip( ], ) -DriverChip( +# Standalone display +# Product page: https://www.waveshare.com/wiki/3.4-DSI-TOUCH-C +JD9365_3_4_DSI_TOUCH_C.extend( + "WAVESHARE-3.4-DSI-TOUCH-C", +) + +# Source for parameters and initsequence: +# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365 +# Product page: https://www.waveshare.com/wiki/ESP32-P4-WIFI6-Touch-LCD-4C +JD9365_4_DSI_TOUCH_C = DriverChip( "WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-4C", height=720, width=720, @@ -218,3 +246,108 @@ DriverChip( (0xE0, 0x00), # select userpage ] ) + +# Standalone display +# Product page: https://www.waveshare.com/wiki/4-DSI-TOUCH-C +JD9365_4_DSI_TOUCH_C.extend( + "WAVESHARE-4-DSI-TOUCH-C", +) + +# Source for parameters and initsequence: +# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365 +# Product page: https://www.waveshare.com/wiki/8-DSI-TOUCH-A +DriverChip( + "WAVESHARE-8-DSI-TOUCH-A", + height=1280, + width=800, + hsync_back_porch=20, + hsync_pulse_width=20, + hsync_front_porch=40, + vsync_back_porch=12, + vsync_pulse_width=4, + vsync_front_porch=30, + pclk_frequency="80MHz", + lane_bit_rate="1.5Gbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + initsequence=[ + (0xE0, 0x00), # select userpage + (0xE1, 0x93), (0xE2, 0x65), (0xE3, 0xF8), + (0x80, 0x01), # Select number of lanes (2) + (0xE0, 0x01), # select page 1 + (0x00, 0x00), (0x01, 0x4E), (0x03, 0x00), (0x04, 0x65), (0x0C, 0x74), (0x17, 0x00), (0x18, 0xB7), (0x19, 0x00), + (0x1A, 0x00), (0x1B, 0xB7), (0x1C, 0x00), (0x24, 0xFE), (0x37, 0x19), (0x38, 0x05), (0x39, 0x00), (0x3A, 0x01), + (0x3B, 0x01), (0x3C, 0x70), (0x3D, 0xFF), (0x3E, 0xFF), (0x3F, 0xFF), (0x40, 0x06), (0x41, 0xA0), (0x43, 0x1E), + (0x44, 0x0F), (0x45, 0x28), (0x4B, 0x04), (0x55, 0x02), (0x56, 0x01), (0x57, 0xA9), (0x58, 0x0A), (0x59, 0x0A), + (0x5A, 0x37), (0x5B, 0x19), (0x5D, 0x78), (0x5E, 0x63), (0x5F, 0x54), (0x60, 0x49), (0x61, 0x45), (0x62, 0x38), + (0x63, 0x3D), (0x64, 0x28), (0x65, 0x43), (0x66, 0x41), (0x67, 0x43), (0x68, 0x62), (0x69, 0x50), (0x6A, 0x57), + (0x6B, 0x49), (0x6C, 0x44), (0x6D, 0x37), (0x6E, 0x23), (0x6F, 0x10), (0x70, 0x78), (0x71, 0x63), (0x72, 0x54), + (0x73, 0x49), (0x74, 0x45), (0x75, 0x38), (0x76, 0x3D), (0x77, 0x28), (0x78, 0x43), (0x79, 0x41), (0x7A, 0x43), + (0x7B, 0x62), (0x7C, 0x50), (0x7D, 0x57), (0x7E, 0x49), (0x7F, 0x44), (0x80, 0x37), (0x81, 0x23), (0x82, 0x10), + (0xE0, 0x02), # select page 2 + (0x00, 0x47), (0x01, 0x47), (0x02, 0x45), (0x03, 0x45), (0x04, 0x4B), (0x05, 0x4B), (0x06, 0x49), (0x07, 0x49), + (0x08, 0x41), (0x09, 0x1F), (0x0A, 0x1F), (0x0B, 0x1F), (0x0C, 0x1F), (0x0D, 0x1F), (0x0E, 0x1F), (0x0F, 0x5F), + (0x10, 0x5F), (0x11, 0x57), (0x12, 0x77), (0x13, 0x35), (0x14, 0x1F), (0x15, 0x1F), (0x16, 0x46), (0x17, 0x46), + (0x18, 0x44), (0x19, 0x44), (0x1A, 0x4A), (0x1B, 0x4A), (0x1C, 0x48), (0x1D, 0x48), (0x1E, 0x40), (0x1F, 0x1F), + (0x20, 0x1F), (0x21, 0x1F), (0x22, 0x1F), (0x23, 0x1F), (0x24, 0x1F), (0x25, 0x5F), (0x26, 0x5F), (0x27, 0x57), + (0x28, 0x77), (0x29, 0x35), (0x2A, 0x1F), (0x2B, 0x1F), (0x58, 0x40), (0x59, 0x00), (0x5A, 0x00), (0x5B, 0x10), + (0x5C, 0x06), (0x5D, 0x40), (0x5E, 0x01), (0x5F, 0x02), (0x60, 0x30), (0x61, 0x01), (0x62, 0x02), (0x63, 0x03), + (0x64, 0x6B), (0x65, 0x05), (0x66, 0x0C), (0x67, 0x73), (0x68, 0x09), (0x69, 0x03), (0x6A, 0x56), (0x6B, 0x08), + (0x6C, 0x00), (0x6D, 0x04), (0x6E, 0x04), (0x6F, 0x88), (0x70, 0x00), (0x71, 0x00), (0x72, 0x06), (0x73, 0x7B), + (0x74, 0x00), (0x75, 0xF8), (0x76, 0x00), (0x77, 0xD5), (0x78, 0x2E), (0x79, 0x12), (0x7A, 0x03), (0x7B, 0x00), + (0x7C, 0x00), (0x7D, 0x03), (0x7E, 0x7B), + (0xE0, 0x04), # select page 4 + (0x00, 0x0E), (0x02, 0xB3), (0x09, 0x60), (0x0E, 0x2A), (0x36, 0x59), (0x37, 0x58), (0x2B, 0x0F), + (0xE0, 0x00), # select userpage + ] +) + +# Source for parameters and initsequence: +# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_ili9881c +# Product page: https://www.waveshare.com/wiki/7-DSI-TOUCH-A +DriverChip( + "WAVESHARE-7-DSI-TOUCH-A", + height=1280, + width=720, + hsync_back_porch=239, + hsync_pulse_width=50, + hsync_front_porch=33, + vsync_back_porch=20, + vsync_pulse_width=30, + vsync_front_porch=2, + pclk_frequency="80MHz", + lane_bit_rate="1000Mbps", + no_transform=True, + color_order="RGB", + initsequence=[ + (0xFF, 0x98, 0x81, 0x03), + (0x01, 0x00), (0x02, 0x00), (0x03, 0x73), (0x04, 0x00), (0x05, 0x00), (0x06, 0x0A), (0x07, 0x00), (0x08, 0x00), + (0x09, 0x61), (0x0A, 0x00), (0x0B, 0x00), (0x0C, 0x01), (0x0D, 0x00), (0x0E, 0x00), (0x0F, 0x61), (0x10, 0x61), + (0x11, 0x00), (0x12, 0x00), (0x13, 0x00), (0x14, 0x00), (0x15, 0x00), (0x16, 0x00), (0x17, 0x00), (0x18, 0x00), + (0x19, 0x00), (0x1A, 0x00), (0x1B, 0x00), (0x1C, 0x00), (0x1D, 0x00), (0x1E, 0x40), (0x1F, 0x80), (0x20, 0x06), + (0x21, 0x01), (0x22, 0x00), (0x23, 0x00), (0x24, 0x00), (0x25, 0x00), (0x26, 0x00), (0x27, 0x00), (0x28, 0x33), + (0x29, 0x03), (0x2A, 0x00), (0x2B, 0x00), (0x2C, 0x00), (0x2D, 0x00), (0x2E, 0x00), (0x2F, 0x00), (0x30, 0x00), + (0x31, 0x00), (0x32, 0x00), (0x33, 0x00), (0x34, 0x04), (0x35, 0x00), (0x36, 0x00), (0x37, 0x00), (0x38, 0x3C), + (0x39, 0x00), (0x3A, 0x00), (0x3B, 0x00), (0x3C, 0x00), (0x3D, 0x00), (0x3E, 0x00), (0x3F, 0x00), (0x40, 0x00), + (0x41, 0x00), (0x42, 0x00), (0x43, 0x00), (0x44, 0x00), (0x50, 0x10), (0x51, 0x32), (0x52, 0x54), (0x53, 0x76), + (0x54, 0x98), (0x55, 0xBA), (0x56, 0x10), (0x57, 0x32), (0x58, 0x54), (0x59, 0x76), (0x5A, 0x98), (0x5B, 0xBA), + (0x5C, 0xDC), (0x5D, 0xFE), (0x5E, 0x00), (0x5F, 0x0E), (0x60, 0x0F), (0x61, 0x0C), (0x62, 0x0D), (0x63, 0x06), + (0x64, 0x07), (0x65, 0x02), (0x66, 0x02), (0x67, 0x02), (0x68, 0x02), (0x69, 0x01), (0x6A, 0x00), (0x6B, 0x02), + (0x6C, 0x15), (0x6D, 0x14), (0x6E, 0x02), (0x6F, 0x02), (0x70, 0x02), (0x71, 0x02), (0x72, 0x02), (0x73, 0x02), + (0x74, 0x02), (0x75, 0x0E), (0x76, 0x0F), (0x77, 0x0C), (0x78, 0x0D), (0x79, 0x06), (0x7A, 0x07), (0x7B, 0x02), + (0x7C, 0x02), (0x7D, 0x02), (0x7E, 0x02), (0x7F, 0x01), (0x80, 0x00), (0x81, 0x02), (0x82, 0x14), (0x83, 0x15), + (0x84, 0x02), (0x85, 0x02), (0x86, 0x02), (0x87, 0x02), (0x88, 0x02), (0x89, 0x02), (0x8A, 0x02), + (0xFF, 0x98, 0x81, 0x04), + (0x38, 0x01), (0x39, 0x00), (0x6C, 0x15), (0x6E, 0x2A), (0x6F, 0x33), (0x3A, 0x94), (0x8D, 0x14), (0x87, 0xBA), + (0x26, 0x76), (0xB2, 0xD1), (0xB5, 0x06), (0x3B, 0x98), + (0xFF, 0x98, 0x81, 0x01), + (0x22, 0x0A), (0x31, 0x00), (0x53, 0x71), (0x55, 0x8F), (0x40, 0x33), (0x50, 0x96), (0x51, 0x96), (0x60, 0x23), + (0xA0, 0x08), (0xA1, 0x1D), (0xA2, 0x2A), (0xA3, 0x10), (0xA4, 0x15), (0xA5, 0x28), (0xA6, 0x1C), (0xA7, 0x1D), + (0xA8, 0x7E), (0xA9, 0x1D), (0xAA, 0x29), (0xAB, 0x6B), (0xAC, 0x1A), (0xAD, 0x18), (0xAE, 0x4B), (0xAF, 0x20), + (0xB0, 0x27), (0xB1, 0x50), (0xB2, 0x64), (0xB3, 0x39), (0xC0, 0x08), (0xC1, 0x1D), (0xC2, 0x2A), (0xC3, 0x10), + (0xC4, 0x15), (0xC5, 0x28), (0xC6, 0x1C), (0xC7, 0x1D), (0xC8, 0x7E), (0xC9, 0x1D), (0xCA, 0x29), (0xCB, 0x6B), + (0xCC, 0x1A), (0xCD, 0x18), (0xCE, 0x4B), (0xCF, 0x20), (0xD0, 0x27), (0xD1, 0x50), (0xD2, 0x64), (0xD3, 0x39), + (0xFF, 0x98, 0x81, 0x00), + (0x3A, 0x77), (0x36, 0x00), (0x35, 0x00), (0x35, 0x00), + ], +) From 1ccfcfc8d85593f81e96ea40cd4e8a162392a678 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 18:12:44 -0700 Subject: [PATCH 235/609] [time] Eliminate libc timezone bloat (~9.5KB flash ESP32, ~2% RAM on ESP8266) (#13635) Co-authored-by: Claude Opus 4.6 --- esphome/components/time/posix_tz.cpp | 488 +++++++ esphome/components/time/posix_tz.h | 144 +++ esphome/components/time/real_time_clock.cpp | 61 +- esphome/components/time/real_time_clock.h | 35 +- esphome/core/time.cpp | 232 +++- esphome/core/time.h | 18 +- script/cpp_unit_test.py | 1 + tests/components/time/posix_tz_parser.cpp | 1275 +++++++++++++++++++ 8 files changed, 2163 insertions(+), 91 deletions(-) create mode 100644 esphome/components/time/posix_tz.cpp create mode 100644 esphome/components/time/posix_tz.h create mode 100644 tests/components/time/posix_tz_parser.cpp diff --git a/esphome/components/time/posix_tz.cpp b/esphome/components/time/posix_tz.cpp new file mode 100644 index 0000000000..4d1f0c74c2 --- /dev/null +++ b/esphome/components/time/posix_tz.cpp @@ -0,0 +1,488 @@ +#include "esphome/core/defines.h" + +#ifdef USE_TIME_TIMEZONE + +#include "posix_tz.h" +#include + +namespace esphome::time { + +// Global timezone - set once at startup, rarely changes +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) - intentional mutable state +static ParsedTimezone global_tz_{}; + +void set_global_tz(const ParsedTimezone &tz) { global_tz_ = tz; } + +const ParsedTimezone &get_global_tz() { return global_tz_; } + +namespace internal { + +// Remove before 2026.9.0: parse_uint, skip_tz_name, parse_offset, parse_dst_rule, +// and parse_transition_time are only used by parse_posix_tz() (bridge code). +static uint32_t parse_uint(const char *&p) { + uint32_t value = 0; + while (std::isdigit(static_cast(*p))) { + value = value * 10 + (*p - '0'); + p++; + } + return value; +} + +bool is_leap_year(int year) { return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); } + +// Get days in year (avoids duplicate is_leap_year calls) +static inline int days_in_year(int year) { return is_leap_year(year) ? 366 : 365; } + +// Convert days since epoch to year, updating days to remainder +static int __attribute__((noinline)) days_to_year(int64_t &days) { + int year = 1970; + int diy; + while (days >= (diy = days_in_year(year)) && year < 2200) { + days -= diy; + year++; + } + while (days < 0 && year > 1900) { + year--; + days += days_in_year(year); + } + return year; +} + +// Extract just the year from a UTC epoch +static int epoch_to_year(time_t epoch) { + int64_t days = epoch / 86400; + if (epoch < 0 && epoch % 86400 != 0) + days--; + return days_to_year(days); +} + +int days_in_month(int year, int month) { + switch (month) { + case 2: + return is_leap_year(year) ? 29 : 28; + case 4: + case 6: + case 9: + case 11: + return 30; + default: + return 31; + } +} + +// Zeller-like algorithm for day of week (0 = Sunday) +int __attribute__((noinline)) day_of_week(int year, int month, int day) { + // Adjust for January/February + if (month < 3) { + month += 12; + year--; + } + int k = year % 100; + int j = year / 100; + int h = (day + (13 * (month + 1)) / 5 + k + k / 4 + j / 4 - 2 * j) % 7; + // Convert from Zeller (0=Sat) to standard (0=Sun) + return ((h + 6) % 7); +} + +void __attribute__((noinline)) epoch_to_tm_utc(time_t epoch, struct tm *out_tm) { + // Days since epoch + int64_t days = epoch / 86400; + int32_t remaining_secs = epoch % 86400; + if (remaining_secs < 0) { + days--; + remaining_secs += 86400; + } + + out_tm->tm_sec = remaining_secs % 60; + remaining_secs /= 60; + out_tm->tm_min = remaining_secs % 60; + out_tm->tm_hour = remaining_secs / 60; + + // Day of week (Jan 1, 1970 was Thursday = 4) + out_tm->tm_wday = static_cast((days + 4) % 7); + if (out_tm->tm_wday < 0) + out_tm->tm_wday += 7; + + // Calculate year (updates days to day-of-year) + int year = days_to_year(days); + out_tm->tm_year = year - 1900; + out_tm->tm_yday = static_cast(days); + + // Calculate month and day + int month = 1; + int dim; + while (days >= (dim = days_in_month(year, month))) { + days -= dim; + month++; + } + + out_tm->tm_mon = month - 1; + out_tm->tm_mday = static_cast(days) + 1; + out_tm->tm_isdst = 0; +} + +bool skip_tz_name(const char *&p) { + if (*p == '<') { + // Angle-bracket quoted name: <+07>, <-03>, + p++; // skip '<' + while (*p && *p != '>') { + p++; + } + if (*p == '>') { + p++; // skip '>' + return true; + } + return false; // Unterminated + } + + // Standard name: 3+ letters + const char *start = p; + while (*p && std::isalpha(static_cast(*p))) { + p++; + } + return (p - start) >= 3; +} + +int32_t __attribute__((noinline)) parse_offset(const char *&p) { + int sign = 1; + if (*p == '-') { + sign = -1; + p++; + } else if (*p == '+') { + p++; + } + + int hours = parse_uint(p); + int minutes = 0; + int seconds = 0; + + if (*p == ':') { + p++; + minutes = parse_uint(p); + if (*p == ':') { + p++; + seconds = parse_uint(p); + } + } + + return sign * (hours * 3600 + minutes * 60 + seconds); +} + +// Helper to parse the optional /time suffix (reuses parse_offset logic) +static void parse_transition_time(const char *&p, DSTRule &rule) { + rule.time_seconds = 2 * 3600; // Default 02:00 + if (*p == '/') { + p++; + rule.time_seconds = parse_offset(p); + } +} + +void __attribute__((noinline)) julian_to_month_day(int julian_day, int &out_month, int &out_day) { + // J format: day 1-365, Feb 29 is NOT counted even in leap years + // So day 60 is always March 1 + // Iterate forward through months (no array needed) + int remaining = julian_day; + out_month = 1; + while (out_month <= 12) { + // Days in month for non-leap year (J format ignores leap years) + int dim = days_in_month(2001, out_month); // 2001 is non-leap year + if (remaining <= dim) { + out_day = remaining; + return; + } + remaining -= dim; + out_month++; + } + out_day = remaining; +} + +void __attribute__((noinline)) day_of_year_to_month_day(int day_of_year, int year, int &out_month, int &out_day) { + // Plain format: day 0-365, Feb 29 IS counted in leap years + // Day 0 = Jan 1 + int remaining = day_of_year; + out_month = 1; + + while (out_month <= 12) { + int days_this_month = days_in_month(year, out_month); + if (remaining < days_this_month) { + out_day = remaining + 1; + return; + } + remaining -= days_this_month; + out_month++; + } + + // Shouldn't reach here with valid input + out_month = 12; + out_day = 31; +} + +bool parse_dst_rule(const char *&p, DSTRule &rule) { + rule = {}; // Zero initialize + + if (*p == 'M' || *p == 'm') { + // M format: Mm.w.d (month.week.day) + rule.type = DSTRuleType::MONTH_WEEK_DAY; + p++; + + rule.month = parse_uint(p); + if (rule.month < 1 || rule.month > 12) + return false; + + if (*p++ != '.') + return false; + + rule.week = parse_uint(p); + if (rule.week < 1 || rule.week > 5) + return false; + + if (*p++ != '.') + return false; + + rule.day_of_week = parse_uint(p); + if (rule.day_of_week > 6) + return false; + + } else if (*p == 'J' || *p == 'j') { + // J format: Jn (Julian day 1-365, not counting Feb 29) + rule.type = DSTRuleType::JULIAN_NO_LEAP; + p++; + + rule.day = parse_uint(p); + if (rule.day < 1 || rule.day > 365) + return false; + + } else if (std::isdigit(static_cast(*p))) { + // Plain number format: n (day 0-365, counting Feb 29) + rule.type = DSTRuleType::DAY_OF_YEAR; + + rule.day = parse_uint(p); + if (rule.day > 365) + return false; + + } else { + return false; + } + + // Parse optional /time suffix + parse_transition_time(p, rule); + + return true; +} + +// Calculate days from Jan 1 of given year to given month/day +static int __attribute__((noinline)) days_from_year_start(int year, int month, int day) { + int days = day - 1; + for (int m = 1; m < month; m++) { + days += days_in_month(year, m); + } + return days; +} + +// Calculate days from epoch to Jan 1 of given year (for DST transition calculations) +// Only supports years >= 1970. Timezone is either compiled in from YAML or set by +// Home Assistant, so pre-1970 dates are not a concern. +static int64_t __attribute__((noinline)) days_to_year_start(int year) { + int64_t days = 0; + for (int y = 1970; y < year; y++) { + days += days_in_year(y); + } + return days; +} + +time_t __attribute__((noinline)) calculate_dst_transition(int year, const DSTRule &rule, int32_t base_offset_seconds) { + int month, day; + + switch (rule.type) { + case DSTRuleType::MONTH_WEEK_DAY: { + // Find the nth occurrence of day_of_week in the given month + int first_dow = day_of_week(year, rule.month, 1); + + // Days until first occurrence of target day + int days_until_first = (rule.day_of_week - first_dow + 7) % 7; + int first_occurrence = 1 + days_until_first; + + if (rule.week == 5) { + // "Last" occurrence - find the last one in the month + int dim = days_in_month(year, rule.month); + day = first_occurrence; + while (day + 7 <= dim) { + day += 7; + } + } else { + // nth occurrence + day = first_occurrence + (rule.week - 1) * 7; + } + month = rule.month; + break; + } + + case DSTRuleType::JULIAN_NO_LEAP: + // J format: day 1-365, Feb 29 not counted + julian_to_month_day(rule.day, month, day); + break; + + case DSTRuleType::DAY_OF_YEAR: + // Plain format: day 0-365, Feb 29 counted + day_of_year_to_month_day(rule.day, year, month, day); + break; + + case DSTRuleType::NONE: + // Should never be called with NONE, but handle it gracefully + month = 1; + day = 1; + break; + } + + // Calculate days from epoch to this date + int64_t days = days_to_year_start(year) + days_from_year_start(year, month, day); + + // Convert to epoch and add transition time and base offset + return days * 86400 + rule.time_seconds + base_offset_seconds; +} + +} // namespace internal + +bool __attribute__((noinline)) is_in_dst(time_t utc_epoch, const ParsedTimezone &tz) { + if (!tz.has_dst()) { + return false; + } + + int year = internal::epoch_to_year(utc_epoch); + + // Calculate DST start and end for this year + // DST start transition happens in standard time + time_t dst_start = internal::calculate_dst_transition(year, tz.dst_start, tz.std_offset_seconds); + // DST end transition happens in daylight time + time_t dst_end = internal::calculate_dst_transition(year, tz.dst_end, tz.dst_offset_seconds); + + if (dst_start < dst_end) { + // Northern hemisphere: DST is between start and end + return (utc_epoch >= dst_start && utc_epoch < dst_end); + } else { + // Southern hemisphere: DST is outside the range (wraps around year) + return (utc_epoch >= dst_start || utc_epoch < dst_end); + } +} + +// Remove before 2026.9.0: This parser is bridge code for backward compatibility with +// older Home Assistant clients that send the timezone as a POSIX TZ string instead of +// the pre-parsed ParsedTimezone protobuf struct. Once all clients send the struct +// directly, this function and the parsing helpers above (skip_tz_name, parse_offset, +// parse_dst_rule, parse_transition_time) can be removed. +// See https://github.com/esphome/backlog/issues/91 +bool parse_posix_tz(const char *tz_string, ParsedTimezone &result) { + if (!tz_string || !*tz_string) { + return false; + } + + const char *p = tz_string; + + // Initialize result (dst_start/dst_end default to type=NONE, so has_dst() returns false) + result.std_offset_seconds = 0; + result.dst_offset_seconds = 0; + result.dst_start = {}; + result.dst_end = {}; + + // Skip standard timezone name + if (!internal::skip_tz_name(p)) { + return false; + } + + // Parse standard offset (required) + if (!*p || (!std::isdigit(static_cast(*p)) && *p != '+' && *p != '-')) { + return false; + } + result.std_offset_seconds = internal::parse_offset(p); + + // Check for DST name + if (!*p) { + return true; // No DST + } + + // If next char is comma, there's no DST name but there are rules (invalid) + if (*p == ',') { + return false; + } + + // Check if there's something that looks like a DST name start + // (letter or angle bracket). If not, treat as trailing garbage and return success. + if (!std::isalpha(static_cast(*p)) && *p != '<') { + return true; // No DST, trailing characters ignored + } + + if (!internal::skip_tz_name(p)) { + return false; // Invalid DST name (started but malformed) + } + + // Optional DST offset (default is std - 1 hour) + if (*p && *p != ',' && (std::isdigit(static_cast(*p)) || *p == '+' || *p == '-')) { + result.dst_offset_seconds = internal::parse_offset(p); + } else { + result.dst_offset_seconds = result.std_offset_seconds - 3600; + } + + // Parse DST rules (required when DST name is present) + if (*p != ',') { + // DST name without rules - treat as no DST since we can't determine transitions + return true; + } + + p++; + if (!internal::parse_dst_rule(p, result.dst_start)) { + return false; + } + + // Second rule is required per POSIX + if (*p != ',') { + return false; + } + p++; + // has_dst() now returns true since dst_start.type was set by parse_dst_rule + return internal::parse_dst_rule(p, result.dst_end); +} + +bool epoch_to_local_tm(time_t utc_epoch, const ParsedTimezone &tz, struct tm *out_tm) { + if (!out_tm) { + return false; + } + + // Determine DST status once (avoids duplicate is_in_dst calculation) + bool in_dst = is_in_dst(utc_epoch, tz); + int32_t offset = in_dst ? tz.dst_offset_seconds : tz.std_offset_seconds; + + // Apply offset (POSIX offset is positive west, so subtract to get local) + time_t local_epoch = utc_epoch - offset; + + internal::epoch_to_tm_utc(local_epoch, out_tm); + out_tm->tm_isdst = in_dst ? 1 : 0; + + return true; +} + +} // namespace esphome::time + +#ifndef USE_HOST +// Override libc's localtime functions to use our timezone on embedded platforms. +// This allows user lambdas calling ::localtime() to get correct local time +// without needing the TZ environment variable (which pulls in scanf bloat). +// On host, we use the normal TZ mechanism since there's no memory constraint. + +// Thread-safe version +extern "C" struct tm *localtime_r(const time_t *timer, struct tm *result) { + if (timer == nullptr || result == nullptr) { + return nullptr; + } + esphome::time::epoch_to_local_tm(*timer, esphome::time::get_global_tz(), result); + return result; +} + +// Non-thread-safe version (uses static buffer, standard libc behavior) +extern "C" struct tm *localtime(const time_t *timer) { + // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) + static struct tm localtime_buf; + return localtime_r(timer, &localtime_buf); +} +#endif // !USE_HOST + +#endif // USE_TIME_TIMEZONE diff --git a/esphome/components/time/posix_tz.h b/esphome/components/time/posix_tz.h new file mode 100644 index 0000000000..c71ba15cd1 --- /dev/null +++ b/esphome/components/time/posix_tz.h @@ -0,0 +1,144 @@ +#pragma once + +#ifdef USE_TIME_TIMEZONE + +#include +#include + +namespace esphome::time { + +/// Type of DST transition rule +enum class DSTRuleType : uint8_t { + NONE = 0, ///< No DST rule (used to indicate no DST) + MONTH_WEEK_DAY, ///< M format: Mm.w.d (e.g., M3.2.0 = 2nd Sunday of March) + JULIAN_NO_LEAP, ///< J format: Jn (day 1-365, Feb 29 not counted) + DAY_OF_YEAR, ///< Plain number: n (day 0-365, Feb 29 counted in leap years) +}; + +/// Rule for DST transition (packed for 32-bit: 12 bytes) +struct DSTRule { + int32_t time_seconds; ///< Seconds after midnight (default 7200 = 2:00 AM) + uint16_t day; ///< Day of year (for JULIAN_NO_LEAP and DAY_OF_YEAR) + DSTRuleType type; ///< Type of rule + uint8_t month; ///< Month 1-12 (for MONTH_WEEK_DAY) + uint8_t week; ///< Week 1-5, 5 = last (for MONTH_WEEK_DAY) + uint8_t day_of_week; ///< Day 0-6, 0 = Sunday (for MONTH_WEEK_DAY) +}; + +/// Parsed POSIX timezone information (packed for 32-bit: 32 bytes) +struct ParsedTimezone { + int32_t std_offset_seconds; ///< Standard time offset from UTC in seconds (positive = west) + int32_t dst_offset_seconds; ///< DST offset from UTC in seconds + DSTRule dst_start; ///< When DST starts + DSTRule dst_end; ///< When DST ends + + /// Check if this timezone has DST rules + bool has_dst() const { return this->dst_start.type != DSTRuleType::NONE; } +}; + +/// Parse a POSIX TZ string into a ParsedTimezone struct. +/// +/// @deprecated Remove before 2026.9.0 (bridge code for backward compatibility). +/// This parser only exists so that older Home Assistant clients that send the timezone +/// as a string (instead of the pre-parsed ParsedTimezone protobuf struct) can still +/// set the timezone on the device. Once all clients are updated to send the struct +/// directly, this function and all internal parsing helpers will be removed. +/// See https://github.com/esphome/backlog/issues/91 +/// +/// Supports formats like: +/// - "EST5" (simple offset, no DST) +/// - "EST5EDT,M3.2.0,M11.1.0" (with DST, M-format rules) +/// - "CST6CDT,M3.2.0/2,M11.1.0/2" (with transition times) +/// - "<+07>-7" (angle-bracket notation for special names) +/// - "IST-5:30" (half-hour offsets) +/// - "EST5EDT,J60,J300" (J-format: Julian day without leap day) +/// - "EST5EDT,60,300" (plain day number: day of year with leap day) +/// @param tz_string The POSIX TZ string to parse +/// @param result Output: the parsed timezone data +/// @return true if parsing succeeded, false on error +bool parse_posix_tz(const char *tz_string, ParsedTimezone &result); + +/// Convert a UTC epoch to local time using the parsed timezone. +/// This replaces libc's localtime() to avoid scanf dependency. +/// @param utc_epoch Unix timestamp in UTC +/// @param tz The parsed timezone +/// @param[out] out_tm Output tm struct with local time +/// @return true on success +bool epoch_to_local_tm(time_t utc_epoch, const ParsedTimezone &tz, struct tm *out_tm); + +/// Set the global timezone used by epoch_to_local_tm() when called without a timezone. +/// This is called by RealTimeClock::apply_timezone_() to enable ESPTime::from_epoch_local() +/// to work without libc's localtime(). +void set_global_tz(const ParsedTimezone &tz); + +/// Get the global timezone. +const ParsedTimezone &get_global_tz(); + +/// Check if a given UTC epoch falls within DST for the parsed timezone. +/// @param utc_epoch Unix timestamp in UTC +/// @param tz The parsed timezone +/// @return true if DST is in effect at the given time +bool is_in_dst(time_t utc_epoch, const ParsedTimezone &tz); + +// Internal helper functions exposed for testing. +// Remove before 2026.9.0: skip_tz_name, parse_offset, parse_dst_rule are only +// used by parse_posix_tz() which is bridge code for backward compatibility. +// The remaining helpers (epoch_to_tm_utc, day_of_week, days_in_month, etc.) +// are used by the conversion functions and will stay. + +namespace internal { + +/// Skip a timezone name (letters or <...> quoted format) +/// @param p Pointer to current position, updated on return +/// @return true if a valid name was found +bool skip_tz_name(const char *&p); + +/// Parse an offset in format [-]hh[:mm[:ss]] +/// @param p Pointer to current position, updated on return +/// @return Offset in seconds +int32_t parse_offset(const char *&p); + +/// Parse a DST rule in format Mm.w.d[/time], Jn[/time], or n[/time] +/// @param p Pointer to current position, updated on return +/// @param rule Output: the parsed rule +/// @return true if parsing succeeded +bool parse_dst_rule(const char *&p, DSTRule &rule); + +/// Convert Julian day (J format, 1-365 not counting Feb 29) to month/day +/// @param julian_day Day number 1-365 +/// @param[out] month Output: month 1-12 +/// @param[out] day Output: day of month +void julian_to_month_day(int julian_day, int &month, int &day); + +/// Convert day of year (plain format, 0-365 counting Feb 29) to month/day +/// @param day_of_year Day number 0-365 +/// @param year The year (for leap year calculation) +/// @param[out] month Output: month 1-12 +/// @param[out] day Output: day of month +void day_of_year_to_month_day(int day_of_year, int year, int &month, int &day); + +/// Calculate day of week for any date (0 = Sunday) +/// Uses a simplified algorithm that works for years 1970-2099 +int day_of_week(int year, int month, int day); + +/// Get the number of days in a month +int days_in_month(int year, int month); + +/// Check if a year is a leap year +bool is_leap_year(int year); + +/// Convert epoch to year/month/day/hour/min/sec (UTC) +void epoch_to_tm_utc(time_t epoch, struct tm *out_tm); + +/// Calculate the epoch timestamp for a DST transition in a given year. +/// @param year The year (e.g., 2026) +/// @param rule The DST rule (month, week, day_of_week, time) +/// @param base_offset_seconds The timezone offset to apply (std or dst depending on context) +/// @return Unix epoch timestamp of the transition +time_t calculate_dst_transition(int year, const DSTRule &rule, int32_t base_offset_seconds); + +} // namespace internal + +} // namespace esphome::time + +#endif // USE_TIME_TIMEZONE diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 8a78186178..2e758ad8e7 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -14,8 +14,8 @@ #include #endif #include - #include +#include namespace esphome::time { @@ -23,9 +23,33 @@ static const char *const TAG = "time"; RealTimeClock::RealTimeClock() = default; +ESPTime __attribute__((noinline)) RealTimeClock::now() { +#ifdef USE_TIME_TIMEZONE + time_t epoch = this->timestamp_now(); + struct tm local_tm; + if (epoch_to_local_tm(epoch, get_global_tz(), &local_tm)) { + return ESPTime::from_c_tm(&local_tm, epoch); + } + // Fallback to UTC if parsing failed + return ESPTime::from_epoch_utc(epoch); +#else + return ESPTime::from_epoch_local(this->timestamp_now()); +#endif +} + void RealTimeClock::dump_config() { #ifdef USE_TIME_TIMEZONE - ESP_LOGCONFIG(TAG, "Timezone: '%s'", this->timezone_.c_str()); + const auto &tz = get_global_tz(); + // POSIX offset is positive west, negate for conventional UTC+X display + int std_h = -tz.std_offset_seconds / 3600; + int std_m = (std::abs(tz.std_offset_seconds) % 3600) / 60; + if (tz.has_dst()) { + int dst_h = -tz.dst_offset_seconds / 3600; + int dst_m = (std::abs(tz.dst_offset_seconds) % 3600) / 60; + ESP_LOGCONFIG(TAG, "Timezone: UTC%+d:%02d (DST UTC%+d:%02d)", std_h, std_m, dst_h, dst_m); + } else { + ESP_LOGCONFIG(TAG, "Timezone: UTC%+d:%02d", std_h, std_m); + } #endif auto time = this->now(); ESP_LOGCONFIG(TAG, "Current time: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour, @@ -72,11 +96,6 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { ret = settimeofday(&timev, nullptr); } -#ifdef USE_TIME_TIMEZONE - // Move timezone back to local timezone. - this->apply_timezone_(); -#endif - if (ret != 0) { ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); } @@ -89,9 +108,33 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { } #ifdef USE_TIME_TIMEZONE -void RealTimeClock::apply_timezone_() { - setenv("TZ", this->timezone_.c_str(), 1); +void RealTimeClock::apply_timezone_(const char *tz) { + ParsedTimezone parsed{}; + + // Handle null or empty input - use UTC + if (tz == nullptr || *tz == '\0') { + // Skip if already UTC + if (!get_global_tz().has_dst() && get_global_tz().std_offset_seconds == 0) { + return; + } + set_global_tz(parsed); + return; + } + +#ifdef USE_HOST + // On host platform, also set TZ environment variable for libc compatibility + setenv("TZ", tz, 1); tzset(); +#endif + + // Parse the POSIX TZ string using our custom parser + if (!parse_posix_tz(tz, parsed)) { + ESP_LOGW(TAG, "Failed to parse timezone: %s", tz); + return; + } + + // Set global timezone for all time conversions + set_global_tz(parsed); } #endif diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index 19aa1a4f4a..f9de5f5614 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -6,6 +6,9 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/time.h" +#ifdef USE_TIME_TIMEZONE +#include "posix_tz.h" +#endif namespace esphome::time { @@ -20,26 +23,31 @@ class RealTimeClock : public PollingComponent { explicit RealTimeClock(); #ifdef USE_TIME_TIMEZONE - /// Set the time zone. - void set_timezone(const std::string &tz) { - this->timezone_ = tz; - this->apply_timezone_(); - } + /// Set the time zone from a POSIX TZ string. + void set_timezone(const char *tz) { this->apply_timezone_(tz); } - /// Set the time zone from raw buffer, only if it differs from the current one. + /// Set the time zone from a character buffer with known length. + /// The buffer does not need to be null-terminated. void set_timezone(const char *tz, size_t len) { - if (this->timezone_.length() != len || memcmp(this->timezone_.c_str(), tz, len) != 0) { - this->timezone_.assign(tz, len); - this->apply_timezone_(); + if (tz == nullptr) { + this->apply_timezone_(nullptr); + return; } + // Stack buffer - TZ strings from tzdata are typically short (< 50 chars) + char buf[128]; + if (len >= sizeof(buf)) + len = sizeof(buf) - 1; + memcpy(buf, tz, len); + buf[len] = '\0'; + this->apply_timezone_(buf); } - /// Get the time zone currently in use. - std::string get_timezone() { return this->timezone_; } + /// Set the time zone from a std::string. + void set_timezone(const std::string &tz) { this->apply_timezone_(tz.c_str()); } #endif /// Get the time in the currently defined timezone. - ESPTime now() { return ESPTime::from_epoch_local(this->timestamp_now()); } + ESPTime now(); /// Get the time without any time zone or DST corrections. ESPTime utcnow() { return ESPTime::from_epoch_utc(this->timestamp_now()); } @@ -58,8 +66,7 @@ class RealTimeClock : public PollingComponent { void synchronize_epoch_(uint32_t epoch); #ifdef USE_TIME_TIMEZONE - std::string timezone_{}; - void apply_timezone_(); + void apply_timezone_(const char *tz); #endif LazyCallbackManager time_sync_callback_; diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 1aea18ac8d..73ba0a9be7 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -2,7 +2,6 @@ #include "helpers.h" #include -#include namespace esphome { @@ -67,58 +66,123 @@ std::string ESPTime::strftime(const char *format) { std::string ESPTime::strftime(const std::string &format) { return this->strftime(format.c_str()); } -bool ESPTime::strptime(const char *time_to_parse, size_t len, ESPTime &esp_time) { - uint16_t year; - uint8_t month; - uint8_t day; - uint8_t hour; - uint8_t minute; - uint8_t second; - int num; - const int ilen = static_cast(len); - - if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %02hhu:%02hhu:%02hhu %n", &year, &month, &day, // NOLINT - &hour, // NOLINT - &minute, // NOLINT - &second, &num) == 6 && // NOLINT - num == ilen) { - esp_time.year = year; - esp_time.month = month; - esp_time.day_of_month = day; - esp_time.hour = hour; - esp_time.minute = minute; - esp_time.second = second; - } else if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %02hhu:%02hhu %n", &year, &month, &day, // NOLINT - &hour, // NOLINT - &minute, &num) == 5 && // NOLINT - num == ilen) { - esp_time.year = year; - esp_time.month = month; - esp_time.day_of_month = day; - esp_time.hour = hour; - esp_time.minute = minute; - esp_time.second = 0; - } else if (sscanf(time_to_parse, "%02hhu:%02hhu:%02hhu %n", &hour, &minute, &second, &num) == 3 && // NOLINT - num == ilen) { - esp_time.hour = hour; - esp_time.minute = minute; - esp_time.second = second; - } else if (sscanf(time_to_parse, "%02hhu:%02hhu %n", &hour, &minute, &num) == 2 && // NOLINT - num == ilen) { - esp_time.hour = hour; - esp_time.minute = minute; - esp_time.second = 0; - } else if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %n", &year, &month, &day, &num) == 3 && // NOLINT - num == ilen) { - esp_time.year = year; - esp_time.month = month; - esp_time.day_of_month = day; - } else { - return false; +// Helper to parse exactly N digits, returns false if not enough digits +static bool parse_digits(const char *&p, const char *end, int count, uint16_t &value) { + value = 0; + for (int i = 0; i < count; i++) { + if (p >= end || *p < '0' || *p > '9') + return false; + value = value * 10 + (*p - '0'); + p++; } return true; } +// Helper to check for expected character +static bool expect_char(const char *&p, const char *end, char expected) { + if (p >= end || *p != expected) + return false; + p++; + return true; +} + +bool ESPTime::strptime(const char *time_to_parse, size_t len, ESPTime &esp_time) { + // Supported formats: + // YYYY-MM-DD HH:MM:SS (19 chars) + // YYYY-MM-DD HH:MM (16 chars) + // YYYY-MM-DD (10 chars) + // HH:MM:SS (8 chars) + // HH:MM (5 chars) + + if (time_to_parse == nullptr || len == 0) + return false; + + const char *p = time_to_parse; + const char *end = time_to_parse + len; + uint16_t v1, v2, v3, v4, v5, v6; + + // Try date formats first (start with 4-digit year) + if (len >= 10 && time_to_parse[4] == '-') { + // YYYY-MM-DD... + if (!parse_digits(p, end, 4, v1)) + return false; + if (!expect_char(p, end, '-')) + return false; + if (!parse_digits(p, end, 2, v2)) + return false; + if (!expect_char(p, end, '-')) + return false; + if (!parse_digits(p, end, 2, v3)) + return false; + + esp_time.year = v1; + esp_time.month = v2; + esp_time.day_of_month = v3; + + if (p == end) { + // YYYY-MM-DD (date only) + return true; + } + + if (!expect_char(p, end, ' ')) + return false; + + // Continue with time part: HH:MM[:SS] + if (!parse_digits(p, end, 2, v4)) + return false; + if (!expect_char(p, end, ':')) + return false; + if (!parse_digits(p, end, 2, v5)) + return false; + + esp_time.hour = v4; + esp_time.minute = v5; + + if (p == end) { + // YYYY-MM-DD HH:MM + esp_time.second = 0; + return true; + } + + if (!expect_char(p, end, ':')) + return false; + if (!parse_digits(p, end, 2, v6)) + return false; + + esp_time.second = v6; + return p == end; // YYYY-MM-DD HH:MM:SS + } + + // Try time-only formats (HH:MM[:SS]) + if (len >= 5 && time_to_parse[2] == ':') { + if (!parse_digits(p, end, 2, v1)) + return false; + if (!expect_char(p, end, ':')) + return false; + if (!parse_digits(p, end, 2, v2)) + return false; + + esp_time.hour = v1; + esp_time.minute = v2; + + if (p == end) { + // HH:MM + esp_time.second = 0; + return true; + } + + if (!expect_char(p, end, ':')) + return false; + if (!parse_digits(p, end, 2, v3)) + return false; + + esp_time.second = v3; + return p == end; // HH:MM:SS + } + + return false; +} + void ESPTime::increment_second() { this->timestamp++; if (!increment_time_value(this->second, 0, 60)) @@ -193,27 +257,67 @@ void ESPTime::recalc_timestamp_utc(bool use_day_of_year) { } void ESPTime::recalc_timestamp_local() { - struct tm tm; +#ifdef USE_TIME_TIMEZONE + // Calculate timestamp as if fields were UTC + this->recalc_timestamp_utc(false); + if (this->timestamp == -1) { + return; // Invalid time + } - tm.tm_year = this->year - 1900; - tm.tm_mon = this->month - 1; - tm.tm_mday = this->day_of_month; - tm.tm_hour = this->hour; - tm.tm_min = this->minute; - tm.tm_sec = this->second; - tm.tm_isdst = -1; + // Now convert from local to UTC by adding the offset + // POSIX: local = utc - offset, so utc = local + offset + const auto &tz = time::get_global_tz(); - this->timestamp = mktime(&tm); + if (!tz.has_dst()) { + // No DST - just apply standard offset + this->timestamp += tz.std_offset_seconds; + return; + } + + // Try both interpretations to match libc mktime() with tm_isdst=-1 + // For ambiguous times (fall-back repeated hour), prefer standard time + // For invalid times (spring-forward skipped hour), libc normalizes forward + time_t utc_if_dst = this->timestamp + tz.dst_offset_seconds; + time_t utc_if_std = this->timestamp + tz.std_offset_seconds; + + bool dst_valid = time::is_in_dst(utc_if_dst, tz); + bool std_valid = !time::is_in_dst(utc_if_std, tz); + + if (dst_valid && std_valid) { + // Ambiguous time (repeated hour during fall-back) - prefer standard time + this->timestamp = utc_if_std; + } else if (dst_valid) { + // Only DST interpretation is valid + this->timestamp = utc_if_dst; + } else if (std_valid) { + // Only standard interpretation is valid + this->timestamp = utc_if_std; + } else { + // Invalid time (skipped hour during spring-forward) + // libc normalizes forward: 02:30 CST -> 08:30 UTC -> 03:30 CDT + // Using std offset achieves this since the UTC result falls during DST + this->timestamp = utc_if_std; + } +#else + // No timezone support - treat as UTC + this->recalc_timestamp_utc(false); +#endif } int32_t ESPTime::timezone_offset() { +#ifdef USE_TIME_TIMEZONE time_t now = ::time(nullptr); - struct tm local_tm = *::localtime(&now); - local_tm.tm_isdst = 0; // Cause mktime to ignore daylight saving time because we want to include it in the offset. - time_t local_time = mktime(&local_tm); - struct tm utc_tm = *::gmtime(&now); - time_t utc_time = mktime(&utc_tm); - return static_cast(local_time - utc_time); + const auto &tz = time::get_global_tz(); + // POSIX offset is positive west, but we return offset to add to UTC to get local + // So we negate the POSIX offset + if (time::is_in_dst(now, tz)) { + return -tz.dst_offset_seconds; + } + return -tz.std_offset_seconds; +#else + // No timezone support - no offset + return 0; +#endif } bool ESPTime::operator<(const ESPTime &other) const { return this->timestamp < other.timestamp; } diff --git a/esphome/core/time.h b/esphome/core/time.h index d9ce86131c..874f0db4b4 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -7,6 +7,10 @@ #include #include +#ifdef USE_TIME_TIMEZONE +#include "esphome/components/time/posix_tz.h" +#endif + namespace esphome { template bool increment_time_value(T ¤t, uint16_t begin, uint16_t end); @@ -105,11 +109,17 @@ struct ESPTime { * @return The generated ESPTime */ static ESPTime from_epoch_local(time_t epoch) { - struct tm *c_tm = ::localtime(&epoch); - if (c_tm == nullptr) { - return ESPTime{}; // Return an invalid ESPTime +#ifdef USE_TIME_TIMEZONE + struct tm local_tm; + if (time::epoch_to_local_tm(epoch, time::get_global_tz(), &local_tm)) { + return ESPTime::from_c_tm(&local_tm, epoch); } - return ESPTime::from_c_tm(c_tm, epoch); + // Fallback to UTC if conversion failed + return ESPTime::from_epoch_utc(epoch); +#else + // No timezone support - return UTC (no TZ configured, localtime would return UTC anyway) + return ESPTime::from_epoch_utc(epoch); +#endif } /** Convert an UTC epoch timestamp to a UTC time ESPTime instance. * diff --git a/script/cpp_unit_test.py b/script/cpp_unit_test.py index e97b5bd7b0..78b65092ae 100755 --- a/script/cpp_unit_test.py +++ b/script/cpp_unit_test.py @@ -66,6 +66,7 @@ def create_test_config(config_name: str, includes: list[str]) -> dict: ], "build_flags": [ "-Og", # optimize for debug + "-DUSE_TIME_TIMEZONE", # enable timezone code paths for testing ], "debug_build_flags": [ # only for debug builds "-g3", # max debug info diff --git a/tests/components/time/posix_tz_parser.cpp b/tests/components/time/posix_tz_parser.cpp new file mode 100644 index 0000000000..d1747ef5b1 --- /dev/null +++ b/tests/components/time/posix_tz_parser.cpp @@ -0,0 +1,1275 @@ +// Tests for the POSIX TZ parser, time conversion functions, and ESPTime::strptime. +// +// Most tests here cover the C++ POSIX TZ string parser (parse_posix_tz), which is +// bridge code for backward compatibility — it will be removed before ESPHome 2026.9.0. +// After https://github.com/esphome/esphome/pull/14233 merges, the parser is solely +// used to handle timezone strings from Home Assistant clients older than 2026.3.0 +// that haven't been updated to send the pre-parsed ParsedTimezone protobuf struct. +// See https://github.com/esphome/backlog/issues/91 +// +// The epoch_to_local_tm, is_in_dst, and ESPTime::strptime tests cover conversion +// functions that will remain permanently. + +// Enable USE_TIME_TIMEZONE for tests +#define USE_TIME_TIMEZONE + +#include +#include +#include +#include "esphome/components/time/posix_tz.h" +#include "esphome/core/time.h" + +namespace esphome::time::testing { + +// Helper to create UTC epoch from date/time components (for test readability) +static time_t make_utc(int year, int month, int day, int hour = 0, int min = 0, int sec = 0) { + int64_t days = 0; + for (int y = 1970; y < year; y++) { + days += (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)) ? 366 : 365; + } + static const int DAYS_BEFORE[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; + days += DAYS_BEFORE[month - 1]; + if (month > 2 && (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0))) + days++; // Leap year adjustment + days += day - 1; + return days * 86400 + hour * 3600 + min * 60 + sec; +} + +// ============================================================================ +// Basic TZ string parsing tests +// ============================================================================ + +TEST(PosixTzParser, ParseSimpleOffsetEST5) { + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("EST5", tz)); + EXPECT_EQ(tz.std_offset_seconds, 5 * 3600); // +5 hours (west of UTC) + EXPECT_FALSE(tz.has_dst()); +} + +TEST(PosixTzParser, ParseNegativeOffsetCET) { + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("CET-1", tz)); + EXPECT_EQ(tz.std_offset_seconds, -1 * 3600); // -1 hour (east of UTC) + EXPECT_FALSE(tz.has_dst()); +} + +TEST(PosixTzParser, ParseExplicitPositiveOffset) { + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("TEST+5", tz)); + EXPECT_EQ(tz.std_offset_seconds, 5 * 3600); + EXPECT_FALSE(tz.has_dst()); +} + +TEST(PosixTzParser, ParseZeroOffset) { + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("UTC0", tz)); + EXPECT_EQ(tz.std_offset_seconds, 0); + EXPECT_FALSE(tz.has_dst()); +} + +TEST(PosixTzParser, ParseUSEasternWithDST) { + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("EST5EDT,M3.2.0,M11.1.0", tz)); + EXPECT_EQ(tz.std_offset_seconds, 5 * 3600); + EXPECT_EQ(tz.dst_offset_seconds, 4 * 3600); // Default: STD - 1hr + EXPECT_TRUE(tz.has_dst()); + EXPECT_EQ(tz.dst_start.month, 3); + EXPECT_EQ(tz.dst_start.week, 2); + EXPECT_EQ(tz.dst_start.day_of_week, 0); // Sunday + EXPECT_EQ(tz.dst_start.time_seconds, 2 * 3600); // Default 2:00 AM + EXPECT_EQ(tz.dst_end.month, 11); + EXPECT_EQ(tz.dst_end.week, 1); + EXPECT_EQ(tz.dst_end.day_of_week, 0); +} + +TEST(PosixTzParser, ParseUSCentralWithTime) { + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("CST6CDT,M3.2.0/2,M11.1.0/2", tz)); + EXPECT_EQ(tz.std_offset_seconds, 6 * 3600); + EXPECT_EQ(tz.dst_offset_seconds, 5 * 3600); + EXPECT_EQ(tz.dst_start.time_seconds, 2 * 3600); // 2:00 AM + EXPECT_EQ(tz.dst_end.time_seconds, 2 * 3600); +} + +TEST(PosixTzParser, ParseEuropeBerlin) { + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("CET-1CEST,M3.5.0,M10.5.0/3", tz)); + EXPECT_EQ(tz.std_offset_seconds, -1 * 3600); + EXPECT_EQ(tz.dst_offset_seconds, -2 * 3600); // Default: STD - 1hr + EXPECT_TRUE(tz.has_dst()); + EXPECT_EQ(tz.dst_start.month, 3); + EXPECT_EQ(tz.dst_start.week, 5); // Last week + EXPECT_EQ(tz.dst_end.month, 10); + EXPECT_EQ(tz.dst_end.week, 5); // Last week + EXPECT_EQ(tz.dst_end.time_seconds, 3 * 3600); // 3:00 AM +} + +TEST(PosixTzParser, ParseNewZealand) { + ParsedTimezone tz; + // Southern hemisphere - DST starts in Sept, ends in April + ASSERT_TRUE(parse_posix_tz("NZST-12NZDT,M9.5.0,M4.1.0/3", tz)); + EXPECT_EQ(tz.std_offset_seconds, -12 * 3600); + EXPECT_EQ(tz.dst_offset_seconds, -13 * 3600); // Default: STD - 1hr + EXPECT_TRUE(tz.has_dst()); + EXPECT_EQ(tz.dst_start.month, 9); // September + EXPECT_EQ(tz.dst_end.month, 4); // April +} + +TEST(PosixTzParser, ParseExplicitDstOffset) { + ParsedTimezone tz; + // Some places have non-standard DST offsets + ASSERT_TRUE(parse_posix_tz("TEST5DST4,M3.2.0,M11.1.0", tz)); + EXPECT_EQ(tz.std_offset_seconds, 5 * 3600); + EXPECT_EQ(tz.dst_offset_seconds, 4 * 3600); + EXPECT_TRUE(tz.has_dst()); +} + +// ============================================================================ +// Angle-bracket notation tests (espressif/newlib-esp32#8) +// ============================================================================ + +TEST(PosixTzParser, ParseAngleBracketPositive) { + // Format: <+07>-7 means UTC+7 (name is "+07", offset is -7 hours east) + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("<+07>-7", tz)); + EXPECT_EQ(tz.std_offset_seconds, -7 * 3600); // -7 = 7 hours east of UTC + EXPECT_FALSE(tz.has_dst()); +} + +TEST(PosixTzParser, ParseAngleBracketNegative) { + // <-03>3 means UTC-3 (name is "-03", offset is 3 hours west) + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("<-03>3", tz)); + EXPECT_EQ(tz.std_offset_seconds, 3 * 3600); + EXPECT_FALSE(tz.has_dst()); +} + +TEST(PosixTzParser, ParseAngleBracketWithDST) { + // <+10>-10<+11>,M10.1.0,M4.1.0/3 (Australia/Sydney style) + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("<+10>-10<+11>,M10.1.0,M4.1.0/3", tz)); + EXPECT_EQ(tz.std_offset_seconds, -10 * 3600); + EXPECT_EQ(tz.dst_offset_seconds, -11 * 3600); + EXPECT_TRUE(tz.has_dst()); + EXPECT_EQ(tz.dst_start.month, 10); + EXPECT_EQ(tz.dst_end.month, 4); +} + +TEST(PosixTzParser, ParseAngleBracketNamed) { + // -10 (Australian Eastern Standard Time) + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("-10", tz)); + EXPECT_EQ(tz.std_offset_seconds, -10 * 3600); + EXPECT_FALSE(tz.has_dst()); +} + +TEST(PosixTzParser, ParseAngleBracketWithMinutes) { + // <+0545>-5:45 (Nepal) + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("<+0545>-5:45", tz)); + EXPECT_EQ(tz.std_offset_seconds, -(5 * 3600 + 45 * 60)); + EXPECT_FALSE(tz.has_dst()); +} + +// ============================================================================ +// Half-hour and unusual offset tests +// ============================================================================ + +TEST(PosixTzParser, ParseOffsetWithMinutesIndia) { + ParsedTimezone tz; + // India: UTC+5:30 + ASSERT_TRUE(parse_posix_tz("IST-5:30", tz)); + EXPECT_EQ(tz.std_offset_seconds, -(5 * 3600 + 30 * 60)); + EXPECT_FALSE(tz.has_dst()); +} + +TEST(PosixTzParser, ParseOffsetWithMinutesNepal) { + ParsedTimezone tz; + // Nepal: UTC+5:45 + ASSERT_TRUE(parse_posix_tz("NPT-5:45", tz)); + EXPECT_EQ(tz.std_offset_seconds, -(5 * 3600 + 45 * 60)); + EXPECT_FALSE(tz.has_dst()); +} + +TEST(PosixTzParser, ParseOffsetWithSeconds) { + ParsedTimezone tz; + // Unusual but valid: offset with seconds + ASSERT_TRUE(parse_posix_tz("TEST-1:30:30", tz)); + EXPECT_EQ(tz.std_offset_seconds, -(1 * 3600 + 30 * 60 + 30)); +} + +TEST(PosixTzParser, ParseChathamIslands) { + // Chatham Islands: UTC+12:45 with DST + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("<+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45", tz)); + EXPECT_EQ(tz.std_offset_seconds, -(12 * 3600 + 45 * 60)); + EXPECT_EQ(tz.dst_offset_seconds, -(13 * 3600 + 45 * 60)); + EXPECT_TRUE(tz.has_dst()); +} + +// ============================================================================ +// Invalid input tests +// ============================================================================ + +TEST(PosixTzParser, ParseEmptyStringFails) { + ParsedTimezone tz; + EXPECT_FALSE(parse_posix_tz("", tz)); +} + +TEST(PosixTzParser, ParseNullFails) { + ParsedTimezone tz; + EXPECT_FALSE(parse_posix_tz(nullptr, tz)); +} + +TEST(PosixTzParser, ParseShortNameFails) { + ParsedTimezone tz; + // TZ name must be at least 3 characters + EXPECT_FALSE(parse_posix_tz("AB5", tz)); +} + +TEST(PosixTzParser, ParseMissingOffsetFails) { + ParsedTimezone tz; + EXPECT_FALSE(parse_posix_tz("EST", tz)); +} + +TEST(PosixTzParser, ParseUnterminatedBracketFails) { + ParsedTimezone tz; + EXPECT_FALSE(parse_posix_tz("<+07-7", tz)); // Missing closing > +} + +// ============================================================================ +// J-format and plain day number tests +// ============================================================================ + +TEST(PosixTzParser, ParseJFormatBasic) { + ParsedTimezone tz; + // J format: Julian day 1-365, not counting Feb 29 + ASSERT_TRUE(parse_posix_tz("EST5EDT,J60,J305", tz)); + EXPECT_TRUE(tz.has_dst()); + EXPECT_EQ(tz.dst_start.type, DSTRuleType::JULIAN_NO_LEAP); + EXPECT_EQ(tz.dst_start.day, 60); // March 1 + EXPECT_EQ(tz.dst_end.type, DSTRuleType::JULIAN_NO_LEAP); + EXPECT_EQ(tz.dst_end.day, 305); // November 1 +} + +TEST(PosixTzParser, ParseJFormatWithTime) { + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("EST5EDT,J60/2,J305/2", tz)); + EXPECT_EQ(tz.dst_start.day, 60); + EXPECT_EQ(tz.dst_start.time_seconds, 2 * 3600); + EXPECT_EQ(tz.dst_end.day, 305); + EXPECT_EQ(tz.dst_end.time_seconds, 2 * 3600); +} + +TEST(PosixTzParser, ParsePlainDayNumber) { + ParsedTimezone tz; + // Plain format: day 0-365, counting Feb 29 in leap years + ASSERT_TRUE(parse_posix_tz("EST5EDT,59,304", tz)); + EXPECT_TRUE(tz.has_dst()); + EXPECT_EQ(tz.dst_start.type, DSTRuleType::DAY_OF_YEAR); + EXPECT_EQ(tz.dst_start.day, 59); + EXPECT_EQ(tz.dst_end.type, DSTRuleType::DAY_OF_YEAR); + EXPECT_EQ(tz.dst_end.day, 304); +} + +TEST(PosixTzParser, JFormatInvalidDayZero) { + ParsedTimezone tz; + // J format day must be 1-365, not 0 + EXPECT_FALSE(parse_posix_tz("EST5EDT,J0,J305", tz)); +} + +TEST(PosixTzParser, JFormatInvalidDay366) { + ParsedTimezone tz; + // J format day must be 1-365 + EXPECT_FALSE(parse_posix_tz("EST5EDT,J366,J305", tz)); +} + +TEST(PosixTzParser, ParsePlainDayNumberWithTime) { + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("EST5EDT,59/3,304/1:30", tz)); + EXPECT_EQ(tz.dst_start.day, 59); + EXPECT_EQ(tz.dst_start.time_seconds, 3 * 3600); + EXPECT_EQ(tz.dst_end.day, 304); + EXPECT_EQ(tz.dst_end.time_seconds, 1 * 3600 + 30 * 60); +} + +TEST(PosixTzParser, PlainDayInvalidDay366) { + ParsedTimezone tz; + // Plain format day must be 0-365 + EXPECT_FALSE(parse_posix_tz("EST5EDT,366,304", tz)); +} + +// ============================================================================ +// Transition time edge cases (POSIX V3 allows -167 to +167 hours) +// ============================================================================ + +TEST(PosixTzParser, NegativeTransitionTime) { + ParsedTimezone tz; + // Negative transition time: /-1 means 11 PM (23:00) the previous day + ASSERT_TRUE(parse_posix_tz("EST5EDT,M3.2.0/-1,M11.1.0/2", tz)); + EXPECT_EQ(tz.dst_start.time_seconds, -1 * 3600); // -1 hour = 11 PM previous day + EXPECT_EQ(tz.dst_end.time_seconds, 2 * 3600); +} + +TEST(PosixTzParser, NegativeTransitionTimeWithMinutes) { + ParsedTimezone tz; + // /-1:30 means 10:30 PM the previous day + ASSERT_TRUE(parse_posix_tz("EST5EDT,M3.2.0/-1:30,M11.1.0", tz)); + EXPECT_EQ(tz.dst_start.time_seconds, -(1 * 3600 + 30 * 60)); +} + +TEST(PosixTzParser, LargeTransitionTime) { + ParsedTimezone tz; + // POSIX V3 allows transition times from -167 to +167 hours + // /25 means 1:00 AM the next day + ASSERT_TRUE(parse_posix_tz("EST5EDT,M3.2.0/25,M11.1.0", tz)); + EXPECT_EQ(tz.dst_start.time_seconds, 25 * 3600); +} + +TEST(PosixTzParser, MaxTransitionTime167Hours) { + ParsedTimezone tz; + // Maximum allowed transition time per POSIX V3 + ASSERT_TRUE(parse_posix_tz("EST5EDT,M3.2.0/167,M11.1.0", tz)); + EXPECT_EQ(tz.dst_start.time_seconds, 167 * 3600); +} + +TEST(PosixTzParser, TransitionTimeWithHoursMinutesSeconds) { + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("EST5EDT,M3.2.0/2:30:45,M11.1.0", tz)); + EXPECT_EQ(tz.dst_start.time_seconds, 2 * 3600 + 30 * 60 + 45); +} + +// ============================================================================ +// Invalid M format tests +// ============================================================================ + +TEST(PosixTzParser, MFormatInvalidMonth13) { + ParsedTimezone tz; + // Month must be 1-12 + EXPECT_FALSE(parse_posix_tz("EST5EDT,M13.1.0,M11.1.0", tz)); +} + +TEST(PosixTzParser, MFormatInvalidMonth0) { + ParsedTimezone tz; + // Month must be 1-12 + EXPECT_FALSE(parse_posix_tz("EST5EDT,M0.1.0,M11.1.0", tz)); +} + +TEST(PosixTzParser, MFormatInvalidWeek6) { + ParsedTimezone tz; + // Week must be 1-5 + EXPECT_FALSE(parse_posix_tz("EST5EDT,M3.6.0,M11.1.0", tz)); +} + +TEST(PosixTzParser, MFormatInvalidWeek0) { + ParsedTimezone tz; + // Week must be 1-5 + EXPECT_FALSE(parse_posix_tz("EST5EDT,M3.0.0,M11.1.0", tz)); +} + +TEST(PosixTzParser, MFormatInvalidDayOfWeek7) { + ParsedTimezone tz; + // Day of week must be 0-6 + EXPECT_FALSE(parse_posix_tz("EST5EDT,M3.2.7,M11.1.0", tz)); +} + +TEST(PosixTzParser, MissingEndRule) { + ParsedTimezone tz; + // POSIX requires both start and end rules if any rules are specified + EXPECT_FALSE(parse_posix_tz("EST5EDT,M3.2.0", tz)); +} + +TEST(PosixTzParser, MissingEndRuleJFormat) { + ParsedTimezone tz; + // POSIX requires both start and end rules if any rules are specified + EXPECT_FALSE(parse_posix_tz("EST5EDT,J60", tz)); +} + +TEST(PosixTzParser, MissingEndRulePlainDay) { + ParsedTimezone tz; + // POSIX requires both start and end rules if any rules are specified + EXPECT_FALSE(parse_posix_tz("EST5EDT,60", tz)); +} + +TEST(PosixTzParser, LowercaseMFormat) { + ParsedTimezone tz; + // Lowercase 'm' should be accepted + ASSERT_TRUE(parse_posix_tz("EST5EDT,m3.2.0,m11.1.0", tz)); + EXPECT_TRUE(tz.has_dst()); + EXPECT_EQ(tz.dst_start.month, 3); + EXPECT_EQ(tz.dst_end.month, 11); +} + +TEST(PosixTzParser, LowercaseJFormat) { + ParsedTimezone tz; + // Lowercase 'j' should be accepted + ASSERT_TRUE(parse_posix_tz("EST5EDT,j60,j305", tz)); + EXPECT_EQ(tz.dst_start.type, DSTRuleType::JULIAN_NO_LEAP); + EXPECT_EQ(tz.dst_start.day, 60); +} + +TEST(PosixTzParser, DstNameWithoutRules) { + ParsedTimezone tz; + // DST name present but no rules - treat as no DST since we can't determine transitions + ASSERT_TRUE(parse_posix_tz("EST5EDT", tz)); + EXPECT_FALSE(tz.has_dst()); + EXPECT_EQ(tz.std_offset_seconds, 5 * 3600); +} + +TEST(PosixTzParser, TrailingCharactersIgnored) { + ParsedTimezone tz; + // Trailing characters after valid TZ should be ignored (parser stops at end of valid input) + // This matches libc behavior + ASSERT_TRUE(parse_posix_tz("EST5 extra garbage here", tz)); + EXPECT_EQ(tz.std_offset_seconds, 5 * 3600); + EXPECT_FALSE(tz.has_dst()); +} + +TEST(PosixTzParser, PlainDay365LeapYear) { + // Day 365 in leap year is Dec 31 + int month, day; + internal::day_of_year_to_month_day(365, 2024, month, day); + EXPECT_EQ(month, 12); + EXPECT_EQ(day, 31); +} + +TEST(PosixTzParser, PlainDay364NonLeapYear) { + // Day 364 (0-indexed) is Dec 31 in non-leap year (last valid day) + int month, day; + internal::day_of_year_to_month_day(364, 2025, month, day); + EXPECT_EQ(month, 12); + EXPECT_EQ(day, 31); +} + +// ============================================================================ +// Large offset tests +// ============================================================================ + +TEST(PosixTzParser, MaxOffset14Hours) { + ParsedTimezone tz; + // Line Islands (Kiribati) is UTC+14, the maximum offset + ASSERT_TRUE(parse_posix_tz("<+14>-14", tz)); + EXPECT_EQ(tz.std_offset_seconds, -14 * 3600); +} + +TEST(PosixTzParser, MaxNegativeOffset12Hours) { + ParsedTimezone tz; + // Baker Island is UTC-12 + ASSERT_TRUE(parse_posix_tz("<-12>12", tz)); + EXPECT_EQ(tz.std_offset_seconds, 12 * 3600); +} + +// ============================================================================ +// Helper function tests +// ============================================================================ + +TEST(PosixTzParser, JulianDay60IsMarch1) { + // J60 is always March 1 (J format ignores leap years by design) + int month, day; + internal::julian_to_month_day(60, month, day); + EXPECT_EQ(month, 3); + EXPECT_EQ(day, 1); +} + +TEST(PosixTzParser, DayOfYear59DiffersByLeap) { + int month, day; + // Day 59 in leap year is Feb 29 + internal::day_of_year_to_month_day(59, 2024, month, day); + EXPECT_EQ(month, 2); + EXPECT_EQ(day, 29); + // Day 59 in non-leap year is March 1 + internal::day_of_year_to_month_day(59, 2025, month, day); + EXPECT_EQ(month, 3); + EXPECT_EQ(day, 1); +} + +TEST(PosixTzParser, DayOfWeekKnownDates) { + // January 1, 1970 was Thursday (4) + EXPECT_EQ(internal::day_of_week(1970, 1, 1), 4); + // January 1, 2000 was Saturday (6) + EXPECT_EQ(internal::day_of_week(2000, 1, 1), 6); + // March 8, 2026 is Sunday (0) - US DST start + EXPECT_EQ(internal::day_of_week(2026, 3, 8), 0); +} + +TEST(PosixTzParser, LeapYearDetection) { + EXPECT_FALSE(internal::is_leap_year(1900)); // Divisible by 100 but not 400 + EXPECT_TRUE(internal::is_leap_year(2000)); // Divisible by 400 + EXPECT_TRUE(internal::is_leap_year(2024)); // Divisible by 4 + EXPECT_FALSE(internal::is_leap_year(2025)); // Not divisible by 4 +} + +TEST(PosixTzParser, JulianDay1IsJan1) { + int month, day; + internal::julian_to_month_day(1, month, day); + EXPECT_EQ(month, 1); + EXPECT_EQ(day, 1); +} + +TEST(PosixTzParser, JulianDay31IsJan31) { + int month, day; + internal::julian_to_month_day(31, month, day); + EXPECT_EQ(month, 1); + EXPECT_EQ(day, 31); +} + +TEST(PosixTzParser, JulianDay32IsFeb1) { + int month, day; + internal::julian_to_month_day(32, month, day); + EXPECT_EQ(month, 2); + EXPECT_EQ(day, 1); +} + +TEST(PosixTzParser, JulianDay59IsFeb28) { + int month, day; + internal::julian_to_month_day(59, month, day); + EXPECT_EQ(month, 2); + EXPECT_EQ(day, 28); +} + +TEST(PosixTzParser, JulianDay365IsDec31) { + int month, day; + internal::julian_to_month_day(365, month, day); + EXPECT_EQ(month, 12); + EXPECT_EQ(day, 31); +} + +TEST(PosixTzParser, DayOfYear0IsJan1) { + int month, day; + internal::day_of_year_to_month_day(0, 2025, month, day); + EXPECT_EQ(month, 1); + EXPECT_EQ(day, 1); +} + +TEST(PosixTzParser, DaysInMonthRegular) { + // Test all 12 months to ensure switch coverage + EXPECT_EQ(internal::days_in_month(2025, 1), 31); // Jan - default case + EXPECT_EQ(internal::days_in_month(2025, 2), 28); // Feb - case 2 + EXPECT_EQ(internal::days_in_month(2025, 3), 31); // Mar - default case + EXPECT_EQ(internal::days_in_month(2025, 4), 30); // Apr - case 4 + EXPECT_EQ(internal::days_in_month(2025, 5), 31); // May - default case + EXPECT_EQ(internal::days_in_month(2025, 6), 30); // Jun - case 6 + EXPECT_EQ(internal::days_in_month(2025, 7), 31); // Jul - default case + EXPECT_EQ(internal::days_in_month(2025, 8), 31); // Aug - default case + EXPECT_EQ(internal::days_in_month(2025, 9), 30); // Sep - case 9 + EXPECT_EQ(internal::days_in_month(2025, 10), 31); // Oct - default case + EXPECT_EQ(internal::days_in_month(2025, 11), 30); // Nov - case 11 + EXPECT_EQ(internal::days_in_month(2025, 12), 31); // Dec - default case +} + +TEST(PosixTzParser, DaysInMonthLeapYear) { + EXPECT_EQ(internal::days_in_month(2024, 2), 29); + EXPECT_EQ(internal::days_in_month(2025, 2), 28); +} + +// ============================================================================ +// DST transition calculation tests +// ============================================================================ + +TEST(PosixTzParser, DstStartUSEastern2026) { + // March 8, 2026 is 2nd Sunday of March + ParsedTimezone tz; + parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz); + + time_t dst_start = internal::calculate_dst_transition(2026, tz.dst_start, tz.std_offset_seconds); + struct tm tm; + internal::epoch_to_tm_utc(dst_start, &tm); + + // At 2:00 AM EST (UTC-5), so 7:00 AM UTC + EXPECT_EQ(tm.tm_year + 1900, 2026); + EXPECT_EQ(tm.tm_mon + 1, 3); // March + EXPECT_EQ(tm.tm_mday, 8); // 8th + EXPECT_EQ(tm.tm_hour, 7); // 7:00 UTC = 2:00 EST +} + +TEST(PosixTzParser, DstEndUSEastern2026) { + // November 1, 2026 is 1st Sunday of November + ParsedTimezone tz; + parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz); + + time_t dst_end = internal::calculate_dst_transition(2026, tz.dst_end, tz.dst_offset_seconds); + struct tm tm; + internal::epoch_to_tm_utc(dst_end, &tm); + + // At 2:00 AM EDT (UTC-4), so 6:00 AM UTC + EXPECT_EQ(tm.tm_year + 1900, 2026); + EXPECT_EQ(tm.tm_mon + 1, 11); // November + EXPECT_EQ(tm.tm_mday, 1); // 1st + EXPECT_EQ(tm.tm_hour, 6); // 6:00 UTC = 2:00 EDT +} + +TEST(PosixTzParser, LastSundayOfMarch2026) { + // Europe: M3.5.0 = last Sunday of March = March 29, 2026 + DSTRule rule{}; + rule.type = DSTRuleType::MONTH_WEEK_DAY; + rule.month = 3; + rule.week = 5; + rule.day_of_week = 0; + rule.time_seconds = 2 * 3600; + time_t transition = internal::calculate_dst_transition(2026, rule, 0); + struct tm tm; + internal::epoch_to_tm_utc(transition, &tm); + EXPECT_EQ(tm.tm_mday, 29); + EXPECT_EQ(tm.tm_wday, 0); // Sunday +} + +TEST(PosixTzParser, LastSundayOfOctober2026) { + // Europe: M10.5.0 = last Sunday of October = October 25, 2026 + DSTRule rule{}; + rule.type = DSTRuleType::MONTH_WEEK_DAY; + rule.month = 10; + rule.week = 5; + rule.day_of_week = 0; + rule.time_seconds = 3 * 3600; + time_t transition = internal::calculate_dst_transition(2026, rule, 0); + struct tm tm; + internal::epoch_to_tm_utc(transition, &tm); + EXPECT_EQ(tm.tm_mday, 25); + EXPECT_EQ(tm.tm_wday, 0); // Sunday +} + +TEST(PosixTzParser, FirstSundayOfApril2026) { + // April 5, 2026 is 1st Sunday + DSTRule rule{}; + rule.type = DSTRuleType::MONTH_WEEK_DAY; + rule.month = 4; + rule.week = 1; + rule.day_of_week = 0; + rule.time_seconds = 0; + time_t transition = internal::calculate_dst_transition(2026, rule, 0); + struct tm tm; + internal::epoch_to_tm_utc(transition, &tm); + EXPECT_EQ(tm.tm_mday, 5); + EXPECT_EQ(tm.tm_wday, 0); +} + +// ============================================================================ +// DST detection tests +// ============================================================================ + +TEST(PosixTzParser, IsInDstUSEasternSummer) { + ParsedTimezone tz; + parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz); + + // July 4, 2026 12:00 UTC - definitely in DST + time_t summer = make_utc(2026, 7, 4, 12); + EXPECT_TRUE(is_in_dst(summer, tz)); +} + +TEST(PosixTzParser, IsInDstUSEasternWinter) { + ParsedTimezone tz; + parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz); + + // January 15, 2026 12:00 UTC - definitely not in DST + time_t winter = make_utc(2026, 1, 15, 12); + EXPECT_FALSE(is_in_dst(winter, tz)); +} + +TEST(PosixTzParser, IsInDstNoDstTimezone) { + ParsedTimezone tz; + parse_posix_tz("IST-5:30", tz); + + // July 15, 2026 12:00 UTC + time_t epoch = make_utc(2026, 7, 15, 12); + EXPECT_FALSE(is_in_dst(epoch, tz)); +} + +TEST(PosixTzParser, SouthernHemisphereDstSummer) { + ParsedTimezone tz; + parse_posix_tz("NZST-12NZDT,M9.5.0,M4.1.0/3", tz); + + // December 15, 2025 12:00 UTC - summer in NZ, should be in DST + time_t nz_summer = make_utc(2025, 12, 15, 12); + EXPECT_TRUE(is_in_dst(nz_summer, tz)); +} + +TEST(PosixTzParser, SouthernHemisphereDstWinter) { + ParsedTimezone tz; + parse_posix_tz("NZST-12NZDT,M9.5.0,M4.1.0/3", tz); + + // July 15, 2026 12:00 UTC - winter in NZ, should NOT be in DST + time_t nz_winter = make_utc(2026, 7, 15, 12); + EXPECT_FALSE(is_in_dst(nz_winter, tz)); +} + +// ============================================================================ +// epoch_to_local_tm tests +// ============================================================================ + +TEST(PosixTzParser, EpochToLocalBasic) { + ParsedTimezone tz; + parse_posix_tz("UTC0", tz); + + time_t epoch = 0; // Jan 1, 1970 00:00:00 UTC + struct tm local; + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_year, 70); + EXPECT_EQ(local.tm_mon, 0); + EXPECT_EQ(local.tm_mday, 1); + EXPECT_EQ(local.tm_hour, 0); +} + +TEST(PosixTzParser, EpochToLocalNegativeEpoch) { + ParsedTimezone tz; + parse_posix_tz("UTC0", tz); + + // Dec 31, 1969 23:59:59 UTC (1 second before epoch) + time_t epoch = -1; + struct tm local; + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_year, 69); // 1969 + EXPECT_EQ(local.tm_mon, 11); // December + EXPECT_EQ(local.tm_mday, 31); + EXPECT_EQ(local.tm_hour, 23); + EXPECT_EQ(local.tm_min, 59); + EXPECT_EQ(local.tm_sec, 59); +} + +TEST(PosixTzParser, EpochToLocalNullTmFails) { + ParsedTimezone tz; + parse_posix_tz("UTC0", tz); + EXPECT_FALSE(epoch_to_local_tm(0, tz, nullptr)); +} + +TEST(PosixTzParser, EpochToLocalWithOffset) { + ParsedTimezone tz; + parse_posix_tz("EST5", tz); // UTC-5 + + // Jan 1, 2026 05:00:00 UTC should be Jan 1, 2026 00:00:00 EST + time_t utc_epoch = make_utc(2026, 1, 1, 5); + + struct tm local; + ASSERT_TRUE(epoch_to_local_tm(utc_epoch, tz, &local)); + EXPECT_EQ(local.tm_hour, 0); // Midnight EST + EXPECT_EQ(local.tm_mday, 1); + EXPECT_EQ(local.tm_isdst, 0); +} + +TEST(PosixTzParser, EpochToLocalDstTransition) { + ParsedTimezone tz; + parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz); + + // July 4, 2026 16:00 UTC = 12:00 EDT (noon) + time_t utc_epoch = make_utc(2026, 7, 4, 16); + + struct tm local; + ASSERT_TRUE(epoch_to_local_tm(utc_epoch, tz, &local)); + EXPECT_EQ(local.tm_hour, 12); // Noon EDT + EXPECT_EQ(local.tm_isdst, 1); +} + +// ============================================================================ +// Verification against libc +// ============================================================================ + +class LibcVerificationTest : public ::testing::TestWithParam> { + protected: + // NOLINTNEXTLINE(readability-identifier-naming) - Google Test requires this name + void SetUp() override { + // Save current TZ + const char *current_tz = getenv("TZ"); + saved_tz_ = current_tz ? current_tz : ""; + had_tz_ = current_tz != nullptr; + } + + // NOLINTNEXTLINE(readability-identifier-naming) - Google Test requires this name + void TearDown() override { + // Restore TZ + if (had_tz_) { + setenv("TZ", saved_tz_.c_str(), 1); + } else { + unsetenv("TZ"); + } + tzset(); + } + + private: + std::string saved_tz_; + bool had_tz_{false}; +}; + +TEST_P(LibcVerificationTest, MatchesLibc) { + auto [tz_str, epoch] = GetParam(); + + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + + // Our implementation + struct tm our_tm {}; + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &our_tm)); + + // libc implementation + setenv("TZ", tz_str, 1); + tzset(); + struct tm *libc_tm = localtime(&epoch); + ASSERT_NE(libc_tm, nullptr); + + EXPECT_EQ(our_tm.tm_year, libc_tm->tm_year); + EXPECT_EQ(our_tm.tm_mon, libc_tm->tm_mon); + EXPECT_EQ(our_tm.tm_mday, libc_tm->tm_mday); + EXPECT_EQ(our_tm.tm_hour, libc_tm->tm_hour); + EXPECT_EQ(our_tm.tm_min, libc_tm->tm_min); + EXPECT_EQ(our_tm.tm_sec, libc_tm->tm_sec); + EXPECT_EQ(our_tm.tm_isdst, libc_tm->tm_isdst); +} + +INSTANTIATE_TEST_SUITE_P(USEastern, LibcVerificationTest, + ::testing::Values(std::make_tuple("EST5EDT,M3.2.0/2,M11.1.0/2", 1704067200), + std::make_tuple("EST5EDT,M3.2.0/2,M11.1.0/2", 1720000000), + std::make_tuple("EST5EDT,M3.2.0/2,M11.1.0/2", 1735689600))); + +INSTANTIATE_TEST_SUITE_P(AngleBracket, LibcVerificationTest, + ::testing::Values(std::make_tuple("<+07>-7", 1704067200), + std::make_tuple("<+07>-7", 1720000000))); + +INSTANTIATE_TEST_SUITE_P(India, LibcVerificationTest, + ::testing::Values(std::make_tuple("IST-5:30", 1704067200), + std::make_tuple("IST-5:30", 1720000000))); + +INSTANTIATE_TEST_SUITE_P(NewZealand, LibcVerificationTest, + ::testing::Values(std::make_tuple("NZST-12NZDT,M9.5.0,M4.1.0/3", 1704067200), + std::make_tuple("NZST-12NZDT,M9.5.0,M4.1.0/3", 1720000000))); + +INSTANTIATE_TEST_SUITE_P(USCentral, LibcVerificationTest, + ::testing::Values(std::make_tuple("CST6CDT,M3.2.0/2,M11.1.0/2", 1704067200), + std::make_tuple("CST6CDT,M3.2.0/2,M11.1.0/2", 1720000000), + std::make_tuple("CST6CDT,M3.2.0/2,M11.1.0/2", 1735689600))); + +INSTANTIATE_TEST_SUITE_P(EuropeBerlin, LibcVerificationTest, + ::testing::Values(std::make_tuple("CET-1CEST,M3.5.0,M10.5.0/3", 1704067200), + std::make_tuple("CET-1CEST,M3.5.0,M10.5.0/3", 1720000000), + std::make_tuple("CET-1CEST,M3.5.0,M10.5.0/3", 1735689600))); + +INSTANTIATE_TEST_SUITE_P(AustraliaSydney, LibcVerificationTest, + ::testing::Values(std::make_tuple("AEST-10AEDT,M10.1.0,M4.1.0/3", 1704067200), + std::make_tuple("AEST-10AEDT,M10.1.0,M4.1.0/3", 1720000000), + std::make_tuple("AEST-10AEDT,M10.1.0,M4.1.0/3", 1735689600))); + +// ============================================================================ +// DST boundary edge cases +// ============================================================================ + +TEST(PosixTzParser, DstBoundaryJustBeforeSpringForward) { + // Test 1 second before DST starts + ParsedTimezone tz; + parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz); + + // March 8, 2026 06:59:59 UTC = 01:59:59 EST (1 second before spring forward) + time_t before_epoch = make_utc(2026, 3, 8, 6, 59, 59); + EXPECT_FALSE(is_in_dst(before_epoch, tz)); + + // March 8, 2026 07:00:00 UTC = 02:00:00 EST -> 03:00:00 EDT (DST started) + time_t after_epoch = make_utc(2026, 3, 8, 7); + EXPECT_TRUE(is_in_dst(after_epoch, tz)); +} + +TEST(PosixTzParser, DstBoundaryJustBeforeFallBack) { + // Test 1 second before DST ends + ParsedTimezone tz; + parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz); + + // November 1, 2026 05:59:59 UTC = 01:59:59 EDT (1 second before fall back) + time_t before_epoch = make_utc(2026, 11, 1, 5, 59, 59); + EXPECT_TRUE(is_in_dst(before_epoch, tz)); + + // November 1, 2026 06:00:00 UTC = 02:00:00 EDT -> 01:00:00 EST (DST ended) + time_t after_epoch = make_utc(2026, 11, 1, 6); + EXPECT_FALSE(is_in_dst(after_epoch, tz)); +} + +} // namespace esphome::time::testing + +// ============================================================================ +// ESPTime::strptime tests (replaces sscanf-based parsing) +// ============================================================================ + +namespace esphome::testing { + +TEST(ESPTimeStrptime, FullDateTime) { + ESPTime t{}; + ASSERT_TRUE(ESPTime::strptime("2026-03-15 14:30:45", 19, t)); + EXPECT_EQ(t.year, 2026); + EXPECT_EQ(t.month, 3); + EXPECT_EQ(t.day_of_month, 15); + EXPECT_EQ(t.hour, 14); + EXPECT_EQ(t.minute, 30); + EXPECT_EQ(t.second, 45); +} + +TEST(ESPTimeStrptime, DateTimeNoSeconds) { + ESPTime t{}; + ASSERT_TRUE(ESPTime::strptime("2026-03-15 14:30", 16, t)); + EXPECT_EQ(t.year, 2026); + EXPECT_EQ(t.month, 3); + EXPECT_EQ(t.day_of_month, 15); + EXPECT_EQ(t.hour, 14); + EXPECT_EQ(t.minute, 30); + EXPECT_EQ(t.second, 0); +} + +TEST(ESPTimeStrptime, DateOnly) { + ESPTime t{}; + ASSERT_TRUE(ESPTime::strptime("2026-03-15", 10, t)); + EXPECT_EQ(t.year, 2026); + EXPECT_EQ(t.month, 3); + EXPECT_EQ(t.day_of_month, 15); +} + +TEST(ESPTimeStrptime, TimeWithSeconds) { + ESPTime t{}; + ASSERT_TRUE(ESPTime::strptime("14:30:45", 8, t)); + EXPECT_EQ(t.hour, 14); + EXPECT_EQ(t.minute, 30); + EXPECT_EQ(t.second, 45); +} + +TEST(ESPTimeStrptime, TimeNoSeconds) { + ESPTime t{}; + ASSERT_TRUE(ESPTime::strptime("14:30", 5, t)); + EXPECT_EQ(t.hour, 14); + EXPECT_EQ(t.minute, 30); + EXPECT_EQ(t.second, 0); +} + +TEST(ESPTimeStrptime, Midnight) { + ESPTime t{}; + ASSERT_TRUE(ESPTime::strptime("00:00:00", 8, t)); + EXPECT_EQ(t.hour, 0); + EXPECT_EQ(t.minute, 0); + EXPECT_EQ(t.second, 0); +} + +TEST(ESPTimeStrptime, EndOfDay) { + ESPTime t{}; + ASSERT_TRUE(ESPTime::strptime("23:59:59", 8, t)); + EXPECT_EQ(t.hour, 23); + EXPECT_EQ(t.minute, 59); + EXPECT_EQ(t.second, 59); +} + +TEST(ESPTimeStrptime, LeapYearDate) { + ESPTime t{}; + ASSERT_TRUE(ESPTime::strptime("2024-02-29", 10, t)); + EXPECT_EQ(t.year, 2024); + EXPECT_EQ(t.month, 2); + EXPECT_EQ(t.day_of_month, 29); +} + +TEST(ESPTimeStrptime, NewYearsEve) { + ESPTime t{}; + ASSERT_TRUE(ESPTime::strptime("2026-12-31 23:59:59", 19, t)); + EXPECT_EQ(t.year, 2026); + EXPECT_EQ(t.month, 12); + EXPECT_EQ(t.day_of_month, 31); + EXPECT_EQ(t.hour, 23); + EXPECT_EQ(t.minute, 59); + EXPECT_EQ(t.second, 59); +} + +TEST(ESPTimeStrptime, EmptyStringFails) { + ESPTime t{}; + EXPECT_FALSE(ESPTime::strptime("", 0, t)); +} + +TEST(ESPTimeStrptime, NullInputFails) { + ESPTime t{}; + EXPECT_FALSE(ESPTime::strptime(nullptr, 0, t)); +} + +TEST(ESPTimeStrptime, InvalidFormatFails) { + ESPTime t{}; + EXPECT_FALSE(ESPTime::strptime("not-a-date", 10, t)); +} + +TEST(ESPTimeStrptime, PartialDateFails) { + ESPTime t{}; + EXPECT_FALSE(ESPTime::strptime("2026-03", 7, t)); +} + +TEST(ESPTimeStrptime, PartialTimeFails) { + ESPTime t{}; + EXPECT_FALSE(ESPTime::strptime("14:", 3, t)); +} + +TEST(ESPTimeStrptime, ExtraCharactersFails) { + ESPTime t{}; + // Full datetime with extra characters should fail + EXPECT_FALSE(ESPTime::strptime("2026-03-15 14:30:45x", 20, t)); +} + +TEST(ESPTimeStrptime, WrongSeparatorFails) { + ESPTime t{}; + EXPECT_FALSE(ESPTime::strptime("2026/03/15", 10, t)); +} + +TEST(ESPTimeStrptime, LeadingZeroTime) { + ESPTime t{}; + ASSERT_TRUE(ESPTime::strptime("01:05:09", 8, t)); + EXPECT_EQ(t.hour, 1); + EXPECT_EQ(t.minute, 5); + EXPECT_EQ(t.second, 9); +} + +// ============================================================================ +// recalc_timestamp_local() tests - verify behavior matches libc mktime() +// ============================================================================ + +// Helper to call libc mktime with same fields +static time_t libc_mktime(int year, int month, int day, int hour, int min, int sec) { + struct tm tm {}; + tm.tm_year = year - 1900; + tm.tm_mon = month - 1; + tm.tm_mday = day; + tm.tm_hour = hour; + tm.tm_min = min; + tm.tm_sec = sec; + tm.tm_isdst = -1; // Let libc determine DST + return mktime(&tm); +} + +// Helper to create ESPTime and call recalc_timestamp_local +static time_t esptime_recalc_local(int year, int month, int day, int hour, int min, int sec) { + ESPTime t{}; + t.year = year; + t.month = month; + t.day_of_month = day; + t.hour = hour; + t.minute = min; + t.second = sec; + t.day_of_week = 1; // Placeholder for fields_in_range() + t.day_of_year = 1; + t.recalc_timestamp_local(); + return t.timestamp; +} + +TEST(RecalcTimestampLocal, NormalTimeMatchesLibc) { + // Set timezone to US Central (CST6CDT) + const char *tz_str = "CST6CDT,M3.2.0,M11.1.0"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + // Test a normal time in winter (no DST) + // January 15, 2026 at 10:30:00 CST + time_t libc_result = libc_mktime(2026, 1, 15, 10, 30, 0); + time_t esp_result = esptime_recalc_local(2026, 1, 15, 10, 30, 0); + EXPECT_EQ(esp_result, libc_result); + + // Test a normal time in summer (DST active) + // July 15, 2026 at 10:30:00 CDT + libc_result = libc_mktime(2026, 7, 15, 10, 30, 0); + esp_result = esptime_recalc_local(2026, 7, 15, 10, 30, 0); + EXPECT_EQ(esp_result, libc_result); +} + +TEST(RecalcTimestampLocal, SpringForwardSkippedHour) { + // Set timezone to US Central (CST6CDT) + // DST starts March 8, 2026 at 2:00 AM -> clocks jump to 3:00 AM + const char *tz_str = "CST6CDT,M3.2.0,M11.1.0"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + // Test time before the transition (1:30 AM CST exists) + time_t libc_result = libc_mktime(2026, 3, 8, 1, 30, 0); + time_t esp_result = esptime_recalc_local(2026, 3, 8, 1, 30, 0); + EXPECT_EQ(esp_result, libc_result); + + // Test time after the transition (3:30 AM CDT exists) + libc_result = libc_mktime(2026, 3, 8, 3, 30, 0); + esp_result = esptime_recalc_local(2026, 3, 8, 3, 30, 0); + EXPECT_EQ(esp_result, libc_result); + + // Test the skipped hour (2:30 AM doesn't exist - gets normalized) + // Both implementations should produce the same result + libc_result = libc_mktime(2026, 3, 8, 2, 30, 0); + esp_result = esptime_recalc_local(2026, 3, 8, 2, 30, 0); + EXPECT_EQ(esp_result, libc_result); +} + +TEST(RecalcTimestampLocal, FallBackRepeatedHour) { + // Set timezone to US Central (CST6CDT) + // DST ends November 1, 2026 at 2:00 AM -> clocks fall back to 1:00 AM + const char *tz_str = "CST6CDT,M3.2.0,M11.1.0"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + // Test time before the transition (midnight CDT) + time_t libc_result = libc_mktime(2026, 11, 1, 0, 30, 0); + time_t esp_result = esptime_recalc_local(2026, 11, 1, 0, 30, 0); + EXPECT_EQ(esp_result, libc_result); + + // Test time well after the transition (3:00 AM CST) + libc_result = libc_mktime(2026, 11, 1, 3, 0, 0); + esp_result = esptime_recalc_local(2026, 11, 1, 3, 0, 0); + EXPECT_EQ(esp_result, libc_result); + + // Test the repeated hour (1:30 AM occurs twice) + // libc behavior varies by platform for this edge case, so we verify our + // consistent behavior: prefer standard time (later UTC timestamp) + esp_result = esptime_recalc_local(2026, 11, 1, 1, 30, 0); + time_t std_interpretation = esptime_recalc_local(2026, 11, 1, 2, 30, 0) - 3600; // 2:30 CST - 1 hour + EXPECT_EQ(esp_result, std_interpretation); +} + +TEST(RecalcTimestampLocal, SouthernHemisphereDST) { + // Set timezone to Australia/Sydney (AEST-10AEDT,M10.1.0,M4.1.0) + // DST starts first Sunday of October, ends first Sunday of April + const char *tz_str = "AEST-10AEDT,M10.1.0,M4.1.0"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + // Test winter time (July - no DST in southern hemisphere) + time_t libc_result = libc_mktime(2026, 7, 15, 10, 30, 0); + time_t esp_result = esptime_recalc_local(2026, 7, 15, 10, 30, 0); + EXPECT_EQ(esp_result, libc_result); + + // Test summer time (January - DST active in southern hemisphere) + libc_result = libc_mktime(2026, 1, 15, 10, 30, 0); + esp_result = esptime_recalc_local(2026, 1, 15, 10, 30, 0); + EXPECT_EQ(esp_result, libc_result); +} + +TEST(RecalcTimestampLocal, ExactTransitionBoundary) { + // Test exact boundary of spring forward transition + // Mar 8, 2026 at 2:00 AM CST -> 3:00 AM CDT (clocks skip forward) + const char *tz_str = "CST6CDT,M3.2.0,M11.1.0"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + // 1:59:59 AM CST - last second before transition (still standard time) + time_t libc_result = libc_mktime(2026, 3, 8, 1, 59, 59); + time_t esp_result = esptime_recalc_local(2026, 3, 8, 1, 59, 59); + EXPECT_EQ(esp_result, libc_result); + + // 3:00:00 AM CDT - first second after transition (now DST) + libc_result = libc_mktime(2026, 3, 8, 3, 0, 0); + esp_result = esptime_recalc_local(2026, 3, 8, 3, 0, 0); + EXPECT_EQ(esp_result, libc_result); + + // Verify the gap: 3:00 AM CDT should be exactly 1 second after 1:59:59 AM CST + time_t before_transition = esptime_recalc_local(2026, 3, 8, 1, 59, 59); + time_t after_transition = esptime_recalc_local(2026, 3, 8, 3, 0, 0); + EXPECT_EQ(after_transition - before_transition, 1); +} + +TEST(RecalcTimestampLocal, NonDefaultTransitionTime) { + // Test DST transition at 3:00 AM instead of default 2:00 AM + // Using custom transition time: CST6CDT,M3.2.0/3,M11.1.0/3 + const char *tz_str = "CST6CDT,M3.2.0/3,M11.1.0/3"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + // 2:30 AM should still be standard time (transition at 3:00 AM) + time_t libc_result = libc_mktime(2026, 3, 8, 2, 30, 0); + time_t esp_result = esptime_recalc_local(2026, 3, 8, 2, 30, 0); + EXPECT_EQ(esp_result, libc_result); + + // 4:00 AM should be DST (after 3:00 AM transition) + libc_result = libc_mktime(2026, 3, 8, 4, 0, 0); + esp_result = esptime_recalc_local(2026, 3, 8, 4, 0, 0); + EXPECT_EQ(esp_result, libc_result); +} + +TEST(RecalcTimestampLocal, YearBoundaryDST) { + // Test southern hemisphere DST across year boundary + // Australia/Sydney: DST active from October to April (spans Jan 1) + const char *tz_str = "AEST-10AEDT,M10.1.0,M4.1.0"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + // Dec 31, 2025 at 23:30 - DST should be active + time_t libc_result = libc_mktime(2025, 12, 31, 23, 30, 0); + time_t esp_result = esptime_recalc_local(2025, 12, 31, 23, 30, 0); + EXPECT_EQ(esp_result, libc_result); + + // Jan 1, 2026 at 00:30 - DST should still be active + libc_result = libc_mktime(2026, 1, 1, 0, 30, 0); + esp_result = esptime_recalc_local(2026, 1, 1, 0, 30, 0); + EXPECT_EQ(esp_result, libc_result); + + // Verify both are in DST (11 hour offset from UTC, not 10) + // The timestamps should be 1 hour apart + time_t dec31 = esptime_recalc_local(2025, 12, 31, 23, 30, 0); + time_t jan1 = esptime_recalc_local(2026, 1, 1, 0, 30, 0); + EXPECT_EQ(jan1 - dec31, 3600); // 1 hour difference +} + +// ============================================================================ +// ESPTime::timezone_offset() tests +// ============================================================================ + +TEST(TimezoneOffset, NoTimezone) { + // When no timezone is set, offset should be 0 + time::ParsedTimezone tz{}; + set_global_tz(tz); + + int32_t offset = ESPTime::timezone_offset(); + EXPECT_EQ(offset, 0); +} + +TEST(TimezoneOffset, FixedOffsetPositive) { + // India: UTC+5:30 (no DST) + const char *tz_str = "IST-5:30"; + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + int32_t offset = ESPTime::timezone_offset(); + // Offset should be +5:30 = 19800 seconds (to add to UTC to get local) + EXPECT_EQ(offset, 5 * 3600 + 30 * 60); +} + +TEST(TimezoneOffset, FixedOffsetNegative) { + // US Eastern Standard Time: UTC-5 (testing without DST rules) + const char *tz_str = "EST5"; + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + int32_t offset = ESPTime::timezone_offset(); + // Offset should be -5 hours = -18000 seconds + EXPECT_EQ(offset, -5 * 3600); +} + +TEST(TimezoneOffset, WithDstReturnsCorrectOffsetBasedOnCurrentTime) { + // US Eastern with DST + const char *tz_str = "EST5EDT,M3.2.0,M11.1.0"; + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + // Get current time and check offset matches expected based on DST status + time_t now = ::time(nullptr); + int32_t offset = ESPTime::timezone_offset(); + + // Verify offset matches what is_in_dst says + if (time::is_in_dst(now, tz)) { + // During DST, offset should be -4 hours (EDT) + EXPECT_EQ(offset, -4 * 3600); + } else { + // During standard time, offset should be -5 hours (EST) + EXPECT_EQ(offset, -5 * 3600); + } +} + +} // namespace esphome::testing From 50e7571f4c603f460a2dffa2b78eacc0a6dd5014 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 18:17:25 -0700 Subject: [PATCH 236/609] [web_server_idf] Prefer make_unique_for_overwrite for noninit recv buffer (#14279) --- esphome/components/web_server_idf/web_server_idf.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 4034a22586..f18570965b 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -921,7 +921,7 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c }); // Use heap buffer - 1460 bytes is too large for the httpd task stack - auto buffer = std::make_unique(MULTIPART_CHUNK_SIZE); + auto buffer = std::make_unique_for_overwrite(MULTIPART_CHUNK_SIZE); size_t bytes_since_yield = 0; for (size_t remaining = r->content_len; remaining > 0;) { From 15846137a6c64bbbc44345f29ff17d40ac92f1bb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 18:17:52 -0700 Subject: [PATCH 237/609] [rp2040] Update arduino-pico framework from 3.9.4 to 5.5.0 (#14328) --- .clang-tidy.hash | 2 +- esphome/components/rp2040/__init__.py | 8 +-- esphome/components/rp2040/gpio.cpp | 2 +- .../components/uart/uart_component_rp2040.cpp | 4 +- .../components/wifi/wifi_component_pico_w.cpp | 53 +++++++++++++------ platformio.ini | 4 +- 6 files changed, 46 insertions(+), 27 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 777c846371..767da3f33e 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -5eb1e5852765114ad06533220d3160b6c23f5ccefc4de41828699de5dfff5ad6 +b97e16a84153b2a4cfc51137cd6121db3c32374504b2bea55144413b3e573052 diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 3a1ea16fa3..23f12e651f 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -91,18 +91,18 @@ def _parse_platform_version(value): # The default/recommended arduino framework version # - https://github.com/earlephilhower/arduino-pico/releases # - https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 9, 4) +RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(5, 5, 0) # The raspberrypi platform version to use for arduino frameworks # - https://github.com/maxgerhardt/platform-raspberrypi/tags -RECOMMENDED_ARDUINO_PLATFORM_VERSION = "v1.2.0-gcc12" +RECOMMENDED_ARDUINO_PLATFORM_VERSION = "v1.4.0-gcc14-arduinopico460" def _arduino_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(3, 9, 4), "https://github.com/earlephilhower/arduino-pico"), - "latest": (cv.Version(3, 9, 4), None), + "dev": (cv.Version(5, 5, 0), "https://github.com/earlephilhower/arduino-pico"), + "latest": (cv.Version(5, 5, 0), None), "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } diff --git a/esphome/components/rp2040/gpio.cpp b/esphome/components/rp2040/gpio.cpp index 2b1699f888..4b3c98104c 100644 --- a/esphome/components/rp2040/gpio.cpp +++ b/esphome/components/rp2040/gpio.cpp @@ -106,7 +106,7 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { sio_hw->gpio_oe_set = arg->mask; } else if (flags & gpio::FLAG_INPUT) { sio_hw->gpio_oe_clr = arg->mask; - hw_write_masked(&padsbank0_hw->io[arg->pin], + hw_write_masked(&pads_bank0_hw->io[arg->pin], (bool_to_bit(flags & gpio::FLAG_PULLUP) << PADS_BANK0_GPIO0_PUE_LSB) | (bool_to_bit(flags & gpio::FLAG_PULLDOWN) << PADS_BANK0_GPIO0_PDE_LSB), PADS_BANK0_GPIO0_PUE_BITS | PADS_BANK0_GPIO0_PDE_BITS); diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index 0c6834055c..faf8f4d90f 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -115,8 +115,8 @@ void RP2040UartComponent::setup() { if (tx_hw == -1 || rx_hw == -1 || tx_hw != rx_hw) { ESP_LOGV(TAG, "Using SerialPIO"); - pin_size_t tx = this->tx_pin_ == nullptr ? SerialPIO::NOPIN : this->tx_pin_->get_pin(); - pin_size_t rx = this->rx_pin_ == nullptr ? SerialPIO::NOPIN : this->rx_pin_->get_pin(); + pin_size_t tx = this->tx_pin_ == nullptr ? NOPIN : this->tx_pin_->get_pin(); + pin_size_t rx = this->rx_pin_ == nullptr ? NOPIN : this->rx_pin_->get_pin(); auto *serial = new SerialPIO(tx, rx, this->rx_buffer_size_); // NOLINT(cppcoreguidelines-owning-memory) serial->begin(this->baud_rate_, config); if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 1baf21e2b2..9b2c077dc5 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -78,8 +78,13 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return false; #endif - auto ret = WiFi.begin(ap.ssid_.c_str(), ap.password_.c_str()); - if (ret != WL_CONNECTED) + // Use beginNoBlock to avoid WiFi.begin()'s additional 2x timeout wait loop on top of + // CYW43::begin()'s internal blocking join. CYW43::begin() blocks for up to 10 seconds + // (default timeout) to complete the join - this is required because the LwipIntfDev netif + // setup depends on begin() succeeding. beginNoBlock() skips the outer wait loop, saving + // up to 20 additional seconds of blocking per attempt. + auto ret = WiFi.beginNoBlock(ap.ssid_.c_str(), ap.password_.c_str()); + if (ret == WL_IDLE_STATUS) return false; return true; @@ -116,13 +121,19 @@ const char *get_disconnect_reason_str(uint8_t reason) { } WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { - int status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA); + // Use cyw43_wifi_link_status instead of cyw43_tcpip_link_status because the Arduino + // framework's __wrap_cyw43_cb_tcpip_init is a no-op — the SDK's internal netif + // (cyw43_state.netif[]) is never initialized. cyw43_tcpip_link_status checks that netif's + // flags and would only fall through to cyw43_wifi_link_status when the flags aren't set. + // Using cyw43_wifi_link_status directly gives us the actual WiFi radio join state. + int status = cyw43_wifi_link_status(&cyw43_state, CYW43_ITF_STA); switch (status) { case CYW43_LINK_JOIN: - case CYW43_LINK_NOIP: + // WiFi joined, check if we have an IP address via the Arduino framework's WiFi class + if (WiFi.status() == WL_CONNECTED) { + return WiFiSTAConnectStatus::CONNECTED; + } return WiFiSTAConnectStatus::CONNECTING; - case CYW43_LINK_UP: - return WiFiSTAConnectStatus::CONNECTED; case CYW43_LINK_FAIL: case CYW43_LINK_BADAUTH: return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; @@ -139,18 +150,24 @@ int WiFiComponent::s_wifi_scan_result(void *env, const cyw43_ev_scan_result_t *r void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result) { s_scan_result_count++; - const char *ssid_cstr = reinterpret_cast(result->ssid); + + // CYW43 scan results have ssid as a 32-byte buffer that is NOT null-terminated. + // Use ssid_len to create a properly terminated copy for string operations. + uint8_t len = std::min(result->ssid_len, static_cast(sizeof(result->ssid))); + char ssid_buf[33]; // 32 max + null terminator + memcpy(ssid_buf, result->ssid, len); + ssid_buf[len] = '\0'; // Skip networks that don't match any configured network (unless full results needed) - if (!this->needs_full_scan_results_() && !this->matches_configured_network_(ssid_cstr, result->bssid)) { - this->log_discarded_scan_result_(ssid_cstr, result->bssid, result->rssi, result->channel); + if (!this->needs_full_scan_results_() && !this->matches_configured_network_(ssid_buf, result->bssid)) { + this->log_discarded_scan_result_(ssid_buf, result->bssid, result->rssi, result->channel); return; } bssid_t bssid; std::copy(result->bssid, result->bssid + 6, bssid.begin()); - WiFiScanResult res(bssid, ssid_cstr, strlen(ssid_cstr), result->channel, result->rssi, - result->auth_mode != CYW43_AUTH_OPEN, ssid_cstr[0] == '\0'); + WiFiScanResult res(bssid, ssid_buf, len, result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN, + len == 0); if (std::find(this->scan_result_.begin(), this->scan_result_.end(), res) == this->scan_result_.end()) { this->scan_result_.push_back(res); } @@ -167,7 +184,6 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { ESP_LOGV(TAG, "cyw43_wifi_scan failed"); } return err == 0; - return true; } #ifdef USE_WIFI_AP @@ -212,8 +228,10 @@ network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {(const ip_addr_t * #endif // USE_WIFI_AP bool WiFiComponent::wifi_disconnect_() { - int err = cyw43_wifi_leave(&cyw43_state, CYW43_ITF_STA); - return err == 0; + // Use Arduino WiFi.disconnect() instead of raw cyw43_wifi_leave() to properly + // clean up the lwIP netif, DHCP client, and internal Arduino state. + WiFi.disconnect(); + return true; } bssid_t WiFiComponent::wifi_bssid() { @@ -269,9 +287,10 @@ void WiFiComponent::wifi_loop_() { // Poll for connection state changes // The arduino-pico WiFi library doesn't have event callbacks like ESP8266/ESP32, - // so we need to poll the link status to detect state changes - auto status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA); - bool is_connected = (status == CYW43_LINK_UP); + // so we need to poll the link status to detect state changes. + // Use WiFi.connected() which checks both the WiFi link and IP address via the + // Arduino framework's own netif (not the SDK's uninitialized one). + bool is_connected = WiFi.connected(); // Detect connection state change if (is_connected && !s_sta_was_connected) { diff --git a/platformio.ini b/platformio.ini index e35dce2228..16a1b18211 100644 --- a/platformio.ini +++ b/platformio.ini @@ -193,10 +193,10 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script extends = common:arduino board_build.filesystem_size = 0.5m -platform = https://github.com/maxgerhardt/platform-raspberrypi.git#v1.2.0-gcc12 +platform = https://github.com/maxgerhardt/platform-raspberrypi.git#v1.4.0-gcc14-arduinopico460 platform_packages = ; earlephilhower/framework-arduinopico@~1.20602.0 ; Cannot use the platformio package until old releases stop getting deleted - earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/3.9.4/rp2040-3.9.4.zip + earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/5.5.0/rp2040-5.5.0.zip framework = arduino lib_deps = From 04db37a34ac43d0a9c5fb8c807b56ed58a551bce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 20:38:38 -0700 Subject: [PATCH 238/609] [esp8266] Remove forced scanf linkage to save ~8KB flash (#13678) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp8266/__init__.py | 6 +++ .../esp8266/remove_float_scanf.py.script | 46 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 esphome/components/esp8266/remove_float_scanf.py.script diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index c7b5d5c130..927a59fd61 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -205,6 +205,7 @@ async def to_code(config): "pre:testing_mode.py", "pre:exclude_updater.py", "pre:exclude_waveform.py", + "pre:remove_float_scanf.py", "post:post_build.py", ], ) @@ -342,3 +343,8 @@ def copy_files() -> None: exclude_waveform_file, CORE.relative_build_path("exclude_waveform.py"), ) + remove_float_scanf_file = dir / "remove_float_scanf.py.script" + copy_file_if_changed( + remove_float_scanf_file, + CORE.relative_build_path("remove_float_scanf.py"), + ) diff --git a/esphome/components/esp8266/remove_float_scanf.py.script b/esphome/components/esp8266/remove_float_scanf.py.script new file mode 100644 index 0000000000..b1d03a4d46 --- /dev/null +++ b/esphome/components/esp8266/remove_float_scanf.py.script @@ -0,0 +1,46 @@ +# pylint: disable=E0602 +Import("env") # noqa + +# Remove forced scanf linkage to allow garbage collection of unused code +# +# The ESP8266 Arduino framework unconditionally adds: +# -u _printf_float -u _scanf_float +# +# The -u flag forces symbols to be linked even if unreferenced, which pulls +# in the entire scanf family (~7-8KB). ESPHome doesn't use scanf at all +# (verified by CI check in PR #13657), so this is pure dead weight. +# +# By removing -u _scanf_float, --gc-sections can eliminate: +# - scanf family functions (~7KB) +# - _strtod_l (~3.7KB) +# - Related parsing infrastructure +# +# We keep -u _printf_float because components still use %f in logging. + + +def remove_scanf_float_flag(source, target, env): + """Remove -u _scanf_float from linker flags. + + This is called as a pre-action before the link step, after the + Arduino framework has added its default flags. + """ + linkflags = env.get("LINKFLAGS", []) + new_linkflags = [] + i = 0 + + while i < len(linkflags): + flag = linkflags[i] + if flag == "-u" and i + 1 < len(linkflags): + next_flag = linkflags[i + 1] + if next_flag == "_scanf_float": + print("ESPHome: Removing _scanf_float (saves ~8KB flash)") + i += 2 # Skip both -u and the symbol + continue + new_linkflags.append(flag) + i += 1 + + env.Replace(LINKFLAGS=new_linkflags) + + +# Register the callback to run before the link step +env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", remove_scanf_float_flag) From 656389f2158b0723630839c385aa26648c6f53b1 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 26 Feb 2026 23:41:35 -0600 Subject: [PATCH 239/609] [usb_uart] Performance, correctness and reliability improvements (#14333) --- .../components/usb_host/usb_host_client.cpp | 19 +- esphome/components/usb_uart/ch34x.cpp | 9 +- esphome/components/usb_uart/cp210x.cpp | 9 +- esphome/components/usb_uart/usb_uart.cpp | 205 ++++++++++++++---- esphome/components/usb_uart/usb_uart.h | 45 +++- 5 files changed, 224 insertions(+), 63 deletions(-) diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 5b0aed4c59..c77d738ace 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -424,9 +424,9 @@ bool USBClient::control_transfer(uint8_t type, uint8_t request, uint16_t value, if (trq == nullptr) return false; auto length = data.size(); - if (length > sizeof(trq->transfer->data_buffer_size) - SETUP_PACKET_SIZE) { + if (length > trq->transfer->data_buffer_size - SETUP_PACKET_SIZE) { ESP_LOGE(TAG, "Control transfer data size too large: %u > %u", length, - sizeof(trq->transfer->data_buffer_size) - sizeof(usb_setup_packet_t)); + trq->transfer->data_buffer_size - SETUP_PACKET_SIZE); this->release_trq(trq); return false; } @@ -507,9 +507,13 @@ bool USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u /** * Performs an output transfer operation. - * THREAD CONTEXT: Called from main loop thread only - * - USB UART output uses defer() to ensure main loop context - * - Modbus and other components call from loop() + * THREAD CONTEXT: Called from both USB task and main loop threads. + * - USB task: output transfer callback restarts output directly (no defer) + * - Main loop: initial output trigger from write_array() and loop() + * Thread safety is ensured by: + * - get_trq_() uses atomic CAS (multi-consumer safe) + * - claimed trq slot is exclusively owned until submission + * - usb_host_transfer_submit() is safe to call from any task context * * @param ep_address The endpoint address. * @param callback The callback function to be called when the transfer is complete. @@ -524,6 +528,11 @@ bool USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, ESP_LOGE(TAG, "Too many requests queued"); return false; } + if (length > trq->transfer->data_buffer_size) { + ESP_LOGE(TAG, "transfer_out: data length %u exceeds buffer size %u", length, trq->transfer->data_buffer_size); + this->release_trq(trq); + return false; + } trq->callback = callback; trq->transfer->callback = transfer_callback; trq->transfer->bEndpointAddress = ep_address | USB_DIR_OUT; diff --git a/esphome/components/usb_uart/ch34x.cpp b/esphome/components/usb_uart/ch34x.cpp index caa4b65657..7fa964c0cb 100644 --- a/esphome/components/usb_uart/ch34x.cpp +++ b/esphome/components/usb_uart/ch34x.cpp @@ -5,8 +5,7 @@ #include "esphome/components/bytebuffer/bytebuffer.h" -namespace esphome { -namespace usb_uart { +namespace esphome::usb_uart { using namespace bytebuffer; /** @@ -74,8 +73,8 @@ void USBUartTypeCH34X::enable_channels() { this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, cmd, value, (factor << 8) | divisor, callback); this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, cmd + 3, 0x80, 0, callback); } - USBUartTypeCdcAcm::enable_channels(); + this->start_channels(); } -} // namespace usb_uart -} // namespace esphome +} // namespace esphome::usb_uart + #endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_uart/cp210x.cpp b/esphome/components/usb_uart/cp210x.cpp index fa8c2331b2..ae9170c5fb 100644 --- a/esphome/components/usb_uart/cp210x.cpp +++ b/esphome/components/usb_uart/cp210x.cpp @@ -5,8 +5,7 @@ #include "esphome/components/bytebuffer/bytebuffer.h" -namespace esphome { -namespace usb_uart { +namespace esphome::usb_uart { using namespace bytebuffer; /** @@ -119,8 +118,8 @@ void USBUartTypeCP210X::enable_channels() { this->control_transfer(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, SET_BAUDRATE, 0, channel->index_, callback, baud.get_data()); } - USBUartTypeCdcAcm::enable_channels(); + this->start_channels(); } -} // namespace usb_uart -} // namespace esphome +} // namespace esphome::usb_uart + #endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 5c2806c456..de81bfc587 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -3,12 +3,10 @@ #include "usb_uart.h" #include "esphome/core/log.h" #include "esphome/core/application.h" -#include "esphome/components/uart/uart_debugger.h" #include -namespace esphome { -namespace usb_uart { +namespace esphome::usb_uart { /** * @@ -135,16 +133,51 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) { ESP_LOGV(TAG, "Channel not initialised - write ignored"); return; } - while (this->output_buffer_.get_free_space() != 0 && len-- != 0) { - this->output_buffer_.push(*data++); +#ifdef USE_UART_DEBUGGER + if (this->debug_) { + constexpr size_t BATCH = 16; + char buf[4 + format_hex_pretty_size(BATCH)]; // ">>> " + "XX,XX,...,XX\0" + for (size_t off = 0; off < len; off += BATCH) { + size_t n = std::min(len - off, BATCH); + memcpy(buf, ">>> ", 4); + format_hex_pretty_to(buf + 4, sizeof(buf) - 4, data + off, n, ','); + ESP_LOGD(TAG, "%s", buf); + } } - len++; - if (len > 0) { - ESP_LOGE(TAG, "Buffer full - failed to write %d bytes", len); +#endif + while (len > 0) { + UsbOutputChunk *chunk = this->output_pool_.allocate(); + if (chunk == nullptr) { + ESP_LOGE(TAG, "Output pool full - lost %zu bytes", len); + break; + } + size_t chunk_len = std::min(len, UsbOutputChunk::MAX_CHUNK_SIZE); + memcpy(chunk->data, data, chunk_len); + chunk->length = static_cast(chunk_len); + if (!this->output_queue_.push(chunk)) { + this->output_pool_.release(chunk); + ESP_LOGE(TAG, "Output queue full - lost %zu bytes", len); + break; + } + data += chunk_len; + len -= chunk_len; } this->parent_->start_output(this); } +void USBUartChannel::flush() { + // Spin until the output queue is drained and the last USB transfer completes. + // Safe to call from the main loop only. + // The 100 ms timeout guards against a device that stops responding mid-flush; + // in that case the main loop is blocked for the full duration. + uint32_t deadline = millis() + 100; // 100 ms safety timeout + while ((!this->output_queue_.empty() || this->output_started_.load()) && millis() < deadline) { + // Kick start_output() in case data arrived but no transfer is in flight yet. + this->parent_->start_output(this); + yield(); + } +} + bool USBUartChannel::peek_byte(uint8_t *data) { if (this->input_buffer_.is_empty()) { return false; @@ -182,8 +215,10 @@ void USBUartComponent::loop() { #ifdef USE_UART_DEBUGGER if (channel->debug_) { - uart::UARTDebug::log_hex(uart::UART_DIRECTION_RX, std::vector(chunk->data, chunk->data + chunk->length), - ','); // NOLINT() + char buf[4 + format_hex_pretty_size(UsbDataChunk::MAX_CHUNK_SIZE)]; // "<<< " + hex + memcpy(buf, "<<< ", 4); + format_hex_pretty_to(buf + 4, sizeof(buf) - 4, chunk->data, chunk->length, ','); + ESP_LOGD(TAG, "%s", buf); } #endif @@ -192,6 +227,13 @@ void USBUartComponent::loop() { // Return chunk to pool for reuse this->chunk_pool_.release(chunk); + + // Invoke the RX callback (if registered) immediately after data lands in the + // ring buffer. This lets consumers such as ZigbeeProxy process incoming bytes + // in the same loop iteration they are delivered, avoiding an extra wakeup cycle. + if (channel->rx_callback_) { + channel->rx_callback_(); + } } // Log dropped USB data periodically @@ -234,10 +276,10 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // The underlying transfer_in() uses lock-free atomic allocation from the // TransferRequest pool, making this multi-threaded access safe - // if already started, don't restart. A spurious failure in compare_exchange_weak - // is not a problem, as it will be retried on the next read_array() + // Use compare_exchange_strong to avoid spurious failures: a missed submit here is + // never retried by read_array() because no data will ever arrive to trigger it. auto started = false; - if (!channel->input_started_.compare_exchange_weak(started, true)) + if (!channel->input_started_.compare_exchange_strong(started, true)) return; const auto *ep = channel->cdc_dev_.in_ep; // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback @@ -285,37 +327,56 @@ void USBUartComponent::start_input(USBUartChannel *channel) { this->start_input(channel); }; if (!this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize)) { + ESP_LOGE(TAG, "IN transfer submission failed for ep=0x%02X", ep->bEndpointAddress); channel->input_started_.store(false); } } void USBUartComponent::start_output(USBUartChannel *channel) { - // IMPORTANT: This function must only be called from the main loop! - // The output_buffer_ is not thread-safe and can only be accessed from main loop. - // USB callbacks use defer() to ensure this function runs in the correct context. - if (channel->output_started_.load()) - return; - if (channel->output_buffer_.is_empty()) { + // THREAD CONTEXT: Called from both main loop and USB task threads. + // The output_queue_ is a lock-free SPSC queue, so pop() is safe from either thread. + // The output_started_ atomic flag is claimed via compare_exchange to guarantee that + // only one thread starts a transfer at a time. + + // Atomically claim the "output in progress" flag. If already set, another thread + // is handling the transfer; return immediately. + bool expected = false; + if (!channel->output_started_.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) { return; } + + UsbOutputChunk *chunk = channel->output_queue_.pop(); + if (chunk == nullptr) { + // Nothing to send — release the flag and return. + channel->output_started_.store(false, std::memory_order_release); + return; + } + const auto *ep = channel->cdc_dev_.out_ep; - // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback - auto callback = [this, channel](const usb_host::TransferStatus &status) { - ESP_LOGV(TAG, "Output Transfer result: length: %u; status %X", status.data_len, status.error_code); - channel->output_started_.store(false); - // Defer restart to main loop (defer is thread-safe) - this->defer([this, channel] { this->start_output(channel); }); + // CALLBACK CONTEXT: This lambda is executed in the USB task via transfer_callback. + // It releases the chunk, clears the flag, and directly restarts output without + // going through defer() — eliminating one full main-loop-wakeup cycle of latency. + auto callback = [this, channel, chunk](const usb_host::TransferStatus &status) { + if (!status.success) { + ESP_LOGW(TAG, "Output transfer failed: status %X", status.error_code); + } else { + ESP_LOGV(TAG, "Output Transfer result: length: %u; status %X", status.data_len, status.error_code); + } + channel->output_pool_.release(chunk); + channel->output_started_.store(false, std::memory_order_release); + // Restart directly from USB task — safe because output_queue_ is lock-free + // and transfer_out() uses thread-safe atomic slot allocation. + this->start_output(channel); }; - channel->output_started_.store(true); - uint8_t data[ep->wMaxPacketSize]; - auto len = channel->output_buffer_.pop(data, ep->wMaxPacketSize); - this->transfer_out(ep->bEndpointAddress, callback, data, len); -#ifdef USE_UART_DEBUGGER - if (channel->debug_) { - uart::UARTDebug::log_hex(uart::UART_DIRECTION_TX, std::vector(data, data + len), ','); // NOLINT() + + const uint8_t len = chunk->length; + if (!this->transfer_out(ep->bEndpointAddress, callback, chunk->data, len)) { + // Transfer submission failed — return chunk and release flag so callers can retry. + channel->output_pool_.release(chunk); + channel->output_started_.store(false, std::memory_order_release); + return; } -#endif - ESP_LOGV(TAG, "Output %d bytes started", len); + ESP_LOGV(TAG, "Output %u bytes started", len); } /** @@ -351,6 +412,20 @@ void USBUartTypeCdcAcm::on_connected() { fix_mps(channel->cdc_dev_.in_ep); fix_mps(channel->cdc_dev_.out_ep); channel->initialised_.store(true); + // Claim the communication (interrupt) interface so CDC class requests are accepted + // by the device. Some CDC ACM implementations (e.g. EFR32 NCP) require this before + // they enable data flow on the bulk endpoints. + if (channel->cdc_dev_.interrupt_interface_number != channel->cdc_dev_.bulk_interface_number) { + auto err_comm = usb_host_interface_claim(this->handle_, this->device_handle_, + channel->cdc_dev_.interrupt_interface_number, 0); + if (err_comm != ESP_OK) { + ESP_LOGW(TAG, "Could not claim comm interface %d: %s", channel->cdc_dev_.interrupt_interface_number, + esp_err_to_name(err_comm)); + } else { + channel->cdc_dev_.comm_interface_claimed = true; + ESP_LOGD(TAG, "Claimed comm interface %d", channel->cdc_dev_.interrupt_interface_number); + } + } auto err = usb_host_interface_claim(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number, 0); if (err != ESP_OK) { @@ -378,18 +453,74 @@ void USBUartTypeCdcAcm::on_disconnected() { usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress); usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress); } + if (channel->cdc_dev_.comm_interface_claimed) { + usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.interrupt_interface_number); + channel->cdc_dev_.comm_interface_claimed = false; + } usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number); // Reset the input and output started flags to their initial state to avoid the possibility of spurious restarts channel->input_started_.store(true); channel->output_started_.store(true); channel->input_buffer_.clear(); - channel->output_buffer_.clear(); + // Drain any pending output chunks and return them to the pool + { + UsbOutputChunk *chunk; + while ((chunk = channel->output_queue_.pop()) != nullptr) { + channel->output_pool_.release(chunk); + } + } channel->initialised_.store(false); } USBClient::on_disconnected(); } void USBUartTypeCdcAcm::enable_channels() { + static constexpr uint8_t CDC_REQUEST_TYPE = usb_host::USB_TYPE_CLASS | usb_host::USB_RECIP_INTERFACE; + static constexpr uint8_t CDC_SET_LINE_CODING = 0x20; + static constexpr uint8_t CDC_SET_CONTROL_LINE_STATE = 0x22; + static constexpr uint16_t CDC_DTR_RTS = 0x0003; // D0=DTR, D1=RTS + + for (auto *channel : this->channels_) { + if (!channel->initialised_.load()) + continue; + // Configure the bridge's UART parameters. A USB-UART bridge will not forward data + // at the correct speed until SET_LINE_CODING is sent; without it the UART may run + // at an indeterminate default rate so the NCP receives garbled bytes and never + // sends RSTACK. + uint32_t baud = channel->baud_rate_; + std::vector line_coding = { + static_cast(baud & 0xFF), static_cast((baud >> 8) & 0xFF), + static_cast((baud >> 16) & 0xFF), static_cast((baud >> 24) & 0xFF), + static_cast(channel->stop_bits_), // bCharFormat: 0=1stop, 1=1.5stop, 2=2stop + static_cast(channel->parity_), // bParityType: 0=None, 1=Odd, 2=Even, 3=Mark, 4=Space + static_cast(channel->data_bits_), // bDataBits + }; + ESP_LOGD(TAG, "SET_LINE_CODING: baud=%u stop=%u parity=%u data=%u", (unsigned) baud, channel->stop_bits_, + (unsigned) channel->parity_, channel->data_bits_); + this->control_transfer( + CDC_REQUEST_TYPE, CDC_SET_LINE_CODING, 0, channel->cdc_dev_.interrupt_interface_number, + [](const usb_host::TransferStatus &status) { + if (!status.success) { + ESP_LOGW(TAG, "SET_LINE_CODING failed: %X", status.error_code); + } else { + ESP_LOGD(TAG, "SET_LINE_CODING OK"); + } + }, + line_coding); + // Assert DTR+RTS to signal DTE is present. + this->control_transfer(CDC_REQUEST_TYPE, CDC_SET_CONTROL_LINE_STATE, CDC_DTR_RTS, + channel->cdc_dev_.interrupt_interface_number, [](const usb_host::TransferStatus &status) { + if (!status.success) { + ESP_LOGW(TAG, "SET_CONTROL_LINE_STATE failed: %X", status.error_code); + } else { + ESP_LOGD(TAG, "SET_CONTROL_LINE_STATE (DTR+RTS) OK"); + } + }); + } + this->start_channels(); +} + +void USBUartTypeCdcAcm::start_channels() { for (auto *channel : this->channels_) { if (!channel->initialised_.load()) continue; @@ -399,6 +530,6 @@ void USBUartTypeCdcAcm::enable_channels() { } } -} // namespace usb_uart -} // namespace esphome +} // namespace esphome::usb_uart + #endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 94e6120457..9a9fe1c2ca 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -8,9 +8,10 @@ #include "esphome/core/lock_free_queue.h" #include "esphome/core/event_pool.h" #include +#include + +namespace esphome::usb_uart { -namespace esphome { -namespace usb_uart { class USBUartTypeCdcAcm; class USBUartComponent; class USBUartChannel; @@ -31,6 +32,7 @@ struct CdcEps { const usb_ep_desc_t *out_ep; uint8_t bulk_interface_number; uint8_t interrupt_interface_number; + bool comm_interface_claimed{false}; }; enum UARTParityOptions { @@ -83,6 +85,16 @@ struct UsbDataChunk { void release() {} }; +// Structure for queuing outgoing USB data chunks (one per USB FS packet) +struct UsbOutputChunk { + static constexpr size_t MAX_CHUNK_SIZE = 64; // USB FS MPS + uint8_t data[MAX_CHUNK_SIZE]; + uint8_t length; + + // Required for EventPool - no cleanup needed for POD types + void release() {} +}; + class USBUartChannel : public uart::UARTComponent, public Parented { friend class USBUartComponent; friend class USBUartTypeCdcAcm; @@ -90,24 +102,32 @@ class USBUartChannel : public uart::UARTComponent, public Parentedinput_buffer_.get_available(); } - void flush() override {} + void flush() override; void check_logger_conflict() override {} void set_parity(UARTParityOptions parity) { this->parity_ = parity; } void set_debug(bool debug) { this->debug_ = debug; } void set_dummy_receiver(bool dummy_receiver) { this->dummy_receiver_ = dummy_receiver; } + /// Register a callback invoked immediately after data is pushed to the input ring buffer. + /// Called from USBUartComponent::loop() in the main loop context. + /// Allows consumers (e.g. ZigbeeProxy) to process bytes in the same loop iteration + /// they arrive, eliminating one full main-loop-wakeup cycle of latency. + void set_rx_callback(std::function cb) { this->rx_callback_ = std::move(cb); } + protected: // Larger structures first for better alignment RingBuffer input_buffer_; - RingBuffer output_buffer_; + LockFreeQueue output_queue_; + EventPool output_pool_; + std::function rx_callback_{}; CdcEps cdc_dev_{}; // Enum (likely 4 bytes) UARTParityOptions parity_{UART_CONFIG_PARITY_NONE}; @@ -150,8 +170,12 @@ class USBUartTypeCdcAcm : public USBUartComponent { protected: virtual std::vector parse_descriptors(usb_device_handle_t dev_hdl); void on_connected() override; - virtual void enable_channels(); void on_disconnected() override; + virtual void enable_channels(); + /// Resets per-channel transfer flags and posts the first bulk IN transfer. + /// Called by enable_channels() and by vendor-specific subclass overrides that + /// handle their own line-coding setup before starting data flow. + void start_channels(); }; class USBUartTypeCP210X : public USBUartTypeCdcAcm { @@ -170,7 +194,6 @@ class USBUartTypeCH34X : public USBUartTypeCdcAcm { void enable_channels() override; }; -} // namespace usb_uart -} // namespace esphome +} // namespace esphome::usb_uart #endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 From 4044520ccc3a17b1162a6b7aec970f5658f1f641 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Feb 2026 01:38:36 -0500 Subject: [PATCH 240/609] [esp32_touch] Migrate to new unified touch sensor driver (esp_driver_touch_sens) (#14033) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/esp32_touch/__init__.py | 319 ++++++----- .../components/esp32_touch/esp32_touch.cpp | 500 ++++++++++++++++++ esphome/components/esp32_touch/esp32_touch.h | 284 ++++------ .../esp32_touch/esp32_touch_common.cpp | 173 ------ .../components/esp32_touch/esp32_touch_v1.cpp | 244 --------- .../components/esp32_touch/esp32_touch_v2.cpp | 402 -------------- .../esp32_touch/common-variants.yaml | 1 - .../esp32_touch/test.esp32-p4-idf.yaml | 5 + 8 files changed, 800 insertions(+), 1128 deletions(-) create mode 100644 esphome/components/esp32_touch/esp32_touch.cpp delete mode 100644 esphome/components/esp32_touch/esp32_touch_common.cpp delete mode 100644 esphome/components/esp32_touch/esp32_touch_v1.cpp delete mode 100644 esphome/components/esp32_touch/esp32_touch_v2.cpp create mode 100644 tests/components/esp32_touch/test.esp32-p4-idf.yaml diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py index a02370a343..10ad339b12 100644 --- a/esphome/components/esp32_touch/__init__.py +++ b/esphome/components/esp32_touch/__init__.py @@ -1,7 +1,10 @@ +import logging + import esphome.codegen as cg from esphome.components import esp32 from esphome.components.esp32 import ( VARIANT_ESP32, + VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, get_esp32_variant, @@ -21,6 +24,8 @@ from esphome.const import ( ) from esphome.core import TimePeriod +_LOGGER = logging.getLogger(__name__) + AUTO_LOAD = ["binary_sensor"] DEPENDENCIES = ["esp32"] @@ -37,135 +42,161 @@ CONF_WATERPROOF_SHIELD_DRIVER = "waterproof_shield_driver" esp32_touch_ns = cg.esphome_ns.namespace("esp32_touch") ESP32TouchComponent = esp32_touch_ns.class_("ESP32TouchComponent", cg.Component) +# Channel ID mappings: GPIO pin number -> integer channel ID +# These are plain integers - the new unified API uses int chan_id directly. TOUCH_PADS = { VARIANT_ESP32: { - 4: cg.global_ns.TOUCH_PAD_NUM0, - 0: cg.global_ns.TOUCH_PAD_NUM1, - 2: cg.global_ns.TOUCH_PAD_NUM2, - 15: cg.global_ns.TOUCH_PAD_NUM3, - 13: cg.global_ns.TOUCH_PAD_NUM4, - 12: cg.global_ns.TOUCH_PAD_NUM5, - 14: cg.global_ns.TOUCH_PAD_NUM6, - 27: cg.global_ns.TOUCH_PAD_NUM7, - 33: cg.global_ns.TOUCH_PAD_NUM8, - 32: cg.global_ns.TOUCH_PAD_NUM9, + 4: 0, + 0: 1, + 2: 2, + 15: 3, + 13: 4, + 12: 5, + 14: 6, + 27: 7, + 33: 8, + 32: 9, }, VARIANT_ESP32S2: { - 1: cg.global_ns.TOUCH_PAD_NUM1, - 2: cg.global_ns.TOUCH_PAD_NUM2, - 3: cg.global_ns.TOUCH_PAD_NUM3, - 4: cg.global_ns.TOUCH_PAD_NUM4, - 5: cg.global_ns.TOUCH_PAD_NUM5, - 6: cg.global_ns.TOUCH_PAD_NUM6, - 7: cg.global_ns.TOUCH_PAD_NUM7, - 8: cg.global_ns.TOUCH_PAD_NUM8, - 9: cg.global_ns.TOUCH_PAD_NUM9, - 10: cg.global_ns.TOUCH_PAD_NUM10, - 11: cg.global_ns.TOUCH_PAD_NUM11, - 12: cg.global_ns.TOUCH_PAD_NUM12, - 13: cg.global_ns.TOUCH_PAD_NUM13, - 14: cg.global_ns.TOUCH_PAD_NUM14, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + 6: 6, + 7: 7, + 8: 8, + 9: 9, + 10: 10, + 11: 11, + 12: 12, + 13: 13, + 14: 14, }, VARIANT_ESP32S3: { - 1: cg.global_ns.TOUCH_PAD_NUM1, - 2: cg.global_ns.TOUCH_PAD_NUM2, - 3: cg.global_ns.TOUCH_PAD_NUM3, - 4: cg.global_ns.TOUCH_PAD_NUM4, - 5: cg.global_ns.TOUCH_PAD_NUM5, - 6: cg.global_ns.TOUCH_PAD_NUM6, - 7: cg.global_ns.TOUCH_PAD_NUM7, - 8: cg.global_ns.TOUCH_PAD_NUM8, - 9: cg.global_ns.TOUCH_PAD_NUM9, - 10: cg.global_ns.TOUCH_PAD_NUM10, - 11: cg.global_ns.TOUCH_PAD_NUM11, - 12: cg.global_ns.TOUCH_PAD_NUM12, - 13: cg.global_ns.TOUCH_PAD_NUM13, - 14: cg.global_ns.TOUCH_PAD_NUM14, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + 6: 6, + 7: 7, + 8: 8, + 9: 9, + 10: 10, + 11: 11, + 12: 12, + 13: 13, + 14: 14, + }, + VARIANT_ESP32P4: { + 2: 1, + 3: 2, + 4: 3, + 5: 4, + 6: 5, + 7: 6, + 8: 7, + 9: 8, + 10: 9, + 11: 10, + 12: 11, + 13: 12, + 14: 13, + 15: 14, }, } TOUCH_PAD_DENOISE_GRADE = { - "BIT12": cg.global_ns.TOUCH_PAD_DENOISE_BIT12, - "BIT10": cg.global_ns.TOUCH_PAD_DENOISE_BIT10, - "BIT8": cg.global_ns.TOUCH_PAD_DENOISE_BIT8, - "BIT4": cg.global_ns.TOUCH_PAD_DENOISE_BIT4, + "BIT12": cg.global_ns.TOUCH_DENOISE_CHAN_RESOLUTION_BIT12, + "BIT10": cg.global_ns.TOUCH_DENOISE_CHAN_RESOLUTION_BIT10, + "BIT8": cg.global_ns.TOUCH_DENOISE_CHAN_RESOLUTION_BIT8, + "BIT4": cg.global_ns.TOUCH_DENOISE_CHAN_RESOLUTION_BIT4, } TOUCH_PAD_DENOISE_CAP_LEVEL = { - "L0": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L0, - "L1": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L1, - "L2": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L2, - "L3": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L3, - "L4": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L4, - "L5": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L5, - "L6": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L6, - "L7": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L7, + "L0": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_5PF, + "L1": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_6PF, + "L2": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_7PF, + "L3": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_9PF, + "L4": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_10PF, + "L5": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_12PF, + "L6": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_13PF, + "L7": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_14PF, } TOUCH_PAD_FILTER_MODE = { - "IIR_4": cg.global_ns.TOUCH_PAD_FILTER_IIR_4, - "IIR_8": cg.global_ns.TOUCH_PAD_FILTER_IIR_8, - "IIR_16": cg.global_ns.TOUCH_PAD_FILTER_IIR_16, - "IIR_32": cg.global_ns.TOUCH_PAD_FILTER_IIR_32, - "IIR_64": cg.global_ns.TOUCH_PAD_FILTER_IIR_64, - "IIR_128": cg.global_ns.TOUCH_PAD_FILTER_IIR_128, - "IIR_256": cg.global_ns.TOUCH_PAD_FILTER_IIR_256, - "JITTER": cg.global_ns.TOUCH_PAD_FILTER_JITTER, + "IIR_4": cg.global_ns.TOUCH_BM_IIR_FILTER_4, + "IIR_8": cg.global_ns.TOUCH_BM_IIR_FILTER_8, + "IIR_16": cg.global_ns.TOUCH_BM_IIR_FILTER_16, + "IIR_32": cg.global_ns.TOUCH_BM_IIR_FILTER_32, + "IIR_64": cg.global_ns.TOUCH_BM_IIR_FILTER_64, + "IIR_128": cg.global_ns.TOUCH_BM_IIR_FILTER_128, + "IIR_256": cg.global_ns.TOUCH_BM_IIR_FILTER_256, + "JITTER": cg.global_ns.TOUCH_BM_JITTER_FILTER, } TOUCH_PAD_SMOOTH_MODE = { - "OFF": cg.global_ns.TOUCH_PAD_SMOOTH_OFF, - "IIR_2": cg.global_ns.TOUCH_PAD_SMOOTH_IIR_2, - "IIR_4": cg.global_ns.TOUCH_PAD_SMOOTH_IIR_4, - "IIR_8": cg.global_ns.TOUCH_PAD_SMOOTH_IIR_8, + "OFF": cg.global_ns.TOUCH_SMOOTH_NO_FILTER, + "IIR_2": cg.global_ns.TOUCH_SMOOTH_IIR_FILTER_2, + "IIR_4": cg.global_ns.TOUCH_SMOOTH_IIR_FILTER_4, + "IIR_8": cg.global_ns.TOUCH_SMOOTH_IIR_FILTER_8, } LOW_VOLTAGE_REFERENCE = { - "0.5V": cg.global_ns.TOUCH_LVOLT_0V5, - "0.6V": cg.global_ns.TOUCH_LVOLT_0V6, - "0.7V": cg.global_ns.TOUCH_LVOLT_0V7, - "0.8V": cg.global_ns.TOUCH_LVOLT_0V8, + "0.5V": cg.global_ns.TOUCH_VOLT_LIM_L_0V5, + "0.6V": cg.global_ns.TOUCH_VOLT_LIM_L_0V6, + "0.7V": cg.global_ns.TOUCH_VOLT_LIM_L_0V7, + "0.8V": cg.global_ns.TOUCH_VOLT_LIM_L_0V8, } HIGH_VOLTAGE_REFERENCE = { - "2.4V": cg.global_ns.TOUCH_HVOLT_2V4, - "2.5V": cg.global_ns.TOUCH_HVOLT_2V5, - "2.6V": cg.global_ns.TOUCH_HVOLT_2V6, - "2.7V": cg.global_ns.TOUCH_HVOLT_2V7, + "2.4V": cg.global_ns.TOUCH_VOLT_LIM_H_2V4, + "2.5V": cg.global_ns.TOUCH_VOLT_LIM_H_2V5, + "2.6V": cg.global_ns.TOUCH_VOLT_LIM_H_2V6, + "2.7V": cg.global_ns.TOUCH_VOLT_LIM_H_2V7, } -VOLTAGE_ATTENUATION = { - "1.5V": cg.global_ns.TOUCH_HVOLT_ATTEN_1V5, - "1V": cg.global_ns.TOUCH_HVOLT_ATTEN_1V, - "0.5V": cg.global_ns.TOUCH_HVOLT_ATTEN_0V5, - "0V": cg.global_ns.TOUCH_HVOLT_ATTEN_0V, -} -TOUCH_PAD_WATERPROOF_SHIELD_DRIVER = { - "L0": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L0, - "L1": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L1, - "L2": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L2, - "L3": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L3, - "L4": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L4, - "L5": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L5, - "L6": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L6, - "L7": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L7, +VOLTAGE_ATTENUATION = {"1.5V", "1V", "0.5V", "0V"} + +# ESP32 V1: The new API's touch_volt_lim_h_t combines the old high_voltage_reference +# and voltage_attenuation into a single enum representing the effective upper voltage. +# Effective voltage = high_voltage_reference - voltage_attenuation +EFFECTIVE_HIGH_VOLTAGE = { + ("2.4V", "1.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_0V9, + ("2.5V", "1.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V0, + ("2.6V", "1.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V1, + ("2.7V", "1.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V2, + ("2.4V", "1V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V4, + ("2.5V", "1V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V5, + ("2.6V", "1V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V6, + ("2.7V", "1V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V7, + ("2.4V", "0.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V9, + ("2.5V", "0.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V0, + ("2.6V", "0.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V1, + ("2.7V", "0.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V2, + ("2.4V", "0V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V4, + ("2.5V", "0V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V5, + ("2.6V", "0V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V6, + ("2.7V", "0V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V7, } def validate_touch_pad(value): value = gpio.gpio_pin_number_validator(value) variant = get_esp32_variant() - if variant not in TOUCH_PADS: + pads = TOUCH_PADS.get(variant) + if pads is None: raise cv.Invalid(f"ESP32 variant {variant} does not support touch pads.") - - pads = TOUCH_PADS[variant] if value not in pads: raise cv.Invalid(f"Pin {value} does not support touch pads.") - return cv.enum(pads)(value) + return pads[value] # Return integer channel ID def validate_variant_vars(config): - if get_esp32_variant() == VARIANT_ESP32: - variant_vars = { + variant = get_esp32_variant() + invalid_vars = set() + if variant == VARIANT_ESP32: + invalid_vars = { CONF_DEBOUNCE_COUNT, CONF_DENOISE_GRADE, CONF_DENOISE_CAP_LEVEL, @@ -176,15 +207,14 @@ def validate_variant_vars(config): CONF_WATERPROOF_GUARD_RING, CONF_WATERPROOF_SHIELD_DRIVER, } - for vvar in variant_vars: - if vvar in config: - raise cv.Invalid(f"{vvar} is not valid on {VARIANT_ESP32}") - elif ( - get_esp32_variant() == VARIANT_ESP32S2 or get_esp32_variant() == VARIANT_ESP32S3 - ) and CONF_IIR_FILTER in config: - raise cv.Invalid( - f"{CONF_IIR_FILTER} is not valid on {VARIANT_ESP32S2} or {VARIANT_ESP32S3}" - ) + elif variant in (VARIANT_ESP32S2, VARIANT_ESP32S3, VARIANT_ESP32P4): + invalid_vars = {CONF_IIR_FILTER} + if variant == VARIANT_ESP32P4: + invalid_vars |= {CONF_DENOISE_GRADE, CONF_DENOISE_CAP_LEVEL} + unsupported = invalid_vars.intersection(config) + if unsupported: + keys = ", ".join(sorted(f"'{k}'" for k in unsupported)) + raise cv.Invalid(f"{keys} not valid on {variant}") return config @@ -219,12 +249,17 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_HIGH_VOLTAGE_REFERENCE, default="2.7V"): validate_voltage( HIGH_VOLTAGE_REFERENCE ), - cv.Optional(CONF_VOLTAGE_ATTENUATION, default="0V"): validate_voltage( - VOLTAGE_ATTENUATION - ), + # ESP32 V1 only: attenuates the high voltage reference + cv.SplitDefault( + CONF_VOLTAGE_ATTENUATION, + esp32="0V", + esp32_s2=cv.UNDEFINED, + esp32_s3=cv.UNDEFINED, + esp32_p4=cv.UNDEFINED, + ): validate_voltage(VOLTAGE_ATTENUATION), # ESP32 only cv.Optional(CONF_IIR_FILTER): cv.positive_time_period_milliseconds, - # ESP32-S2/S3 only + # ESP32-S2/S3/P4 only cv.Optional(CONF_DEBOUNCE_COUNT): cv.int_range(min=0, max=7), cv.Optional(CONF_FILTER_MODE): cv.enum( TOUCH_PAD_FILTER_MODE, upper=True, space="_" @@ -241,9 +276,7 @@ CONFIG_SCHEMA = cv.All( TOUCH_PAD_DENOISE_CAP_LEVEL, upper=True, space="_" ), cv.Optional(CONF_WATERPROOF_GUARD_RING): validate_touch_pad, - cv.Optional(CONF_WATERPROOF_SHIELD_DRIVER): cv.enum( - TOUCH_PAD_WATERPROOF_SHIELD_DRIVER, upper=True, space="_" - ), + cv.Optional(CONF_WATERPROOF_SHIELD_DRIVER): cv.int_range(min=0, max=7), } ).extend(cv.COMPONENT_SCHEMA), cv.has_none_or_all_keys(CONF_DENOISE_GRADE, CONF_DENOISE_CAP_LEVEL), @@ -260,6 +293,7 @@ CONFIG_SCHEMA = cv.All( esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S2, esp32.VARIANT_ESP32S3, + esp32.VARIANT_ESP32P4, ] ), validate_variant_vars, @@ -267,44 +301,67 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - # Re-enable ESP-IDF's touch sensor driver (excluded by default to save compile time) + # New unified touch sensor driver include_builtin_idf_component("esp_driver_touch_sens") - # Legacy driver component provides driver/touch_sensor.h header - include_builtin_idf_component("driver") touch = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(touch, config) cg.add(touch.set_setup_mode(config[CONF_SETUP_MODE])) - sleep_duration = int(round(config[CONF_SLEEP_DURATION].total_microseconds * 0.15)) - cg.add(touch.set_sleep_duration(sleep_duration)) + # sleep_duration -> meas_interval_us (pass microseconds directly) + cg.add(touch.set_meas_interval_us(config[CONF_SLEEP_DURATION].total_microseconds)) - measurement_duration = int( - round(config[CONF_MEASUREMENT_DURATION].total_microseconds * 7.99987793) - ) - cg.add(touch.set_measurement_duration(measurement_duration)) + variant = get_esp32_variant() - cg.add( - touch.set_low_voltage_reference( - LOW_VOLTAGE_REFERENCE[config[CONF_LOW_VOLTAGE_REFERENCE]] + # measurement_duration handling differs per variant + if variant == VARIANT_ESP32: + # V1: charge_duration_ms (convert from microseconds to milliseconds) + charge_duration_ms = ( + config[CONF_MEASUREMENT_DURATION].total_microseconds / 1000.0 ) - ) - cg.add( - touch.set_high_voltage_reference( - HIGH_VOLTAGE_REFERENCE[config[CONF_HIGH_VOLTAGE_REFERENCE]] + cg.add(touch.set_charge_duration_ms(charge_duration_ms)) + else: + # V2/V3: charge_times (approximate conversion from duration) + # The old API used clock cycles; the new API uses charge_times count. + # Default is 500 for V2/V3. Use measurement_duration as a rough scaling factor. + # 65535 / 8192 ≈ 7.9999 maps the microsecond duration to charge_times. + charge_times = int( + round(config[CONF_MEASUREMENT_DURATION].total_microseconds * (65535 / 8192)) ) - ) - cg.add( - touch.set_voltage_attenuation( - VOLTAGE_ATTENUATION[config[CONF_VOLTAGE_ATTENUATION]] - ) - ) + charge_times = max(charge_times, 1) + cg.add(touch.set_charge_times(charge_times)) - if get_esp32_variant() == VARIANT_ESP32 and CONF_IIR_FILTER in config: + # Voltage references (not applicable to P4) + if variant != VARIANT_ESP32P4: + if CONF_LOW_VOLTAGE_REFERENCE in config: + cg.add( + touch.set_low_voltage_reference( + LOW_VOLTAGE_REFERENCE[config[CONF_LOW_VOLTAGE_REFERENCE]] + ) + ) + if CONF_HIGH_VOLTAGE_REFERENCE in config: + if variant == VARIANT_ESP32: + # V1: combine high_voltage_reference with voltage_attenuation + high_ref = config[CONF_HIGH_VOLTAGE_REFERENCE] + atten = config[CONF_VOLTAGE_ATTENUATION] + cg.add( + touch.set_high_voltage_reference( + EFFECTIVE_HIGH_VOLTAGE[(high_ref, atten)] + ) + ) + else: + # V2/V3: no attenuation concept, use directly + cg.add( + touch.set_high_voltage_reference( + HIGH_VOLTAGE_REFERENCE[config[CONF_HIGH_VOLTAGE_REFERENCE]] + ) + ) + + if variant == VARIANT_ESP32 and CONF_IIR_FILTER in config: cg.add(touch.set_iir_filter(config[CONF_IIR_FILTER])) - if get_esp32_variant() == VARIANT_ESP32S2 or get_esp32_variant() == VARIANT_ESP32S3: + if variant in (VARIANT_ESP32S2, VARIANT_ESP32S3, VARIANT_ESP32P4): if CONF_FILTER_MODE in config: cg.add(touch.set_filter_mode(config[CONF_FILTER_MODE])) if CONF_DEBOUNCE_COUNT in config: diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp new file mode 100644 index 0000000000..e7124ce92f --- /dev/null +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -0,0 +1,500 @@ +#ifdef USE_ESP32 + +#include "esp32_touch.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +#include + +namespace esphome::esp32_touch { + +template static const char *lookup_str(const char *const (&table)[N], size_t index) { + return (index < N) ? table[index] : "UNKNOWN"; +} + +static const char *const TAG = "esp32_touch"; + +static constexpr uint32_t SETUP_MODE_LOG_INTERVAL_MS = 250; +static constexpr uint32_t INITIAL_STATE_DELAY_MS = 1500; +static constexpr uint32_t ONESHOT_SCAN_COUNT = 3; +static constexpr uint32_t ONESHOT_SCAN_TIMEOUT_MS = 2000; + +// V1: called from esp_timer context (software filter) +// V2/V3: called from ISR context +// xQueueSendFromISR is safe from both contexts. + +bool IRAM_ATTR ESP32TouchComponent::on_active_cb(touch_sensor_handle_t handle, const touch_active_event_data_t *event, + void *ctx) { + auto *comp = static_cast(ctx); + TouchEvent te{event->chan_id, true}; + BaseType_t higher = pdFALSE; + xQueueSendFromISR(comp->touch_queue_, &te, &higher); + comp->enable_loop_soon_any_context(); + return higher == pdTRUE; +} + +bool IRAM_ATTR ESP32TouchComponent::on_inactive_cb(touch_sensor_handle_t handle, + const touch_inactive_event_data_t *event, void *ctx) { + auto *comp = static_cast(ctx); + TouchEvent te{event->chan_id, false}; + BaseType_t higher = pdFALSE; + xQueueSendFromISR(comp->touch_queue_, &te, &higher); + comp->enable_loop_soon_any_context(); + return higher == pdTRUE; +} + +void ESP32TouchComponent::setup() { + if (!this->create_touch_queue_()) { + return; + } + + // Create sample config - differs per hardware version +#ifdef USE_ESP32_VARIANT_ESP32 + touch_sensor_sample_config_t sample_cfg = TOUCH_SENSOR_V1_DEFAULT_SAMPLE_CONFIG( + this->charge_duration_ms_, this->low_voltage_reference_, this->high_voltage_reference_); +#elif defined(USE_ESP32_VARIANT_ESP32P4) + // div_num=8 (data scaling divisor), coarse_freq_tune=2, fine_freq_tune=2 + touch_sensor_sample_config_t sample_cfg = TOUCH_SENSOR_V3_DEFAULT_SAMPLE_CONFIG(8, 2, 2); + sample_cfg.charge_times = this->charge_times_; +#else + // ESP32-S2/S3 (V2) + touch_sensor_sample_config_t sample_cfg = TOUCH_SENSOR_V2_DEFAULT_SAMPLE_CONFIG( + this->charge_times_, this->low_voltage_reference_, this->high_voltage_reference_); +#endif + + // Create controller + touch_sensor_config_t sens_cfg = TOUCH_SENSOR_DEFAULT_BASIC_CONFIG(1, &sample_cfg); + sens_cfg.meas_interval_us = this->meas_interval_us_; +#ifndef USE_ESP32_VARIANT_ESP32 + sens_cfg.max_meas_time_us = 0; // Disable measurement timeout (V2/V3 only) +#endif + + esp_err_t err = touch_sensor_new_controller(&sens_cfg, &this->sens_handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to create touch controller: %s", esp_err_to_name(err)); + this->cleanup_touch_queue_(); + this->mark_failed(); + return; + } + + // Create channels for all children + for (auto *child : this->children_) { + touch_channel_config_t chan_cfg = {}; +#ifdef USE_ESP32_VARIANT_ESP32 + chan_cfg.abs_active_thresh[0] = child->get_threshold(); + chan_cfg.charge_speed = TOUCH_CHARGE_SPEED_7; + chan_cfg.init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT; + chan_cfg.group = TOUCH_CHAN_TRIG_GROUP_BOTH; +#elif defined(USE_ESP32_VARIANT_ESP32P4) + chan_cfg.active_thresh[0] = child->get_threshold(); +#else + // ESP32-S2/S3 (V2) + chan_cfg.active_thresh[0] = child->get_threshold(); + chan_cfg.charge_speed = TOUCH_CHARGE_SPEED_7; + chan_cfg.init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT; +#endif + + err = touch_sensor_new_channel(this->sens_handle_, child->get_channel_id(), &chan_cfg, &child->chan_handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to create touch channel %d: %s", child->get_channel_id(), esp_err_to_name(err)); + this->cleanup_touch_queue_(); + this->mark_failed(); + return; + } + } + + // Configure filter +#ifdef USE_ESP32_VARIANT_ESP32 + // Software filter is REQUIRED for V1 on_active/on_inactive callbacks + { + touch_sensor_filter_config_t filter_cfg = TOUCH_SENSOR_DEFAULT_FILTER_CONFIG(); + if (this->iir_filter_enabled_()) { + filter_cfg.interval_ms = this->iir_filter_; + } + err = touch_sensor_config_filter(this->sens_handle_, &filter_cfg); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to configure filter: %s", esp_err_to_name(err)); + this->cleanup_touch_queue_(); + this->mark_failed(); + return; + } + } +#else + // V2/V3: Hardware benchmark filter + { + touch_sensor_filter_config_t filter_cfg = TOUCH_SENSOR_DEFAULT_FILTER_CONFIG(); + if (this->filter_configured_) { + filter_cfg.benchmark.filter_mode = this->filter_mode_; + filter_cfg.benchmark.jitter_step = this->jitter_step_; + filter_cfg.benchmark.denoise_lvl = this->noise_threshold_; + filter_cfg.data.smooth_filter = this->smooth_level_; + filter_cfg.data.debounce_cnt = this->debounce_count_; + } + err = touch_sensor_config_filter(this->sens_handle_, &filter_cfg); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to configure filter: %s", esp_err_to_name(err)); + } + } +#endif + +#if SOC_TOUCH_SUPPORT_DENOISE_CHAN + if (this->denoise_configured_) { + touch_denoise_chan_config_t denoise_cfg = {}; + denoise_cfg.charge_speed = TOUCH_CHARGE_SPEED_7; + denoise_cfg.init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT; + denoise_cfg.ref_cap = this->denoise_cap_level_; + denoise_cfg.resolution = this->denoise_grade_; + err = touch_sensor_config_denoise_channel(this->sens_handle_, &denoise_cfg); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to configure denoise: %s", esp_err_to_name(err)); + } + } +#endif + +#if SOC_TOUCH_SUPPORT_WATERPROOF + if (this->waterproof_configured_) { + touch_channel_handle_t guard_chan = nullptr; + for (auto *child : this->children_) { + if (child->get_channel_id() == this->waterproof_guard_ring_pad_) { + guard_chan = child->chan_handle_; + break; + } + } + + touch_channel_handle_t shield_chan = nullptr; + touch_channel_config_t shield_cfg = {}; +#ifdef USE_ESP32_VARIANT_ESP32P4 + shield_cfg.active_thresh[0] = 0; + err = touch_sensor_new_channel(this->sens_handle_, SOC_TOUCH_MAX_CHAN_ID, &shield_cfg, &shield_chan); +#else + shield_cfg.active_thresh[0] = 0; + shield_cfg.charge_speed = TOUCH_CHARGE_SPEED_7; + shield_cfg.init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT; + err = touch_sensor_new_channel(this->sens_handle_, TOUCH_SHIELD_CHAN_ID, &shield_cfg, &shield_chan); +#endif + if (err == ESP_OK) { + touch_waterproof_config_t wp_cfg = {}; + wp_cfg.guard_chan = guard_chan; + wp_cfg.shield_chan = shield_chan; + wp_cfg.shield_drv = this->waterproof_shield_driver_; + wp_cfg.flags.immersion_proof = 1; + err = touch_sensor_config_waterproof(this->sens_handle_, &wp_cfg); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to configure waterproof: %s", esp_err_to_name(err)); + } + } else { + ESP_LOGW(TAG, "Failed to create shield channel: %s", esp_err_to_name(err)); + } + } +#endif + + // Configure wakeup pads before enabling (must be done in INIT state) + this->configure_wakeup_pads_(); + + // Register callbacks + touch_event_callbacks_t cbs = {}; + cbs.on_active = on_active_cb; + cbs.on_inactive = on_inactive_cb; + err = touch_sensor_register_callbacks(this->sens_handle_, &cbs, this); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to register callbacks: %s", esp_err_to_name(err)); + this->cleanup_touch_queue_(); + this->mark_failed(); + return; + } + + // Enable and start scanning + err = touch_sensor_enable(this->sens_handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to enable touch sensor: %s", esp_err_to_name(err)); + this->cleanup_touch_queue_(); + this->mark_failed(); + return; + } + + // Do initial oneshot scans to populate baseline values + for (uint32_t i = 0; i < ONESHOT_SCAN_COUNT; i++) { + err = touch_sensor_trigger_oneshot_scanning(this->sens_handle_, ONESHOT_SCAN_TIMEOUT_MS); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Oneshot scan %d failed: %s", i, esp_err_to_name(err)); + } + } + + err = touch_sensor_start_continuous_scanning(this->sens_handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to start continuous scanning: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } +} + +void ESP32TouchComponent::dump_config() { +#if !defined(USE_ESP32_VARIANT_ESP32P4) + static constexpr const char *LV_STRS[] = {"0.5V", "0.6V", "0.7V", "0.8V"}; + static constexpr const char *HV_STRS[] = {"0.9V", "1.0V", "1.1V", "1.2V", "1.4V", "1.5V", "1.6V", "1.7V", + "1.9V", "2.0V", "2.1V", "2.2V", "2.4V", "2.5V", "2.6V", "2.7V"}; + const char *lv_s = lookup_str(LV_STRS, this->low_voltage_reference_); + const char *hv_s = lookup_str(HV_STRS, this->high_voltage_reference_); + + ESP_LOGCONFIG(TAG, + "Config for ESP32 Touch Hub:\n" + " Measurement interval: %.1fus\n" + " Low Voltage Reference: %s\n" + " High Voltage Reference: %s", + this->meas_interval_us_, lv_s, hv_s); +#else + ESP_LOGCONFIG(TAG, + "Config for ESP32 Touch Hub:\n" + " Measurement interval: %.1fus", + this->meas_interval_us_); +#endif + +#ifdef USE_ESP32_VARIANT_ESP32 + if (this->iir_filter_enabled_()) { + ESP_LOGCONFIG(TAG, " IIR Filter: %" PRIu32 "ms", this->iir_filter_); + } else { + ESP_LOGCONFIG(TAG, " IIR Filter: 10ms (default)"); + } +#else + if (this->filter_configured_) { + // TOUCH_BM_IIR_FILTER_256 only exists on V2, shifting JITTER's position + static constexpr const char *FILTER_STRS[] = { + "IIR_4", + "IIR_8", + "IIR_16", + "IIR_32", + "IIR_64", + "IIR_128", +#if SOC_TOUCH_SENSOR_VERSION == 2 + "IIR_256", +#endif + "JITTER", + }; + static constexpr const char *SMOOTH_STRS[] = {"OFF", "IIR_2", "IIR_4", "IIR_8"}; + const char *filter_s = lookup_str(FILTER_STRS, this->filter_mode_); + const char *smooth_s = lookup_str(SMOOTH_STRS, this->smooth_level_); + ESP_LOGCONFIG(TAG, + " Filter mode: %s\n" + " Debounce count: %" PRIu32 "\n" + " Noise threshold coefficient: %" PRIu32 "\n" + " Jitter filter step size: %" PRIu32 "\n" + " Smooth level: %s", + filter_s, this->debounce_count_, this->noise_threshold_, this->jitter_step_, smooth_s); + } + +#if SOC_TOUCH_SUPPORT_DENOISE_CHAN + if (this->denoise_configured_) { + static constexpr const char *GRADE_STRS[] = {"BIT12", "BIT10", "BIT8", "BIT4"}; + static constexpr const char *CAP_STRS[] = {"5pF", "6.4pF", "7.8pF", "9.2pF", "10.6pF", "12pF", "13.4pF", "14.8pF"}; + const char *grade_s = lookup_str(GRADE_STRS, this->denoise_grade_); + const char *cap_s = lookup_str(CAP_STRS, this->denoise_cap_level_); + ESP_LOGCONFIG(TAG, + " Denoise grade: %s\n" + " Denoise capacitance level: %s", + grade_s, cap_s); + } +#endif +#endif // !USE_ESP32_VARIANT_ESP32 + + if (this->setup_mode_) { + ESP_LOGCONFIG(TAG, " Setup Mode ENABLED"); + } + + for (auto *child : this->children_) { + LOG_BINARY_SENSOR(" ", "Touch Pad", child); + ESP_LOGCONFIG(TAG, + " Channel: %d\n" + " Threshold: %" PRIu32 "\n" + " Benchmark: %" PRIu32, + child->channel_id_, child->threshold_, child->benchmark_); + } +} + +void ESP32TouchComponent::loop() { + const uint32_t now = App.get_loop_component_start_time(); + + // In setup mode, periodically log all pad values + this->process_setup_mode_logging_(now); + + // Process queued touch events from callbacks + TouchEvent event; + while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) { + for (auto *child : this->children_) { + if (child->get_channel_id() != event.chan_id) { + continue; + } + + // Read current smooth value + uint32_t value = 0; + touch_channel_read_data(child->chan_handle_, TOUCH_CHAN_DATA_TYPE_SMOOTH, &value); + child->value_ = value; + +#ifndef USE_ESP32_VARIANT_ESP32 + // V2/V3: also read benchmark + uint32_t benchmark = 0; + touch_channel_read_data(child->chan_handle_, TOUCH_CHAN_DATA_TYPE_BENCHMARK, &benchmark); + child->benchmark_ = benchmark; +#endif + + bool new_state = event.is_active; + + if (new_state != child->last_state_) { + child->initial_state_published_ = true; + child->last_state_ = new_state; + child->publish_state(new_state); +#ifdef USE_ESP32_VARIANT_ESP32 + ESP_LOGV(TAG, "Touch Pad '%s' state: %s (value: %" PRIu32 ", threshold: %" PRIu32 ")", + child->get_name().c_str(), ONOFF(new_state), value, child->get_threshold()); +#else + if (new_state) { + ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 ", benchmark: %" PRIu32 ", threshold: %" PRIu32 ")", + child->get_name().c_str(), value, benchmark, child->get_threshold()); + } else { + ESP_LOGV(TAG, "Touch Pad '%s' state: OFF", child->get_name().c_str()); + } +#endif + } + break; + } + } + + // Publish initial OFF state for sensors that haven't received events yet + for (auto *child : this->children_) { + this->publish_initial_state_if_needed_(child, now); + } + + if (!this->setup_mode_) { + this->disable_loop(); + } +} + +void ESP32TouchComponent::on_shutdown() { + if (this->sens_handle_ == nullptr) + return; + + touch_sensor_stop_continuous_scanning(this->sens_handle_); + touch_sensor_disable(this->sens_handle_); + + for (auto *child : this->children_) { + if (child->chan_handle_ != nullptr) { + touch_sensor_del_channel(child->chan_handle_); + child->chan_handle_ = nullptr; + } + } + + touch_sensor_del_controller(this->sens_handle_); + this->sens_handle_ = nullptr; + + this->cleanup_touch_queue_(); +} + +bool ESP32TouchComponent::create_touch_queue_() { + size_t queue_size = this->children_.size() * 4; + if (queue_size < 8) + queue_size = 8; + + this->touch_queue_ = xQueueCreate(queue_size, sizeof(TouchEvent)); + + if (this->touch_queue_ == nullptr) { + ESP_LOGE(TAG, "Failed to create touch event queue of size %" PRIu32, (uint32_t) queue_size); + this->mark_failed(); + return false; + } + return true; +} + +void ESP32TouchComponent::cleanup_touch_queue_() { + if (this->touch_queue_) { + vQueueDelete(this->touch_queue_); + this->touch_queue_ = nullptr; + } +} + +void ESP32TouchComponent::configure_wakeup_pads_() { +#if SOC_TOUCH_SUPPORT_SLEEP_WAKEUP + bool has_wakeup = false; + for (auto *child : this->children_) { + if (child->get_wakeup_threshold() != 0) { + has_wakeup = true; + break; + } + } + + if (!has_wakeup) + return; + +#ifdef USE_ESP32_VARIANT_ESP32 + // V1: Simple sleep config - threshold is set via channel config's abs_active_thresh + touch_sleep_config_t sleep_cfg = TOUCH_SENSOR_DEFAULT_DSLP_CONFIG(); + sleep_cfg.deep_slp_sens_cfg = nullptr; + esp_err_t err = touch_sensor_config_sleep_wakeup(this->sens_handle_, &sleep_cfg); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to configure touch sleep wakeup: %s", esp_err_to_name(err)); + } +#else + // V2/V3: Need to specify a deep sleep channel and threshold + touch_channel_handle_t wakeup_chan = nullptr; + uint32_t wakeup_thresh = 0; + for (auto *child : this->children_) { + if (child->get_wakeup_threshold() != 0) { + wakeup_chan = child->chan_handle_; + wakeup_thresh = child->get_wakeup_threshold(); + break; // Only one deep sleep wakeup channel is supported + } + } + + if (wakeup_chan != nullptr) { + touch_sleep_config_t sleep_cfg = TOUCH_SENSOR_DEFAULT_DSLP_CONFIG(); + sleep_cfg.deep_slp_chan = wakeup_chan; + sleep_cfg.deep_slp_thresh[0] = wakeup_thresh; + sleep_cfg.deep_slp_sens_cfg = nullptr; + esp_err_t err = touch_sensor_config_sleep_wakeup(this->sens_handle_, &sleep_cfg); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to configure touch sleep wakeup: %s", esp_err_to_name(err)); + } + } +#endif +#endif // SOC_TOUCH_SUPPORT_SLEEP_WAKEUP +} + +void ESP32TouchComponent::process_setup_mode_logging_(uint32_t now) { + if (this->setup_mode_ && now - this->setup_mode_last_log_print_ > SETUP_MODE_LOG_INTERVAL_MS) { + for (auto *child : this->children_) { + if (child->chan_handle_ == nullptr) + continue; + + uint32_t smooth_value = 0; + touch_channel_read_data(child->chan_handle_, TOUCH_CHAN_DATA_TYPE_SMOOTH, &smooth_value); + child->value_ = smooth_value; + +#ifdef USE_ESP32_VARIANT_ESP32 + ESP_LOGD(TAG, "Touch Pad '%s' (Ch%d): %" PRIu32, child->get_name().c_str(), child->channel_id_, smooth_value); +#else + uint32_t benchmark = 0; + touch_channel_read_data(child->chan_handle_, TOUCH_CHAN_DATA_TYPE_BENCHMARK, &benchmark); + child->benchmark_ = benchmark; + int32_t difference = static_cast(smooth_value) - static_cast(benchmark); + ESP_LOGD(TAG, + "Touch Pad '%s' (Ch%d): value=%" PRIu32 ", benchmark=%" PRIu32 ", difference=%" PRId32 + " (set threshold < %" PRId32 " to detect touch)", + child->get_name().c_str(), child->channel_id_, smooth_value, benchmark, difference, difference); +#endif + } + this->setup_mode_last_log_print_ = now; + } +} + +void ESP32TouchComponent::publish_initial_state_if_needed_(ESP32TouchBinarySensor *child, uint32_t now) { + if (!child->initial_state_published_) { + if (now > INITIAL_STATE_DELAY_MS) { + child->publish_initial_state(false); + child->initial_state_published_ = true; + ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (initial)", child->get_name().c_str()); + } + } +} + +} // namespace esphome::esp32_touch + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index 7f45f2ccb4..d51b2d4922 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -4,49 +4,49 @@ #include "esphome/core/component.h" #include "esphome/components/binary_sensor/binary_sensor.h" -#include #include -#include +#include #include #include -namespace esphome { -namespace esp32_touch { +namespace esphome::esp32_touch { // IMPORTANT: Touch detection logic differs between ESP32 variants: -// - ESP32 v1 (original): Touch detected when value < threshold (capacitance increase causes value decrease) -// - ESP32-S2/S3 v2: Touch detected when value > threshold (capacitance increase causes value increase) -// This inversion is due to different hardware implementations between chip generations. +// - ESP32 v1 (original): Touch detected when value < threshold (absolute threshold, capacitance increase causes +// value decrease) +// - ESP32-S2/S3 v2, ESP32-P4 v3: Touch detected when (smooth - benchmark) > threshold (relative threshold) // -// INTERRUPT BEHAVIOR: -// - ESP32 v1: Interrupts fire when ANY pad is touched and continue while touched. -// Releases are detected by timeout since hardware doesn't generate release interrupts. -// - ESP32-S2/S3 v2: Hardware supports both touch and release interrupts, but release -// interrupts are unreliable and sometimes don't fire. We now only use touch interrupts -// and detect releases via timeout, similar to v1. - -static const uint32_t SETUP_MODE_LOG_INTERVAL_MS = 250; +// CALLBACK BEHAVIOR: +// - ESP32 v1: on_active/on_inactive fire from a software filter timer (esp_timer context). +// The software filter MUST be configured for these callbacks to fire. +// - ESP32-S2/S3 v2, ESP32-P4 v3: on_active/on_inactive fire from hardware ISR context. +// Release detection via on_inactive is used, with timeout as safety fallback. class ESP32TouchBinarySensor; -class ESP32TouchComponent : public Component { +class ESP32TouchComponent final : public Component { public: void register_touch_pad(ESP32TouchBinarySensor *pad) { this->children_.push_back(pad); } void set_setup_mode(bool setup_mode) { this->setup_mode_ = setup_mode; } - void set_sleep_duration(uint16_t sleep_duration) { this->sleep_cycle_ = sleep_duration; } - void set_measurement_duration(uint16_t meas_cycle) { this->meas_cycle_ = meas_cycle; } - void set_low_voltage_reference(touch_low_volt_t low_voltage_reference) { + void set_meas_interval_us(float meas_interval_us) { this->meas_interval_us_ = meas_interval_us; } + +#ifdef USE_ESP32_VARIANT_ESP32 + void set_charge_duration_ms(float charge_duration_ms) { this->charge_duration_ms_ = charge_duration_ms; } +#else + void set_charge_times(uint32_t charge_times) { this->charge_times_ = charge_times; } +#endif + +#if !defined(USE_ESP32_VARIANT_ESP32P4) + void set_low_voltage_reference(touch_volt_lim_l_t low_voltage_reference) { this->low_voltage_reference_ = low_voltage_reference; } - void set_high_voltage_reference(touch_high_volt_t high_voltage_reference) { + void set_high_voltage_reference(touch_volt_lim_h_t high_voltage_reference) { this->high_voltage_reference_ = high_voltage_reference; } - void set_voltage_attenuation(touch_volt_atten_t voltage_attenuation) { - this->voltage_attenuation_ = voltage_attenuation; - } +#endif void setup() override; void dump_config() override; @@ -54,183 +54,130 @@ class ESP32TouchComponent : public Component { void on_shutdown() override; -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - void set_filter_mode(touch_filter_mode_t filter_mode) { this->filter_mode_ = filter_mode; } - void set_debounce_count(uint32_t debounce_count) { this->debounce_count_ = debounce_count; } - void set_noise_threshold(uint32_t noise_threshold) { this->noise_threshold_ = noise_threshold; } - void set_jitter_step(uint32_t jitter_step) { this->jitter_step_ = jitter_step; } - void set_smooth_level(touch_smooth_mode_t smooth_level) { this->smooth_level_ = smooth_level; } - void set_denoise_grade(touch_pad_denoise_grade_t denoise_grade) { this->grade_ = denoise_grade; } - void set_denoise_cap(touch_pad_denoise_cap_t cap_level) { this->cap_level_ = cap_level; } - void set_waterproof_guard_ring_pad(touch_pad_t pad) { this->waterproof_guard_ring_pad_ = pad; } - void set_waterproof_shield_driver(touch_pad_shield_driver_t drive_capability) { +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) + void set_filter_mode(touch_benchmark_filter_mode_t filter_mode) { + this->filter_mode_ = filter_mode; + this->filter_configured_ = true; + } + void set_debounce_count(uint32_t debounce_count) { + this->debounce_count_ = debounce_count; + this->filter_configured_ = true; + } + void set_noise_threshold(uint32_t noise_threshold) { + this->noise_threshold_ = noise_threshold; + this->filter_configured_ = true; + } + void set_jitter_step(uint32_t jitter_step) { + this->jitter_step_ = jitter_step; + this->filter_configured_ = true; + } + void set_smooth_level(touch_smooth_filter_mode_t smooth_level) { + this->smooth_level_ = smooth_level; + this->filter_configured_ = true; + } +#if SOC_TOUCH_SUPPORT_DENOISE_CHAN + void set_denoise_grade(touch_denoise_chan_resolution_t denoise_grade) { + this->denoise_grade_ = denoise_grade; + this->denoise_configured_ = true; + } + void set_denoise_cap(touch_denoise_chan_cap_t cap_level) { + this->denoise_cap_level_ = cap_level; + this->denoise_configured_ = true; + } +#endif + void set_waterproof_guard_ring_pad(int channel_id) { + this->waterproof_guard_ring_pad_ = channel_id; + this->waterproof_configured_ = true; + } + void set_waterproof_shield_driver(uint32_t drive_capability) { this->waterproof_shield_driver_ = drive_capability; + this->waterproof_configured_ = true; } #else void set_iir_filter(uint32_t iir_filter) { this->iir_filter_ = iir_filter; } #endif protected: + // Unified touch event for queue communication + struct TouchEvent { + int chan_id; + bool is_active; + }; + // Common helper methods - void dump_config_base_(); - void dump_config_sensors_(); bool create_touch_queue_(); void cleanup_touch_queue_(); void configure_wakeup_pads_(); // Helper methods for loop() logic void process_setup_mode_logging_(uint32_t now); - bool should_check_for_releases_(uint32_t now); void publish_initial_state_if_needed_(ESP32TouchBinarySensor *child, uint32_t now); - void check_and_disable_loop_if_all_released_(size_t pads_off); - void calculate_release_timeout_(); + + // Unified callbacks for new API + static bool on_active_cb(touch_sensor_handle_t handle, const touch_active_event_data_t *event, void *ctx); + static bool on_inactive_cb(touch_sensor_handle_t handle, const touch_inactive_event_data_t *event, void *ctx); // Common members std::vector children_; bool setup_mode_{false}; uint32_t setup_mode_last_log_print_{0}; - uint32_t last_release_check_{0}; - uint32_t release_timeout_ms_{1500}; - uint32_t release_check_interval_ms_{50}; + + // Controller handle (new API) + touch_sensor_handle_t sens_handle_{nullptr}; + QueueHandle_t touch_queue_{nullptr}; // Common configuration parameters - uint16_t sleep_cycle_{4095}; - uint16_t meas_cycle_{65535}; - touch_low_volt_t low_voltage_reference_{TOUCH_LVOLT_0V5}; - touch_high_volt_t high_voltage_reference_{TOUCH_HVOLT_2V7}; - touch_volt_atten_t voltage_attenuation_{TOUCH_HVOLT_ATTEN_0V}; + float meas_interval_us_{320.0f}; - // Common constants - static constexpr uint32_t MINIMUM_RELEASE_TIME_MS = 100; +#ifdef USE_ESP32_VARIANT_ESP32 + float charge_duration_ms_{1.0f}; +#else + uint32_t charge_times_{500}; +#endif - // ==================== PLATFORM SPECIFIC ==================== +#if !defined(USE_ESP32_VARIANT_ESP32P4) + touch_volt_lim_l_t low_voltage_reference_{TOUCH_VOLT_LIM_L_0V5}; + touch_volt_lim_h_t high_voltage_reference_{TOUCH_VOLT_LIM_H_2V7}; +#endif #ifdef USE_ESP32_VARIANT_ESP32 // ESP32 v1 specific - - static void touch_isr_handler(void *arg); - QueueHandle_t touch_queue_{nullptr}; - - private: - // Touch event structure for ESP32 v1 - // Contains touch pad info, value, and touch state for queue communication - struct TouchPadEventV1 { - touch_pad_t pad; - uint32_t value; - bool is_touched; - }; - - protected: uint32_t iir_filter_{0}; bool iir_filter_enabled_() const { return this->iir_filter_ > 0; } -#elif defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - // ESP32-S2/S3 v2 specific - static void touch_isr_handler(void *arg); - QueueHandle_t touch_queue_{nullptr}; +#elif defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) + // ESP32-S2/S3/P4 v2/v3 specific - private: - // Touch event structure for ESP32 v2 (S2/S3) - // Contains touch pad and interrupt mask for queue communication - struct TouchPadEventV2 { - touch_pad_t pad; - uint32_t intr_mask; - }; - - protected: - // Filter configuration - touch_filter_mode_t filter_mode_{TOUCH_PAD_FILTER_MAX}; + // Filter configuration - use sentinel values to detect "not configured" + touch_benchmark_filter_mode_t filter_mode_{TOUCH_BM_JITTER_FILTER}; uint32_t debounce_count_{0}; uint32_t noise_threshold_{0}; uint32_t jitter_step_{0}; - touch_smooth_mode_t smooth_level_{TOUCH_PAD_SMOOTH_MAX}; + touch_smooth_filter_mode_t smooth_level_{TOUCH_SMOOTH_NO_FILTER}; + bool filter_configured_{false}; +#if SOC_TOUCH_SUPPORT_DENOISE_CHAN // Denoise configuration - touch_pad_denoise_grade_t grade_{TOUCH_PAD_DENOISE_MAX}; - touch_pad_denoise_cap_t cap_level_{TOUCH_PAD_DENOISE_CAP_MAX}; - - // Waterproof configuration - touch_pad_t waterproof_guard_ring_pad_{TOUCH_PAD_MAX}; - touch_pad_shield_driver_t waterproof_shield_driver_{TOUCH_PAD_SHIELD_DRV_MAX}; - - bool filter_configured_() const { - return (this->filter_mode_ != TOUCH_PAD_FILTER_MAX) && (this->smooth_level_ != TOUCH_PAD_SMOOTH_MAX); - } - bool denoise_configured_() const { - return (this->grade_ != TOUCH_PAD_DENOISE_MAX) && (this->cap_level_ != TOUCH_PAD_DENOISE_CAP_MAX); - } - bool waterproof_configured_() const { - return (this->waterproof_guard_ring_pad_ != TOUCH_PAD_MAX) && - (this->waterproof_shield_driver_ != TOUCH_PAD_SHIELD_DRV_MAX); - } - - // Helper method to read touch values - non-blocking operation - // Returns the current touch pad value using either filtered or raw reading - // based on the filter configuration - uint32_t read_touch_value(touch_pad_t pad) const; - - // Helper to update touch state with a known state and value - void update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched, uint32_t value); - - // Helper to read touch value and update state for a given child - bool check_and_update_touch_state_(ESP32TouchBinarySensor *child); + touch_denoise_chan_resolution_t denoise_grade_{TOUCH_DENOISE_CHAN_RESOLUTION_BIT12}; + touch_denoise_chan_cap_t denoise_cap_level_{TOUCH_DENOISE_CHAN_CAP_5PF}; + bool denoise_configured_{false}; #endif - // Helper functions for dump_config - common to both implementations - static const char *get_low_voltage_reference_str(touch_low_volt_t ref) { - switch (ref) { - case TOUCH_LVOLT_0V5: - return "0.5V"; - case TOUCH_LVOLT_0V6: - return "0.6V"; - case TOUCH_LVOLT_0V7: - return "0.7V"; - case TOUCH_LVOLT_0V8: - return "0.8V"; - default: - return "UNKNOWN"; - } - } - - static const char *get_high_voltage_reference_str(touch_high_volt_t ref) { - switch (ref) { - case TOUCH_HVOLT_2V4: - return "2.4V"; - case TOUCH_HVOLT_2V5: - return "2.5V"; - case TOUCH_HVOLT_2V6: - return "2.6V"; - case TOUCH_HVOLT_2V7: - return "2.7V"; - default: - return "UNKNOWN"; - } - } - - static const char *get_voltage_attenuation_str(touch_volt_atten_t atten) { - switch (atten) { - case TOUCH_HVOLT_ATTEN_1V5: - return "1.5V"; - case TOUCH_HVOLT_ATTEN_1V: - return "1V"; - case TOUCH_HVOLT_ATTEN_0V5: - return "0.5V"; - case TOUCH_HVOLT_ATTEN_0V: - return "0V"; - default: - return "UNKNOWN"; - } - } + // Waterproof configuration + int waterproof_guard_ring_pad_{-1}; + uint32_t waterproof_shield_driver_{0}; + bool waterproof_configured_{false}; +#endif }; /// Simple helper class to expose a touch pad value as a binary sensor. class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { public: - ESP32TouchBinarySensor(touch_pad_t touch_pad, uint32_t threshold, uint32_t wakeup_threshold) - : touch_pad_(touch_pad), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {} + ESP32TouchBinarySensor(int channel_id, uint32_t threshold, uint32_t wakeup_threshold) + : channel_id_(channel_id), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {} - touch_pad_t get_touch_pad() const { return this->touch_pad_; } + int get_channel_id() const { return this->channel_id_; } uint32_t get_threshold() const { return this->threshold_; } void set_threshold(uint32_t threshold) { this->threshold_ = threshold; } @@ -242,39 +189,22 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { uint32_t get_wakeup_threshold() const { return this->wakeup_threshold_; } -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - /// Ensure benchmark value is read (v2 touch hardware only). - /// Called from multiple places - kept as helper to document shared usage. - void ensure_benchmark_read() { - if (this->benchmark_ == 0) { - touch_pad_read_benchmark(this->touch_pad_, &this->benchmark_); - } - } -#endif - protected: friend ESP32TouchComponent; - touch_pad_t touch_pad_{TOUCH_PAD_MAX}; + int channel_id_; + touch_channel_handle_t chan_handle_{nullptr}; uint32_t threshold_{0}; - uint32_t benchmark_{}; + uint32_t benchmark_{0}; /// Stores the last raw touch measurement value. uint32_t value_{0}; bool last_state_{false}; const uint32_t wakeup_threshold_{0}; // Track last touch time for timeout-based release detection - // Design note: last_touch_time_ does not require synchronization primitives because: - // 1. ESP32 guarantees atomic 32-bit aligned reads/writes - // 2. ISR only writes timestamps, main loop only reads - // 3. Timing tolerance allows for occasional stale reads (50ms check interval) - // 4. Queue operations provide implicit memory barriers - // Using atomic/critical sections would add overhead without meaningful benefit - uint32_t last_touch_time_{}; - bool initial_state_published_{}; + bool initial_state_published_{false}; }; -} // namespace esp32_touch -} // namespace esphome +} // namespace esphome::esp32_touch #endif diff --git a/esphome/components/esp32_touch/esp32_touch_common.cpp b/esphome/components/esp32_touch/esp32_touch_common.cpp deleted file mode 100644 index 429b5173be..0000000000 --- a/esphome/components/esp32_touch/esp32_touch_common.cpp +++ /dev/null @@ -1,173 +0,0 @@ -#ifdef USE_ESP32 - -#include "esp32_touch.h" -#include "esphome/core/log.h" -#include - -#include "soc/rtc.h" - -namespace esphome { -namespace esp32_touch { - -static const char *const TAG = "esp32_touch"; - -void ESP32TouchComponent::dump_config_base_() { - const char *lv_s = get_low_voltage_reference_str(this->low_voltage_reference_); - const char *hv_s = get_high_voltage_reference_str(this->high_voltage_reference_); - const char *atten_s = get_voltage_attenuation_str(this->voltage_attenuation_); - - ESP_LOGCONFIG(TAG, - "Config for ESP32 Touch Hub:\n" - " Meas cycle: %.2fms\n" - " Sleep cycle: %.2fms\n" - " Low Voltage Reference: %s\n" - " High Voltage Reference: %s\n" - " Voltage Attenuation: %s\n" - " Release Timeout: %" PRIu32 "ms\n", - this->meas_cycle_ / (8000000.0f / 1000.0f), this->sleep_cycle_ / (150000.0f / 1000.0f), lv_s, hv_s, - atten_s, this->release_timeout_ms_); -} - -void ESP32TouchComponent::dump_config_sensors_() { - for (auto *child : this->children_) { - LOG_BINARY_SENSOR(" ", "Touch Pad", child); - ESP_LOGCONFIG(TAG, - " Pad: T%u\n" - " Threshold: %" PRIu32 "\n" - " Benchmark: %" PRIu32, - (unsigned) child->touch_pad_, child->threshold_, child->benchmark_); - } -} - -bool ESP32TouchComponent::create_touch_queue_() { - // Queue size calculation: children * 4 allows for burst scenarios where ISR - // fires multiple times before main loop processes. - size_t queue_size = this->children_.size() * 4; - if (queue_size < 8) - queue_size = 8; - -#ifdef USE_ESP32_VARIANT_ESP32 - this->touch_queue_ = xQueueCreate(queue_size, sizeof(TouchPadEventV1)); -#else - this->touch_queue_ = xQueueCreate(queue_size, sizeof(TouchPadEventV2)); -#endif - - if (this->touch_queue_ == nullptr) { - ESP_LOGE(TAG, "Failed to create touch event queue of size %" PRIu32, (uint32_t) queue_size); - this->mark_failed(); - return false; - } - return true; -} - -void ESP32TouchComponent::cleanup_touch_queue_() { - if (this->touch_queue_) { - vQueueDelete(this->touch_queue_); - this->touch_queue_ = nullptr; - } -} - -void ESP32TouchComponent::configure_wakeup_pads_() { - bool is_wakeup_source = false; - - // Check if any pad is configured for wakeup - for (auto *child : this->children_) { - if (child->get_wakeup_threshold() != 0) { - is_wakeup_source = true; - -#ifdef USE_ESP32_VARIANT_ESP32 - // ESP32 v1: No filter available when using as wake-up source. - touch_pad_config(child->get_touch_pad(), child->get_wakeup_threshold()); -#else - // ESP32-S2/S3 v2: Set threshold for wakeup - touch_pad_set_thresh(child->get_touch_pad(), child->get_wakeup_threshold()); -#endif - } - } - - if (!is_wakeup_source) { - // If no pad is configured for wakeup, deinitialize touch pad - touch_pad_deinit(); - } -} - -void ESP32TouchComponent::process_setup_mode_logging_(uint32_t now) { - if (this->setup_mode_ && now - this->setup_mode_last_log_print_ > SETUP_MODE_LOG_INTERVAL_MS) { - for (auto *child : this->children_) { -#ifdef USE_ESP32_VARIANT_ESP32 - ESP_LOGD(TAG, "Touch Pad '%s' (T%" PRIu32 "): %" PRIu32, child->get_name().c_str(), - (uint32_t) child->get_touch_pad(), child->value_); -#else - // Read the value being used for touch detection - uint32_t value = this->read_touch_value(child->get_touch_pad()); - // Store the value for get_value() access in lambdas - child->value_ = value; - // Read benchmark if not already read - child->ensure_benchmark_read(); - // Calculate difference to help user set threshold - // For ESP32-S2/S3 v2: touch detected when value > benchmark + threshold - // So threshold should be < (value - benchmark) when touched - int32_t difference = static_cast(value) - static_cast(child->benchmark_); - ESP_LOGD(TAG, - "Touch Pad '%s' (T%d): value=%d, benchmark=%" PRIu32 ", difference=%" PRId32 " (set threshold < %" PRId32 - " to detect touch)", - child->get_name().c_str(), child->get_touch_pad(), value, child->benchmark_, difference, difference); -#endif - } - this->setup_mode_last_log_print_ = now; - } -} - -bool ESP32TouchComponent::should_check_for_releases_(uint32_t now) { - if (now - this->last_release_check_ < this->release_check_interval_ms_) { - return false; - } - this->last_release_check_ = now; - return true; -} - -void ESP32TouchComponent::publish_initial_state_if_needed_(ESP32TouchBinarySensor *child, uint32_t now) { - if (!child->initial_state_published_) { - // Check if enough time has passed since startup - if (now > this->release_timeout_ms_) { - child->publish_initial_state(false); - child->initial_state_published_ = true; - ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (initial)", child->get_name().c_str()); - } - } -} - -void ESP32TouchComponent::check_and_disable_loop_if_all_released_(size_t pads_off) { - // Disable the loop to save CPU cycles when all pads are off and not in setup mode. - if (pads_off == this->children_.size() && !this->setup_mode_) { - this->disable_loop(); - } -} - -void ESP32TouchComponent::calculate_release_timeout_() { - // Calculate release timeout based on sleep cycle - // Design note: Hardware limitation - interrupts only fire reliably on touch (not release) - // We must use timeout-based detection for release events - // Formula: 3 sleep cycles converted to ms, with MINIMUM_RELEASE_TIME_MS minimum - // Per ESP-IDF docs: t_sleep = sleep_cycle / SOC_CLK_RC_SLOW_FREQ_APPROX - - uint32_t rtc_freq = rtc_clk_slow_freq_get_hz(); - - // Calculate timeout as 3 sleep cycles - this->release_timeout_ms_ = (this->sleep_cycle_ * 1000 * 3) / rtc_freq; - - if (this->release_timeout_ms_ < MINIMUM_RELEASE_TIME_MS) { - this->release_timeout_ms_ = MINIMUM_RELEASE_TIME_MS; - } - - // Check for releases at 1/4 the timeout interval - // Since hardware doesn't generate reliable release interrupts, we must poll - // for releases in the main loop. Checking at 1/4 the timeout interval provides - // a good balance between responsiveness and efficiency. - this->release_check_interval_ms_ = this->release_timeout_ms_ / 4; -} - -} // namespace esp32_touch -} // namespace esphome - -#endif // USE_ESP32 diff --git a/esphome/components/esp32_touch/esp32_touch_v1.cpp b/esphome/components/esp32_touch/esp32_touch_v1.cpp deleted file mode 100644 index ffb805e008..0000000000 --- a/esphome/components/esp32_touch/esp32_touch_v1.cpp +++ /dev/null @@ -1,244 +0,0 @@ -#ifdef USE_ESP32_VARIANT_ESP32 - -#include "esp32_touch.h" -#include "esphome/core/application.h" -#include "esphome/core/log.h" -#include "esphome/core/hal.h" - -#include -#include - -// Include HAL for ISR-safe touch reading -#include "hal/touch_sensor_ll.h" - -namespace esphome { -namespace esp32_touch { - -static const char *const TAG = "esp32_touch"; - -static const uint32_t SETUP_MODE_THRESHOLD = 0xFFFF; - -void ESP32TouchComponent::setup() { - // Create queue for touch events - // Queue size calculation: children * 4 allows for burst scenarios where ISR - // fires multiple times before main loop processes. This is important because - // ESP32 v1 scans all pads on each interrupt, potentially sending multiple events. - if (!this->create_touch_queue_()) { - return; - } - - touch_pad_init(); - touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); - - // Set up IIR filter if enabled - if (this->iir_filter_enabled_()) { - touch_pad_filter_start(this->iir_filter_); - } - - // Configure measurement parameters -#if ESP_IDF_VERSION_MAJOR >= 5 - touch_pad_set_measurement_clock_cycles(this->meas_cycle_); - touch_pad_set_measurement_interval(this->sleep_cycle_); -#else - touch_pad_set_meas_time(this->sleep_cycle_, this->meas_cycle_); -#endif - touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_); - - // Configure each touch pad - for (auto *child : this->children_) { - if (this->setup_mode_) { - touch_pad_config(child->get_touch_pad(), SETUP_MODE_THRESHOLD); - } else { - touch_pad_config(child->get_touch_pad(), child->get_threshold()); - } - } - - // Register ISR handler - esp_err_t err = touch_pad_isr_register(touch_isr_handler, this); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to register touch ISR: %s", esp_err_to_name(err)); - this->cleanup_touch_queue_(); - this->mark_failed(); - return; - } - - // Calculate release timeout based on sleep cycle - this->calculate_release_timeout_(); - - // Enable touch pad interrupt - touch_pad_intr_enable(); -} - -void ESP32TouchComponent::dump_config() { - this->dump_config_base_(); - - if (this->iir_filter_enabled_()) { - ESP_LOGCONFIG(TAG, " IIR Filter: %" PRIu32 "ms", this->iir_filter_); - } else { - ESP_LOGCONFIG(TAG, " IIR Filter DISABLED"); - } - - if (this->setup_mode_) { - ESP_LOGCONFIG(TAG, " Setup Mode ENABLED"); - } - - this->dump_config_sensors_(); -} - -void ESP32TouchComponent::loop() { - const uint32_t now = App.get_loop_component_start_time(); - - // Print debug info for all pads in setup mode - this->process_setup_mode_logging_(now); - - // Process any queued touch events from interrupts - // Note: Events are only sent by ISR for pads that were measured in that cycle (value != 0) - // This is more efficient than sending all pad states every interrupt - TouchPadEventV1 event; - while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) { - // Find the corresponding sensor - O(n) search is acceptable since events are infrequent - for (auto *child : this->children_) { - if (child->get_touch_pad() != event.pad) { - continue; - } - - // Found matching pad - process it - child->value_ = event.value; - - // The interrupt gives us the touch state directly - bool new_state = event.is_touched; - - // Track when we last saw this pad as touched - if (new_state) { - child->last_touch_time_ = now; - } - - // Only publish if state changed - this filters out repeated events - if (new_state != child->last_state_) { - child->initial_state_published_ = true; - child->last_state_ = new_state; - child->publish_state(new_state); - // Original ESP32: ISR only fires when touched, release is detected by timeout - // Note: ESP32 v1 uses inverted logic - touched when value < threshold - ESP_LOGV(TAG, "Touch Pad '%s' state: %s (value: %" PRIu32 " < threshold: %" PRIu32 ")", - child->get_name().c_str(), ONOFF(new_state), event.value, child->get_threshold()); - } - break; // Exit inner loop after processing matching pad - } - } - - // Check for released pads periodically - if (!this->should_check_for_releases_(now)) { - return; - } - - size_t pads_off = 0; - for (auto *child : this->children_) { - // Handle initial state publication after startup - this->publish_initial_state_if_needed_(child, now); - - if (child->last_state_) { - // Pad is currently in touched state - check for release timeout - // Using subtraction handles 32-bit rollover correctly - uint32_t time_diff = now - child->last_touch_time_; - - // Check if we haven't seen this pad recently - if (time_diff > this->release_timeout_ms_) { - // Haven't seen this pad recently, assume it's released - child->last_state_ = false; - child->publish_state(false); - ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (timeout)", child->get_name().c_str()); - pads_off++; - } - } else { - // Pad is already off - pads_off++; - } - } - - // Disable the loop to save CPU cycles when all pads are off and not in setup mode. - // The loop will be re-enabled by the ISR when any touch pad is touched. - // v1 hardware limitations require us to check all pads are off because: - // - v1 only generates interrupts on touch events (not releases) - // - We must poll for release timeouts in the main loop - // - We can only safely disable when no pads need timeout monitoring - this->check_and_disable_loop_if_all_released_(pads_off); -} - -void ESP32TouchComponent::on_shutdown() { - touch_pad_intr_disable(); - touch_pad_isr_deregister(touch_isr_handler, this); - this->cleanup_touch_queue_(); - - if (this->iir_filter_enabled_()) { - touch_pad_filter_stop(); - touch_pad_filter_delete(); - } - - // Configure wakeup pads if any are set - this->configure_wakeup_pads_(); -} - -void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { - ESP32TouchComponent *component = static_cast(arg); - - uint32_t mask = 0; - touch_ll_read_trigger_status_mask(&mask); - touch_ll_clear_trigger_status_mask(); - touch_pad_clear_status(); - - // INTERRUPT BEHAVIOR: On ESP32 v1 hardware, the interrupt fires when ANY configured - // touch pad detects a touch (value goes below threshold). The hardware does NOT - // generate interrupts on release - only on touch events. - // The interrupt will continue to fire periodically (based on sleep_cycle) as long - // as any pad remains touched. This allows us to detect both new touches and - // continued touches, but releases must be detected by timeout in the main loop. - - // Process all configured pads to check their current state - // Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt, - // so we must scan all configured pads to find which ones were touched - for (auto *child : component->children_) { - touch_pad_t pad = child->get_touch_pad(); - - // Read current value using ISR-safe API - // IMPORTANT: ESP-IDF v5.4 regression - touch_pad_read_filtered() is no longer ISR-safe - // In ESP-IDF v5.3 and earlier it was ISR-safe, but ESP-IDF v5.4 added mutex protection that causes: - // "assert failed: xQueueSemaphoreTake queue.c:1718" - // We must use raw values even when filter is enabled as a workaround. - // Users should adjust thresholds to compensate for the lack of IIR filtering. - // See: https://github.com/espressif/esp-idf/issues/17045 - uint32_t value = touch_ll_read_raw_data(pad); - - // Skip pads that aren’t in the trigger mask - if (((mask >> pad) & 1) == 0) { - continue; - } - - // IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2! - // ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE - // Therefore: touched = (value < threshold) - // This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold) - bool is_touched = value < child->get_threshold(); - - // Always send the current state - the main loop will filter for changes - // We send both touched and untouched states because the ISR doesn't - // track previous state (to keep ISR fast and simple) - TouchPadEventV1 event; - event.pad = pad; - event.value = value; - event.is_touched = is_touched; - - // Send to queue from ISR - non-blocking, drops if queue full - BaseType_t x_higher_priority_task_woken = pdFALSE; - xQueueSendFromISR(component->touch_queue_, &event, &x_higher_priority_task_woken); - component->enable_loop_soon_any_context(); - if (x_higher_priority_task_woken) { - portYIELD_FROM_ISR(); - } - } -} - -} // namespace esp32_touch -} // namespace esphome - -#endif // USE_ESP32_VARIANT_ESP32 diff --git a/esphome/components/esp32_touch/esp32_touch_v2.cpp b/esphome/components/esp32_touch/esp32_touch_v2.cpp deleted file mode 100644 index b34ca1abd3..0000000000 --- a/esphome/components/esp32_touch/esp32_touch_v2.cpp +++ /dev/null @@ -1,402 +0,0 @@ -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - -#include "esp32_touch.h" -#include "esphome/core/application.h" -#include "esphome/core/log.h" -#include "esphome/core/hal.h" - -namespace esphome { -namespace esp32_touch { - -static const char *const TAG = "esp32_touch"; - -// Helper to update touch state with a known state and value -void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched, uint32_t value) { - // Store the value for get_value() access in lambdas - child->value_ = value; - - // Always update timer when touched - if (is_touched) { - child->last_touch_time_ = App.get_loop_component_start_time(); - } - - if (child->last_state_ != is_touched) { - child->last_state_ = is_touched; - child->publish_state(is_touched); - if (is_touched) { - ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " > threshold: %" PRIu32 ")", child->get_name().c_str(), - value, child->threshold_ + child->benchmark_); - } else { - ESP_LOGV(TAG, "Touch Pad '%s' state: OFF", child->get_name().c_str()); - } - } -} - -// Helper to read touch value and update state for a given child (used for timeout events) -bool ESP32TouchComponent::check_and_update_touch_state_(ESP32TouchBinarySensor *child) { - // Read current touch value - uint32_t value = this->read_touch_value(child->touch_pad_); - - // ESP32-S2/S3 v2: Touch is detected when value > threshold + benchmark - ESP_LOGV(TAG, - "Checking touch state for '%s' (T%d): value = %" PRIu32 ", threshold = %" PRIu32 ", benchmark = %" PRIu32, - child->get_name().c_str(), child->touch_pad_, value, child->threshold_, child->benchmark_); - bool is_touched = value > child->benchmark_ + child->threshold_; - - this->update_touch_state_(child, is_touched, value); - return is_touched; -} - -void ESP32TouchComponent::setup() { - // Create queue for touch events first - if (!this->create_touch_queue_()) { - return; - } - - // Initialize touch pad peripheral - esp_err_t init_err = touch_pad_init(); - if (init_err != ESP_OK) { - ESP_LOGE(TAG, "Failed to initialize touch pad: %s", esp_err_to_name(init_err)); - this->mark_failed(); - return; - } - - // Configure each touch pad first - for (auto *child : this->children_) { - esp_err_t config_err = touch_pad_config(child->touch_pad_); - if (config_err != ESP_OK) { - ESP_LOGE(TAG, "Failed to configure touch pad %d: %s", child->touch_pad_, esp_err_to_name(config_err)); - } - } - - // Set up filtering if configured - if (this->filter_configured_()) { - touch_filter_config_t filter_info = { - .mode = this->filter_mode_, - .debounce_cnt = this->debounce_count_, - .noise_thr = this->noise_threshold_, - .jitter_step = this->jitter_step_, - .smh_lvl = this->smooth_level_, - }; - touch_pad_filter_set_config(&filter_info); - touch_pad_filter_enable(); - } - - if (this->denoise_configured_()) { - touch_pad_denoise_t denoise = { - .grade = this->grade_, - .cap_level = this->cap_level_, - }; - touch_pad_denoise_set_config(&denoise); - touch_pad_denoise_enable(); - } - - if (this->waterproof_configured_()) { - touch_pad_waterproof_t waterproof = { - .guard_ring_pad = this->waterproof_guard_ring_pad_, - .shield_driver = this->waterproof_shield_driver_, - }; - touch_pad_waterproof_set_config(&waterproof); - touch_pad_waterproof_enable(); - } - - // Configure measurement parameters - touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_); - touch_pad_set_charge_discharge_times(this->meas_cycle_); - touch_pad_set_measurement_interval(this->sleep_cycle_); - - // Disable hardware timeout - it causes continuous interrupts with high-capacitance - // setups (e.g., pressure sensors under cushions). The periodic release check in - // loop() handles state detection reliably without needing hardware timeout. - touch_pad_timeout_set(false, TOUCH_PAD_THRESHOLD_MAX); - - // Register ISR handler with interrupt mask - esp_err_t err = - touch_pad_isr_register(touch_isr_handler, this, static_cast(TOUCH_PAD_INTR_MASK_ALL)); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to register touch ISR: %s", esp_err_to_name(err)); - this->cleanup_touch_queue_(); - this->mark_failed(); - return; - } - - // Set thresholds for each pad BEFORE starting FSM - for (auto *child : this->children_) { - if (child->threshold_ != 0) { - touch_pad_set_thresh(child->touch_pad_, child->threshold_); - } - } - - // Enable interrupts - only ACTIVE and TIMEOUT - // NOTE: We intentionally don't enable INACTIVE interrupts because they are unreliable - // on ESP32-S2/S3 hardware and sometimes don't fire. Instead, we use timeout-based - // release detection with the ability to verify the actual state. - touch_pad_intr_enable(static_cast(TOUCH_PAD_INTR_MASK_ACTIVE | TOUCH_PAD_INTR_MASK_TIMEOUT)); - - // Set FSM mode before starting - touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); - - // Start FSM - touch_pad_fsm_start(); - - // Calculate release timeout based on sleep cycle - this->calculate_release_timeout_(); -} - -void ESP32TouchComponent::dump_config() { - this->dump_config_base_(); - - if (this->filter_configured_()) { - const char *filter_mode_s; - switch (this->filter_mode_) { - case TOUCH_PAD_FILTER_IIR_4: - filter_mode_s = "IIR_4"; - break; - case TOUCH_PAD_FILTER_IIR_8: - filter_mode_s = "IIR_8"; - break; - case TOUCH_PAD_FILTER_IIR_16: - filter_mode_s = "IIR_16"; - break; - case TOUCH_PAD_FILTER_IIR_32: - filter_mode_s = "IIR_32"; - break; - case TOUCH_PAD_FILTER_IIR_64: - filter_mode_s = "IIR_64"; - break; - case TOUCH_PAD_FILTER_IIR_128: - filter_mode_s = "IIR_128"; - break; - case TOUCH_PAD_FILTER_IIR_256: - filter_mode_s = "IIR_256"; - break; - case TOUCH_PAD_FILTER_JITTER: - filter_mode_s = "JITTER"; - break; - default: - filter_mode_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, - " Filter mode: %s\n" - " Debounce count: %" PRIu32 "\n" - " Noise threshold coefficient: %" PRIu32 "\n" - " Jitter filter step size: %" PRIu32, - filter_mode_s, this->debounce_count_, this->noise_threshold_, this->jitter_step_); - const char *smooth_level_s; - switch (this->smooth_level_) { - case TOUCH_PAD_SMOOTH_OFF: - smooth_level_s = "OFF"; - break; - case TOUCH_PAD_SMOOTH_IIR_2: - smooth_level_s = "IIR_2"; - break; - case TOUCH_PAD_SMOOTH_IIR_4: - smooth_level_s = "IIR_4"; - break; - case TOUCH_PAD_SMOOTH_IIR_8: - smooth_level_s = "IIR_8"; - break; - default: - smooth_level_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Smooth level: %s", smooth_level_s); - } - - if (this->denoise_configured_()) { - const char *grade_s; - switch (this->grade_) { - case TOUCH_PAD_DENOISE_BIT12: - grade_s = "BIT12"; - break; - case TOUCH_PAD_DENOISE_BIT10: - grade_s = "BIT10"; - break; - case TOUCH_PAD_DENOISE_BIT8: - grade_s = "BIT8"; - break; - case TOUCH_PAD_DENOISE_BIT4: - grade_s = "BIT4"; - break; - default: - grade_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Denoise grade: %s", grade_s); - - const char *cap_level_s; - switch (this->cap_level_) { - case TOUCH_PAD_DENOISE_CAP_L0: - cap_level_s = "L0"; - break; - case TOUCH_PAD_DENOISE_CAP_L1: - cap_level_s = "L1"; - break; - case TOUCH_PAD_DENOISE_CAP_L2: - cap_level_s = "L2"; - break; - case TOUCH_PAD_DENOISE_CAP_L3: - cap_level_s = "L3"; - break; - case TOUCH_PAD_DENOISE_CAP_L4: - cap_level_s = "L4"; - break; - case TOUCH_PAD_DENOISE_CAP_L5: - cap_level_s = "L5"; - break; - case TOUCH_PAD_DENOISE_CAP_L6: - cap_level_s = "L6"; - break; - case TOUCH_PAD_DENOISE_CAP_L7: - cap_level_s = "L7"; - break; - default: - cap_level_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Denoise capacitance level: %s", cap_level_s); - } - - if (this->setup_mode_) { - ESP_LOGCONFIG(TAG, " Setup Mode ENABLED"); - } - - this->dump_config_sensors_(); -} - -void ESP32TouchComponent::loop() { - const uint32_t now = App.get_loop_component_start_time(); - - // V2 TOUCH HANDLING: - // Due to unreliable INACTIVE interrupts on ESP32-S2/S3, we use a hybrid approach: - // 1. Process ACTIVE interrupts when pads are touched - // 2. Use timeout-based release detection (like v1) - // 3. But smarter than v1: verify actual state before releasing on timeout - // This prevents false releases if we missed interrupts - - // In setup mode, periodically log all pad values - this->process_setup_mode_logging_(now); - - // Process any queued touch events from interrupts - TouchPadEventV2 event; - while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) { - ESP_LOGD(TAG, "Event received, mask = 0x%" PRIx32 ", pad = %d", event.intr_mask, event.pad); - // Handle timeout events - if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) { - // Resume measurement after timeout - touch_pad_timeout_resume(); - // For timeout events, always check the current state - } else if (!(event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE)) { - // Skip if not an active/timeout event - continue; - } - - // Find the child for the pad that triggered the interrupt - for (auto *child : this->children_) { - if (child->touch_pad_ == event.pad) { - if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) { - // For timeout events, we need to read the value to determine state - this->check_and_update_touch_state_(child); - } else if (event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE) { - // We only get ACTIVE interrupts now, releases are detected by timeout - // Read the current value - uint32_t value = this->read_touch_value(child->touch_pad_); - this->update_touch_state_(child, true, value); // Always touched for ACTIVE interrupts - } - break; - } - } - } - - // Check for released pads periodically (like v1) - if (!this->should_check_for_releases_(now)) { - return; - } - - size_t pads_off = 0; - for (auto *child : this->children_) { - child->ensure_benchmark_read(); - // Handle initial state publication after startup - this->publish_initial_state_if_needed_(child, now); - - if (child->last_state_) { - // Pad is currently in touched state - check for release timeout - // Using subtraction handles 32-bit rollover correctly - uint32_t time_diff = now - child->last_touch_time_; - - // Check if we haven't seen this pad recently - if (time_diff > this->release_timeout_ms_) { - // Haven't seen this pad recently - verify actual state - // Unlike v1, v2 hardware allows us to read the current state anytime - // This makes v2 smarter: we can verify if it's actually released before - // declaring a timeout, preventing false releases if interrupts were missed - bool still_touched = this->check_and_update_touch_state_(child); - - if (still_touched) { - // Still touched! Timer was reset in update_touch_state_ - ESP_LOGVV(TAG, "Touch Pad '%s' still touched after %" PRIu32 "ms timeout, resetting timer", - child->get_name().c_str(), this->release_timeout_ms_); - } else { - // Actually released - already handled by check_and_update_touch_state_ - pads_off++; - } - } - } else { - // Pad is already off - pads_off++; - } - } - - // Disable the loop when all pads are off and not in setup mode (like v1) - // We need to keep checking for timeouts, so only disable when all pads are confirmed off - this->check_and_disable_loop_if_all_released_(pads_off); -} - -void ESP32TouchComponent::on_shutdown() { - // Disable interrupts - touch_pad_intr_disable(TOUCH_PAD_INTR_MASK_ACTIVE); - touch_pad_isr_deregister(touch_isr_handler, this); - this->cleanup_touch_queue_(); - - // Configure wakeup pads if any are set - this->configure_wakeup_pads_(); -} - -void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { - ESP32TouchComponent *component = static_cast(arg); - BaseType_t x_higher_priority_task_woken = pdFALSE; - - // Read interrupt status - TouchPadEventV2 event; - event.intr_mask = touch_pad_read_intr_status_mask(); - event.pad = touch_pad_get_current_meas_channel(); - - // Send event to queue for processing in main loop - xQueueSendFromISR(component->touch_queue_, &event, &x_higher_priority_task_woken); - component->enable_loop_soon_any_context(); - - if (x_higher_priority_task_woken) { - portYIELD_FROM_ISR(); - } -} - -uint32_t ESP32TouchComponent::read_touch_value(touch_pad_t pad) const { - // Unlike ESP32 v1, touch reads on ESP32-S2/S3 v2 are non-blocking operations. - // The hardware continuously samples in the background and we can read the - // latest value at any time without waiting. - uint32_t value = 0; - if (this->filter_configured_()) { - // Read filtered/smoothed value when filter is enabled - touch_pad_filter_read_smooth(pad, &value); - } else { - // Read raw value when filter is not configured - touch_pad_read_raw_data(pad, &value); - } - return value; -} - -} // namespace esp32_touch -} // namespace esphome - -#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/tests/components/esp32_touch/common-variants.yaml b/tests/components/esp32_touch/common-variants.yaml index 69a3dbd969..5d6d0bbd19 100644 --- a/tests/components/esp32_touch/common-variants.yaml +++ b/tests/components/esp32_touch/common-variants.yaml @@ -4,7 +4,6 @@ esp32_touch: measurement_duration: 8ms low_voltage_reference: 0.5V high_voltage_reference: 2.7V - voltage_attenuation: 1.5V binary_sensor: - platform: esp32_touch diff --git a/tests/components/esp32_touch/test.esp32-p4-idf.yaml b/tests/components/esp32_touch/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..ab5484af44 --- /dev/null +++ b/tests/components/esp32_touch/test.esp32-p4-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + pin: GPIO5 + +<<: !include common-variants.yaml +<<: !include common-get-value.yaml From 07406c96e136b61a4521ba86c00325b96f65fc66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:35:15 -1000 Subject: [PATCH 241/609] Bump actions/upload-artifact from 6.0.0 to 7.0.0 (#14326) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-api-proto.yml | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index 0328611f5c..6d200956e9 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -62,7 +62,7 @@ jobs: run: git diff - if: failure() name: Archive artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: generated-proto-files path: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8718772f53..c2a93d37fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -822,7 +822,7 @@ jobs: fi - name: Upload memory analysis JSON - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: memory-analysis-target path: memory-analysis-target.json @@ -886,7 +886,7 @@ jobs: --platform "$platform" - name: Upload memory analysis JSON - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: memory-analysis-pr path: memory-analysis-pr.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 479b01ee37..0cca4dcaf6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -138,7 +138,7 @@ jobs: # version: ${{ needs.init.outputs.tag }} - name: Upload digests - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: digests-${{ matrix.platform.arch }} path: /tmp/digests From bd3f8e006c40dfc5e64dc37f3f90e9bc2c4ec5ac Mon Sep 17 00:00:00 2001 From: whitty Date: Sat, 28 Feb 2026 03:02:29 +1100 Subject: [PATCH 242/609] [esp32_ble] allow setting of min/max key_size and auth_req_mode (#7138) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/components/esp32_ble/__init__.py | 52 +++++++++++++ esphome/components/esp32_ble/ble.cpp | 74 ++++++++++++++++++- esphome/components/esp32_ble/ble.h | 26 +++++++ esphome/core/defines.h | 1 + ...nded-auth-req-params-single.esp32-idf.yaml | 6 ++ ...st-extended-auth-req-params.esp32-idf.yaml | 5 ++ 6 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 tests/components/esp32_ble/test-extended-auth-req-params-single.esp32-idf.yaml create mode 100644 tests/components/esp32_ble/test-extended-auth-req-params.esp32-idf.yaml diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index c0e2f78bde..8b368afc2e 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -21,6 +21,7 @@ from esphome.const import ( ) from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority import esphome.final_validate as fv +from esphome.types import ConfigType DEPENDENCIES = ["esp32"] CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"] @@ -188,6 +189,9 @@ def register_bt_logger(*loggers: BTLoggers) -> None: CONF_BLE_ID = "ble_id" CONF_IO_CAPABILITY = "io_capability" +CONF_AUTH_REQ_MODE = "auth_req_mode" +CONF_MAX_KEY_SIZE = "max_key_size" +CONF_MIN_KEY_SIZE = "min_key_size" CONF_ADVERTISING = "advertising" CONF_ADVERTISING_CYCLE_TIME = "advertising_cycle_time" CONF_DISABLE_BT_LOGS = "disable_bt_logs" @@ -238,6 +242,18 @@ IO_CAPABILITY = { "display_yes_no": IoCapability.IO_CAP_IO, } +AuthReqMode = esp32_ble_ns.enum("AuthReqMode") +AUTH_REQ_MODE = { + "no_bond": AuthReqMode.AUTH_REQ_NO_BOND, + "bond": AuthReqMode.AUTH_REQ_BOND, + "mitm": AuthReqMode.AUTH_REQ_MITM, + "bond_mitm": AuthReqMode.AUTH_REQ_BOND_MITM, + "sc_only": AuthReqMode.AUTH_REQ_SC_ONLY, + "sc_bond": AuthReqMode.AUTH_REQ_SC_BOND, + "sc_mitm": AuthReqMode.AUTH_REQ_SC_MITM, + "sc_mitm_bond": AuthReqMode.AUTH_REQ_SC_MITM_BOND, +} + esp_power_level_t = cg.global_ns.enum("esp_power_level_t") TX_POWER_LEVELS = { @@ -258,6 +274,10 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_IO_CAPABILITY, default="none"): cv.enum( IO_CAPABILITY, lower=True ), + # note: no defaults so we can action them not being present + cv.Optional(CONF_AUTH_REQ_MODE): cv.enum(AUTH_REQ_MODE, lower=True), + cv.Optional(CONF_MAX_KEY_SIZE): cv.int_range(min=7, max=16), + cv.Optional(CONF_MIN_KEY_SIZE): cv.int_range(min=7, max=16), cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, cv.Optional(CONF_ADVERTISING, default=False): cv.boolean, cv.Optional( @@ -279,6 +299,23 @@ CONFIG_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) +def _validate_key_sizes(config: ConfigType) -> ConfigType: + if ( + CONF_MIN_KEY_SIZE in config + and CONF_MAX_KEY_SIZE in config + and config[CONF_MIN_KEY_SIZE] > config[CONF_MAX_KEY_SIZE] + ): + raise cv.Invalid( + f"min_key_size ({config[CONF_MIN_KEY_SIZE]}) must be " + f"less than or equal to " + f"max_key_size ({config[CONF_MAX_KEY_SIZE]})" + ) + return config + + +CONFIG_SCHEMA = cv.All(CONFIG_SCHEMA, _validate_key_sizes) + + bt_uuid16_format = "XXXX" bt_uuid32_format = "XXXXXXXX" bt_uuid128_format = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" @@ -487,6 +524,21 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY])) + + if ( + CONF_AUTH_REQ_MODE in config + or CONF_MAX_KEY_SIZE in config + or CONF_MIN_KEY_SIZE in config + ): + cg.add_define("ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS", None) + + if CONF_AUTH_REQ_MODE in config: + cg.add(var.set_auth_req(config[CONF_AUTH_REQ_MODE])) + if CONF_MAX_KEY_SIZE in config: + cg.add(var.set_max_key_size(config[CONF_MAX_KEY_SIZE])) + if CONF_MIN_KEY_SIZE in config: + cg.add(var.set_min_key_size(config[CONF_MIN_KEY_SIZE])) + cg.add(var.set_advertising_cycle_time(config[CONF_ADVERTISING_CYCLE_TIME])) if (name := config.get(CONF_NAME)) is not None: cg.add(var.set_name(name)) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index acbe9d88fc..9d26018800 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -296,12 +296,39 @@ bool ESP32BLE::ble_setup_() { return false; } - err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &(this->io_cap_), sizeof(uint8_t)); + err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &(this->io_cap_), sizeof(esp_ble_io_cap_t)); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gap_set_security_param failed: %d", err); + ESP_LOGE(TAG, "esp_ble_gap_set_security_param iocap_mode failed: %d", err); return false; } +#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS + if (this->max_key_size_) { + err = esp_ble_gap_set_security_param(ESP_BLE_SM_MAX_KEY_SIZE, &(this->max_key_size_), sizeof(uint8_t)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_set_security_param max_key_size failed: %d", err); + return false; + } + } + + if (this->min_key_size_) { + err = esp_ble_gap_set_security_param(ESP_BLE_SM_MIN_KEY_SIZE, &(this->min_key_size_), sizeof(uint8_t)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_set_security_param min_key_size failed: %d", err); + return false; + } + } + + if (this->auth_req_mode_) { + err = esp_ble_gap_set_security_param(ESP_BLE_SM_AUTHEN_REQ_MODE, &(this->auth_req_mode_.value()), + sizeof(esp_ble_auth_req_t)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_set_security_param authen_req_mode failed: %d", err); + return false; + } + } +#endif // ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS + // BLE takes some time to be fully set up, 200ms should be more than enough delay(200); // NOLINT @@ -645,6 +672,7 @@ void ESP32BLE::dump_config() { io_capability_s = "invalid"; break; } + char mac_s[18]; format_mac_addr_upper(mac_address, mac_s); ESP_LOGCONFIG(TAG, @@ -652,6 +680,48 @@ void ESP32BLE::dump_config() { " MAC address: %s\n" " IO Capability: %s", mac_s, io_capability_s); + +#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS + const char *auth_req_mode_s = ""; + if (this->auth_req_mode_) { + switch (this->auth_req_mode_.value()) { + case AUTH_REQ_NO_BOND: + auth_req_mode_s = "no_bond"; + break; + case AUTH_REQ_BOND: + auth_req_mode_s = "bond"; + break; + case AUTH_REQ_MITM: + auth_req_mode_s = "mitm"; + break; + case AUTH_REQ_BOND_MITM: + auth_req_mode_s = "bond_mitm"; + break; + case AUTH_REQ_SC_ONLY: + auth_req_mode_s = "sc_only"; + break; + case AUTH_REQ_SC_BOND: + auth_req_mode_s = "sc_bond"; + break; + case AUTH_REQ_SC_MITM: + auth_req_mode_s = "sc_mitm"; + break; + case AUTH_REQ_SC_MITM_BOND: + auth_req_mode_s = "sc_mitm_bond"; + break; + } + } + + ESP_LOGCONFIG(TAG, " Auth Req Mode: %s", auth_req_mode_s); + if (this->max_key_size_ && this->min_key_size_) { + ESP_LOGCONFIG(TAG, " Key Size: %u - %u", this->min_key_size_, this->max_key_size_); + } else if (this->max_key_size_) { + ESP_LOGCONFIG(TAG, " Key Size: - %u", this->max_key_size_); + } else if (this->min_key_size_) { + ESP_LOGCONFIG(TAG, " Key Size: %u - ", this->min_key_size_); + } +#endif // ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS + } else { ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled"); } diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index f1ab81b6dc..2ce17e97be 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -52,6 +52,19 @@ enum IoCapability { IO_CAP_KBDISP = ESP_IO_CAP_KBDISP, }; +#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS +enum AuthReqMode { + AUTH_REQ_NO_BOND = ESP_LE_AUTH_NO_BOND, + AUTH_REQ_BOND = ESP_LE_AUTH_BOND, + AUTH_REQ_MITM = ESP_LE_AUTH_REQ_MITM, + AUTH_REQ_BOND_MITM = ESP_LE_AUTH_REQ_BOND_MITM, + AUTH_REQ_SC_ONLY = ESP_LE_AUTH_REQ_SC_ONLY, + AUTH_REQ_SC_BOND = ESP_LE_AUTH_REQ_SC_BOND, + AUTH_REQ_SC_MITM = ESP_LE_AUTH_REQ_SC_MITM, + AUTH_REQ_SC_MITM_BOND = ESP_LE_AUTH_REQ_SC_MITM_BOND, +}; +#endif + enum BLEComponentState : uint8_t { /** Nothing has been initialized yet. */ BLE_COMPONENT_STATE_OFF = 0, @@ -100,6 +113,12 @@ class ESP32BLE : public Component { public: void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; } +#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS + void set_max_key_size(uint8_t key_size) { this->max_key_size_ = key_size; } + void set_min_key_size(uint8_t key_size) { this->min_key_size_ = key_size; } + void set_auth_req(AuthReqMode req) { this->auth_req_mode_ = (esp_ble_auth_req_t) req; } +#endif + void set_advertising_cycle_time(uint32_t advertising_cycle_time) { this->advertising_cycle_time_ = advertising_cycle_time; } @@ -209,6 +228,13 @@ class ESP32BLE : public Component { // 1-byte aligned members (grouped together to minimize padding) BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; // 1 byte (uint8_t enum) bool enable_on_boot_{}; // 1 byte + +#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS + optional auth_req_mode_; + + uint8_t max_key_size_{0}; // range is 7..16, 0 is unset + uint8_t min_key_size_{0}; // range is 7..16, 0 is unset +#endif }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 673aa246fe..9b55ae93b1 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -209,6 +209,7 @@ #define ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT 1 #define ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT 1 #define ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT 2 +#define ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS #define ESPHOME_LOOP_TASK_STACK_SIZE 8192 #define USE_ESP32_CAMERA_JPEG_ENCODER #define USE_HTTP_REQUEST_RESPONSE diff --git a/tests/components/esp32_ble/test-extended-auth-req-params-single.esp32-idf.yaml b/tests/components/esp32_ble/test-extended-auth-req-params-single.esp32-idf.yaml new file mode 100644 index 0000000000..6e191c132f --- /dev/null +++ b/tests/components/esp32_ble/test-extended-auth-req-params-single.esp32-idf.yaml @@ -0,0 +1,6 @@ +esp32_ble: + io_capability: keyboard_display + # Explicitly not setting some parameters to test ifdef selection + # max_key_size: 16 + # min_key_size: 7 + auth_req_mode: sc_mitm_bond diff --git a/tests/components/esp32_ble/test-extended-auth-req-params.esp32-idf.yaml b/tests/components/esp32_ble/test-extended-auth-req-params.esp32-idf.yaml new file mode 100644 index 0000000000..f05b9bac96 --- /dev/null +++ b/tests/components/esp32_ble/test-extended-auth-req-params.esp32-idf.yaml @@ -0,0 +1,5 @@ +esp32_ble: + io_capability: keyboard_display + max_key_size: 16 + min_key_size: 7 + auth_req_mode: sc_mitm_bond From 0f7ac1726d9fea8cdb7ff481fbfcaa8b4b49620e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 09:03:37 -0700 Subject: [PATCH 243/609] [core] Extend fast select optimization to LibreTiny platforms (#14254) Co-authored-by: Claude Opus 4.6 --- esphome/components/socket/__init__.py | 11 ++-- esphome/core/application.cpp | 43 ++++++++------- esphome/core/application.h | 27 +++++----- esphome/core/defines.h | 2 + esphome/core/lwip_fast_select.c | 54 +++++++++++-------- esphome/core/lwip_fast_select.h | 2 +- .../socket/test_wake_loop_threadsafe.py | 23 +++++--- 7 files changed, 100 insertions(+), 62 deletions(-) diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 5f4d04eb44..a83648979c 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -149,9 +149,10 @@ def require_wake_loop_threadsafe() -> None: ): CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True cg.add_define("USE_WAKE_LOOP_THREADSAFE") - if not CORE.is_esp32: - # Only non-ESP32 platforms need a UDP socket for wake notifications. - # ESP32 uses FreeRTOS task notifications instead (no socket needed). + if not CORE.is_esp32 and not CORE.is_libretiny: + # Only platforms without fast select need a UDP socket for wake + # notifications. ESP32 and LibreTiny use FreeRTOS task notifications + # instead (no socket needed). consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({}) @@ -187,6 +188,10 @@ async def to_code(config): elif impl == IMPLEMENTATION_BSD_SOCKETS: cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS") cg.add_define("USE_SOCKET_SELECT_SUPPORT") + # ESP32 and LibreTiny both have LwIP >= 2.1.3 with lwip_socket_dbg_get_socket() + # and FreeRTOS task notifications — enable fast select to bypass lwip_select() + if CORE.is_esp32 or CORE.is_libretiny: + cg.add_define("USE_LWIP_FAST_SELECT") def FILTER_SOURCE_FILES() -> list[str]: diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index a9753da1b5..3bd4c1c670 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -9,10 +9,17 @@ #endif #ifdef USE_ESP32 #include +#endif +#ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" +#ifdef USE_ESP32 #include #include +#else +#include +#include #endif +#endif // USE_LWIP_FAST_SELECT #include "esphome/core/version.h" #include "esphome/core/hal.h" #include @@ -147,14 +154,14 @@ void Application::setup() { clear_setup_priority_overrides(); #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_ESP32) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) // Initialize fast select: saves main loop task handle for xTaskNotifyGive wake. - // Always init on ESP32 — the fast path (rcvevent reads + ulTaskNotifyTake) is used - // unconditionally when USE_SOCKET_SELECT_SUPPORT is enabled. + // The fast path (rcvevent reads + ulTaskNotifyTake) is used unconditionally + // when USE_LWIP_FAST_SELECT is enabled (ESP32 and LibreTiny). esphome_lwip_fast_select_init(); #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) - // Set up wake socket for waking main loop from tasks (non-ESP32 only) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) + // Set up wake socket for waking main loop from tasks (platforms without fast select only) this->setup_wake_loop_threadsafe_(); #endif @@ -532,7 +539,7 @@ void Application::enable_pending_loops_() { } void Application::before_loop_tasks_(uint32_t loop_start_time) { -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) // Drain wake notifications first to clear socket for next wake this->drain_wake_notifications_(); #endif @@ -585,7 +592,7 @@ bool Application::register_socket_fd(int fd) { #endif this->socket_fds_.push_back(fd); -#ifdef USE_ESP32 +#ifdef USE_LWIP_FAST_SELECT // Hook the socket's netconn callback for instant wake on receive events esphome_lwip_hook_socket(fd); #else @@ -609,12 +616,13 @@ void Application::unregister_socket_fd(int fd) { continue; // Swap with last element and pop - O(1) removal since order doesn't matter. - // No need to unhook the netconn callback on ESP32 — all LwIP sockets share - // the same static event_callback, and the socket will be closed by the caller. + // No need to unhook the netconn callback on fast select platforms — all LwIP + // sockets share the same static event_callback, and the socket will be closed + // by the caller. if (i < this->socket_fds_.size() - 1) this->socket_fds_[i] = this->socket_fds_.back(); this->socket_fds_.pop_back(); -#ifndef USE_ESP32 +#ifndef USE_LWIP_FAST_SELECT this->socket_fds_changed_ = true; // Only recalculate max_fd if we removed the current max if (fd == this->max_fd_) { @@ -633,8 +641,8 @@ void Application::unregister_socket_fd(int fd) { void Application::yield_with_select_(uint32_t delay_ms) { // Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run. -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_ESP32) - // ESP32 fast path: reads rcvevent directly via lwip_socket_dbg_get_socket() (~215 ns per socket). +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) + // Fast path (ESP32/LibreTiny): reads rcvevent directly via lwip_socket_dbg_get_socket(). // Safe because this runs on the main loop which owns socket lifetime (create, read, close). if (delay_ms == 0) [[unlikely]] { yield(); @@ -659,9 +667,8 @@ void Application::yield_with_select_(uint32_t delay_ms) { ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms)); #elif defined(USE_SOCKET_SELECT_SUPPORT) - // Non-ESP32 select() path (LibreTiny bk72xx/rtl87xx, host platform). - // ESP32 is excluded by the #if above — both BSD_SOCKETS and LWIP_SOCKETS on ESP32 - // use LwIP under the hood, so the fast path handles all ESP32 socket implementations. + // Fallback select() path (host platform and any future platforms without fast select). + // ESP32 and LibreTiny are excluded by the #if above — they use the fast path. if (!this->socket_fds_.empty()) [[likely]] { // Update fd_set if socket list has changed if (this->socket_fds_changed_) [[unlikely]] { @@ -725,12 +732,12 @@ alignas(Application) char app_storage[sizeof(Application)] asm("_ZN7esphome3AppE #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) -#ifdef USE_ESP32 +#ifdef USE_LWIP_FAST_SELECT void Application::wake_loop_threadsafe() { // Direct FreeRTOS task notification — <1 us, task context only (NOT ISR-safe) esphome_lwip_wake_main_loop(); } -#else // !USE_ESP32 +#else // !USE_LWIP_FAST_SELECT void Application::setup_wake_loop_threadsafe_() { // Create UDP socket for wake notifications @@ -798,7 +805,7 @@ void Application::wake_loop_threadsafe() { lwip_send(this->wake_socket_fd_, &dummy, 1, 0); } } -#endif // USE_ESP32 +#endif // USE_LWIP_FAST_SELECT #endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) diff --git a/esphome/core/application.h b/esphome/core/application.h index f5df5e7bdf..0cc29af8e7 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -24,7 +24,7 @@ #endif #ifdef USE_SOCKET_SELECT_SUPPORT -#ifdef USE_ESP32 +#ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" #else #include @@ -511,10 +511,11 @@ class Application { #ifdef USE_SOCKET_SELECT_SUPPORT /// Fast path for Socket::ready() via friendship - skips negative fd check. - /// Main loop only — on ESP32, reads rcvevent via lwip_socket_dbg_get_socket() - /// which has no refcount; safe only because the main loop owns socket lifetime - /// (creates, reads, and closes sockets on the same thread). -#ifdef USE_ESP32 + /// Main loop only — with USE_LWIP_FAST_SELECT, reads rcvevent via + /// lwip_socket_dbg_get_socket(), which has no refcount; safe only because + /// the main loop owns socket lifetime (creates, reads, and closes sockets + /// on the same thread). +#ifdef USE_LWIP_FAST_SELECT bool is_socket_ready_(int fd) const { return esphome_lwip_socket_has_data(fd); } #else bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); } @@ -546,7 +547,7 @@ class Application { /// Perform a delay while also monitoring socket file descriptors for readiness void yield_with_select_(uint32_t delay_ms); -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) void setup_wake_loop_threadsafe_(); // Create wake notification socket inline void drain_wake_notifications_(); // Read pending wake notifications in main loop (hot path - inlined) #endif @@ -576,7 +577,7 @@ class Application { FixedVector looping_components_{}; #ifdef USE_SOCKET_SELECT_SUPPORT std::vector socket_fds_; // Vector of all monitored socket file descriptors -#if defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) +#if defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks #endif #endif @@ -589,7 +590,7 @@ class Application { uint32_t last_loop_{0}; uint32_t loop_component_start_time_{0}; -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_ESP32) +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) int max_fd_{-1}; // Highest file descriptor number for select() #endif @@ -605,12 +606,12 @@ class Application { bool in_loop_{false}; volatile bool has_pending_enable_loop_requests_{false}; -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_ESP32) +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_ESP32) - // Variable-sized members (not needed on ESP32 — is_socket_ready_ reads rcvevent directly) +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) + // Variable-sized members (not needed with fast select — is_socket_ready_ reads rcvevent directly) fd_set read_fds_{}; // Working fd_set: populated by select() fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes #endif @@ -699,7 +700,7 @@ class Application { /// Global storage of Application pointer - only one Application can exist. extern Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) // Inline implementations for hot-path functions // drain_wake_notifications_() is called on every loop iteration @@ -721,6 +722,6 @@ inline void Application::drain_wake_notifications_() { } } } -#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) +#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 9b55ae93b1..5c982c94b1 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -222,6 +222,7 @@ #define USE_SENDSPIN_PORT 8928 // NOLINT #define USE_SOCKET_IMPL_BSD_SOCKETS #define USE_SOCKET_SELECT_SUPPORT +#define USE_LWIP_FAST_SELECT #define USE_WAKE_LOOP_THREADSAFE #define USE_SPEAKER #define USE_SPI @@ -330,6 +331,7 @@ #define USE_CAPTIVE_PORTAL #define USE_SOCKET_IMPL_LWIP_SOCKETS #define USE_SOCKET_SELECT_SUPPORT +#define USE_LWIP_FAST_SELECT #define USE_WEBSERVER #define USE_WEBSERVER_AUTH #define USE_WEBSERVER_PORT 80 // NOLINT diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index 70a6482d48..88cf23b67e 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -1,11 +1,12 @@ -// Fast socket monitoring for ESP32 (ESP-IDF LwIP) +// Fast socket monitoring for ESP32 and LibreTiny (LwIP >= 2.1.3) // Replaces lwip_select() with direct rcvevent reads and FreeRTOS task notifications. // // This must be a .c file (not .cpp) because: -// 1. lwip/priv/sockets_priv.h conflicts with C++ compilation units that include bootloader headers +// 1. lwip/priv/sockets_priv.h conflicts with C++ compilation units // 2. The netconn callback is a C function pointer // -// defines.h is force-included by the build system (-include flag), providing USE_ESP32 etc. +// USE_ESP32 and USE_LIBRETINY platform flags (-D) control compilation of this file. +// See the guard at the bottom of the header comment for details. // // Thread safety analysis // ====================== @@ -81,20 +82,21 @@ // Written by main loop in hook_socket(). Never restored — all LwIP sockets share // the same static event_callback (DEFAULT_SOCKET_EVENTCB), so the wrapper stays permanently. // Read by TCP/IP thread when invoking the callback. -// Safe: 32-bit aligned pointer writes are atomic on Xtensa and RISC-V (ESP32). -// The TCP/IP thread will see either the old or new pointer atomically — never a -// torn value. Both the wrapper and original callbacks are valid at all times -// (the wrapper itself calls the original), so either value is correct. +// Safe: 32-bit aligned pointer writes are atomic on Xtensa, RISC-V (ESP32), +// and ARM Cortex-M (LibreTiny). The TCP/IP thread will see either the old or +// new pointer atomically — never a torn value. Both the wrapper and original +// callbacks are valid at all times (the wrapper itself calls the original), +// so either value is correct. // // sock->rcvevent (s16_t, 2 bytes): // Written by TCP/IP thread in event_callback under SYS_ARCH_PROTECT. // Read by main loop in has_data() via volatile cast. -// Safe: SYS_ARCH_UNPROTECT releases a FreeRTOS mutex, which internally -// uses a critical section with memory barrier (rsync on dual-core Xtensa; on -// single-core builds the spinlock is compiled out, but cross-core visibility is -// not an issue). The volatile cast prevents the compiler -// from caching the read. Aligned 16-bit reads are single-instruction loads on -// Xtensa (L16SI) and RISC-V (LH), which cannot produce torn values. +// Safe: SYS_ARCH_UNPROTECT releases a FreeRTOS mutex (ESP32) or resumes the +// scheduler (LibreTiny), both providing a memory barrier. The volatile cast +// prevents the compiler from caching the read. Aligned 16-bit reads are +// single-instruction loads on Xtensa (L16SI), RISC-V (LH), and ARM Cortex-M +// (LDRH), which cannot produce torn values. On single-core chips (LibreTiny, +// ESP32-C3/C6/H2) cross-core visibility is not an issue. // // FreeRTOS task notification value: // Written by TCP/IP thread (xTaskNotifyGive in callback) and background tasks @@ -103,20 +105,30 @@ // critical sections). Multiple concurrent xTaskNotifyGive calls are safe — // the notification count simply increments. -#ifdef USE_ESP32 +// USE_ESP32 and USE_LIBRETINY are compiler -D flags, so they are always visible in this .c file. +// Feature macros like USE_LWIP_FAST_SELECT may come from generated headers that are not included here, +// so this implementation is enabled based on platform flags instead of USE_LWIP_FAST_SELECT. +#if defined(USE_ESP32) || defined(USE_LIBRETINY) // LwIP headers must come first — they define netconn_callback, struct lwip_sock, etc. #include #include +// FreeRTOS include paths differ: ESP-IDF uses freertos/ prefix, LibreTiny does not +#ifdef USE_ESP32 #include #include +#else +#include +#include +#endif #include "esphome/core/lwip_fast_select.h" #include // Compile-time verification of thread safety assumptions. -// On ESP32 (Xtensa/RISC-V), naturally-aligned reads/writes up to 32 bits are atomic. +// On ESP32 (Xtensa/RISC-V) and LibreTiny (ARM Cortex-M), naturally-aligned +// reads/writes up to 32 bits are atomic. // These asserts ensure our cross-thread shared state meets those requirements. // Pointer types must fit in a single 32-bit store (atomic write) @@ -126,7 +138,7 @@ _Static_assert(sizeof(netconn_callback) <= 4, "netconn_callback must be <= 4 byt // rcvevent must fit in a single atomic read _Static_assert(sizeof(((struct lwip_sock *) 0)->rcvevent) <= 4, "rcvevent must be <= 4 bytes for atomic access"); -// Struct member alignment — natural alignment guarantees atomicity on Xtensa/RISC-V. +// Struct member alignment — natural alignment guarantees atomicity on Xtensa/RISC-V/ARM. // Misaligned access would not be atomic even if the size is <= 4 bytes. _Static_assert(offsetof(struct netconn, callback) % sizeof(netconn_callback) == 0, "netconn.callback must be naturally aligned for atomic access"); @@ -183,9 +195,9 @@ bool esphome_lwip_socket_has_data(int fd) { return false; // volatile prevents the compiler from caching/reordering this cross-thread read. // The write side (TCP/IP thread) commits via SYS_ARCH_UNPROTECT which releases a - // FreeRTOS mutex with a memory barrier (rsync on Xtensa), ensuring the value is - // visible. Aligned 16-bit reads are single-instruction loads (L16SI/LH) on - // Xtensa/RISC-V and cannot produce torn values. + // FreeRTOS mutex (ESP32) or resumes the scheduler (LibreTiny), ensuring the value + // is visible. Aligned 16-bit reads are single-instruction loads (L16SI/LH/LDRH) on + // Xtensa/RISC-V/ARM and cannot produce torn values. return *(volatile s16_t *) &sock->rcvevent > 0; } @@ -200,7 +212,7 @@ void esphome_lwip_hook_socket(int fd) { s_original_callback = sock->conn->callback; } - // Replace with our wrapper. Atomic on ESP32 (32-bit aligned pointer write). + // Replace with our wrapper. Atomic on all supported platforms (32-bit aligned pointer write). // TCP/IP thread sees either old or new pointer — both are valid. sock->conn->callback = esphome_socket_event_callback; } @@ -213,4 +225,4 @@ void esphome_lwip_wake_main_loop(void) { } } -#endif // USE_ESP32 +#endif // defined(USE_ESP32) || defined(USE_LIBRETINY) diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h index 73a89fdc3d..b08c946212 100644 --- a/esphome/core/lwip_fast_select.h +++ b/esphome/core/lwip_fast_select.h @@ -1,6 +1,6 @@ #pragma once -// Fast socket monitoring for ESP32 (ESP-IDF LwIP) +// Fast socket monitoring for ESP32 and LibreTiny (LwIP >= 2.1.3) // Replaces lwip_select() with direct rcvevent reads and FreeRTOS task notifications. #include diff --git a/tests/components/socket/test_wake_loop_threadsafe.py b/tests/components/socket/test_wake_loop_threadsafe.py index 28b4ee564f..0434b3e1b5 100644 --- a/tests/components/socket/test_wake_loop_threadsafe.py +++ b/tests/components/socket/test_wake_loop_threadsafe.py @@ -1,9 +1,14 @@ +import pytest + from esphome.components import socket from esphome.const import ( KEY_CORE, KEY_TARGET_PLATFORM, + PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_LN882X, + PLATFORM_RTL87XX, ) from esphome.core import CORE @@ -90,9 +95,15 @@ def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() - assert udp_consumers == initial_udp -def test_require_wake_loop_threadsafe__esp32_no_udp_socket() -> None: - """Test that ESP32 uses task notifications instead of UDP socket.""" - _setup_platform(PLATFORM_ESP32) +@pytest.mark.parametrize( + "platform", + [PLATFORM_ESP32, PLATFORM_BK72XX, PLATFORM_RTL87XX, PLATFORM_LN882X], +) +def test_require_wake_loop_threadsafe__fast_select_no_udp_socket( + platform: str, +) -> None: + """Test that fast select platforms use task notifications instead of UDP socket.""" + _setup_platform(platform) CORE.config = {"wifi": True} socket.require_wake_loop_threadsafe() @@ -100,13 +111,13 @@ def test_require_wake_loop_threadsafe__esp32_no_udp_socket() -> None: assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) - # Verify no UDP socket was consumed (ESP32 uses FreeRTOS task notifications) + # Verify no UDP socket was consumed (fast select platforms use FreeRTOS task notifications) udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) assert "socket.wake_loop_threadsafe" not in udp_consumers -def test_require_wake_loop_threadsafe__non_esp32_consumes_udp_socket() -> None: - """Test that non-ESP32 platforms consume a UDP socket for wake notifications.""" +def test_require_wake_loop_threadsafe__non_fast_select_consumes_udp_socket() -> None: + """Test that platforms without fast select consume a UDP socket for wake notifications.""" _setup_platform(PLATFORM_ESP8266) CORE.config = {"wifi": True} socket.require_wake_loop_threadsafe() From ef9fc87351a288293c0ad3e9ab59c7788af8db09 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:17:04 -0500 Subject: [PATCH 244/609] [zigbee] Fix codegen ordering for basic/identify attribute lists (#14343) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/zigbee/__init__.py | 3 ++- esphome/components/zigbee/zigbee_zephyr.py | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index 7e917a9d70..a327cc2988 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -8,7 +8,7 @@ from esphome.components.zephyr import zephyr_add_pm_static, zephyr_data from esphome.components.zephyr.const import KEY_BOOTLOADER import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_INTERNAL, CONF_NAME -from esphome.core import CORE +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.types import ConfigType from .const_zephyr import ( @@ -96,6 +96,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config: ConfigType) -> None: cg.add_define("USE_ZIGBEE") if CORE.using_zephyr: diff --git a/esphome/components/zigbee/zigbee_zephyr.py b/esphome/components/zigbee/zigbee_zephyr.py index 0b6daa9476..a1e6ad3097 100644 --- a/esphome/components/zigbee/zigbee_zephyr.py +++ b/esphome/components/zigbee/zigbee_zephyr.py @@ -179,6 +179,13 @@ async def zephyr_to_code(config: ConfigType) -> None: "USE_ZIGBEE_WIPE_ON_BOOT_MAGIC", random.randint(0x000001, 0xFFFFFF) ) cg.add_define("USE_ZIGBEE_WIPE_ON_BOOT") + + # Generate attribute lists before any await that could yield (e.g., build_automation + # waiting for variables from other components). If the hub's priority decays while + # yielding, deferred entity jobs may add cluster list globals that reference these + # attribute lists before they're declared. + await _attr_to_code(config) + var = cg.new_Pvariable(config[CONF_ID]) if on_join_config := config.get(CONF_ON_JOIN): @@ -186,7 +193,6 @@ async def zephyr_to_code(config: ConfigType) -> None: await cg.register_component(var, config) - await _attr_to_code(config) CORE.add_job(_ctx_to_code, config) From 017d1b2872b9ad85bf5b8a2fb563d14ff517975c Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Fri, 27 Feb 2026 11:12:50 -0600 Subject: [PATCH 245/609] [audio] Bump microOpus to v0.3.4 (#14346) --- esphome/components/audio/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index d8d426ec63..d95fcf66d7 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -214,4 +214,4 @@ async def to_code(config): cg.add_define("USE_AUDIO_MP3_SUPPORT") if data.opus_support: cg.add_define("USE_AUDIO_OPUS_SUPPORT") - add_idf_component(name="esphome/micro-opus", ref="0.3.3") + add_idf_component(name="esphome/micro-opus", ref="0.3.4") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 83b2d9d95c..37bda65afd 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -4,7 +4,7 @@ dependencies: esphome/esp-audio-libs: version: 2.0.3 esphome/micro-opus: - version: 0.3.3 + version: 0.3.4 espressif/esp-tflite-micro: version: 1.3.3~1 espressif/esp32-camera: From 20314b4d63ee07d97840c821cf1ca0003393a339 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 10:20:08 -0700 Subject: [PATCH 246/609] [mdns] Update espressif/mdns to v1.10.0 (#14338) --- esphome/components/mdns/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 420e6a60e3..0d535d6970 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -163,7 +163,7 @@ async def to_code(config): cg.add_library("LEAmDNS", None) if CORE.is_esp32: - add_idf_component(name="espressif/mdns", ref="1.9.1") + add_idf_component(name="espressif/mdns", ref="1.10.0") cg.add_define("USE_MDNS") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 37bda65afd..550e7b9af7 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -10,7 +10,7 @@ dependencies: espressif/esp32-camera: version: 2.1.1 espressif/mdns: - version: 1.9.1 + version: 1.10.0 espressif/esp_wifi_remote: version: 1.3.2 rules: From 72ca514cc2650e641faa774a7d43a81dcdf6fb98 Mon Sep 17 00:00:00 2001 From: deirdreobyrne Date: Fri, 27 Feb 2026 17:25:53 +0000 Subject: [PATCH 247/609] [esp32_hosted] Add configurable SDIO clock frequency (#14319) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Deirdre Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/esp32_hosted/__init__.py | 8 ++++++++ .../test-sdio-speed.esp32-p4-idf.yaml | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 tests/components/esp32_hosted/test-sdio-speed.esp32-p4-idf.yaml diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index 287c780769..720d81acc4 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -23,6 +23,7 @@ CONF_D1_PIN = "d1_pin" CONF_D2_PIN = "d2_pin" CONF_D3_PIN = "d3_pin" CONF_SLOT = "slot" +CONF_SDIO_FREQUENCY = "sdio_frequency" CONFIG_SCHEMA = cv.All( cv.Schema( @@ -37,6 +38,9 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_D3_PIN): pins.internal_gpio_output_pin_number, cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, cv.Optional(CONF_SLOT, default=1): cv.int_range(min=0, max=1), + cv.Optional(CONF_SDIO_FREQUENCY, default="40MHz"): cv.All( + cv.frequency, cv.Range(min=400e3, max=50e6) + ), } ), ) @@ -91,6 +95,10 @@ async def to_code(config): config[CONF_D3_PIN], ) esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_CUSTOM_SDIO_PINS", True) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SDIO_CLOCK_FREQ_KHZ", + int(config[CONF_SDIO_FREQUENCY] // 1000), + ) framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}" diff --git a/tests/components/esp32_hosted/test-sdio-speed.esp32-p4-idf.yaml b/tests/components/esp32_hosted/test-sdio-speed.esp32-p4-idf.yaml new file mode 100644 index 0000000000..9268c9ae41 --- /dev/null +++ b/tests/components/esp32_hosted/test-sdio-speed.esp32-p4-idf.yaml @@ -0,0 +1,16 @@ +esp32_hosted: + variant: ESP32C6 + slot: 1 + active_high: true + reset_pin: GPIO15 + cmd_pin: GPIO13 + clk_pin: GPIO12 + d0_pin: GPIO11 + d1_pin: GPIO10 + d2_pin: GPIO9 + d3_pin: GPIO8 + sdio_frequency: 8MHz + +wifi: + ssid: MySSID + password: password1 From 1c7f769ec7f54775c3d31f426dcfbb14f66a911f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 10:48:21 -0700 Subject: [PATCH 248/609] [core] Add millis_64() HAL function with native ESP32 implementation (#14339) --- esphome/components/esp32/core.cpp | 1 + esphome/components/esp8266/core.cpp | 2 ++ esphome/components/host/core.cpp | 2 ++ esphome/components/libretiny/core.cpp | 2 ++ esphome/components/rp2040/core.cpp | 2 ++ .../uptime/sensor/uptime_seconds_sensor.cpp | 4 +-- .../uptime/text_sensor/uptime_text_sensor.cpp | 4 +-- esphome/components/web_server/web_server.cpp | 4 +-- esphome/components/zephyr/core.cpp | 2 ++ esphome/core/hal.h | 1 + esphome/core/scheduler.cpp | 34 ++++++++++-------- esphome/core/scheduler.h | 35 +++++++++++++++---- 12 files changed, 66 insertions(+), 27 deletions(-) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 202d929ab9..b9ae871abf 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -23,6 +23,7 @@ namespace esphome { void HOT yield() { vPortYield(); } uint32_t IRAM_ATTR HOT millis() { return (uint32_t) (esp_timer_get_time() / 1000ULL); } +uint64_t HOT millis_64() { return static_cast(esp_timer_get_time()) / 1000ULL; } void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 236d3022be..497e99b61f 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -3,6 +3,7 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "preferences.h" #include @@ -16,6 +17,7 @@ namespace esphome { void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } +uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); } void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index c20a33fa37..5c08717ac9 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -1,5 +1,6 @@ #ifdef USE_HOST +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -19,6 +20,7 @@ uint32_t IRAM_ATTR HOT millis() { uint32_t ms = round(spec.tv_nsec / 1e6); return ((uint32_t) seconds) * 1000U + ms; } +uint64_t millis_64() { return App.scheduler.millis_64_impl_(millis()); } void HOT delay(uint32_t ms) { struct timespec ts; ts.tv_sec = ms / 1000; diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index 4dda7c3856..6cbc81938d 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -3,6 +3,7 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -13,6 +14,7 @@ namespace esphome { void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } +uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void HOT delay(uint32_t ms) { ::delay(ms); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index 37378d88bb..52c6f1185c 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -3,6 +3,7 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "hardware/watchdog.h" @@ -11,6 +12,7 @@ namespace esphome { void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } +uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); } void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } diff --git a/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp b/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp index 20e8ed8fda..552e4e5da4 100644 --- a/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp +++ b/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp @@ -1,6 +1,6 @@ #include "uptime_seconds_sensor.h" -#include "esphome/core/application.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome::uptime { @@ -8,7 +8,7 @@ namespace esphome::uptime { static const char *const TAG = "uptime.sensor"; void UptimeSecondsSensor::update() { - const uint64_t uptime = App.scheduler.millis_64(); + const uint64_t uptime = millis_64(); const uint64_t seconds_int = uptime / 1000ULL; const float seconds = float(seconds_int) + (uptime % 1000ULL) / 1000.0f; this->publish_state(seconds); diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp index 88ae53fbfc..c89d23672e 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp @@ -1,6 +1,6 @@ #include "uptime_text_sensor.h" -#include "esphome/core/application.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -19,7 +19,7 @@ static void append_unit(char *buf, size_t buf_size, size_t &pos, const char *sep void UptimeTextSensor::setup() { this->update(); } void UptimeTextSensor::update() { - uint32_t uptime = static_cast(App.scheduler.millis_64() / 1000); + uint32_t uptime = static_cast(millis_64() / 1000); unsigned interval = this->get_update_interval() / 1000; // Calculate all time units diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index e2f9c21331..16674321c9 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -385,7 +385,7 @@ json::SerializationBuffer<> WebServer::get_config_json() { #endif root[ESPHOME_F("log")] = this->expose_log_; root[ESPHOME_F("lang")] = "en"; - root[ESPHOME_F("uptime")] = static_cast(App.scheduler.millis_64() / 1000); + root[ESPHOME_F("uptime")] = static_cast(millis_64() / 1000); return builder.serialize(); } @@ -414,7 +414,7 @@ void WebServer::setup() { // getting a lot of events this->set_interval(10000, [this]() { char buf[32]; - auto uptime = static_cast(App.scheduler.millis_64() / 1000); + auto uptime = static_cast(millis_64() / 1000); buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%u}", uptime); this->events_.try_send_nodefer(buf, "ping", millis(), 30000); }); diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index d7027b33f5..7f5d6d44fa 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -4,6 +4,7 @@ #include #include #include +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/defines.h" @@ -17,6 +18,7 @@ static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0)); void yield() { ::k_yield(); } uint32_t millis() { return k_ticks_to_ms_floor32(k_uptime_ticks()); } +uint64_t millis_64() { return App.scheduler.millis_64_impl_(millis()); } uint32_t micros() { return k_ticks_to_us_floor32(k_uptime_ticks()); } void delayMicroseconds(uint32_t us) { ::k_usleep(us); } void delay(uint32_t ms) { ::k_msleep(ms); } diff --git a/esphome/core/hal.h b/esphome/core/hal.h index 1a4230e421..fa5ca646f2 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -32,6 +32,7 @@ namespace esphome { void yield(); uint32_t millis(); +uint64_t millis_64(); uint32_t micros(); void delay(uint32_t ms); void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index d9c66b2000..06c0bb8b1b 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -28,8 +28,10 @@ static constexpr size_t MAX_POOL_SIZE = 5; // Set to 5 to match the pool size - when we have as many cancelled items as our // pool can hold, it's time to clean up and recycle them. static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5; +#ifndef USE_ESP32 // Half the 32-bit range - used to detect rollovers vs normal time progression static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; +#endif // max delay to start an interval sequence static constexpr uint32_t MAX_INTERVAL_DELAY = 5000; @@ -150,8 +152,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type return; } - // Get fresh timestamp BEFORE taking lock - millis_64_ may need to acquire lock itself - const uint64_t now = this->millis_64_(millis()); + // Get fresh 64-bit timestamp BEFORE taking lock + const uint64_t now_64 = millis_64(); // Take lock early to protect scheduler_item_pool_ access LockGuard guard{this->lock_}; @@ -184,7 +186,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type item->interval = delay; // first execution happens immediately after a random smallish offset uint32_t offset = this->calculate_interval_offset_(delay); - item->set_next_execution(now + offset); + item->set_next_execution(now_64 + offset); #ifdef ESPHOME_LOG_HAS_VERBOSE SchedulerNameLog name_log; ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", @@ -192,11 +194,11 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type #endif } else { item->interval = 0; - item->set_next_execution(now + delay); + item->set_next_execution(now_64 + delay); } #ifdef ESPHOME_DEBUG_SCHEDULER - this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now); + this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now_64); #endif /* ESPHOME_DEBUG_SCHEDULER */ // For retries, check if there's a cancelled timeout first @@ -399,8 +401,7 @@ optional HOT Scheduler::next_schedule_in(uint32_t now) { return {}; auto &item = this->items_[0]; - // Convert the fresh timestamp from caller (usually Application::loop()) to 64-bit - const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from caller + const auto now_64 = this->millis_64_from_(now); const uint64_t next_exec = item->get_next_execution(); if (next_exec < now_64) return 0; @@ -461,8 +462,8 @@ void HOT Scheduler::call(uint32_t now) { this->process_defer_queue_(now); #endif /* not ESPHOME_THREAD_SINGLE */ - // Convert the fresh timestamp from main loop to 64-bit for scheduler operations - const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from Application::loop() + // Extend the caller's 32-bit timestamp to 64-bit for scheduler operations + const auto now_64 = this->millis_64_from_(now); this->process_to_add(); // Track if any items were added to to_add_ during this call (intervals or from callbacks) @@ -474,15 +475,18 @@ void HOT Scheduler::call(uint32_t now) { if (now_64 - last_print > 2000) { last_print = now_64; std::vector old_items; -#ifdef ESPHOME_THREAD_MULTI_ATOMICS +#if !defined(USE_ESP32) && defined(ESPHOME_THREAD_MULTI_ATOMICS) const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg); -#else /* not ESPHOME_THREAD_MULTI_ATOMICS */ +#elif !defined(USE_ESP32) ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_); -#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ +#else + ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64, this->items_.size(), this->scheduler_item_pool_.size(), + now_64); +#endif // Cleanup before debug output this->cleanup_(); while (!this->items_.empty()) { @@ -710,9 +714,8 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type return total_cancelled > 0; } -uint64_t Scheduler::millis_64() { return this->millis_64_(millis()); } - -uint64_t Scheduler::millis_64_(uint32_t now) { +#ifndef USE_ESP32 +uint64_t Scheduler::millis_64_impl_(uint32_t now) { // THREAD SAFETY NOTE: // This function has three implementations, based on the precompiler flags // - ESPHOME_THREAD_SINGLE - Runs on single-threaded platforms (ESP8266, RP2040, etc.) @@ -869,6 +872,7 @@ uint64_t Scheduler::millis_64_(uint32_t now) { "No platform threading model defined. One of ESPHOME_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_MULTI_ATOMICS must be defined." #endif } +#endif // not USE_ESP32 bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) { // High bits are almost always equal (change only on 32-bit rollover ~49 days) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 840ee7159a..c605325810 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -10,6 +10,7 @@ #endif #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" namespace esphome { @@ -117,12 +118,12 @@ class Scheduler { bool cancel_retry(Component *component, uint32_t id); /// Get 64-bit millisecond timestamp (handles 32-bit millis() rollover) - uint64_t millis_64(); + uint64_t millis_64() { return esphome::millis_64(); } - // Calculate when the next scheduled item should run - // @param now Fresh timestamp from millis() - must not be stale/cached - // Returns the time in milliseconds until the next scheduled item, or nullopt if no items - // This method performs cleanup of removed items before checking the schedule + // Calculate when the next scheduled item should run. + // @param now On ESP32, unused for 64-bit extension (native); on other platforms, extended to 64-bit via rollover. + // Returns the time in milliseconds until the next scheduled item, or nullopt if no items. + // This method performs cleanup of removed items before checking the schedule. // IMPORTANT: This method should only be called from the main thread (loop task). optional next_schedule_in(uint32_t now); @@ -282,7 +283,25 @@ class Scheduler { // Common implementation for cancel_retry bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id); - uint64_t millis_64_(uint32_t now); + // Extend a 32-bit millis() value to 64-bit. Use when the caller already has a fresh now. + // On ESP32, ignores now and uses esp_timer_get_time() directly (native 64-bit). + // On non-ESP32, extends now to 64-bit using rollover tracking. + uint64_t millis_64_from_(uint32_t now) { +#ifdef USE_ESP32 + (void) now; + return millis_64(); +#else + return this->millis_64_impl_(now); +#endif + } + +#ifndef USE_ESP32 + // On non-ESP32 platforms, millis_64() HAL function delegates to this method + // which tracks 32-bit millis() rollover using millis_major_ and last_millis_. + // On ESP32, millis_64() uses esp_timer_get_time() directly. + friend uint64_t millis_64(); + uint64_t millis_64_impl_(uint32_t now); +#endif // Cleanup logically deleted items from the scheduler // Returns the number of items remaining after cleanup // IMPORTANT: This method should only be called from the main thread (loop task). @@ -549,6 +568,9 @@ class Scheduler { // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52) std::vector scheduler_item_pool_; +#ifndef USE_ESP32 + // On ESP32, millis_64() uses esp_timer_get_time() directly; no rollover tracking needed. + // On other platforms, these fields track 32-bit millis() rollover for millis_64_impl_(). #ifdef ESPHOME_THREAD_MULTI_ATOMICS /* * Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates @@ -577,6 +599,7 @@ class Scheduler { #else /* not ESPHOME_THREAD_MULTI_ATOMICS */ uint16_t millis_major_{0}; #endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ +#endif /* not USE_ESP32 */ }; } // namespace esphome From 4fe173b64487b90d17fbca26727e0ad19bdd0d20 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 10:56:57 -0700 Subject: [PATCH 249/609] [wifi] Remove stale TODO comment for ESP8266 callback deferral (#14347) --- esphome/components/wifi/wifi_component_esp8266.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index cbf7d7d80f..8911bf15e0 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -470,10 +470,6 @@ const LogString *get_disconnect_reason_str(uint8_t reason) { return LOG_STR("Unspecified"); } -// TODO: This callback runs in ESP8266 system context with limited stack (~2KB). -// All listener notifications should be deferred to wifi_loop_() via pending_ flags -// to avoid stack overflow. Currently only connect_state is deferred; disconnect, -// IP, and scan listeners still run in this context and should be migrated. void WiFiComponent::wifi_event_callback(System_Event_t *event) { switch (event->event) { case EVENT_STAMODE_CONNECTED: { From c3a0eeceec21b6f7e97a2044078c611c843679e2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 11:17:17 -0700 Subject: [PATCH 250/609] [wifi] Use direct SDK APIs for LibreTiny SSID retrieval (#14349) --- .../wifi/wifi_component_libretiny.cpp | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 2cc05928af..1c5490a3ac 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -13,6 +13,19 @@ #include #include +#ifdef USE_BK72XX +extern "C" { +#include +} +#endif + +#ifdef USE_RTL87XX +extern "C" { +#include +#include +} +#endif + #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -760,10 +773,22 @@ bssid_t WiFiComponent::wifi_bssid() { } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } const char *WiFiComponent::wifi_ssid_to(std::span buffer) { - // TODO: Find direct LibreTiny API to avoid Arduino String allocation +#ifdef USE_BK72XX + LinkStatusTypeDef link_status{}; + bk_wlan_get_link_status(&link_status); + size_t len = strnlen(reinterpret_cast(link_status.ssid), SSID_BUFFER_SIZE - 1); + memcpy(buffer.data(), link_status.ssid, len); +#elif defined(USE_RTL87XX) + rtw_wifi_setting_t setting{}; + wifi_get_setting("wlan0", &setting); + size_t len = strnlen(reinterpret_cast(setting.ssid), SSID_BUFFER_SIZE - 1); + memcpy(buffer.data(), setting.ssid, len); +#else + // LN882X: wifi_get_sta_conn_info() provides direct pointer access String ssid = WiFi.SSID(); size_t len = std::min(static_cast(ssid.length()), SSID_BUFFER_SIZE - 1); memcpy(buffer.data(), ssid.c_str(), len); +#endif buffer[len] = '\0'; return buffer.data(); } From 4ae7633418716d4f606e189ad1eb532d7ec00cb5 Mon Sep 17 00:00:00 2001 From: Michael Cassaniti Date: Sat, 28 Feb 2026 05:23:02 +1100 Subject: [PATCH 251/609] [safe_mode] Add feature to explicitly mark a boot as successful (#14306) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/safe_mode/__init__.py | 17 ++++++++++++++++ esphome/components/safe_mode/automation.h | 14 +++++++------ esphome/components/safe_mode/safe_mode.cpp | 20 +++++++++++-------- esphome/components/safe_mode/safe_mode.h | 2 ++ .../components/safe_mode/common-enabled.yaml | 4 ++++ 5 files changed, 43 insertions(+), 14 deletions(-) diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index d1754aaad7..f54151b746 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -21,6 +21,7 @@ CONF_ON_SAFE_MODE = "on_safe_mode" safe_mode_ns = cg.esphome_ns.namespace("safe_mode") SafeModeComponent = safe_mode_ns.class_("SafeModeComponent", cg.Component) SafeModeTrigger = safe_mode_ns.class_("SafeModeTrigger", automation.Trigger.template()) +MarkSuccessfulAction = safe_mode_ns.class_("MarkSuccessfulAction", automation.Action) def _remove_id_if_disabled(value): @@ -53,6 +54,22 @@ CONFIG_SCHEMA = cv.All( ) +@automation.register_action( + "safe_mode.mark_successful", + MarkSuccessfulAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(SafeModeComponent), + } + ), +) +async def safe_mode_mark_successful_to_code(config, action_id, template_arg, args): + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg) + cg.add(var.set_parent(parent)) + return var + + @coroutine_with_priority(CoroPriority.APPLICATION) async def to_code(config): if not config[CONF_DISABLED]: diff --git a/esphome/components/safe_mode/automation.h b/esphome/components/safe_mode/automation.h index 1d82ac45f1..dee02c64a0 100644 --- a/esphome/components/safe_mode/automation.h +++ b/esphome/components/safe_mode/automation.h @@ -1,20 +1,22 @@ #pragma once #include "esphome/core/defines.h" - -#ifdef USE_SAFE_MODE_CALLBACK -#include "safe_mode.h" - #include "esphome/core/automation.h" +#include "safe_mode.h" namespace esphome::safe_mode { +#ifdef USE_SAFE_MODE_CALLBACK class SafeModeTrigger final : public Trigger<> { public: explicit SafeModeTrigger(SafeModeComponent *parent) { parent->add_on_safe_mode_callback([this]() { trigger(); }); } }; +#endif // USE_SAFE_MODE_CALLBACK + +template class MarkSuccessfulAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->mark_successful(); } +}; } // namespace esphome::safe_mode - -#endif // USE_SAFE_MODE_CALLBACK diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index aa4fdb8291..fe2acd9612 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -63,18 +63,22 @@ void SafeModeComponent::dump_config() { float SafeModeComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; } +void SafeModeComponent::mark_successful() { + this->clean_rtc(); + this->boot_successful_ = true; +#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) + // Mark OTA partition as valid to prevent rollback + esp_ota_mark_app_valid_cancel_rollback(); +#endif + // Disable loop since we no longer need to check + this->disable_loop(); +} + void SafeModeComponent::loop() { if (!this->boot_successful_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_boot_is_good_after_) { // successful boot, reset counter ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter"); - this->clean_rtc(); - this->boot_successful_ = true; -#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) - // Mark OTA partition as valid to prevent rollback - esp_ota_mark_app_valid_cancel_rollback(); -#endif - // Disable loop since we no longer need to check - this->disable_loop(); + this->mark_successful(); } } diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index 902b8c415d..1b28ea28f2 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -31,6 +31,8 @@ class SafeModeComponent final : public Component { void on_safe_shutdown() override; + void mark_successful(); + #ifdef USE_SAFE_MODE_CALLBACK void add_on_safe_mode_callback(std::function &&callback) { this->safe_mode_callback_.add(std::move(callback)); diff --git a/tests/components/safe_mode/common-enabled.yaml b/tests/components/safe_mode/common-enabled.yaml index c24f49e6b6..43025c60db 100644 --- a/tests/components/safe_mode/common-enabled.yaml +++ b/tests/components/safe_mode/common-enabled.yaml @@ -16,3 +16,7 @@ button: switch: - platform: safe_mode name: Safe Mode Switch + +esphome: + on_boot: + - safe_mode.mark_successful From 317dd5b2da73d39839695abb066cb976443dba05 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 11:42:08 -0700 Subject: [PATCH 252/609] [ci] Skip memory impact target branch build when tests don't exist (#14316) --- .github/workflows/ci.yml | 49 ++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2a93d37fd..b752701920 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -686,7 +686,7 @@ jobs: ram_usage: ${{ steps.extract.outputs.ram_usage }} flash_usage: ${{ steps.extract.outputs.flash_usage }} cache_hit: ${{ steps.cache-memory-analysis.outputs.cache-hit }} - skip: ${{ steps.check-script.outputs.skip }} + skip: ${{ steps.check-script.outputs.skip || steps.check-tests.outputs.skip }} steps: - name: Check out target branch uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -705,10 +705,39 @@ jobs: echo "::warning::ci_memory_impact_extract.py not found on target branch, skipping memory impact analysis" fi - # All remaining steps only run if script exists + # Check if test files exist on the target branch for the requested + # components and platform. When a PR adds new test files for a platform, + # the target branch won't have them yet, so skip instead of failing. + # This check must be done here (not in determine-jobs.py) because + # determine-jobs runs on the PR branch and cannot see what the target + # branch has. + - name: Check for test files on target branch + id: check-tests + if: steps.check-script.outputs.skip != 'true' + run: | + components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}' + platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}" + found=false + for component in $(echo "$components" | jq -r '.[]'); do + # Check for test files matching the platform (test.platform.yaml or test-*.platform.yaml) + for f in tests/components/${component}/test*.${platform}.yaml; do + if [ -f "$f" ]; then + found=true + break 2 + fi + done + done + if [ "$found" = false ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::warning::No test files found on target branch for platform ${platform}, skipping memory impact analysis" + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + # All remaining steps only run if script and tests exist - name: Generate cache key id: cache-key - if: steps.check-script.outputs.skip != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' run: | # Get the commit SHA of the target branch target_sha=$(git rev-parse HEAD) @@ -735,14 +764,14 @@ jobs: - name: Restore cached memory analysis id: cache-memory-analysis - if: steps.check-script.outputs.skip != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: memory-analysis-target.json key: ${{ steps.cache-key.outputs.cache-key }} - name: Cache status - if: steps.check-script.outputs.skip != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' run: | if [ "${{ steps.cache-memory-analysis.outputs.cache-hit }}" == "true" ]; then echo "✓ Cache hit! Using cached memory analysis results." @@ -752,21 +781,21 @@ jobs: fi - name: Restore Python - if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' uses: ./.github/actions/restore-python with: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Cache platformio - if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.platformio key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} - name: Build, compile, and analyze memory - if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' id: build run: | . venv/bin/activate @@ -800,7 +829,7 @@ jobs: --platform "$platform" - name: Save memory analysis to cache - if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success' uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: memory-analysis-target.json @@ -808,7 +837,7 @@ jobs: - name: Extract memory usage for outputs id: extract - if: steps.check-script.outputs.skip != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' run: | if [ -f memory-analysis-target.json ]; then ram=$(jq -r '.ram_bytes' memory-analysis-target.json) From 29e1e8bdfd9ce8068cb97f8e0b1c8e8f64af50c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 11:45:20 -0700 Subject: [PATCH 253/609] [wifi] Add LibreTiny component test configs (#14351) --- tests/components/wifi/test.bk72xx-ard.yaml | 1 + tests/components/wifi/test.ln882x-ard.yaml | 1 + tests/components/wifi/test.rtl87xx-ard.yaml | 1 + 3 files changed, 3 insertions(+) create mode 100644 tests/components/wifi/test.bk72xx-ard.yaml create mode 100644 tests/components/wifi/test.ln882x-ard.yaml create mode 100644 tests/components/wifi/test.rtl87xx-ard.yaml diff --git a/tests/components/wifi/test.bk72xx-ard.yaml b/tests/components/wifi/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi/test.bk72xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi/test.ln882x-ard.yaml b/tests/components/wifi/test.ln882x-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi/test.ln882x-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi/test.rtl87xx-ard.yaml b/tests/components/wifi/test.rtl87xx-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi/test.rtl87xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 3411ce21505cd8fdcf74885199ba7627f5ac105b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 11:49:57 -0700 Subject: [PATCH 254/609] [core] Fix Application asm label for Mach-O using __USER_LABEL_PREFIX__ (#14334) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/core/application.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 3bd4c1c670..e27c2cdfc6 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -727,8 +727,17 @@ void Application::yield_with_select_(uint32_t delay_ms) { #error "Application placement new requires Itanium C++ ABI (GCC/Clang)" #endif static_assert(std::is_default_constructible::value, "Application must be default-constructible"); +// __USER_LABEL_PREFIX__ is "_" on Mach-O (macOS) and empty on ELF (embedded targets). +// String literal concatenation produces the correct platform-specific mangled symbol. +// Two-level macro needed: # stringifies before expansion, so the +// indirection forces __USER_LABEL_PREFIX__ to expand first. +#define ESPHOME_STRINGIFY_IMPL_(x) #x +#define ESPHOME_STRINGIFY_(x) ESPHOME_STRINGIFY_IMPL_(x) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -alignas(Application) char app_storage[sizeof(Application)] asm("_ZN7esphome3AppE"); +alignas(Application) char app_storage[sizeof(Application)] asm( + ESPHOME_STRINGIFY_(__USER_LABEL_PREFIX__) "_ZN7esphome3AppE"); +#undef ESPHOME_STRINGIFY_ +#undef ESPHOME_STRINGIFY_IMPL_ #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) From 8698b01bc7314443867327e11f8431a7f14bf7e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 11:54:13 -0700 Subject: [PATCH 255/609] [host] Use native clock_gettime for millis_64() (#14340) Co-authored-by: Claude Opus 4.6 --- esphome/components/host/core.cpp | 6 +++++- esphome/core/scheduler.cpp | 10 +++++----- esphome/core/scheduler.h | 15 +++++++-------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index 5c08717ac9..9af85bec58 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -20,7 +20,11 @@ uint32_t IRAM_ATTR HOT millis() { uint32_t ms = round(spec.tv_nsec / 1e6); return ((uint32_t) seconds) * 1000U + ms; } -uint64_t millis_64() { return App.scheduler.millis_64_impl_(millis()); } +uint64_t millis_64() { + struct timespec spec; + clock_gettime(CLOCK_MONOTONIC, &spec); + return static_cast(spec.tv_sec) * 1000ULL + static_cast(spec.tv_nsec) / 1000000ULL; +} void HOT delay(uint32_t ms) { struct timespec ts; ts.tv_sec = ms / 1000; diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 06c0bb8b1b..97735c4876 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -28,7 +28,7 @@ static constexpr size_t MAX_POOL_SIZE = 5; // Set to 5 to match the pool size - when we have as many cancelled items as our // pool can hold, it's time to clean up and recycle them. static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5; -#ifndef USE_ESP32 +#if !defined(USE_ESP32) && !defined(USE_HOST) // Half the 32-bit range - used to detect rollovers vs normal time progression static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; #endif @@ -475,12 +475,12 @@ void HOT Scheduler::call(uint32_t now) { if (now_64 - last_print > 2000) { last_print = now_64; std::vector old_items; -#if !defined(USE_ESP32) && defined(ESPHOME_THREAD_MULTI_ATOMICS) +#if !defined(USE_ESP32) && !defined(USE_HOST) && defined(ESPHOME_THREAD_MULTI_ATOMICS) const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg); -#elif !defined(USE_ESP32) +#elif !defined(USE_ESP32) && !defined(USE_HOST) ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_); #else @@ -714,7 +714,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type return total_cancelled > 0; } -#ifndef USE_ESP32 +#if !defined(USE_ESP32) && !defined(USE_HOST) uint64_t Scheduler::millis_64_impl_(uint32_t now) { // THREAD SAFETY NOTE: // This function has three implementations, based on the precompiler flags @@ -872,7 +872,7 @@ uint64_t Scheduler::millis_64_impl_(uint32_t now) { "No platform threading model defined. One of ESPHOME_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_MULTI_ATOMICS must be defined." #endif } -#endif // not USE_ESP32 +#endif // !USE_ESP32 && !USE_HOST bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) { // High bits are almost always equal (change only on 32-bit rollover ~49 days) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index c605325810..9a62ac1634 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -287,7 +287,7 @@ class Scheduler { // On ESP32, ignores now and uses esp_timer_get_time() directly (native 64-bit). // On non-ESP32, extends now to 64-bit using rollover tracking. uint64_t millis_64_from_(uint32_t now) { -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_HOST) (void) now; return millis_64(); #else @@ -295,10 +295,9 @@ class Scheduler { #endif } -#ifndef USE_ESP32 - // On non-ESP32 platforms, millis_64() HAL function delegates to this method - // which tracks 32-bit millis() rollover using millis_major_ and last_millis_. - // On ESP32, millis_64() uses esp_timer_get_time() directly. +#if !defined(USE_ESP32) && !defined(USE_HOST) + // On platforms without native 64-bit time, millis_64() HAL function delegates to this + // method which tracks 32-bit millis() rollover using millis_major_ and last_millis_. friend uint64_t millis_64(); uint64_t millis_64_impl_(uint32_t now); #endif @@ -568,8 +567,8 @@ class Scheduler { // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52) std::vector scheduler_item_pool_; -#ifndef USE_ESP32 - // On ESP32, millis_64() uses esp_timer_get_time() directly; no rollover tracking needed. +#if !defined(USE_ESP32) && !defined(USE_HOST) + // On platforms with native 64-bit time (ESP32, Host), no rollover tracking needed. // On other platforms, these fields track 32-bit millis() rollover for millis_64_impl_(). #ifdef ESPHOME_THREAD_MULTI_ATOMICS /* @@ -599,7 +598,7 @@ class Scheduler { #else /* not ESPHOME_THREAD_MULTI_ATOMICS */ uint16_t millis_major_{0}; #endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ -#endif /* not USE_ESP32 */ +#endif /* !USE_ESP32 && !USE_HOST */ }; } // namespace esphome From 9c1d1a0d9fba57b92e083437d422a3e7ca3137ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 12:25:13 -0700 Subject: [PATCH 256/609] [color] Use integer math in Color::gradient to reduce code size (#14354) --- esphome/core/color.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/core/color.cpp b/esphome/core/color.cpp index 14c41c2b0d..edbc771472 100644 --- a/esphome/core/color.cpp +++ b/esphome/core/color.cpp @@ -7,12 +7,12 @@ constinit const Color Color::BLACK(0, 0, 0, 0); constinit const Color Color::WHITE(255, 255, 255, 255); Color Color::gradient(const Color &to_color, uint8_t amnt) { + uint8_t inv = 255 - amnt; Color new_color; - float amnt_f = float(amnt) / 255.0f; - new_color.r = amnt_f * (to_color.r - this->r) + this->r; - new_color.g = amnt_f * (to_color.g - this->g) + this->g; - new_color.b = amnt_f * (to_color.b - this->b) + this->b; - new_color.w = amnt_f * (to_color.w - this->w) + this->w; + new_color.r = (uint16_t(this->r) * inv + uint16_t(to_color.r) * amnt) / 255; + new_color.g = (uint16_t(this->g) * inv + uint16_t(to_color.g) * amnt) / 255; + new_color.b = (uint16_t(this->b) * inv + uint16_t(to_color.b) * amnt) / 255; + new_color.w = (uint16_t(this->w) * inv + uint16_t(to_color.w) * amnt) / 255; return new_color; } From 2255c68377304c81ab75da9fda8b79cbcf5c756c Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 28 Feb 2026 06:40:55 +1100 Subject: [PATCH 257/609] [esp32] Enable `execute_from_psram` for P4 (#14329) --- esphome/components/esp32/__init__.py | 13 +++-- .../config/execute_from_psram_disabled.yaml | 10 ++++ .../esp32/config/execute_from_psram_p4.yaml | 11 ++++ .../esp32/config/execute_from_psram_s3.yaml | 12 ++++ tests/component_tests/esp32/test_esp32.py | 55 ++++++++++++++++++- 5 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 tests/component_tests/esp32/config/execute_from_psram_disabled.yaml create mode 100644 tests/component_tests/esp32/config/execute_from_psram_p4.yaml create mode 100644 tests/component_tests/esp32/config/execute_from_psram_s3.yaml diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index a34747a183..998913ecec 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -890,10 +890,10 @@ def final_validate(config): ) ) if advanced[CONF_EXECUTE_FROM_PSRAM]: - if config[CONF_VARIANT] != VARIANT_ESP32S3: + if config[CONF_VARIANT] not in {VARIANT_ESP32S3, VARIANT_ESP32P4}: errs.append( cv.Invalid( - f"'{CONF_EXECUTE_FROM_PSRAM}' is only supported on {VARIANT_ESP32S3} variant", + f"'{CONF_EXECUTE_FROM_PSRAM}' is not available on this esp32 variant", path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_EXECUTE_FROM_PSRAM], ) ) @@ -1627,8 +1627,13 @@ async def to_code(config): _configure_lwip_max_sockets(conf) if advanced[CONF_EXECUTE_FROM_PSRAM]: - add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True) - add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True) + if variant == VARIANT_ESP32S3: + add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True) + add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True) + elif variant == VARIANT_ESP32P4: + add_idf_sdkconfig_option("CONFIG_SPIRAM_XIP_FROM_PSRAM", True) + else: + raise ValueError("Unhandled ESP32 variant") # Apply LWIP core locking for better socket performance # This is already enabled by default in Arduino framework, where it provides diff --git a/tests/component_tests/esp32/config/execute_from_psram_disabled.yaml b/tests/component_tests/esp32/config/execute_from_psram_disabled.yaml new file mode 100644 index 0000000000..b52255c5ad --- /dev/null +++ b/tests/component_tests/esp32/config/execute_from_psram_disabled.yaml @@ -0,0 +1,10 @@ +esphome: + name: test + +esp32: + variant: esp32s3 + framework: + type: esp-idf + +psram: + mode: octal diff --git a/tests/component_tests/esp32/config/execute_from_psram_p4.yaml b/tests/component_tests/esp32/config/execute_from_psram_p4.yaml new file mode 100644 index 0000000000..e9070a5ae1 --- /dev/null +++ b/tests/component_tests/esp32/config/execute_from_psram_p4.yaml @@ -0,0 +1,11 @@ +esphome: + name: test + +esp32: + variant: esp32p4 + framework: + type: esp-idf + advanced: + execute_from_psram: true + +psram: diff --git a/tests/component_tests/esp32/config/execute_from_psram_s3.yaml b/tests/component_tests/esp32/config/execute_from_psram_s3.yaml new file mode 100644 index 0000000000..aec020ddff --- /dev/null +++ b/tests/component_tests/esp32/config/execute_from_psram_s3.yaml @@ -0,0 +1,12 @@ +esphome: + name: test + +esp32: + variant: esp32s3 + framework: + type: esp-idf + advanced: + execute_from_psram: true + +psram: + mode: octal diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index 68bd3a5965..bd4f9828ce 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -2,13 +2,17 @@ Test ESP32 configuration """ +from collections.abc import Callable +from pathlib import Path from typing import Any import pytest from esphome.components.esp32 import VARIANTS +from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS import esphome.config_validation as cv from esphome.const import CONF_ESPHOME, PlatformFramework +from esphome.core import CORE from tests.component_tests.types import SetCoreConfigCallable @@ -70,7 +74,7 @@ def test_esp32_config( "advanced": {"execute_from_psram": True}, }, }, - r"'execute_from_psram' is only supported on ESP32S3 variant @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]", + r"'execute_from_psram' is not available on this esp32 variant @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]", id="execute_from_psram_invalid_for_variant_config", ), pytest.param( @@ -82,7 +86,18 @@ def test_esp32_config( }, }, r"'execute_from_psram' requires PSRAM to be configured @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]", - id="execute_from_psram_requires_psram_config", + id="execute_from_psram_requires_psram_s3_config", + ), + pytest.param( + { + "variant": "esp32p4", + "framework": { + "type": "esp-idf", + "advanced": {"execute_from_psram": True}, + }, + }, + r"'execute_from_psram' requires PSRAM to be configured @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]", + id="execute_from_psram_requires_psram_p4_config", ), pytest.param( { @@ -108,3 +123,39 @@ def test_esp32_configuration_errors( with pytest.raises(cv.Invalid, match=error_match): FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config)) + + +def test_execute_from_psram_s3_sdkconfig( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Test that execute_from_psram on ESP32-S3 sets the correct sdkconfig options.""" + generate_main(component_config_path("execute_from_psram_s3.yaml")) + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert sdkconfig.get("CONFIG_SPIRAM_FETCH_INSTRUCTIONS") is True + assert sdkconfig.get("CONFIG_SPIRAM_RODATA") is True + assert "CONFIG_SPIRAM_XIP_FROM_PSRAM" not in sdkconfig + + +def test_execute_from_psram_p4_sdkconfig( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Test that execute_from_psram on ESP32-P4 sets the correct sdkconfig options.""" + generate_main(component_config_path("execute_from_psram_p4.yaml")) + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert sdkconfig.get("CONFIG_SPIRAM_XIP_FROM_PSRAM") is True + assert "CONFIG_SPIRAM_FETCH_INSTRUCTIONS" not in sdkconfig + assert "CONFIG_SPIRAM_RODATA" not in sdkconfig + + +def test_execute_from_psram_disabled_sdkconfig( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Test that without execute_from_psram, no XIP sdkconfig options are set.""" + generate_main(component_config_path("execute_from_psram_disabled.yaml")) + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert "CONFIG_SPIRAM_FETCH_INSTRUCTIONS" not in sdkconfig + assert "CONFIG_SPIRAM_RODATA" not in sdkconfig + assert "CONFIG_SPIRAM_XIP_FROM_PSRAM" not in sdkconfig From 32133e2f464ec93c968d44cf97e9e689ac81f1ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:42:20 -0500 Subject: [PATCH 258/609] Bump ruff from 0.15.3 to 0.15.4 (#14357) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 88a38ffa99..6b2617b656 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.3 # also change in .pre-commit-config.yaml when updating +ruff==0.15.4 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From edd63e3d2d82bfc6585c391ebdae25b0313c91e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:43:10 -0500 Subject: [PATCH 259/609] Bump actions/download-artifact from 7.0.0 to 8.0.0 (#14327) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b752701920..4c9e8c58bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -945,13 +945,13 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Download target analysis JSON - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: memory-analysis-target path: ./memory-analysis continue-on-error: true - name: Download PR analysis JSON - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: memory-analysis-pr path: ./memory-analysis diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0cca4dcaf6..17a2616dff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -171,7 +171,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Download digests - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: pattern: digests-* path: /tmp/digests From 63e757807ee2a522f493bc65dd38ced7f3d601dc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 13:01:09 -0700 Subject: [PATCH 260/609] [zephyr] Use native k_uptime_get() for millis_64() (#14350) Co-authored-by: Claude Opus 4.6 --- esphome/components/zephyr/core.cpp | 5 ++--- esphome/core/scheduler.cpp | 10 +++++----- esphome/core/scheduler.h | 10 +++++----- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index 7f5d6d44fa..f0772a4422 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -4,7 +4,6 @@ #include #include #include -#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/defines.h" @@ -17,8 +16,8 @@ static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0)); #endif void yield() { ::k_yield(); } -uint32_t millis() { return k_ticks_to_ms_floor32(k_uptime_ticks()); } -uint64_t millis_64() { return App.scheduler.millis_64_impl_(millis()); } +uint32_t millis() { return static_cast(millis_64()); } +uint64_t millis_64() { return static_cast(k_uptime_get()); } uint32_t micros() { return k_ticks_to_us_floor32(k_uptime_ticks()); } void delayMicroseconds(uint32_t us) { ::k_usleep(us); } void delay(uint32_t ms) { ::k_msleep(ms); } diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 97735c4876..73b6a6c9b3 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -28,7 +28,7 @@ static constexpr size_t MAX_POOL_SIZE = 5; // Set to 5 to match the pool size - when we have as many cancelled items as our // pool can hold, it's time to clean up and recycle them. static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5; -#if !defined(USE_ESP32) && !defined(USE_HOST) +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) // Half the 32-bit range - used to detect rollovers vs normal time progression static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; #endif @@ -475,12 +475,12 @@ void HOT Scheduler::call(uint32_t now) { if (now_64 - last_print > 2000) { last_print = now_64; std::vector old_items; -#if !defined(USE_ESP32) && !defined(USE_HOST) && defined(ESPHOME_THREAD_MULTI_ATOMICS) +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && defined(ESPHOME_THREAD_MULTI_ATOMICS) const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg); -#elif !defined(USE_ESP32) && !defined(USE_HOST) +#elif !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_); #else @@ -714,7 +714,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type return total_cancelled > 0; } -#if !defined(USE_ESP32) && !defined(USE_HOST) +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) uint64_t Scheduler::millis_64_impl_(uint32_t now) { // THREAD SAFETY NOTE: // This function has three implementations, based on the precompiler flags @@ -872,7 +872,7 @@ uint64_t Scheduler::millis_64_impl_(uint32_t now) { "No platform threading model defined. One of ESPHOME_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_MULTI_ATOMICS must be defined." #endif } -#endif // !USE_ESP32 && !USE_HOST +#endif // !USE_ESP32 && !USE_HOST && !USE_ZEPHYR bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) { // High bits are almost always equal (change only on 32-bit rollover ~49 days) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 9a62ac1634..282f4c66ef 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -287,7 +287,7 @@ class Scheduler { // On ESP32, ignores now and uses esp_timer_get_time() directly (native 64-bit). // On non-ESP32, extends now to 64-bit using rollover tracking. uint64_t millis_64_from_(uint32_t now) { -#if defined(USE_ESP32) || defined(USE_HOST) +#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_ZEPHYR) (void) now; return millis_64(); #else @@ -295,7 +295,7 @@ class Scheduler { #endif } -#if !defined(USE_ESP32) && !defined(USE_HOST) +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) // On platforms without native 64-bit time, millis_64() HAL function delegates to this // method which tracks 32-bit millis() rollover using millis_major_ and last_millis_. friend uint64_t millis_64(); @@ -567,8 +567,8 @@ class Scheduler { // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52) std::vector scheduler_item_pool_; -#if !defined(USE_ESP32) && !defined(USE_HOST) - // On platforms with native 64-bit time (ESP32, Host), no rollover tracking needed. +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) + // On platforms with native 64-bit time (ESP32, Host, Zephyr), no rollover tracking needed. // On other platforms, these fields track 32-bit millis() rollover for millis_64_impl_(). #ifdef ESPHOME_THREAD_MULTI_ATOMICS /* @@ -598,7 +598,7 @@ class Scheduler { #else /* not ESPHOME_THREAD_MULTI_ATOMICS */ uint16_t millis_major_{0}; #endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ -#endif /* !USE_ESP32 && !USE_HOST */ +#endif /* !USE_ESP32 && !USE_HOST && !USE_ZEPHYR */ }; } // namespace esphome From 52af4bced0fdbe0c89a21aa8bb826422467f1936 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 13:01:23 -0700 Subject: [PATCH 261/609] [component] Devirtualize call_dump_config (#14355) --- esphome/components/mqtt/mqtt_component.cpp | 6 ------ esphome/components/mqtt/mqtt_component.h | 2 -- esphome/core/application.cpp | 2 +- esphome/core/component.cpp | 2 +- esphome/core/component.h | 2 +- 5 files changed, 3 insertions(+), 11 deletions(-) diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 09570106df..f49069960b 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -405,12 +405,6 @@ void MQTTComponent::process_resend() { this->schedule_resend_state(); } } -void MQTTComponent::call_dump_config() { - if (this->is_internal()) - return; - - this->dump_config(); -} void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; } bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); } diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 853712940a..0ffe6341d3 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -98,8 +98,6 @@ class MQTTComponent : public Component { /// Override setup_ so that we can call send_discovery() when needed. void call_setup() override; - void call_dump_config() override; - /// Send discovery info the Home Assistant, override this. virtual void send_discovery(JsonObject root, SendDiscoveryConfig &config) = 0; diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index e27c2cdfc6..b1ece86701 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -256,7 +256,7 @@ void Application::process_dump_config_() { #endif } - this->components_[this->dump_config_at_]->call_dump_config(); + this->components_[this->dump_config_at_]->call_dump_config_(); this->dump_config_at_++; } diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index b4a19a0776..e14fae5e08 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -211,7 +211,7 @@ bool Component::cancel_retry(uint32_t id) { void Component::call_loop_() { this->loop(); } void Component::call_setup() { this->setup(); } -void Component::call_dump_config() { +void Component::call_dump_config_() { this->dump_config(); if (this->is_failed()) { // Look up error message from global vector diff --git a/esphome/core/component.h b/esphome/core/component.h index 7ea9fdf3b3..2620e8eb2a 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -291,7 +291,7 @@ class Component { void call_loop_(); virtual void call_setup(); - virtual void call_dump_config(); + void call_dump_config_(); /// Helper to set component state (clears state bits and sets new state) void set_component_state_(uint8_t state); From f6755aabae2fad7ea5c082571502baf281c6d8b8 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:18:07 -0500 Subject: [PATCH 262/609] [ci] Add PR title format check (#14345) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/scripts/auto-label-pr/detectors.js | 42 +++++------- .github/scripts/detect-tags.js | 66 +++++++++++++++++++ .github/workflows/pr-title-check.yml | 76 ++++++++++++++++++++++ 3 files changed, 157 insertions(+), 27 deletions(-) create mode 100644 .github/scripts/detect-tags.js create mode 100644 .github/workflows/pr-title-check.yml diff --git a/.github/scripts/auto-label-pr/detectors.js b/.github/scripts/auto-label-pr/detectors.js index a45a84f219..80d8847bc1 100644 --- a/.github/scripts/auto-label-pr/detectors.js +++ b/.github/scripts/auto-label-pr/detectors.js @@ -1,5 +1,12 @@ const fs = require('fs'); const { DOCS_PR_PATTERNS } = require('./constants'); +const { + COMPONENT_REGEX, + detectComponents, + hasCoreChanges, + hasDashboardChanges, + hasGitHubActionsChanges, +} = require('../detect-tags'); // Strategy: Merge branch detection async function detectMergeBranch(context) { @@ -20,15 +27,13 @@ async function detectMergeBranch(context) { // Strategy: Component and platform labeling async function detectComponentPlatforms(changedFiles, apiData) { const labels = new Set(); - const componentRegex = /^esphome\/components\/([^\/]+)\//; const targetPlatformRegex = new RegExp(`^esphome\/components\/(${apiData.targetPlatforms.join('|')})/`); - for (const file of changedFiles) { - const componentMatch = file.match(componentRegex); - if (componentMatch) { - labels.add(`component: ${componentMatch[1]}`); - } + for (const comp of detectComponents(changedFiles)) { + labels.add(`component: ${comp}`); + } + for (const file of changedFiles) { const platformMatch = file.match(targetPlatformRegex); if (platformMatch) { labels.add(`platform: ${platformMatch[1]}`); @@ -90,15 +95,9 @@ async function detectNewPlatforms(prFiles, apiData) { // Strategy: Core files detection async function detectCoreChanges(changedFiles) { const labels = new Set(); - const coreFiles = changedFiles.filter(file => - file.startsWith('esphome/core/') || - (file.startsWith('esphome/') && file.split('/').length === 2) - ); - - if (coreFiles.length > 0) { + if (hasCoreChanges(changedFiles)) { labels.add('core'); } - return labels; } @@ -131,29 +130,18 @@ async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChange // Strategy: Dashboard changes async function detectDashboardChanges(changedFiles) { const labels = new Set(); - const dashboardFiles = changedFiles.filter(file => - file.startsWith('esphome/dashboard/') || - file.startsWith('esphome/components/dashboard_import/') - ); - - if (dashboardFiles.length > 0) { + if (hasDashboardChanges(changedFiles)) { labels.add('dashboard'); } - return labels; } // Strategy: GitHub Actions changes async function detectGitHubActionsChanges(changedFiles) { const labels = new Set(); - const githubActionsFiles = changedFiles.filter(file => - file.startsWith('.github/workflows/') - ); - - if (githubActionsFiles.length > 0) { + if (hasGitHubActionsChanges(changedFiles)) { labels.add('github-actions'); } - return labels; } @@ -259,7 +247,7 @@ async function detectDeprecatedComponents(github, context, changedFiles) { const { owner, repo } = context.repo; // Compile regex once for better performance - const componentFileRegex = /^esphome\/components\/([^\/]+)\//; + const componentFileRegex = COMPONENT_REGEX; // Get files that are modified or added in components directory const componentFiles = changedFiles.filter(file => componentFileRegex.test(file)); diff --git a/.github/scripts/detect-tags.js b/.github/scripts/detect-tags.js new file mode 100644 index 0000000000..3933776c61 --- /dev/null +++ b/.github/scripts/detect-tags.js @@ -0,0 +1,66 @@ +/** + * Shared tag detection from changed file paths. + * Used by pr-title-check and auto-label-pr workflows. + */ + +const COMPONENT_REGEX = /^esphome\/components\/([^\/]+)\//; + +/** + * Detect component names from changed files. + * @param {string[]} changedFiles - List of changed file paths + * @returns {Set} Set of component names + */ +function detectComponents(changedFiles) { + const components = new Set(); + for (const file of changedFiles) { + const match = file.match(COMPONENT_REGEX); + if (match) { + components.add(match[1]); + } + } + return components; +} + +/** + * Detect if core files were changed. + * Core files are in esphome/core/ or top-level esphome/ directory. + * @param {string[]} changedFiles - List of changed file paths + * @returns {boolean} + */ +function hasCoreChanges(changedFiles) { + return changedFiles.some(file => + file.startsWith('esphome/core/') || + (file.startsWith('esphome/') && file.split('/').length === 2) + ); +} + +/** + * Detect if dashboard files were changed. + * @param {string[]} changedFiles - List of changed file paths + * @returns {boolean} + */ +function hasDashboardChanges(changedFiles) { + return changedFiles.some(file => + file.startsWith('esphome/dashboard/') || + file.startsWith('esphome/components/dashboard_import/') + ); +} + +/** + * Detect if GitHub Actions files were changed. + * @param {string[]} changedFiles - List of changed file paths + * @returns {boolean} + */ +function hasGitHubActionsChanges(changedFiles) { + return changedFiles.some(file => + file.startsWith('.github/workflows/') + ); +} + +module.exports = { + COMPONENT_REGEX, + detectComponents, + hasCoreChanges, + hasDashboardChanges, + hasGitHubActionsChanges, +}; diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml new file mode 100644 index 0000000000..f23c2c870e --- /dev/null +++ b/.github/workflows/pr-title-check.yml @@ -0,0 +1,76 @@ +name: PR Title Check + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + contents: read + pull-requests: read + +jobs: + check: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { + detectComponents, + hasCoreChanges, + hasDashboardChanges, + hasGitHubActionsChanges, + } = require('./.github/scripts/detect-tags.js'); + + const title = context.payload.pull_request.title; + + // Block titles starting with "word:" or "word(scope):" patterns + const commitStylePattern = /^\w+(\(.*?\))?[!]?\s*:/; + if (commitStylePattern.test(title)) { + core.setFailed( + `PR title should not start with a "prefix:" style format.\n` + + `Please use the format: [component] Brief description\n` + + `Example: [pn532] Add health checking and auto-reset` + ); + return; + } + + // Get changed files to detect tags + const files = await github.paginate(github.rest.pulls.listFiles, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + const filenames = files.map(f => f.filename); + + // Detect tags from changed files using shared logic + const tags = new Set(); + + for (const comp of detectComponents(filenames)) { + tags.add(comp); + } + if (hasCoreChanges(filenames)) tags.add('core'); + if (hasDashboardChanges(filenames)) tags.add('dashboard'); + if (hasGitHubActionsChanges(filenames)) tags.add('ci'); + + if (tags.size === 0) { + return; + } + + // Check title starts with [tag] prefix + const bracketPattern = /^\[\w+\]/; + if (!bracketPattern.test(title)) { + const suggestion = [...tags].map(c => `[${c}]`).join(''); + // Skip if the suggested prefix would be too long for a readable title + if (suggestion.length > 40) { + return; + } + core.setFailed( + `PR modifies: ${[...tags].join(', ')}\n` + + `Title must start with a [tag] prefix.\n` + + `Suggested: ${suggestion} ` + ); + } From 280f874edc36ce5127d005a526c5f4537d453f70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 14:18:02 -0700 Subject: [PATCH 263/609] [rp2040] Use native time_us_64() for millis_64() (#14356) Co-authored-by: Claude Opus 4.6 --- esphome/components/rp2040/core.cpp | 12 ++++++------ esphome/core/scheduler.cpp | 11 ++++++----- esphome/core/scheduler.h | 14 +++++++------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index 52c6f1185c..8b86de4be1 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -3,19 +3,19 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" -#include "esphome/core/application.h" #include "esphome/core/helpers.h" +#include "hardware/timer.h" #include "hardware/watchdog.h" namespace esphome { void HOT yield() { ::yield(); } -uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); } +uint64_t millis_64() { return time_us_64() / 1000ULL; } +uint32_t HOT millis() { return static_cast(millis_64()); } void HOT delay(uint32_t ms) { ::delay(ms); } -uint32_t IRAM_ATTR HOT micros() { return ::micros(); } -void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } +uint32_t HOT micros() { return ::micros(); } +void HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { watchdog_reboot(0, 0, 10); while (1) { @@ -34,7 +34,7 @@ void HOT arch_feed_wdt() { watchdog_update(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT } -uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); } +uint32_t HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); } uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); } } // namespace esphome diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 73b6a6c9b3..2c10e7e2da 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -28,7 +28,7 @@ static constexpr size_t MAX_POOL_SIZE = 5; // Set to 5 to match the pool size - when we have as many cancelled items as our // pool can hold, it's time to clean up and recycle them. static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5; -#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) // Half the 32-bit range - used to detect rollovers vs normal time progression static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; #endif @@ -475,12 +475,13 @@ void HOT Scheduler::call(uint32_t now) { if (now_64 - last_print > 2000) { last_print = now_64; std::vector old_items; -#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && defined(ESPHOME_THREAD_MULTI_ATOMICS) +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) && \ + defined(ESPHOME_THREAD_MULTI_ATOMICS) const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg); -#elif !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) +#elif !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_); #else @@ -714,7 +715,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type return total_cancelled > 0; } -#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) uint64_t Scheduler::millis_64_impl_(uint32_t now) { // THREAD SAFETY NOTE: // This function has three implementations, based on the precompiler flags @@ -872,7 +873,7 @@ uint64_t Scheduler::millis_64_impl_(uint32_t now) { "No platform threading model defined. One of ESPHOME_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_MULTI_ATOMICS must be defined." #endif } -#endif // !USE_ESP32 && !USE_HOST && !USE_ZEPHYR +#endif // !USE_ESP32 && !USE_HOST && !USE_ZEPHYR && !USE_RP2040 bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) { // High bits are almost always equal (change only on 32-bit rollover ~49 days) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 282f4c66ef..d52cf5147d 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -284,10 +284,10 @@ class Scheduler { bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id); // Extend a 32-bit millis() value to 64-bit. Use when the caller already has a fresh now. - // On ESP32, ignores now and uses esp_timer_get_time() directly (native 64-bit). - // On non-ESP32, extends now to 64-bit using rollover tracking. + // On ESP32, Host, Zephyr, and RP2040, ignores now and uses the native 64-bit time source via millis_64(). + // On other platforms, extends now to 64-bit using rollover tracking. uint64_t millis_64_from_(uint32_t now) { -#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_ZEPHYR) +#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_ZEPHYR) || defined(USE_RP2040) (void) now; return millis_64(); #else @@ -295,7 +295,7 @@ class Scheduler { #endif } -#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) // On platforms without native 64-bit time, millis_64() HAL function delegates to this // method which tracks 32-bit millis() rollover using millis_major_ and last_millis_. friend uint64_t millis_64(); @@ -567,8 +567,8 @@ class Scheduler { // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52) std::vector scheduler_item_pool_; -#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) - // On platforms with native 64-bit time (ESP32, Host, Zephyr), no rollover tracking needed. +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) + // On platforms with native 64-bit time (ESP32, Host, Zephyr, RP2040), no rollover tracking needed. // On other platforms, these fields track 32-bit millis() rollover for millis_64_impl_(). #ifdef ESPHOME_THREAD_MULTI_ATOMICS /* @@ -598,7 +598,7 @@ class Scheduler { #else /* not ESPHOME_THREAD_MULTI_ATOMICS */ uint16_t millis_major_{0}; #endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ -#endif /* !USE_ESP32 && !USE_HOST && !USE_ZEPHYR */ +#endif /* !USE_ESP32 && !USE_HOST && !USE_ZEPHYR && !USE_RP2040 */ }; } // namespace esphome From bb567827a1e2f67a362d0a385868e438df8b71aa Mon Sep 17 00:00:00 2001 From: Laura Wratten Date: Sat, 28 Feb 2026 08:23:32 +1100 Subject: [PATCH 264/609] [sht3xd] Allow sensors that don't support serial number read (#14224) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/sht3xd/sht3xd.cpp | 43 ++++++++-------------------- esphome/components/sht3xd/sht3xd.h | 9 ------ 2 files changed, 12 insertions(+), 40 deletions(-) diff --git a/esphome/components/sht3xd/sht3xd.cpp b/esphome/components/sht3xd/sht3xd.cpp index bd3dec6fb8..8050a2d5f9 100644 --- a/esphome/components/sht3xd/sht3xd.cpp +++ b/esphome/components/sht3xd/sht3xd.cpp @@ -12,6 +12,8 @@ static const char *const TAG = "sht3xd"; // To ensure compatibility, reading serial number using the register with clock stretching register enabled // (used originally in this component) is tried first and if that fails the alternate register address // with clock stretching disabled is read. +// If both fail (e.g. some clones don't support the command), we continue so temp/humidity still work. +// Second attempt uses 10ms delay for boards that need more time before read (max permitted by ESPHome guidelines). static const uint16_t SHT3XD_COMMAND_READ_SERIAL_NUMBER_CLOCK_STRETCHING = 0x3780; static const uint16_t SHT3XD_COMMAND_READ_SERIAL_NUMBER = 0x3682; @@ -25,49 +27,28 @@ static const uint16_t SHT3XD_COMMAND_POLLING_H = 0x2400; static const uint16_t SHT3XD_COMMAND_FETCH_DATA = 0xE000; void SHT3XDComponent::setup() { - uint16_t raw_serial_number[2]; + uint16_t raw_serial_number[2]{0}; if (!this->get_register(SHT3XD_COMMAND_READ_SERIAL_NUMBER_CLOCK_STRETCHING, raw_serial_number, 2)) { - this->error_code_ = READ_SERIAL_STRETCHED_FAILED; - if (!this->get_register(SHT3XD_COMMAND_READ_SERIAL_NUMBER, raw_serial_number, 2)) { - this->error_code_ = READ_SERIAL_FAILED; - this->mark_failed(); - return; + if (!this->get_register(SHT3XD_COMMAND_READ_SERIAL_NUMBER, raw_serial_number, 2, 10)) { + ESP_LOGW(TAG, "Serial number read failed, continuing without it (clone or non-standard sensor)"); } } - this->serial_number_ = (uint32_t(raw_serial_number[0]) << 16) | uint32_t(raw_serial_number[1]); - if (!this->write_command(heater_enabled_ ? SHT3XD_COMMAND_HEATER_ENABLE : SHT3XD_COMMAND_HEATER_DISABLE)) { - this->error_code_ = WRITE_HEATER_MODE_FAILED; - this->mark_failed(); + if (!this->write_command(this->heater_enabled_ ? SHT3XD_COMMAND_HEATER_ENABLE : SHT3XD_COMMAND_HEATER_DISABLE)) { + this->mark_failed(LOG_STR("Failed to set heater mode")); return; } } void SHT3XDComponent::dump_config() { - ESP_LOGCONFIG(TAG, "SHT3xD:"); - switch (this->error_code_) { - case READ_SERIAL_FAILED: - ESP_LOGD(TAG, " Error reading serial number"); - break; - case WRITE_HEATER_MODE_FAILED: - ESP_LOGD(TAG, " Error writing heater mode"); - break; - default: - break; - } - if (this->is_failed()) { - ESP_LOGE(TAG, " Communication with SHT3xD failed!"); - return; - } - ESP_LOGD(TAG, - " Serial Number: 0x%08" PRIX32 "\n" - " Heater Enabled: %s", - this->serial_number_, TRUEFALSE(this->heater_enabled_)); - + ESP_LOGCONFIG(TAG, + "SHT3xD:\n" + " Serial Number: 0x%08" PRIX32 "\n" + " Heater Enabled: %s", + this->serial_number_, TRUEFALSE(this->heater_enabled_)); LOG_I2C_DEVICE(this); LOG_UPDATE_INTERVAL(this); - LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } diff --git a/esphome/components/sht3xd/sht3xd.h b/esphome/components/sht3xd/sht3xd.h index 43f1a4d8e2..54514d6de7 100644 --- a/esphome/components/sht3xd/sht3xd.h +++ b/esphome/components/sht3xd/sht3xd.h @@ -4,8 +4,6 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" -#include - namespace esphome { namespace sht3xd { @@ -21,13 +19,6 @@ class SHT3XDComponent : public PollingComponent, public sensirion_common::Sensir void set_heater_enabled(bool heater_enabled) { heater_enabled_ = heater_enabled; } protected: - enum ErrorCode { - NONE = 0, - READ_SERIAL_STRETCHED_FAILED, - READ_SERIAL_FAILED, - WRITE_HEATER_MODE_FAILED, - } error_code_{NONE}; - sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; bool heater_enabled_{true}; From 5e3857abf7dded7d731cdd5663b23ca0edb6491f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:25:36 -0500 Subject: [PATCH 265/609] Bump click from 8.1.7 to 8.3.1 (#11955) Signed-off-by: dependabot[bot] 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 95e3710f9e..f111e05a9d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ tzdata>=2021.1 # from time pyserial==3.5 platformio==6.1.19 esptool==5.2.0 -click==8.1.7 +click==8.3.1 esphome-dashboard==20260210.0 aioesphomeapi==44.2.0 zeroconf==0.148.0 From b9d70dcda2d5ff6bad34c6c72ae12f5c9b9cd6e6 Mon Sep 17 00:00:00 2001 From: Martin Ebner <185941678+mebner86@users.noreply.github.com> Date: Sat, 28 Feb 2026 03:11:28 +0530 Subject: [PATCH 266/609] [sen6x] Add SEN6x sensor support (#12553) Co-authored-by: Martin Ebner Co-authored-by: Tobias Stanzel Co-authored-by: Big Mike Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/sen6x/__init__.py | 0 esphome/components/sen6x/sen6x.cpp | 376 +++++++++++++++++++ esphome/components/sen6x/sen6x.h | 43 +++ esphome/components/sen6x/sensor.py | 149 ++++++++ tests/components/sen6x/common.yaml | 37 ++ tests/components/sen6x/test.esp32-idf.yaml | 4 + tests/components/sen6x/test.esp8266-ard.yaml | 4 + tests/components/sen6x/test.rp2040-ard.yaml | 4 + 9 files changed, 618 insertions(+) create mode 100644 esphome/components/sen6x/__init__.py create mode 100644 esphome/components/sen6x/sen6x.cpp create mode 100644 esphome/components/sen6x/sen6x.h create mode 100644 esphome/components/sen6x/sensor.py create mode 100644 tests/components/sen6x/common.yaml create mode 100644 tests/components/sen6x/test.esp32-idf.yaml create mode 100644 tests/components/sen6x/test.esp8266-ard.yaml create mode 100644 tests/components/sen6x/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 6728e76bba..4c97b7f99d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -429,6 +429,7 @@ esphome/components/select/* @esphome/core esphome/components/sen0321/* @notjj esphome/components/sen21231/* @shreyaskarnik esphome/components/sen5x/* @martgras +esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core esphome/components/sfa30/* @ghsensdev diff --git a/esphome/components/sen6x/__init__.py b/esphome/components/sen6x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sen6x/sen6x.cpp b/esphome/components/sen6x/sen6x.cpp new file mode 100644 index 0000000000..baaadd6463 --- /dev/null +++ b/esphome/components/sen6x/sen6x.cpp @@ -0,0 +1,376 @@ +#include "sen6x.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include +#include +#include + +namespace esphome::sen6x { + +static const char *const TAG = "sen6x"; + +static constexpr uint16_t SEN6X_CMD_GET_DATA_READY_STATUS = 0x0202; +static constexpr uint16_t SEN6X_CMD_GET_FIRMWARE_VERSION = 0xD100; +static constexpr uint16_t SEN6X_CMD_GET_PRODUCT_NAME = 0xD014; +static constexpr uint16_t SEN6X_CMD_GET_SERIAL_NUMBER = 0xD033; + +static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT = 0x0300; // SEN66 only! +static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN62 = 0x04A3; +static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN63C = 0x0471; +static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN65 = 0x0446; +static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN68 = 0x0467; +static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN69C = 0x04B5; + +static constexpr uint16_t SEN6X_CMD_START_MEASUREMENTS = 0x0021; +static constexpr uint16_t SEN6X_CMD_RESET = 0xD304; + +static inline void set_read_command_and_words(SEN6XComponent::Sen6xType type, uint16_t &read_cmd, uint8_t &read_words) { + read_cmd = SEN6X_CMD_READ_MEASUREMENT; + read_words = 9; + switch (type) { + case SEN6XComponent::SEN62: + read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN62; + read_words = 6; + break; + case SEN6XComponent::SEN63C: + read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN63C; + read_words = 7; + break; + case SEN6XComponent::SEN65: + read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN65; + read_words = 8; + break; + case SEN6XComponent::SEN66: + read_cmd = SEN6X_CMD_READ_MEASUREMENT; + read_words = 9; + break; + case SEN6XComponent::SEN68: + read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN68; + read_words = 9; + break; + case SEN6XComponent::SEN69C: + read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN69C; + read_words = 10; + break; + default: + break; + } +} + +void SEN6XComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up sen6x..."); + + // the sensor needs 100 ms to enter the idle state + this->set_timeout(100, [this]() { + // Reset the sensor to ensure a clean state regardless of prior commands or power issues + if (!this->write_command(SEN6X_CMD_RESET)) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + + // After reset the sensor needs 100 ms to become ready + this->set_timeout(100, [this]() { + // Step 1: Read serial number (~25ms with I2C delay) + uint16_t raw_serial_number[16]; + if (!this->get_register(SEN6X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 16, 20)) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + this->serial_number_ = SEN6XComponent::sensirion_convert_to_string_in_place(raw_serial_number, 16); + ESP_LOGI(TAG, "Serial number: %s", this->serial_number_.c_str()); + + // Step 2: Read product name in next loop iteration + this->set_timeout(0, [this]() { + uint16_t raw_product_name[16]; + if (!this->get_register(SEN6X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + + this->product_name_ = SEN6XComponent::sensirion_convert_to_string_in_place(raw_product_name, 16); + + Sen6xType inferred_type = this->infer_type_from_product_name_(this->product_name_); + if (this->sen6x_type_ == UNKNOWN) { + this->sen6x_type_ = inferred_type; + if (inferred_type == UNKNOWN) { + ESP_LOGE(TAG, "Unknown product '%s'", this->product_name_.c_str()); + this->mark_failed(); + return; + } + ESP_LOGD(TAG, "Type inferred from product: %s", this->product_name_.c_str()); + } else if (this->sen6x_type_ != inferred_type && inferred_type != UNKNOWN) { + ESP_LOGW(TAG, "Configured type (used) mismatches product '%s'", this->product_name_.c_str()); + } + ESP_LOGI(TAG, "Product: %s", this->product_name_.c_str()); + + // Validate configured sensors against detected type and disable unsupported ones + const bool has_voc_nox = (this->sen6x_type_ == SEN65 || this->sen6x_type_ == SEN66 || + this->sen6x_type_ == SEN68 || this->sen6x_type_ == SEN69C); + const bool has_co2 = (this->sen6x_type_ == SEN63C || this->sen6x_type_ == SEN66 || this->sen6x_type_ == SEN69C); + const bool has_hcho = (this->sen6x_type_ == SEN68 || this->sen6x_type_ == SEN69C); + if (this->voc_sensor_ && !has_voc_nox) { + ESP_LOGE(TAG, "VOC requires SEN65, SEN66, SEN68, or SEN69C"); + this->voc_sensor_ = nullptr; + } + if (this->nox_sensor_ && !has_voc_nox) { + ESP_LOGE(TAG, "NOx requires SEN65, SEN66, SEN68, or SEN69C"); + this->nox_sensor_ = nullptr; + } + if (this->co2_sensor_ && !has_co2) { + ESP_LOGE(TAG, "CO2 requires SEN63C, SEN66, or SEN69C"); + this->co2_sensor_ = nullptr; + } + if (this->hcho_sensor_ && !has_hcho) { + ESP_LOGE(TAG, "Formaldehyde requires SEN68 or SEN69C"); + this->hcho_sensor_ = nullptr; + } + + // Step 3: Read firmware version and start measurements in next loop iteration + this->set_timeout(0, [this]() { + uint16_t raw_firmware_version = 0; + if (!this->get_register(SEN6X_CMD_GET_FIRMWARE_VERSION, raw_firmware_version, 20)) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + this->firmware_version_major_ = (raw_firmware_version >> 8) & 0xFF; + this->firmware_version_minor_ = raw_firmware_version & 0xFF; + ESP_LOGI(TAG, "Firmware: %u.%u", this->firmware_version_major_, this->firmware_version_minor_); + + if (!this->write_command(SEN6X_CMD_START_MEASUREMENTS)) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + + this->set_timeout(60000, [this]() { this->startup_complete_ = true; }); + this->initialized_ = true; + ESP_LOGD(TAG, "Initialized"); + }); + }); + }); + }); +} + +void SEN6XComponent::dump_config() { + ESP_LOGCONFIG(TAG, + "sen6x:\n" + " Product: %s\n" + " Serial: %s\n" + " Firmware: %u.%u\n" + " Address: 0x%02X", + this->product_name_.c_str(), this->serial_number_.c_str(), this->firmware_version_major_, + this->firmware_version_minor_, this->address_); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_); + LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_); + LOG_SENSOR(" ", "PM 4.0", this->pm_4_0_sensor_); + LOG_SENSOR(" ", "PM 10.0", this->pm_10_0_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + LOG_SENSOR(" ", "VOC", this->voc_sensor_); + LOG_SENSOR(" ", "NOx", this->nox_sensor_); + LOG_SENSOR(" ", "HCHO", this->hcho_sensor_); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); +} + +void SEN6XComponent::update() { + if (!this->initialized_) { + return; + } + + uint16_t read_cmd; + uint8_t read_words; + set_read_command_and_words(this->sen6x_type_, read_cmd, read_words); + + const uint8_t poll_retries = 24; + auto poll_ready = std::make_shared>(); + *poll_ready = [this, poll_ready, read_cmd, read_words](uint8_t retries_left) { + const uint8_t attempt = static_cast(poll_retries - retries_left + 1); + ESP_LOGV(TAG, "Data ready polling attempt %u", attempt); + + if (!this->write_command(SEN6X_CMD_GET_DATA_READY_STATUS)) { + this->status_set_warning(); + ESP_LOGD(TAG, "write data ready status error (%d)", this->last_error_); + return; + } + + this->set_timeout(20, [this, poll_ready, retries_left, read_cmd, read_words]() { + uint16_t raw_read_status; + if (!this->read_data(&raw_read_status, 1)) { + this->status_set_warning(); + ESP_LOGD(TAG, "read data ready status error (%d)", this->last_error_); + return; + } + + if ((raw_read_status & 0x0001) == 0) { + if (retries_left == 0) { + this->status_set_warning(); + ESP_LOGD(TAG, "Data not ready"); + return; + } + this->set_timeout(50, [poll_ready, retries_left]() { (*poll_ready)(retries_left - 1); }); + return; + } + + if (!this->write_command(read_cmd)) { + this->status_set_warning(); + ESP_LOGD(TAG, "Read measurement failed (%d)", this->last_error_); + return; + } + + this->set_timeout(20, [this, read_words]() { + uint16_t measurements[10]; + + if (!this->read_data(measurements, read_words)) { + this->status_set_warning(); + ESP_LOGD(TAG, "Read data failed (%d)", this->last_error_); + return; + } + int8_t voc_index = -1; + int8_t nox_index = -1; + int8_t hcho_index = -1; + int8_t co2_index = -1; + bool co2_uint16 = false; + switch (this->sen6x_type_) { + case SEN62: + break; + case SEN63C: + co2_index = 6; + break; + case SEN65: + voc_index = 6; + nox_index = 7; + break; + case SEN66: + voc_index = 6; + nox_index = 7; + co2_index = 8; + co2_uint16 = true; + break; + case SEN68: + voc_index = 6; + nox_index = 7; + hcho_index = 8; + break; + case SEN69C: + voc_index = 6; + nox_index = 7; + hcho_index = 8; + co2_index = 9; + break; + default: + break; + } + + float pm_1_0 = measurements[0] / 10.0f; + if (measurements[0] == 0xFFFF) + pm_1_0 = NAN; + float pm_2_5 = measurements[1] / 10.0f; + if (measurements[1] == 0xFFFF) + pm_2_5 = NAN; + float pm_4_0 = measurements[2] / 10.0f; + if (measurements[2] == 0xFFFF) + pm_4_0 = NAN; + float pm_10_0 = measurements[3] / 10.0f; + if (measurements[3] == 0xFFFF) + pm_10_0 = NAN; + float humidity = static_cast(measurements[4]) / 100.0f; + if (measurements[4] == 0x7FFF) + humidity = NAN; + float temperature = static_cast(measurements[5]) / 200.0f; + if (measurements[5] == 0x7FFF) + temperature = NAN; + + float voc = NAN; + float nox = NAN; + float hcho = NAN; + float co2 = NAN; + + if (voc_index >= 0) { + voc = static_cast(measurements[voc_index]) / 10.0f; + if (measurements[voc_index] == 0x7FFF) + voc = NAN; + } + if (nox_index >= 0) { + nox = static_cast(measurements[nox_index]) / 10.0f; + if (measurements[nox_index] == 0x7FFF) + nox = NAN; + } + + if (hcho_index >= 0) { + const uint16_t hcho_raw = measurements[hcho_index]; + hcho = hcho_raw / 10.0f; + if (hcho_raw == 0xFFFF) + hcho = NAN; + } + + if (co2_index >= 0) { + if (co2_uint16) { + const uint16_t co2_raw = measurements[co2_index]; + co2 = static_cast(co2_raw); + if (co2_raw == 0xFFFF) + co2 = NAN; + } else { + const int16_t co2_raw = static_cast(measurements[co2_index]); + co2 = static_cast(co2_raw); + if (co2_raw == 0x7FFF) + co2 = NAN; + } + } + + if (!this->startup_complete_) { + ESP_LOGD(TAG, "Startup delay, ignoring values"); + this->status_clear_warning(); + return; + } + + if (this->pm_1_0_sensor_ != nullptr) + this->pm_1_0_sensor_->publish_state(pm_1_0); + if (this->pm_2_5_sensor_ != nullptr) + this->pm_2_5_sensor_->publish_state(pm_2_5); + if (this->pm_4_0_sensor_ != nullptr) + this->pm_4_0_sensor_->publish_state(pm_4_0); + if (this->pm_10_0_sensor_ != nullptr) + this->pm_10_0_sensor_->publish_state(pm_10_0); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); + if (this->voc_sensor_ != nullptr) + this->voc_sensor_->publish_state(voc); + if (this->nox_sensor_ != nullptr) + this->nox_sensor_->publish_state(nox); + if (this->hcho_sensor_ != nullptr) + this->hcho_sensor_->publish_state(hcho); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(co2); + + this->status_clear_warning(); + }); + }); + }; + + (*poll_ready)(poll_retries); +} + +SEN6XComponent::Sen6xType SEN6XComponent::infer_type_from_product_name_(const std::string &product_name) { + if (product_name == "SEN62") + return SEN62; + if (product_name == "SEN63C") + return SEN63C; + if (product_name == "SEN65") + return SEN65; + if (product_name == "SEN66") + return SEN66; + if (product_name == "SEN68") + return SEN68; + if (product_name == "SEN69C") + return SEN69C; + return UNKNOWN; +} + +} // namespace esphome::sen6x diff --git a/esphome/components/sen6x/sen6x.h b/esphome/components/sen6x/sen6x.h new file mode 100644 index 0000000000..01e89dce1b --- /dev/null +++ b/esphome/components/sen6x/sen6x.h @@ -0,0 +1,43 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" + +namespace esphome::sen6x { + +class SEN6XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { + SUB_SENSOR(pm_1_0) + SUB_SENSOR(pm_2_5) + SUB_SENSOR(pm_4_0) + SUB_SENSOR(pm_10_0) + SUB_SENSOR(temperature) + SUB_SENSOR(humidity) + SUB_SENSOR(voc) + SUB_SENSOR(nox) + SUB_SENSOR(co2) + SUB_SENSOR(hcho) + + public: + float get_setup_priority() const override { return setup_priority::DATA; } + void setup() override; + void dump_config() override; + void update() override; + + enum Sen6xType { SEN62, SEN63C, SEN65, SEN66, SEN68, SEN69C, UNKNOWN }; + + void set_type(const std::string &type) { sen6x_type_ = infer_type_from_product_name_(type); } + + protected: + Sen6xType infer_type_from_product_name_(const std::string &product_name); + + bool initialized_{false}; + std::string product_name_; + Sen6xType sen6x_type_{UNKNOWN}; + std::string serial_number_; + uint8_t firmware_version_major_{0}; + uint8_t firmware_version_minor_{0}; + bool startup_complete_{false}; +}; + +} // namespace esphome::sen6x diff --git a/esphome/components/sen6x/sensor.py b/esphome/components/sen6x/sensor.py new file mode 100644 index 0000000000..071478e719 --- /dev/null +++ b/esphome/components/sen6x/sensor.py @@ -0,0 +1,149 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensirion_common, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_CO2, + CONF_FORMALDEHYDE, + CONF_HUMIDITY, + CONF_ID, + CONF_NOX, + CONF_PM_1_0, + CONF_PM_2_5, + CONF_PM_4_0, + CONF_PM_10_0, + CONF_TEMPERATURE, + CONF_TYPE, + CONF_VOC, + DEVICE_CLASS_AQI, + DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, + DEVICE_CLASS_TEMPERATURE, + ICON_CHEMICAL_WEAPON, + ICON_MOLECULE_CO2, + ICON_RADIATOR, + ICON_THERMOMETER, + ICON_WATER_PERCENT, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_MICROGRAMS_PER_CUBIC_METER, + UNIT_PARTS_PER_MILLION, + UNIT_PERCENT, +) + +CODEOWNERS = ["@martgras", "@mebner86", "@mikelawrence", "@tuct"] +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] + +sen6x_ns = cg.esphome_ns.namespace("sen6x") +SEN6XComponent = sen6x_ns.class_( + "SEN6XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SEN6XComponent), + cv.Optional(CONF_TYPE): cv.one_of( + "SEN62", "SEN63C", "SEN65", "SEN66", "SEN68", "SEN69C", upper=True + ), + cv.Optional(CONF_PM_1_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM1, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_2_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_4_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_10_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM10, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_VOC): sensor.sensor_schema( + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_AQI, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_NOX): sensor.sensor_schema( + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_AQI, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CO2): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema( + unit_of_measurement="ppb", + icon=ICON_RADIATOR, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x6B)) +) + +SENSOR_MAP = { + CONF_PM_1_0: "set_pm_1_0_sensor", + CONF_PM_2_5: "set_pm_2_5_sensor", + CONF_PM_4_0: "set_pm_4_0_sensor", + CONF_PM_10_0: "set_pm_10_0_sensor", + CONF_TEMPERATURE: "set_temperature_sensor", + CONF_HUMIDITY: "set_humidity_sensor", + CONF_VOC: "set_voc_sensor", + CONF_NOX: "set_nox_sensor", + CONF_CO2: "set_co2_sensor", + CONF_FORMALDEHYDE: "set_hcho_sensor", +} + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_TYPE in config: + cg.add(var.set_type(config[CONF_TYPE])) + + for key, func_name in SENSOR_MAP.items(): + if cfg := config.get(key): + sens = await sensor.new_sensor(cfg) + cg.add(getattr(var, func_name)(sens)) diff --git a/tests/components/sen6x/common.yaml b/tests/components/sen6x/common.yaml new file mode 100644 index 0000000000..fdb8a485e2 --- /dev/null +++ b/tests/components/sen6x/common.yaml @@ -0,0 +1,37 @@ +sensor: + # Test with explicit type parameter + - platform: sen6x + id: sen6x_sensor + type: SEN69C + i2c_id: i2c_bus + temperature: + name: Temperature + accuracy_decimals: 1 + humidity: + name: Humidity + accuracy_decimals: 0 + pm_1_0: + name: PM <1µm Weight concentration + id: pm_1_0 + accuracy_decimals: 1 + pm_2_5: + name: PM <2.5µm Weight concentration + id: pm_2_5 + accuracy_decimals: 1 + pm_4_0: + name: PM <4µm Weight concentration + id: pm_4_0 + accuracy_decimals: 1 + pm_10_0: + name: PM <10µm Weight concentration + id: pm_10_0 + accuracy_decimals: 1 + nox: + name: NOx + voc: + name: VOC + co2: + name: Carbon Dioxide + formaldehyde: + name: Formaldehyde + address: 0x6B diff --git a/tests/components/sen6x/test.esp32-idf.yaml b/tests/components/sen6x/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/sen6x/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/sen6x/test.esp8266-ard.yaml b/tests/components/sen6x/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/sen6x/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/sen6x/test.rp2040-ard.yaml b/tests/components/sen6x/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/sen6x/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml From 5c56b99742a12cfde9cbeb0f91641db96cd132d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 16:19:11 -0700 Subject: [PATCH 267/609] [ci] Fix C++ unit tests missing time component dependency (#14364) --- script/cpp_unit_test.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/script/cpp_unit_test.py b/script/cpp_unit_test.py index 78b65092ae..02b133060a 100755 --- a/script/cpp_unit_test.py +++ b/script/cpp_unit_test.py @@ -98,8 +98,12 @@ def run_tests(selected_components: list[str]) -> int: components = sorted(components) - # Obtain possible dependencies for the requested components: - components_with_dependencies = sorted(get_all_dependencies(set(components))) + # Obtain possible dependencies for the requested components. + # Always include 'time' because USE_TIME_TIMEZONE is defined as a build flag, + # which causes core/time.h to include components/time/posix_tz.h. + components_with_dependencies = sorted( + get_all_dependencies(set(components) | {"time"}) + ) # Build a list of include folders, one folder per component containing tests. # A special replacement main.cpp is located in /tests/components/main.cpp From 298ee7b92e4d0a1ad17fcd3995a689deadcb890f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:08:42 -0500 Subject: [PATCH 268/609] [gps] Fix codegen deadlock when automations reference sibling sensors (#14365) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/gps/__init__.py | 44 ++++++++++++------------------ tests/components/gps/common.yaml | 7 +++++ 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index 2135189bd5..045a5a6c84 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -98,33 +98,25 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) - if latitude_config := config.get(CONF_LATITUDE): - sens = await sensor.new_sensor(latitude_config) - cg.add(var.set_latitude_sensor(sens)) + # Pre-create all sensor variables so automations that reference + # sibling sensors don't deadlock waiting for unregistered IDs. + sensors = [ + (cg.new_Pvariable(conf[CONF_ID]), conf, setter) + for key, setter in ( + (CONF_LATITUDE, "set_latitude_sensor"), + (CONF_LONGITUDE, "set_longitude_sensor"), + (CONF_SPEED, "set_speed_sensor"), + (CONF_COURSE, "set_course_sensor"), + (CONF_ALTITUDE, "set_altitude_sensor"), + (CONF_SATELLITES, "set_satellites_sensor"), + (CONF_HDOP, "set_hdop_sensor"), + ) + if (conf := config.get(key)) + ] - if longitude_config := config.get(CONF_LONGITUDE): - sens = await sensor.new_sensor(longitude_config) - cg.add(var.set_longitude_sensor(sens)) - - if speed_config := config.get(CONF_SPEED): - sens = await sensor.new_sensor(speed_config) - cg.add(var.set_speed_sensor(sens)) - - if course_config := config.get(CONF_COURSE): - sens = await sensor.new_sensor(course_config) - cg.add(var.set_course_sensor(sens)) - - if altitude_config := config.get(CONF_ALTITUDE): - sens = await sensor.new_sensor(altitude_config) - cg.add(var.set_altitude_sensor(sens)) - - if satellites_config := config.get(CONF_SATELLITES): - sens = await sensor.new_sensor(satellites_config) - cg.add(var.set_satellites_sensor(sens)) - - if hdop_config := config.get(CONF_HDOP): - sens = await sensor.new_sensor(hdop_config) - cg.add(var.set_hdop_sensor(sens)) + for sens, conf, setter in sensors: + await sensor.register_sensor(sens, conf) + cg.add(getattr(var, setter)(sens)) # https://platformio.org/lib/show/1655/TinyGPSPlus # Using fork of TinyGPSPlus patched to build on ESP-IDF diff --git a/tests/components/gps/common.yaml b/tests/components/gps/common.yaml index a99e3ef7e0..568f2cc7c7 100644 --- a/tests/components/gps/common.yaml +++ b/tests/components/gps/common.yaml @@ -1,8 +1,15 @@ gps: latitude: name: "Latitude" + id: gps_lat + on_value: + then: + - logger.log: + format: "%.6f, %.6f" + args: [id(gps_lat).state, id(gps_long).state] longitude: name: "Longitude" + id: gps_long altitude: name: "Altitude" speed: From d1b481319773a38d4a24cae0a4e7c86ea0ab1908 Mon Sep 17 00:00:00 2001 From: Ryan Wagoner <8441200+rwagoner@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:20:13 -0500 Subject: [PATCH 269/609] [web_server] Add climate preset, fan mode, and humidity support (#14061) --- esphome/components/web_server/web_server.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 16674321c9..4824e33dcd 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1490,6 +1490,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url parse_string_param_(request, ESPHOME_F("mode"), call, &decltype(call)::set_mode); parse_string_param_(request, ESPHOME_F("fan_mode"), call, &decltype(call)::set_fan_mode); parse_string_param_(request, ESPHOME_F("swing_mode"), call, &decltype(call)::set_swing_mode); + parse_string_param_(request, ESPHOME_F("preset"), call, &decltype(call)::set_preset); // Parse temperature parameters // static_cast needed to disambiguate overloaded setters (float vs optional) @@ -1530,7 +1531,7 @@ json::SerializationBuffer<> WebServer::climate_json_(climate::Climate *obj, Json JsonArray opt = root[ESPHOME_F("modes")].to(); for (climate::ClimateMode m : traits.get_supported_modes()) opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m))); - if (!traits.get_supported_custom_fan_modes().empty()) { + if (traits.get_supports_fan_modes()) { JsonArray opt = root[ESPHOME_F("fan_modes")].to(); for (climate::ClimateFanMode m : traits.get_supported_fan_modes()) opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m))); @@ -1546,12 +1547,12 @@ json::SerializationBuffer<> WebServer::climate_json_(climate::Climate *obj, Json for (auto swing_mode : traits.get_supported_swing_modes()) opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode))); } - if (traits.get_supports_presets() && obj->preset.has_value()) { + if (traits.get_supports_presets()) { JsonArray opt = root[ESPHOME_F("presets")].to(); for (climate::ClimatePreset m : traits.get_supported_presets()) opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); } - if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) { + if (!traits.get_supported_custom_presets().empty()) { JsonArray opt = root[ESPHOME_F("custom_presets")].to(); for (auto const &custom_preset : traits.get_supported_custom_presets()) opt.add(custom_preset); @@ -1592,6 +1593,11 @@ json::SerializationBuffer<> WebServer::climate_json_(climate::Climate *obj, Json ? "NA" : (value_accuracy_to_buf(temp_buf, obj->current_temperature, current_accuracy), temp_buf); } + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) { + root[ESPHOME_F("current_humidity")] = std::isnan(obj->current_humidity) + ? "NA" + : (value_accuracy_to_buf(temp_buf, obj->current_humidity, 0), temp_buf); + } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { root[ESPHOME_F("target_temperature_low")] = From e7d4f2608b20d4a7f1c2ea0bd99c8c25cd04e6a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 16:01:17 -1000 Subject: [PATCH 270/609] [sen6x] Fix test sensor ID collisions with sen5x (#14367) --- tests/components/sen6x/common.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/sen6x/common.yaml b/tests/components/sen6x/common.yaml index fdb8a485e2..61ff9f1e0c 100644 --- a/tests/components/sen6x/common.yaml +++ b/tests/components/sen6x/common.yaml @@ -12,19 +12,19 @@ sensor: accuracy_decimals: 0 pm_1_0: name: PM <1µm Weight concentration - id: pm_1_0 + id: sen6x_pm_1_0 accuracy_decimals: 1 pm_2_5: name: PM <2.5µm Weight concentration - id: pm_2_5 + id: sen6x_pm_2_5 accuracy_decimals: 1 pm_4_0: name: PM <4µm Weight concentration - id: pm_4_0 + id: sen6x_pm_4_0 accuracy_decimals: 1 pm_10_0: name: PM <10µm Weight concentration - id: pm_10_0 + id: sen6x_pm_10_0 accuracy_decimals: 1 nox: name: NOx From 8480e8df9f0b834ebeb2694b30e15eede8b716a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 17:27:51 -1000 Subject: [PATCH 271/609] [uart] Revert UART0 default pin workarounds (fixed in ESP-IDF 5.5.2) (#14363) --- .../uart/uart_component_esp_idf.cpp | 36 +++---------------- 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index ea7a09fee6..8699d37d7a 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -9,7 +9,6 @@ #include "esphome/core/gpio.h" #include "driver/gpio.h" #include "soc/gpio_num.h" -#include "soc/uart_pins.h" #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -19,13 +18,6 @@ namespace esphome::uart { static const char *const TAG = "uart.idf"; -/// Check if a pin number matches one of the default UART0 GPIO pins. -/// These pins may have residual state from the boot console that requires -/// explicit reset before UART reconfiguration (ESP-IDF issue #17459). -static constexpr bool is_default_uart0_pin(int8_t pin_num) { - return pin_num == U0TXD_GPIO_NUM || pin_num == U0RXD_GPIO_NUM; -} - uart_config_t IDFUARTComponent::get_config_() { uart_parity_t parity = UART_PARITY_DISABLE; if (this->parity_ == UART_CONFIG_PARITY_EVEN) { @@ -149,34 +141,12 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } - int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; - int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; - int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; - - // Workaround for ESP-IDF issue: https://github.com/espressif/esp-idf/issues/17459 - // Commit 9ed617fb17 removed gpio_func_sel() calls from uart_set_pin(), which breaks - // UART on default UART0 pins that may have residual state from boot console. - // Reset these pins before configuring UART to ensure they're in a clean state. - if (is_default_uart0_pin(tx)) { - gpio_reset_pin(static_cast(tx)); - } - if (is_default_uart0_pin(rx)) { - gpio_reset_pin(static_cast(rx)); - } - - // Setup pins after reset to configure GPIO direction and pull resistors. - // For UART0 default pins, setup() must always be called because gpio_reset_pin() - // above sets GPIO_MODE_DISABLE which disables the input buffer. Without setup(), - // uart_set_pin() on ESP-IDF 5.4.2+ does not re-enable the input buffer for - // IOMUX-connected pins, so the RX pin cannot receive data (see issue #10132). - // For other pins, only call setup() if pull or open-drain flags are set to avoid - // disturbing the default pin state which breaks some external components (#11823). auto setup_pin_if_needed = [](InternalGPIOPin *pin) { if (!pin) { return; } const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN; - if (is_default_uart0_pin(pin->get_pin()) || (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { + if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { pin->setup(); } }; @@ -186,6 +156,10 @@ void IDFUARTComponent::load_settings(bool dump_config) { setup_pin_if_needed(this->tx_pin_); } + int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; + int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; + int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; + uint32_t invert = 0; if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) { invert |= UART_SIGNAL_TXD_INV; From 49cc389bf0e0e4853b4bbc5de38f89b6224ee28f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 17:28:05 -1000 Subject: [PATCH 272/609] [esp32] Wrap printf/vprintf/fprintf to eliminate _vfprintf_r (~11 KB flash) (#14362) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32/__init__.py | 10 +++ esphome/components/esp32/printf_stubs.cpp | 85 ++++++++++++++++++++++ tests/components/esp32/test.esp32-idf.yaml | 1 + 3 files changed, 96 insertions(+) create mode 100644 esphome/components/esp32/printf_stubs.cpp diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 998913ecec..dd9e394fd2 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -952,6 +952,7 @@ CONF_HEAP_IN_IRAM = "heap_in_iram" CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" CONF_USE_FULL_CERTIFICATE_BUNDLE = "use_full_certificate_bundle" CONF_DISABLE_DEBUG_STUBS = "disable_debug_stubs" +CONF_ENABLE_FULL_PRINTF = "enable_full_printf" CONF_DISABLE_OCD_AWARE = "disable_ocd_aware" CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY = "disable_usb_serial_jtag_secondary" CONF_DISABLE_DEV_NULL_VFS = "disable_dev_null_vfs" @@ -1126,6 +1127,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional( CONF_INCLUDE_BUILTIN_IDF_COMPONENTS, default=[] ): cv.ensure_list(cv.string_strict), + cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean, cv.Optional(CONF_DISABLE_DEBUG_STUBS, default=True): cv.boolean, cv.Optional(CONF_DISABLE_OCD_AWARE, default=True): cv.boolean, cv.Optional( @@ -1469,6 +1471,14 @@ async def to_code(config): "_ZSt25__throw_bad_function_callv", ]: cg.add_build_flag(f"-Wl,--wrap={mangled}") + + # Wrap FILE*-based printf functions to eliminate newlib's _vfprintf_r + # (~11 KB). See printf_stubs.cpp for implementation. + if conf[CONF_ADVANCED][CONF_ENABLE_FULL_PRINTF]: + cg.add_define("USE_FULL_PRINTF") + else: + for symbol in ("vprintf", "printf", "fprintf"): + cg.add_build_flag(f"-Wl,--wrap={symbol}") else: cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") diff --git a/esphome/components/esp32/printf_stubs.cpp b/esphome/components/esp32/printf_stubs.cpp new file mode 100644 index 0000000000..c6f03bc363 --- /dev/null +++ b/esphome/components/esp32/printf_stubs.cpp @@ -0,0 +1,85 @@ +/* + * Linker wrap stubs for FILE*-based printf functions. + * + * ESP-IDF SDK components (gpio driver, ringbuf, log_write) reference + * fprintf(), printf(), and vprintf() which pull in newlib's _vfprintf_r + * (~11 KB). This is a separate implementation from _svfprintf_r (used by + * snprintf/vsnprintf) that handles FILE* stream I/O with buffering and + * locking. + * + * ESPHome replaces the ESP-IDF log handler via esp_log_set_vprintf_(), + * so the SDK's vprintf() path is dead code at runtime. The fprintf() + * and printf() calls in SDK components are only in debug/assert paths + * (gpio_dump_io_configuration, ringbuf diagnostics) that are either + * GC'd or never called. Crash backtraces and panic output are + * unaffected — they use esp_rom_printf() which is a ROM function + * and does not go through libc. + * + * These stubs redirect through vsnprintf() (which uses _svfprintf_r + * already in the binary) and fwrite(), allowing the linker to + * dead-code eliminate _vfprintf_r. + * + * Saves ~11 KB of flash. + * + * To disable these wraps, set enable_full_printf: true in the esp32 + * advanced config section. + */ + +#if defined(USE_ESP_IDF) && !defined(USE_FULL_PRINTF) +#include +#include + +#include "esp_system.h" + +namespace esphome::esp32 {} + +static constexpr size_t PRINTF_BUFFER_SIZE = 512; + +// These stubs are essentially dead code at runtime — ESPHome replaces the +// ESP-IDF log handler, and the SDK's printf/fprintf calls only exist in +// debug/assert paths that are never reached in normal operation. +// The buffer overflow check is purely defensive and should never trigger. +static int write_printf_buffer(FILE *stream, char *buf, int len) { + if (len < 0) { + return len; + } + size_t write_len = len; + if (write_len >= PRINTF_BUFFER_SIZE) { + fwrite(buf, 1, PRINTF_BUFFER_SIZE - 1, stream); + esp_system_abort("printf buffer overflow; set enable_full_printf: true in esp32 framework advanced config"); + } + if (fwrite(buf, 1, write_len, stream) < write_len || ferror(stream)) { + return -1; + } + return len; +} + +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +extern "C" { + +int __wrap_vprintf(const char *fmt, va_list ap) { + char buf[PRINTF_BUFFER_SIZE]; + return write_printf_buffer(stdout, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); +} + +int __wrap_printf(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int len = __wrap_vprintf(fmt, ap); + va_end(ap); + return len; +} + +int __wrap_fprintf(FILE *stream, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + char buf[PRINTF_BUFFER_SIZE]; + int len = write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); + va_end(ap); + return len; +} + +} // extern "C" +// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) + +#endif // USE_ESP_IDF && !USE_FULL_PRINTF diff --git a/tests/components/esp32/test.esp32-idf.yaml b/tests/components/esp32/test.esp32-idf.yaml index f80c854de5..da85aa3b0f 100644 --- a/tests/components/esp32/test.esp32-idf.yaml +++ b/tests/components/esp32/test.esp32-idf.yaml @@ -10,6 +10,7 @@ esp32: use_full_certificate_bundle: false # Test CMN bundle (default) include_builtin_idf_components: - freertos # Test escape hatch (freertos is always included anyway) + enable_full_printf: false disable_debug_stubs: true disable_ocd_aware: true disable_usb_serial_jtag_secondary: true From 6c0998f220fcd2d6ba1a3a12a6ebcfeef1166ace Mon Sep 17 00:00:00 2001 From: Raymond Richmond Date: Sat, 28 Feb 2026 00:26:06 -0700 Subject: [PATCH 273/609] [gt911] Support for interrupt signal via IO Expander (#14358) --- .../components/gt911/touchscreen/__init__.py | 2 +- .../gt911/touchscreen/gt911_touchscreen.cpp | 29 ++++++++++++++----- .../gt911/touchscreen/gt911_touchscreen.h | 6 ++-- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/esphome/components/gt911/touchscreen/__init__.py b/esphome/components/gt911/touchscreen/__init__.py index 6c80ff280f..b850eeea8b 100644 --- a/esphome/components/gt911/touchscreen/__init__.py +++ b/esphome/components/gt911/touchscreen/__init__.py @@ -16,7 +16,7 @@ GT911Touchscreen = gt911_ns.class_( CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(GT911Touchscreen), - cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_INTERRUPT_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, } ).extend(i2c.i2c_device_schema(0x5D)) diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index b11880a042..17bfa82cb4 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -2,6 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/gpio.h" namespace esphome { namespace gt911 { @@ -26,15 +27,17 @@ static const size_t MAX_BUTTONS = 4; // max number of buttons scanned void GT911Touchscreen::setup() { if (this->reset_pin_ != nullptr) { + // temporarily set the interrupt pin to output to control address selection this->reset_pin_->setup(); this->reset_pin_->digital_write(false); if (this->interrupt_pin_ != nullptr) { - // temporarily set the interrupt pin to output to control address selection + this->interrupt_pin_->setup(); this->interrupt_pin_->pin_mode(gpio::FLAG_OUTPUT); this->interrupt_pin_->digital_write(false); } delay(2); - this->reset_pin_->digital_write(true); // wait at least T3+T4 ms as per the datasheet + this->reset_pin_->digital_write(true); + // wait at least T3+T4 ms as per the datasheet this->set_timeout(5 + 50 + 1, [this] { this->setup_internal_(); }); return; } @@ -43,11 +46,10 @@ void GT911Touchscreen::setup() { void GT911Touchscreen::setup_internal_() { if (this->interrupt_pin_ != nullptr) { - // set pre-configured input mode - this->interrupt_pin_->setup(); + if (this->interrupt_pin_->is_internal()) + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT); } - // check the configuration of the int line. uint8_t data[4]; i2c::ErrorCode err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES)); if (err != i2c::ERROR_OK && this->address_ == PRIMARY_ADDRESS) { @@ -58,12 +60,25 @@ void GT911Touchscreen::setup_internal_() { err = this->read(data, 1); if (err == i2c::ERROR_OK) { ESP_LOGD(TAG, "Switches ADDR: 0x%02X DATA: 0x%02X", this->address_, data[0]); + + // data[0] & 1 == 1 => controller uses falling edge => active-low + // data[0] & 1 == 0 => controller uses rising edge => active-high + bool active_high = !(data[0] & 1); + if (this->interrupt_pin_ != nullptr) { - this->attach_interrupt_(this->interrupt_pin_, - (data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE); + if (this->interrupt_pin_->is_internal()) { + // Direct MCU pin: attach a hardware interrupt, no polling needed. + this->attach_interrupt_(static_cast(this->interrupt_pin_), + active_high ? gpio::INTERRUPT_RISING_EDGE : gpio::INTERRUPT_FALLING_EDGE); + ESP_LOGD(TAG, "Interrupt pin: hardware interrupt, active %s", active_high ? "HIGH" : "LOW"); + } else { + // IO expander pin: leave as output for configuration only. + ESP_LOGD(TAG, "Interrupt pin: IO expander polling mode, active %s", active_high ? "HIGH" : "LOW"); + } } } } + if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) { // no calibration? Attempt to read the max values from the touchscreen. if (err == i2c::ERROR_OK) { diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.h b/esphome/components/gt911/touchscreen/gt911_touchscreen.h index 85025b5522..a6577b5879 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.h +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.h @@ -30,7 +30,9 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice void dump_config() override; bool can_proceed() override { return this->setup_done_; } - void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + /// Set a interrupt pin (supports hardware interrupts or expander connected). + void set_interrupt_pin(GPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } void register_button_listener(GT911ButtonListener *listener) { this->button_listeners_.push_back(listener); } @@ -49,7 +51,7 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice /// @brief True if the touchscreen setup has completed successfully. bool setup_done_{false}; - InternalGPIOPin *interrupt_pin_{nullptr}; + GPIOPin *interrupt_pin_{nullptr}; GPIOPin *reset_pin_{nullptr}; std::vector button_listeners_; uint8_t button_state_{0xFF}; // last button state. Initial FF guarantees first update. From 089d1e55e729c25755ab76196917db1f090ae5f2 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 28 Feb 2026 20:37:04 +1100 Subject: [PATCH 274/609] [mipi_dsi] Fix Waveshare P4 7B board config (#14372) --- esphome/components/mipi_dsi/models/waveshare.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/mipi_dsi/models/waveshare.py b/esphome/components/mipi_dsi/models/waveshare.py index 69414065f1..c97a0bbd02 100644 --- a/esphome/components/mipi_dsi/models/waveshare.py +++ b/esphome/components/mipi_dsi/models/waveshare.py @@ -103,8 +103,6 @@ DriverChip( (0xE9, 0xC8, 0x10, 0x0A, 0x00, 0x00, 0x80, 0x81, 0x12, 0x31, 0x23, 0x4F, 0x86, 0xA0, 0x00, 0x47, 0x08, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x98, 0x02, 0x8B, 0xAF, 0x46, 0x02, 0x88, 0x88, 0x88, 0x88, 0x88, 0x98, 0x13, 0x8B, 0xAF, 0x57, 0x13, 0x88, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), (0xEA, 0x97, 0x0C, 0x09, 0x09, 0x09, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9F, 0x31, 0x8B, 0xA8, 0x31, 0x75, 0x88, 0x88, 0x88, 0x88, 0x88, 0x9F, 0x20, 0x8B, 0xA8, 0x20, 0x64, 0x88, 0x88, 0x88, 0x88, 0x88, 0x23, 0x00, 0x00, 0x02, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x80, 0x81, 0x00, 0x00, 0x00, 0x00), (0xEF, 0xFF, 0xFF, 0x01), - (0x11, 0x00), - (0x29, 0x00), ], ) @@ -125,6 +123,7 @@ DriverChip( lane_bit_rate="900Mbps", no_transform=True, color_order="RGB", + reset_pin=33, initsequence=[ (0x80, 0x8B), (0x81, 0x78), From 067d773aac5e1ca7911e2260b222b10b9e08444f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 07:19:55 -1000 Subject: [PATCH 275/609] [core] Make register_component protected, remove runtime checks (#14371) --- esphome/core/application.cpp | 19 +------------------ esphome/core/application.h | 13 ++++++------- esphome/cpp_helpers.py | 2 +- .../deep_sleep/test_deep_sleep.py | 2 +- .../ota/test_web_server_ota.py | 2 +- tests/dummy_main.cpp | 4 ++-- tests/unit_tests/test_cpp_helpers.py | 8 ++++---- 7 files changed, 16 insertions(+), 34 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index b1ece86701..f963afa597 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -79,24 +79,7 @@ static void insertion_sort_by_priority(Iterator first, Iterator last) { } } -void Application::register_component_(Component *comp) { - if (comp == nullptr) { - ESP_LOGW(TAG, "Tried to register null component!"); - return; - } - - for (auto *c : this->components_) { - if (comp == c) { - ESP_LOGW(TAG, "Component %s already registered! (%p)", LOG_STR_ARG(c->get_component_log_str()), c); - return; - } - } - if (this->components_.size() >= ESPHOME_COMPONENT_COUNT) { - ESP_LOGE(TAG, "Cannot register component %s - at capacity!", LOG_STR_ARG(comp->get_component_log_str())); - return; - } - this->components_.push_back(comp); -} +void Application::register_component_(Component *comp) { this->components_.push_back(comp); } void Application::setup() { ESP_LOGI(TAG, "Running through setup()"); ESP_LOGV(TAG, "Sorting components by setup priority"); diff --git a/esphome/core/application.h b/esphome/core/application.h index 0cc29af8e7..ba3ce13291 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -108,6 +108,10 @@ namespace esphome::socket { class Socket; } // namespace esphome::socket +// Forward declarations for friend access from codegen-generated setup() +void setup(); // NOLINT(readability-redundant-declaration) - may be declared in Arduino.h +void original_setup(); // NOLINT(readability-redundant-declaration) - used by cpp unit tests + namespace esphome { // Teardown timeout constant (in milliseconds) @@ -247,13 +251,6 @@ class Application { /// Reserve space for components to avoid memory fragmentation - /// Register the component in this Application instance. - template C *register_component(C *c) { - static_assert(std::is_base_of::value, "Only Component subclasses can be registered"); - this->register_component_((Component *) c); - return c; - } - /// Set up all the registered components. Call this at the end of your setup() function. void setup(); @@ -508,6 +505,8 @@ class Application { protected: friend Component; friend class socket::Socket; + friend void ::setup(); + friend void ::original_setup(); #ifdef USE_SOCKET_SELECT_SUPPORT /// Fast path for Socket::ready() via friendship - skips negative fd check. diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 954a28d3d1..b673eaa7e1 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -79,7 +79,7 @@ async def register_component(var, config): if name is not None: add(var.set_component_source(LogStringLiteral(name))) - add(App.register_component(var)) + add(App.register_component_(var)) return var diff --git a/tests/component_tests/deep_sleep/test_deep_sleep.py b/tests/component_tests/deep_sleep/test_deep_sleep.py index 11f1bcb58e..41ddd72feb 100644 --- a/tests/component_tests/deep_sleep/test_deep_sleep.py +++ b/tests/component_tests/deep_sleep/test_deep_sleep.py @@ -8,7 +8,7 @@ def test_deep_sleep_setup(generate_main): main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep1.yaml") assert "deepsleep = new deep_sleep::DeepSleepComponent();" in main_cpp - assert "App.register_component(deepsleep);" in main_cpp + assert "App.register_component_(deepsleep);" in main_cpp def test_deep_sleep_sleep_duration(generate_main): diff --git a/tests/component_tests/ota/test_web_server_ota.py b/tests/component_tests/ota/test_web_server_ota.py index 794eaac9be..4b3a4c705c 100644 --- a/tests/component_tests/ota/test_web_server_ota.py +++ b/tests/component_tests/ota/test_web_server_ota.py @@ -27,7 +27,7 @@ def test_web_server_ota_generated(generate_main: Callable[[str], str]) -> None: assert "global_web_server_base" in main_cpp # Check component is registered - assert "App.register_component(web_server_webserverotacomponent_id)" in main_cpp + assert "App.register_component_(web_server_webserverotacomponent_id)" in main_cpp def test_web_server_ota_with_callbacks(generate_main: Callable[[str], str]) -> None: diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 52f1fbd319..3ccf35e04d 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -16,10 +16,10 @@ void setup() { auto *log = new logger::Logger(115200); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); - App.register_component(log); + App.register_component_(log); auto *wifi = new wifi::WiFiComponent(); // NOLINT - App.register_component(wifi); + App.register_component_(wifi); wifi::WiFiAP ap; ap.set_ssid("Test SSID"); ap.set_password("password1"); diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index 2618803fec..82ded409c7 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -16,7 +16,7 @@ async def test_gpio_pin_expression__conf_is_none(monkeypatch): async def test_register_component(monkeypatch): var = Mock(base="foo.bar") - app_mock = Mock(register_component=Mock(return_value=var)) + app_mock = Mock(register_component_=Mock(return_value=var)) monkeypatch.setattr(ch, "App", app_mock) core_mock = Mock(component_ids=["foo.bar"]) @@ -29,7 +29,7 @@ async def test_register_component(monkeypatch): assert actual is var assert add_mock.call_count == 2 - app_mock.register_component.assert_called_with(var) + app_mock.register_component_.assert_called_with(var) assert core_mock.component_ids == [] @@ -48,7 +48,7 @@ async def test_register_component__no_component_id(monkeypatch): async def test_register_component__with_setup_priority(monkeypatch): var = Mock(base="foo.bar") - app_mock = Mock(register_component=Mock(return_value=var)) + app_mock = Mock(register_component_=Mock(return_value=var)) monkeypatch.setattr(ch, "App", app_mock) core_mock = Mock(component_ids=["foo.bar"]) @@ -68,5 +68,5 @@ async def test_register_component__with_setup_priority(monkeypatch): assert actual is var add_mock.assert_called() assert add_mock.call_count == 4 - app_mock.register_component.assert_called_with(var) + app_mock.register_component_.assert_called_with(var) assert core_mock.component_ids == [] From 7d52a9587f59c01a47c72bacb8c3d7e9f4d40e30 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 07:20:20 -1000 Subject: [PATCH 276/609] [api] Outline keepalive ping logic from APIConnection::loop() (#14374) --- esphome/components/api/api_connection.cpp | 46 ++++++++++++++--------- esphome/components/api/api_connection.h | 4 ++ 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7bc9c45c05..8b2efdde51 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -242,24 +242,11 @@ void APIConnection::loop() { return; } - if (this->flags_.sent_ping) { - // Disconnect if not responded within 2.5*keepalive - if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { - on_fatal_error(); - this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("is unresponsive; disconnecting")); - } - } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) { - // Only send ping if we're not disconnecting - ESP_LOGVV(TAG, "Sending keepalive PING"); - PingRequest req; - this->flags_.sent_ping = this->send_message(req, PingRequest::MESSAGE_TYPE); - if (!this->flags_.sent_ping) { - // If we can't send the ping request directly (tx_buffer full), - // schedule it at the front of the batch so it will be sent with priority - ESP_LOGW(TAG, "Buffer full, ping queued"); - this->schedule_message_front_(nullptr, PingRequest::MESSAGE_TYPE, PingRequest::ESTIMATED_SIZE); - this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings - } + // Keepalive: only call into the cold path when enough time has elapsed. + // When sent_ping is true, last_traffic_ hasn't been updated so this + // condition is already satisfied — covers both send-ping and disconnect cases. + if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS) { + this->check_keepalive_(now); } #ifdef USE_API_HOMEASSISTANT_STATES @@ -275,6 +262,29 @@ void APIConnection::loop() { #endif } +void APIConnection::check_keepalive_(uint32_t now) { + // Caller guarantees: now - last_traffic_ > KEEPALIVE_TIMEOUT_MS + if (this->flags_.sent_ping) { + // Disconnect if not responded within 2.5*keepalive + if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { + on_fatal_error(); + this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("is unresponsive; disconnecting")); + } + } else if (!this->flags_.remove) { + // Only send ping if we're not disconnecting + ESP_LOGVV(TAG, "Sending keepalive PING"); + PingRequest req; + this->flags_.sent_ping = this->send_message(req, PingRequest::MESSAGE_TYPE); + if (!this->flags_.sent_ping) { + // If we can't send the ping request directly (tx_buffer full), + // schedule it at the front of the batch so it will be sent with priority + ESP_LOGW(TAG, "Buffer full, ping queued"); + this->schedule_message_front_(nullptr, PingRequest::MESSAGE_TYPE, PingRequest::ESTIMATED_SIZE); + this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings + } + } +} + void APIConnection::process_active_iterator_() { // Caller ensures active_iterator_ != NONE if (this->active_iterator_ == ActiveIterator::LIST_ENTITIES) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index e34bed8ada..37855b2482 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -370,6 +370,10 @@ class APIConnection final : public APIServerConnectionBase { return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY; } + // Send keepalive ping or disconnect unresponsive client. + // Cold path — extracted from loop() to reduce instruction cache pressure. + void __attribute__((noinline)) check_keepalive_(uint32_t now); + // Process active iterator (list_entities/initial_state) during connection setup. // Extracted from loop() — only runs during initial handshake, NONE in steady state. void __attribute__((noinline)) process_active_iterator_(); From 757e8d90e6a32fb77cad14efb9e00e34e584fee2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 07:20:34 -1000 Subject: [PATCH 277/609] [core] Inline set_component_state_ and use it in Application (#14369) --- esphome/core/application.cpp | 3 +-- esphome/core/component.cpp | 4 ---- esphome/core/component.h | 5 ++++- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index f963afa597..c977fd66b3 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -508,8 +508,7 @@ void Application::enable_pending_loops_() { // Clear the pending flag and enable the loop component->pending_enable_loop_ = false; ESP_LOGVV(TAG, "%s loop enabled from ISR", LOG_STR_ARG(component->get_component_log_str())); - component->component_state_ &= ~COMPONENT_STATE_MASK; - component->component_state_ |= COMPONENT_STATE_LOOP; + component->set_component_state_(COMPONENT_STATE_LOOP); // Move to active section this->activate_looping_component_(i); diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index e14fae5e08..1fd621ea83 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -297,10 +297,6 @@ void Component::mark_failed() { // Also remove from loop since failed components shouldn't loop App.disable_component_loop_(this); } -void Component::set_component_state_(uint8_t state) { - this->component_state_ &= ~COMPONENT_STATE_MASK; - this->component_state_ |= state; -} void Component::disable_loop() { if ((this->component_state_ & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) { ESP_LOGVV(TAG, "%s loop disabled", LOG_STR_ARG(this->get_component_log_str())); diff --git a/esphome/core/component.h b/esphome/core/component.h index 2620e8eb2a..0b0b6345c7 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -294,7 +294,10 @@ class Component { void call_dump_config_(); /// Helper to set component state (clears state bits and sets new state) - void set_component_state_(uint8_t state); + inline void set_component_state_(uint8_t state) { + this->component_state_ &= ~COMPONENT_STATE_MASK; + this->component_state_ |= state; + } /** Set an interval function with a unique name. Empty name means no cancelling possible. * From b7d651dd179ad165e9921bd9c7840de58da3987a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 28 Feb 2026 18:10:53 -0500 Subject: [PATCH 278/609] [core] Defer entity automation codegen to prevent sibling ID deadlocks (#14381) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/binary_sensor/__init__.py | 37 +++++++++++--------- esphome/components/number/__init__.py | 31 +++++++++------- esphome/components/sensor/__init__.py | 37 +++++++++++--------- esphome/components/switch/__init__.py | 16 ++++++--- esphome/components/text_sensor/__init__.py | 19 ++++++---- 5 files changed, 83 insertions(+), 57 deletions(-) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 4500168cb1..036d78da73 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -550,22 +550,8 @@ def binary_sensor_schema( return _BINARY_SENSOR_SCHEMA.extend(schema) -async def setup_binary_sensor_core_(var, config): - await setup_entity(var, config, "binary_sensor") - - if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: - cg.add(var.set_device_class(device_class)) - trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get( - CONF_PUBLISH_INITIAL_STATE, False - ) - cg.add(var.set_trigger_on_initial_state(trigger)) - if inverted := config.get(CONF_INVERTED): - cg.add(var.set_inverted(inverted)) - if filters_config := config.get(CONF_FILTERS): - cg.add_define("USE_BINARY_SENSOR_FILTER") - filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config) - cg.add(var.add_filters(filters)) - +@coroutine_with_priority(CoroPriority.AUTOMATION) +async def _build_binary_sensor_automations(var, config): for conf in config.get(CONF_ON_PRESS, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) @@ -617,6 +603,25 @@ async def setup_binary_sensor_core_(var, config): conf, ) + +async def setup_binary_sensor_core_(var, config): + await setup_entity(var, config, "binary_sensor") + + if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: + cg.add(var.set_device_class(device_class)) + trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get( + CONF_PUBLISH_INITIAL_STATE, False + ) + cg.add(var.set_trigger_on_initial_state(trigger)) + if inverted := config.get(CONF_INVERTED): + cg.add(var.set_inverted(inverted)) + if filters_config := config.get(CONF_FILTERS): + cg.add_define("USE_BINARY_SENSOR_FILTER") + filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config) + cg.add(var.add_filters(filters)) + + CORE.add_job(_build_binary_sensor_automations, var, config) + if mqtt_id := config.get(CONF_MQTT_ID): mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index b23da7799f..d12ec7463b 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -240,6 +240,23 @@ def number_schema( return _NUMBER_SCHEMA.extend(schema) +@coroutine_with_priority(CoroPriority.AUTOMATION) +async def _build_number_automations(var, config): + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(float, "x")], conf) + for conf in config.get(CONF_ON_VALUE_RANGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await cg.register_component(trigger, conf) + if CONF_ABOVE in conf: + template_ = await cg.templatable(conf[CONF_ABOVE], [(float, "x")], float) + cg.add(trigger.set_min(template_)) + if CONF_BELOW in conf: + template_ = await cg.templatable(conf[CONF_BELOW], [(float, "x")], float) + cg.add(trigger.set_max(template_)) + await automation.build_automation(trigger, [(float, "x")], conf) + + async def setup_number_core_( var, config, *, min_value: float, max_value: float, step: float ): @@ -254,19 +271,7 @@ async def setup_number_core_( if config[CONF_MODE] != NumberMode.NUMBER_MODE_AUTO: cg.add(var.traits.set_mode(config[CONF_MODE])) - for conf in config.get(CONF_ON_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(float, "x")], conf) - for conf in config.get(CONF_ON_VALUE_RANGE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await cg.register_component(trigger, conf) - if CONF_ABOVE in conf: - template_ = await cg.templatable(conf[CONF_ABOVE], [(float, "x")], float) - cg.add(trigger.set_min(template_)) - if CONF_BELOW in conf: - template_ = await cg.templatable(conf[CONF_BELOW], [(float, "x")], float) - cg.add(trigger.set_max(template_)) - await automation.build_automation(trigger, [(float, "x")], conf) + CORE.add_job(_build_number_automations, var, config) if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None: cg.add(var.traits.set_unit_of_measurement(unit_of_measurement)) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 770c044efb..338aaae0b5 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -888,6 +888,26 @@ async def build_filters(config): return await cg.build_registry_list(FILTER_REGISTRY, config) +@coroutine_with_priority(CoroPriority.AUTOMATION) +async def _build_sensor_automations(var, config): + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(float, "x")], conf) + for conf in config.get(CONF_ON_RAW_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(float, "x")], conf) + for conf in config.get(CONF_ON_VALUE_RANGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await cg.register_component(trigger, conf) + if (above := conf.get(CONF_ABOVE)) is not None: + template_ = await cg.templatable(above, [(float, "x")], float) + cg.add(trigger.set_min(template_)) + if (below := conf.get(CONF_BELOW)) is not None: + template_ = await cg.templatable(below, [(float, "x")], float) + cg.add(trigger.set_max(template_)) + await automation.build_automation(trigger, [(float, "x")], conf) + + async def setup_sensor_core_(var, config): await setup_entity(var, config, "sensor") @@ -907,22 +927,7 @@ async def setup_sensor_core_(var, config): filters = await build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) - for conf in config.get(CONF_ON_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(float, "x")], conf) - for conf in config.get(CONF_ON_RAW_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(float, "x")], conf) - for conf in config.get(CONF_ON_VALUE_RANGE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await cg.register_component(trigger, conf) - if (above := conf.get(CONF_ABOVE)) is not None: - template_ = await cg.templatable(above, [(float, "x")], float) - cg.add(trigger.set_min(template_)) - if (below := conf.get(CONF_BELOW)) is not None: - template_ = await cg.templatable(below, [(float, "x")], float) - cg.add(trigger.set_max(template_)) - await automation.build_automation(trigger, [(float, "x")], conf) + CORE.add_job(_build_sensor_automations, var, config) if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 7424d7c92f..cfc5e2b6e8 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -141,11 +141,8 @@ def switch_schema( return _SWITCH_SCHEMA.extend(schema) -async def setup_switch_core_(var, config): - await setup_entity(var, config, "switch") - - if (inverted := config.get(CONF_INVERTED)) is not None: - cg.add(var.set_inverted(inverted)) +@coroutine_with_priority(CoroPriority.AUTOMATION) +async def _build_switch_automations(var, config): for conf in config.get(CONF_ON_STATE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [(bool, "x")], conf) @@ -156,6 +153,15 @@ async def setup_switch_core_(var, config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + +async def setup_switch_core_(var, config): + await setup_entity(var, config, "switch") + + if (inverted := config.get(CONF_INVERTED)) is not None: + cg.add(var.set_inverted(inverted)) + + CORE.add_job(_build_switch_automations, var, config) + if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 2e8edb43c9..2edf202cd2 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -197,6 +197,17 @@ async def build_filters(config): return await cg.build_registry_list(FILTER_REGISTRY, config) +@coroutine_with_priority(CoroPriority.AUTOMATION) +async def _build_text_sensor_automations(var, config): + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + for conf in config.get(CONF_ON_RAW_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + async def setup_text_sensor_core_(var, config): await setup_entity(var, config, "text_sensor") @@ -208,13 +219,7 @@ async def setup_text_sensor_core_(var, config): filters = await build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) - for conf in config.get(CONF_ON_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) - - for conf in config.get(CONF_ON_RAW_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + CORE.add_job(_build_text_sensor_automations, var, config) if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) From b679b04d140663770f4a5c4be16e1c9ed93de5c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 13:27:33 -1000 Subject: [PATCH 279/609] [core] Move CONF_STOP_BITS, CONF_DATA_BITS, CONF_PARITY to const.py (#14379) --- esphome/components/uart/__init__.py | 6 +++--- esphome/components/usb_uart/__init__.py | 10 ++++------ esphome/components/weikai/__init__.py | 6 +++--- esphome/const.py | 3 +++ 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 31e37a06e0..a6f4cf5d5f 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -12,6 +12,7 @@ from esphome.const import ( CONF_BAUD_RATE, CONF_BYTES, CONF_DATA, + CONF_DATA_BITS, CONF_DEBUG, CONF_DELIMITER, CONF_DIRECTION, @@ -21,10 +22,12 @@ from esphome.const import ( CONF_ID, CONF_LAMBDA, CONF_NUMBER, + CONF_PARITY, CONF_PORT, CONF_RX_BUFFER_SIZE, CONF_RX_PIN, CONF_SEQUENCE, + CONF_STOP_BITS, CONF_TIMEOUT, CONF_TRIGGER_ID, CONF_TX_PIN, @@ -215,9 +218,6 @@ UART_PARITY_OPTIONS = { "ODD": UARTParityOptions.UART_CONFIG_PARITY_ODD, } -CONF_STOP_BITS = "stop_bits" -CONF_DATA_BITS = "data_bits" -CONF_PARITY = "parity" CONF_RX_FULL_THRESHOLD = "rx_full_threshold" CONF_RX_TIMEOUT = "rx_timeout" diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index d9bb58ae3a..cc69c0cb5a 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -1,20 +1,18 @@ import esphome.codegen as cg from esphome.components import socket -from esphome.components.uart import ( - CONF_DATA_BITS, - CONF_PARITY, - CONF_STOP_BITS, - UARTComponent, -) +from esphome.components.uart import UARTComponent from esphome.components.usb_host import register_usb_client, usb_device_schema import esphome.config_validation as cv from esphome.const import ( CONF_BAUD_RATE, CONF_BUFFER_SIZE, CONF_CHANNELS, + CONF_DATA_BITS, CONF_DEBUG, CONF_DUMMY_RECEIVER, CONF_ID, + CONF_PARITY, + CONF_STOP_BITS, ) from esphome.cpp_types import Component diff --git a/esphome/components/weikai/__init__.py b/esphome/components/weikai/__init__.py index 796423438e..66cd4ce12a 100644 --- a/esphome/components/weikai/__init__.py +++ b/esphome/components/weikai/__init__.py @@ -4,21 +4,21 @@ import esphome.config_validation as cv from esphome.const import ( CONF_BAUD_RATE, CONF_CHANNEL, + CONF_DATA_BITS, CONF_ID, CONF_INPUT, CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_OUTPUT, + CONF_PARITY, + CONF_STOP_BITS, ) CODEOWNERS = ["@DrCoolZic"] AUTO_LOAD = ["uart"] MULTI_CONF = True -CONF_DATA_BITS = "data_bits" -CONF_STOP_BITS = "stop_bits" -CONF_PARITY = "parity" CONF_CRYSTAL = "crystal" CONF_UART = "uart" CONF_TEST_MODE = "test_mode" diff --git a/esphome/const.py b/esphome/const.py index 1611aeb101..7262a106d8 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -280,6 +280,7 @@ CONF_CUSTOM_PRESETS = "custom_presets" CONF_CYCLE = "cycle" CONF_DALLAS_ID = "dallas_id" CONF_DATA = "data" +CONF_DATA_BITS = "data_bits" CONF_DATA_PIN = "data_pin" CONF_DATA_PINS = "data_pins" CONF_DATA_RATE = "data_rate" @@ -759,6 +760,7 @@ CONF_PAGE_ID = "page_id" CONF_PAGES = "pages" CONF_PANASONIC = "panasonic" CONF_PARAMETERS = "parameters" +CONF_PARITY = "parity" CONF_PASSWORD = "password" CONF_PATH = "path" CONF_PATTERN = "pattern" @@ -961,6 +963,7 @@ CONF_STEP_PIN = "step_pin" CONF_STILL_THRESHOLD = "still_threshold" CONF_STOP = "stop" CONF_STOP_ACTION = "stop_action" +CONF_STOP_BITS = "stop_bits" CONF_STORE_BASELINE = "store_baseline" CONF_SUBNET = "subnet" CONF_SUBSCRIBE_QOS = "subscribe_qos" From 28424d6acdb5a406f650d3f6133eb67f19304f2c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 13:50:12 -1000 Subject: [PATCH 280/609] [ld2410][ld2412] Fix signed char causing incorrect distance values (#14380) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/ld2410/ld2410.cpp | 21 ++++++++------------- esphome/components/ld2412/ld2412.cpp | 23 +++++++++-------------- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index dd1d53857d..f10e7ec0aa 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -173,8 +173,6 @@ static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0x // MAC address the module uses when Bluetooth is disabled static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01}; -static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } - static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0; } @@ -361,17 +359,14 @@ void LD2410Component::handle_periodic_data_() { Detect distance: 16~17th bytes */ #ifdef USE_SENSOR - SAFE_PUBLISH_SENSOR( - this->moving_target_distance_sensor_, - ld2410::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH])) + SAFE_PUBLISH_SENSOR(this->moving_target_distance_sensor_, + encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW])) SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY]) - SAFE_PUBLISH_SENSOR( - this->still_target_distance_sensor_, - ld2410::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH])); + SAFE_PUBLISH_SENSOR(this->still_target_distance_sensor_, + encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW])); SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]); - SAFE_PUBLISH_SENSOR( - this->detection_distance_sensor_, - ld2410::two_byte_to_int(this->buffer_data_[DETECT_DISTANCE_LOW], this->buffer_data_[DETECT_DISTANCE_HIGH])); + SAFE_PUBLISH_SENSOR(this->detection_distance_sensor_, + encode_uint16(this->buffer_data_[DETECT_DISTANCE_HIGH], this->buffer_data_[DETECT_DISTANCE_LOW])); if (engineering_mode) { /* @@ -578,8 +573,8 @@ bool LD2410Component::handle_ack_data_() { /* None Duration: 33~34th bytes */ - updates.push_back(set_number_value(this->timeout_number_, - ld2410::two_byte_to_int(this->buffer_data_[32], this->buffer_data_[33]))); + updates.push_back( + set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[33], this->buffer_data_[32]))); for (auto &update : updates) { update(); } diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 484d5bd281..ef0915d0bc 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -192,8 +192,6 @@ static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0x // MAC address the module uses when Bluetooth is disabled static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01}; -static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } - static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0; } @@ -398,22 +396,19 @@ void LD2412Component::handle_periodic_data_() { Detect distance: 16~17th bytes */ #ifdef USE_SENSOR - SAFE_PUBLISH_SENSOR( - this->moving_target_distance_sensor_, - ld2412::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH])) + SAFE_PUBLISH_SENSOR(this->moving_target_distance_sensor_, + encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW])) SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY]) - SAFE_PUBLISH_SENSOR( - this->still_target_distance_sensor_, - ld2412::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH])) + SAFE_PUBLISH_SENSOR(this->still_target_distance_sensor_, + encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW])) SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]) if (this->detection_distance_sensor_ != nullptr) { int new_detect_distance = 0; if (target_state != 0x00 && (target_state & MOVE_BITMASK)) { new_detect_distance = - ld2412::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH]); + encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW]); } else if (target_state != 0x00) { - new_detect_distance = - ld2412::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH]); + new_detect_distance = encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]); } this->detection_distance_sensor_->publish_state_if_not_dup(new_detect_distance); } @@ -637,9 +632,9 @@ bool LD2412Component::handle_ack_data_() { /* None Duration: 11~12th bytes */ - updates.push_back(set_number_value(this->timeout_number_, - ld2412::two_byte_to_int(this->buffer_data_[12], this->buffer_data_[13]))); - ESP_LOGV(TAG, "timeout_number_: %u", ld2412::two_byte_to_int(this->buffer_data_[12], this->buffer_data_[13])); + updates.push_back( + set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[13], this->buffer_data_[12]))); + ESP_LOGV(TAG, "timeout_number_: %u", encode_uint16(this->buffer_data_[13], this->buffer_data_[12])); /* Output pin configuration: 13th bytes */ From fdbfac15db79a329629173c1d6ba967f12b43e46 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 14:21:08 -1000 Subject: [PATCH 281/609] [uart] Replace wake-on-RX task+queue with direct ISR callback (#14382) --- .../uart/uart_component_esp_idf.cpp | 101 +++--------------- .../components/uart/uart_component_esp_idf.h | 21 ++-- esphome/core/application.h | 10 ++ esphome/core/lwip_fast_select.c | 14 +++ esphome/core/lwip_fast_select.h | 5 + 5 files changed, 52 insertions(+), 99 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 8699d37d7a..631db1e5c7 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -2,7 +2,6 @@ #include "uart_component_esp_idf.h" #include -#include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -10,6 +9,10 @@ #include "driver/gpio.h" #include "soc/gpio_num.h" +#ifdef USE_UART_WAKE_LOOP_ON_RX +#include "esphome/core/application.h" +#endif + #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" #endif @@ -107,12 +110,6 @@ void IDFUARTComponent::load_settings(bool dump_config) { esp_err_t err; if (uart_is_driver_installed(this->uart_num_)) { -#ifdef USE_UART_WAKE_LOOP_ON_RX - if (this->rx_event_task_handle_ != nullptr) { - vTaskDelete(this->rx_event_task_handle_); - this->rx_event_task_handle_ = nullptr; - } -#endif err = uart_driver_delete(this->uart_num_); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err)); @@ -120,20 +117,13 @@ 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 - event_queue_size, // event queue size/depth - event_queue_ptr, // event queue - 0 // Flags used to allocate the interrupt + 0, // event queue size/depth + nullptr, // 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)); @@ -213,8 +203,10 @@ void IDFUARTComponent::load_settings(bool dump_config) { } #ifdef USE_UART_WAKE_LOOP_ON_RX - // Start the RX event task to enable low-latency data notifications - this->start_rx_event_task_(); + // Register ISR callback to wake the main loop when UART data arrives. + // The callback runs in ISR context and uses vTaskNotifyGiveFromISR() to + // wake the main loop task directly — no queue or FreeRTOS task needed. + uart_set_select_notif_callback(this->uart_num_, IDFUARTComponent::uart_rx_isr_callback); #endif // USE_UART_WAKE_LOOP_ON_RX if (dump_config) { @@ -345,71 +337,12 @@ void IDFUARTComponent::flush() { void IDFUARTComponent::check_logger_conflict() {} #ifdef USE_UART_WAKE_LOOP_ON_RX -void IDFUARTComponent::start_rx_event_task_() { - // Create FreeRTOS task to monitor UART events - BaseType_t result = xTaskCreate(rx_event_task_func, // Task function - "uart_rx_evt", // Task name (max 16 chars) - 2240, // Stack size in bytes (~2.2KB); increase if needed for logging - this, // Task parameter (this pointer) - tskIDLE_PRIORITY + 1, // Priority (low, just above idle) - &this->rx_event_task_handle_ // Task handle - ); - - if (result != pdPASS) { - ESP_LOGE(TAG, "Failed to create RX event task"); - return; - } - - 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; - - ESP_LOGV(TAG, "RX event task running"); - - // Run forever - task lifecycle matches component lifecycle - while (true) { - // Wait for UART events (blocks efficiently) - if (xQueueReceive(self->uart_event_queue_, &event, portMAX_DELAY) == pdTRUE) { - switch (event.type) { - case UART_DATA: - // Data available in UART RX buffer - wake the main loop - ESP_LOGVV(TAG, "Data event: %d bytes", event.size); -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) - App.wake_loop_threadsafe(); -#endif - break; - - case UART_FIFO_OVF: - case UART_BUFFER_FULL: - // 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 - break; - - default: - // Ignore other event types - ESP_LOGVV(TAG, "Event type: %d", event.type); - break; - } - } +// ISR callback invoked by the ESP-IDF UART driver when data arrives. +// Wakes the main loop directly via vTaskNotifyGiveFromISR() — no queue or task needed. +void IRAM_ATTR IDFUARTComponent::uart_rx_isr_callback(uart_port_t uart_num, uart_select_notif_t uart_select_notif, + BaseType_t *task_woken) { + if (uart_select_notif == UART_SELECT_READ_NOTIF) { + Application::wake_loop_isrsafe(task_woken); } } #endif // USE_UART_WAKE_LOOP_ON_RX diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 1517eab509..631bf54cd5 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -5,6 +5,9 @@ #include #include "esphome/core/component.h" #include "uart_component.h" +#ifdef USE_UART_WAKE_LOOP_ON_RX +#include +#endif namespace esphome::uart { @@ -12,9 +15,7 @@ namespace esphome::uart { /// /// 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(). +/// peek byte state (has_peek_/peek_byte_) is not synchronized. class IDFUARTComponent : public UARTComponent, public Component { public: void setup() override; @@ -33,9 +34,6 @@ 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. @@ -61,15 +59,8 @@ class IDFUARTComponent : public UARTComponent, public Component { uint8_t peek_byte_; #ifdef USE_UART_WAKE_LOOP_ON_RX - // 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}; + // ISR callback for UART RX data notification — wakes the main loop directly. + static void uart_rx_isr_callback(uart_port_t uart_num, uart_select_notif_t uart_select_notif, BaseType_t *task_woken); #endif // USE_UART_WAKE_LOOP_ON_RX }; diff --git a/esphome/core/application.h b/esphome/core/application.h index ba3ce13291..13e0f63885 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -500,6 +500,16 @@ class Application { /// On other platforms: uses UDP loopback socket void wake_loop_threadsafe(); #endif + +#if defined(USE_WAKE_LOOP_THREADSAFE) && defined(USE_LWIP_FAST_SELECT) + /// Wake the main event loop from an ISR. + /// Uses vTaskNotifyGiveFromISR() — <1 us, ISR-safe. + /// Only available on platforms with fast select (ESP32, LibreTiny). + /// @param px_higher_priority_task_woken Set to pdTRUE if a context switch is needed. + static void IRAM_ATTR wake_loop_isrsafe(int *px_higher_priority_task_woken) { + esphome_lwip_wake_main_loop_from_isr(px_higher_priority_task_woken); + } +#endif #endif protected: diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index 88cf23b67e..da0f1f337a 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -126,6 +126,12 @@ #include +// IRAM_ATTR is defined by esp_attr.h (included via FreeRTOS headers) on ESP32. +// On LibreTiny it's not defined — provide a no-op fallback. +#ifndef IRAM_ATTR +#define IRAM_ATTR +#endif + // Compile-time verification of thread safety assumptions. // On ESP32 (Xtensa/RISC-V) and LibreTiny (ARM Cortex-M), naturally-aligned // reads/writes up to 32 bits are atomic. @@ -225,4 +231,12 @@ void esphome_lwip_wake_main_loop(void) { } } +// Wake the main loop from an ISR. ISR-safe variant. +void IRAM_ATTR esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken) { + TaskHandle_t task = s_main_loop_task; + if (task != NULL) { + vTaskNotifyGiveFromISR(task, (BaseType_t *) px_higher_priority_task_woken); + } +} + #endif // defined(USE_ESP32) || defined(USE_LIBRETINY) diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h index b08c946212..b7a70a8f9f 100644 --- a/esphome/core/lwip_fast_select.h +++ b/esphome/core/lwip_fast_select.h @@ -28,6 +28,11 @@ void esphome_lwip_hook_socket(int fd); /// NOT ISR-safe — must only be called from task context. void esphome_lwip_wake_main_loop(void); +/// Wake the main loop task from an ISR — costs <1 us. +/// ISR-safe variant using vTaskNotifyGiveFromISR(). +/// @param px_higher_priority_task_woken Set to pdTRUE if a context switch is needed. +void esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken); + #ifdef __cplusplus } #endif From b7cb65ec4996ba6c1b768908555f53586eb37d4b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 14:23:20 -1000 Subject: [PATCH 282/609] [ci] Fix TypeError in ci-custom.py when POST lint checks fail (#14378) --- script/ci-custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/ci-custom.py b/script/ci-custom.py index 85a446ba0d..f428eb0821 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -959,7 +959,7 @@ def main(): continue run_checks(LINT_CONTENT_CHECKS, fname, fname, content) - run_checks(LINT_POST_CHECKS, "POST") + run_checks(LINT_POST_CHECKS, Path("POST")) for f, errs in sorted(errors.items()): bold = functools.partial(styled, colorama.Style.BRIGHT) From c0781d3680f6c816510f7417eeb4523964e08575 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 14:26:08 -1000 Subject: [PATCH 283/609] [ld2450] Use atan2f for angle calculation (#14388) --- esphome/components/ld2450/ld2450.cpp | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index d30c164769..5ec3ce47d7 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -168,15 +168,6 @@ static inline int16_t hex_to_signed_int(const uint8_t *buffer, uint8_t offset) { return dec_val; } -static inline float calculate_angle(float base, float hypotenuse) { - if (base < 0.0f || hypotenuse <= 0.0f) { - return 0.0f; - } - float angle_radians = acosf(base / hypotenuse); - float angle_degrees = angle_radians * (180.0f / std::numbers::pi_v); - return angle_degrees; -} - static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0; } @@ -510,11 +501,8 @@ void LD2450Component::handle_periodic_data_() { } #ifdef USE_SENSOR SAFE_PUBLISH_SENSOR(this->move_distance_sensors_[index], td); - // ANGLE - angle = ld2450::calculate_angle(static_cast(ty), static_cast(td)); - if (tx > 0) { - angle = angle * -1; - } + // ANGLE - atan2f computes angle from Y axis directly, no sqrt/division needed + angle = atan2f(static_cast(-tx), static_cast(ty)) * (180.0f / std::numbers::pi_v); SAFE_PUBLISH_SENSOR(this->move_angle_sensors_[index], angle); #endif #ifdef USE_TEXT_SENSOR From 80e0761bf1c1085296aa785405e57e2c16cc6e38 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 14:26:31 -1000 Subject: [PATCH 284/609] [ld2450] Use integer dedup for direction text sensor updates (#14386) --- esphome/components/ld2450/ld2450.cpp | 9 +++++---- esphome/components/ld2450/ld2450.h | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 5ec3ce47d7..ca64836bf8 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -516,10 +516,11 @@ void LD2450Component::handle_periodic_data_() { } else { direction = DIRECTION_STATIONARY; } - text_sensor::TextSensor *tsd = this->direction_text_sensors_[index]; - const auto *dir_str = find_str(ld2450::DIRECTION_BY_UINT, direction); - if (tsd != nullptr && (!tsd->has_state() || tsd->get_state() != dir_str)) { - tsd->publish_state(dir_str); + if (this->direction_dedup_[index].next(direction)) { + text_sensor::TextSensor *tsd = this->direction_text_sensors_[index]; + if (tsd != nullptr) { + tsd->publish_state(find_str(ld2450::DIRECTION_BY_UINT, direction)); + } } #endif diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 30f96c0a9c..518b320b0a 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -194,7 +194,8 @@ class LD2450Component : public Component, public uart::UARTDevice { std::array *, MAX_ZONES> zone_moving_target_count_sensors_{}; #endif #ifdef USE_TEXT_SENSOR - std::array direction_text_sensors_{}; + std::array direction_text_sensors_{}; + std::array, MAX_TARGETS> direction_dedup_{}; #endif LazyCallbackManager data_callback_; From 82e620bcf5239bf1ad540986a54c50b2e3689326 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 14:26:55 -1000 Subject: [PATCH 285/609] [ld2450] Single-pass zone target counting (#14387) --- esphome/components/ld2450/ld2450.cpp | 22 ++++++++++++---------- esphome/components/ld2450/ld2450.h | 2 +- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index ca64836bf8..583918e5f5 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -283,16 +283,19 @@ void LD2450Component::loop() { } } -// Count targets in zone -uint8_t LD2450Component::count_targets_in_zone_(const Zone &zone, bool is_moving) { - uint8_t count = 0; - for (auto &index : this->target_info_) { - if (index.x > zone.x1 && index.x < zone.x2 && index.y > zone.y1 && index.y < zone.y2 && - index.is_moving == is_moving) { - count++; +// Count targets in zone (single pass for both still and moving) +void LD2450Component::count_targets_in_zone_(const Zone &zone, uint8_t &still, uint8_t &moving) { + still = 0; + moving = 0; + for (auto &target : this->target_info_) { + if (target.x > zone.x1 && target.x < zone.x2 && target.y > zone.y1 && target.y < zone.y2) { + if (target.is_moving) { + moving++; + } else { + still++; + } } } - return count; } // Service reset_radar_zone @@ -540,8 +543,7 @@ void LD2450Component::handle_periodic_data_() { uint8_t zone_moving_targets = 0; uint8_t zone_all_targets = 0; for (index = 0; index < MAX_ZONES; index++) { - zone_still_targets = this->count_targets_in_zone_(this->zone_config_[index], false); - zone_moving_targets = this->count_targets_in_zone_(this->zone_config_[index], true); + this->count_targets_in_zone_(this->zone_config_[index], zone_still_targets, zone_moving_targets); zone_all_targets = zone_still_targets + zone_moving_targets; // Publish Still Target Count in Zones diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 518b320b0a..39b0ebd9da 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -163,7 +163,7 @@ class LD2450Component : public Component, public uart::UARTDevice { void save_to_flash_(float value); float restore_from_flash_(); bool get_timeout_status_(uint32_t check_millis); - uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving); + void count_targets_in_zone_(const Zone &zone, uint8_t &still, uint8_t &moving); uint32_t presence_millis_ = 0; uint32_t still_presence_millis_ = 0; From 3f97b3b7066c52bb7df8a9e5e686d290d7e88e23 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 14:27:06 -1000 Subject: [PATCH 286/609] [core] Extract set_status_flag_ helper to deduplicate status_set methods (#14384) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/core/component.cpp | 25 +++++++++++-------------- esphome/core/component.h | 6 ++++++ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 1fd621ea83..fd4e0d2984 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -383,31 +383,30 @@ bool Component::is_idle() const { return (this->component_state_ & COMPONENT_STA bool Component::can_proceed() { return true; } bool Component::status_has_warning() const { return this->component_state_ & STATUS_LED_WARNING; } bool Component::status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; } +bool Component::set_status_flag_(uint8_t flag) { + if ((this->component_state_ & flag) != 0) + return false; + this->component_state_ |= flag; + App.app_state_ |= flag; + return true; +} void Component::status_set_warning(const char *message) { - // Don't spam the log. This risks missing different warning messages though. - if ((this->component_state_ & STATUS_LED_WARNING) != 0) + if (!this->set_status_flag_(STATUS_LED_WARNING)) return; - this->component_state_ |= STATUS_LED_WARNING; - App.app_state_ |= STATUS_LED_WARNING; ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? message : LOG_STR_LITERAL("unspecified")); } void Component::status_set_warning(const LogString *message) { - // Don't spam the log. This risks missing different warning messages though. - if ((this->component_state_ & STATUS_LED_WARNING) != 0) + if (!this->set_status_flag_(STATUS_LED_WARNING)) return; - this->component_state_ |= STATUS_LED_WARNING; - App.app_state_ |= STATUS_LED_WARNING; ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); } void Component::status_set_error() { this->status_set_error((const LogString *) nullptr); } void Component::status_set_error(const char *message) { - if ((this->component_state_ & STATUS_LED_ERROR) != 0) + if (!this->set_status_flag_(STATUS_LED_ERROR)) return; - this->component_state_ |= STATUS_LED_ERROR; - App.app_state_ |= STATUS_LED_ERROR; ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? message : LOG_STR_LITERAL("unspecified")); if (message != nullptr) { @@ -415,10 +414,8 @@ void Component::status_set_error(const char *message) { } } void Component::status_set_error(const LogString *message) { - if ((this->component_state_ & STATUS_LED_ERROR) != 0) + if (!this->set_status_flag_(STATUS_LED_ERROR)) return; - this->component_state_ |= STATUS_LED_ERROR; - App.app_state_ |= STATUS_LED_ERROR; ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); if (message != nullptr) { diff --git a/esphome/core/component.h b/esphome/core/component.h index 0b0b6345c7..6b920da290 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -299,6 +299,12 @@ class Component { this->component_state_ |= state; } + /// Helper to set a status LED flag on both this component and the app. + /// Returns true if the flag was newly set, false if it was already set. + /// Note: Callers often use the return value to decide whether to log a warning/error, + /// so once a flag is set, subsequent (potentially different) messages may be suppressed. + bool set_status_flag_(uint8_t flag); + /** Set an interval function with a unique name. Empty name means no cancelling possible. * * This will call f every interval ms. Can be cancelled via CancelInterval(). From 19bbd39e334332d5ebc3f33a2a435e96ab54555f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 17:06:46 -1000 Subject: [PATCH 287/609] [uart] Enable wake-on-RX by default on ESP32 (#14391) --- esphome/components/uart/__init__.py | 42 +++++----------------- esphome/components/zwave_proxy/__init__.py | 3 -- 2 files changed, 8 insertions(+), 37 deletions(-) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index a6f4cf5d5f..69db4b44aa 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -1,4 +1,3 @@ -from dataclasses import dataclass from logging import getLogger import math import re @@ -117,38 +116,6 @@ MULTI_CONF = True MULTI_CONF_NO_DEFAULT = True -@dataclass -class UARTData: - """State data for UART component configuration generation.""" - - wake_loop_on_rx: bool = False - - -def _get_data() -> UARTData: - """Get UART component data from CORE.data.""" - if DOMAIN not in CORE.data: - CORE.data[DOMAIN] = UARTData() - return CORE.data[DOMAIN] - - -def request_wake_loop_on_rx() -> None: - """Request that the UART wake the main loop when data is received. - - Components that need low-latency notification of incoming UART data - should call this function during their code generation. - This enables the RX event task which wakes the main loop when data arrives. - """ - data = _get_data() - if not data.wake_loop_on_rx: - data.wake_loop_on_rx = True - - # UART RX event task uses wake_loop_threadsafe() to notify the main loop - # Automatically enable the socket wake infrastructure when RX wake is requested - from esphome.components import socket - - socket.require_wake_loop_threadsafe() - - def validate_raw_data(value): if isinstance(value, str): return value.encode("utf-8") @@ -542,7 +509,14 @@ async def uart_write_to_code(config, action_id, template_arg, args): @coroutine_with_priority(CoroPriority.FINAL) async def final_step(): """Final code generation step to configure optional UART features.""" - if _get_data().wake_loop_on_rx: + if CORE.is_esp32 and CORE.has_networking: + # Wake-on-RX is essentially free on ESP32 (just an ISR function pointer + # registration) — enable by default to reduce RX buffer overflow risk + # by waking the main loop immediately when data arrives. + # Requires networking for the wake_loop_isrsafe() infrastructure. + from esphome.components import socket + + socket.require_wake_loop_threadsafe() cg.add_define("USE_UART_WAKE_LOOP_ON_RX") diff --git a/esphome/components/zwave_proxy/__init__.py b/esphome/components/zwave_proxy/__init__.py index 5be05bb464..d88f9f7041 100644 --- a/esphome/components/zwave_proxy/__init__.py +++ b/esphome/components/zwave_proxy/__init__.py @@ -41,6 +41,3 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) cg.add_define("USE_ZWAVE_PROXY") - - # Request UART to wake the main loop when data arrives for low-latency processing - uart.request_wake_loop_on_rx() From a1760a198047d92a36fe9c8af01ca5c86f736d80 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Mar 2026 10:23:10 -1000 Subject: [PATCH 288/609] [improv_serial] Add missing USE_IMPROV_SERIAL define to fix WiFi scan filtering (#14359) --- esphome/components/improv_serial/__init__.py | 1 + esphome/components/wifi/wifi_component.cpp | 2 +- esphome/components/wifi/wifi_component.h | 4 ++-- esphome/components/wifi/wifi_component_esp8266.cpp | 2 +- esphome/components/wifi/wifi_component_esp_idf.cpp | 2 +- esphome/components/wifi/wifi_component_libretiny.cpp | 2 +- esphome/components/wifi/wifi_component_pico_w.cpp | 2 +- esphome/core/defines.h | 1 + 8 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index 9a2ac2f40f..4266f5b78b 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -43,3 +43,4 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await improv_base.setup_improv_core(var, config, "improv_serial") + cg.add_define("USE_IMPROV_SERIAL") diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 1e6961b8bd..7d5d0133c1 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -2121,7 +2121,7 @@ bool WiFiComponent::can_proceed() { #endif void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } -bool WiFiComponent::is_connected() { +bool WiFiComponent::is_connected() const { return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 63c7039f21..a6f03a08d9 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -443,7 +443,7 @@ class WiFiComponent : public Component { void set_reboot_timeout(uint32_t reboot_timeout); - bool is_connected(); + bool is_connected() const; void set_power_save_mode(WiFiPowerSaveMode power_save); void set_min_auth_mode(WifiMinAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; } @@ -677,7 +677,7 @@ class WiFiComponent : public Component { bool wifi_apply_hostname_(); bool wifi_sta_connect_(const WiFiAP &ap); void wifi_pre_setup_(); - WiFiSTAConnectStatus wifi_sta_connect_status_(); + WiFiSTAConnectStatus wifi_sta_connect_status_() const; bool wifi_scan_start_(bool passive); #ifdef USE_WIFI_AP diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 8911bf15e0..bd6a18a99b 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -622,7 +622,7 @@ void WiFiComponent::wifi_pre_setup_() { this->wifi_mode_(false, false); } -WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { +WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { station_status_t status = wifi_station_get_connect_status(); if (status == STATION_GOT_IP) return WiFiSTAConnectStatus::CONNECTED; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 57bbceb1b8..734d186205 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -921,7 +921,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } } -WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { +WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { if (s_sta_connected && this->got_ipv4_address_) { #if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) if (this->num_ipv6_addresses_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT) { diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 1c5490a3ac..4c4150e44d 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -634,7 +634,7 @@ void WiFiComponent::wifi_pre_setup_() { // Make sure WiFi is in clean state before anything starts this->wifi_mode_(false, false); } -WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { +WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { // Use state machine instead of querying WiFi.status() directly // State is updated in main loop from queued events, ensuring thread safety switch (s_sta_state) { diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 9b2c077dc5..270425d8c2 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -120,7 +120,7 @@ const char *get_disconnect_reason_str(uint8_t reason) { return "UNKNOWN"; } -WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { +WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { // Use cyw43_wifi_link_status instead of cyw43_tcpip_link_status because the Arduino // framework's __wrap_cyw43_cb_tcpip_init is a no-op — the SDK's internal netif // (cyw43_state.netif[]) is never initialized. cyw43_tcpip_link_status checks that netif's diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 5c982c94b1..181425c162 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -55,6 +55,7 @@ #define USE_HOMEASSISTANT_TIME #define USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT 8000 // NOLINT #define USE_IMAGE +#define USE_IMPROV_SERIAL #define USE_IMPROV_SERIAL_NEXT_URL #define USE_INFRARED #define USE_IR_RF From 48b5cae6c4692c6f563b1c250aa266e31b0b1b98 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Mar 2026 11:32:44 -1000 Subject: [PATCH 289/609] [api] Use StringRef for user service string arguments (#13974) --- esphome/automation.py | 57 +++++- esphome/components/api/__init__.py | 15 +- esphome/components/api/api_connection.cpp | 43 +++-- esphome/components/api/api_frame_helper.h | 4 + .../components/api/api_frame_helper_noise.cpp | 30 ++- .../api/api_frame_helper_plaintext.cpp | 7 +- esphome/components/api/user_services.cpp | 5 + esphome/components/button/__init__.py | 4 +- esphome/components/cover/__init__.py | 20 +- esphome/components/fan/__init__.py | 9 +- esphome/components/globals/__init__.py | 1 + esphome/components/light/automation.py | 12 +- esphome/components/logger/__init__.py | 4 +- esphome/components/mqtt/__init__.py | 7 +- esphome/components/number/__init__.py | 6 + esphome/components/output/__init__.py | 7 +- esphome/components/script/__init__.py | 2 + esphome/components/select/__init__.py | 7 + esphome/components/switch/__init__.py | 14 +- esphome/components/text/__init__.py | 1 + esphome/core/string_ref.h | 14 ++ esphome/util.py | 16 +- tests/unit_tests/test_automation.py | 177 ++++++++++++++++++ 23 files changed, 406 insertions(+), 56 deletions(-) create mode 100644 tests/unit_tests/test_automation.py diff --git a/esphome/automation.py b/esphome/automation.py index 2439b1ddc4..d9b8b2ec57 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -57,8 +57,23 @@ def maybe_conf(conf, *validators): return validate -def register_action(name: str, action_type: MockObjClass, schema: cv.Schema): - return ACTION_REGISTRY.register(name, action_type, schema) +def register_action( + name: str, + action_type: MockObjClass, + schema: cv.Schema, + *, + synchronous: bool = False, +): + """Register an action type. + + Actions default to ``synchronous=False`` (safe default), meaning string + arguments use owning std::string to prevent dangling references. + + Set ``synchronous=True`` only for actions that complete synchronously + and never store trigger arguments for later execution. This allows + the code generator to use non-owning StringRef for zero-copy access. + """ + return ACTION_REGISTRY.register(name, action_type, schema, synchronous=synchronous) def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schema): @@ -335,7 +350,9 @@ async def component_is_idle_condition_to_code( @register_action( - "delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds) + "delay", + DelayAction, + cv.templatable(cv.positive_time_period_milliseconds), ) async def delay_action_to_code( config: ConfigType, @@ -366,6 +383,7 @@ async def delay_action_to_code( cv.has_at_least_one_key(CONF_THEN, CONF_ELSE), cv.has_at_least_one_key(CONF_CONDITION, CONF_ANY, CONF_ALL), ), + synchronous=True, ) async def if_action_to_code( config: ConfigType, @@ -394,6 +412,7 @@ async def if_action_to_code( cv.Required(CONF_THEN): validate_action_list, } ), + synchronous=True, ) async def while_action_to_code( config: ConfigType, @@ -417,6 +436,7 @@ async def while_action_to_code( cv.Required(CONF_THEN): validate_action_list, } ), + synchronous=True, ) async def repeat_action_to_code( config: ConfigType, @@ -461,7 +481,12 @@ async def wait_until_action_to_code( return var -@register_action("lambda", LambdaAction, cv.lambda_) +# Lambda executes user C++ inline and returns — synchronous by execution model. +# User code could theoretically store the StringRef for deferred use, but StringRef +# is a view type and storing views beyond their scope is always unsafe regardless +# of this optimization. Marking non-synchronous would disable StringRef for nearly +# all user services since most use lambda. +@register_action("lambda", LambdaAction, cv.lambda_, synchronous=True) async def lambda_action_to_code( config: ConfigType, action_id: ID, @@ -480,6 +505,7 @@ async def lambda_action_to_code( cv.Required(CONF_ID): cv.use_id(cg.PollingComponent), } ), + synchronous=True, ) async def component_update_action_to_code( config: ConfigType, @@ -499,6 +525,7 @@ async def component_update_action_to_code( cv.Required(CONF_ID): cv.use_id(cg.PollingComponent), } ), + synchronous=True, ) async def component_suspend_action_to_code( config: ConfigType, @@ -521,6 +548,7 @@ async def component_suspend_action_to_code( ), } ), + synchronous=True, ) async def component_resume_action_to_code( config: ConfigType, @@ -578,6 +606,27 @@ async def build_condition_list( return conditions +def has_non_synchronous_actions(actions: ConfigType) -> bool: + """Check if a validated action list contains any non-synchronous actions. + + Non-synchronous actions (delay, wait_until, script.wait, etc.) store + trigger args for later execution, making non-owning types like StringRef + unsafe. Actions that haven't been audited default to non-synchronous. + """ + if isinstance(actions, list): + return any(has_non_synchronous_actions(item) for item in actions) + if isinstance(actions, dict): + for key in actions: + if key in ACTION_REGISTRY and not ACTION_REGISTRY[key].synchronous: + return True + return any( + has_non_synchronous_actions(v) + for v in actions.values() + if isinstance(v, (list, dict)) + ) + return False + + async def build_automation( trigger: MockObj, args: TemplateArgsType, config: ConfigType ) -> MockObj: diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 3f7cafb485..d7b6bec357 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -76,7 +76,7 @@ SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = { "bool": cg.bool_, "int": cg.int32, "float": cg.float_, - "string": cg.std_string, + "string": cg.StringRef, "bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"), "int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"), "float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"), @@ -380,9 +380,18 @@ async def to_code(config: ConfigType) -> None: if is_optional: func_args.append((cg.bool_, "return_response")) + # Check if action chain has non-synchronous actions that would make + # non-owning StringRef dangle (rx_buf_ reused after delay) + has_non_synchronous = automation.has_non_synchronous_actions( + conf.get(CONF_THEN, []) + ) + service_arg_names: list[str] = [] for name, var_ in conf[CONF_VARIABLES].items(): native = SERVICE_ARG_NATIVE_TYPES[var_] + # Fall back to std::string for string args if non-synchronous actions exist + if has_non_synchronous and native is cg.StringRef: + native = cg.std_string service_template_args.append(native) func_args.append((native, name)) service_arg_names.append(name) @@ -509,11 +518,13 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( "homeassistant.action", HomeAssistantServiceCallAction, HOMEASSISTANT_ACTION_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( "homeassistant.service", HomeAssistantServiceCallAction, HOMEASSISTANT_ACTION_ACTION_SCHEMA, + synchronous=True, ) async def homeassistant_service_to_code( config: ConfigType, @@ -604,6 +615,7 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema( "homeassistant.event", HomeAssistantServiceCallAction, HOMEASSISTANT_EVENT_ACTION_SCHEMA, + synchronous=True, ) async def homeassistant_event_to_code(config, action_id, template_arg, args): cg.add_define("USE_API_HOMEASSISTANT_SERVICES") @@ -644,6 +656,7 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value( "homeassistant.tag_scanned", HomeAssistantServiceCallAction, HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA, + synchronous=True, ) async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args): cg.add_define("USE_API_HOMEASSISTANT_SERVICES") diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8b2efdde51..215af611db 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1711,37 +1711,42 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes return; } + // Null-terminate state in-place for safe c_str() usage (e.g., parse_number in callbacks). + // Safe: decode is complete, byte after string data was already consumed during parse, + // and frame helpers reserve RX_BUF_NULL_TERMINATOR extra byte in rx_buf_. + // const_cast is safe: msg references mutable rx_buf_ data; the const& handler + // signature is a generated protobuf pattern, not a true immutability contract. + if (!msg.state.empty()) { + const_cast(msg.state.c_str())[msg.state.size()] = '\0'; + } + for (auto &it : this->parent_->get_state_subs()) { - // Compare entity_id: check length matches and content matches - size_t entity_id_len = strlen(it.entity_id); - if (entity_id_len != msg.entity_id.size() || - memcmp(it.entity_id, msg.entity_id.c_str(), msg.entity_id.size()) != 0) { + if (msg.entity_id != it.entity_id) { continue; } - // Compare attribute: either both have matching attribute, or both have none - size_t sub_attr_len = it.attribute != nullptr ? strlen(it.attribute) : 0; - if (sub_attr_len != msg.attribute.size() || - (sub_attr_len > 0 && memcmp(it.attribute, msg.attribute.c_str(), sub_attr_len) != 0)) { + // If subscriber has attribute filter (non-null), message attribute must match it; + // if subscriber has no filter (nullptr), message must have no attribute. + if (it.attribute != nullptr ? msg.attribute != it.attribute : !msg.attribute.empty()) { continue; } - // Create null-terminated state for callback (parse_number needs null-termination) - // HA state max length is 255 characters, but attributes can be much longer - // Use stack buffer for common case (states), heap fallback for large attributes - size_t state_len = msg.state.size(); - SmallBufferWithHeapFallback state_buf_alloc(state_len + 1); - char *state_buf = reinterpret_cast(state_buf_alloc.get()); - if (state_len > 0) { - memcpy(state_buf, msg.state.c_str(), state_len); - } - state_buf[state_len] = '\0'; - it.callback(StringRef(state_buf, state_len)); + it.callback(msg.state); } } #endif #ifdef USE_API_USER_DEFINED_ACTIONS void APIConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { + // Null-terminate string args in-place for safe c_str() usage in YAML service triggers. + // Safe: full ExecuteServiceRequest decode is complete, all bytes in rx_buf_ consumed, + // and frame helpers reserve RX_BUF_NULL_TERMINATOR extra byte for the last field. + // const_cast is safe: msg references mutable rx_buf_ data; the const& handler + // signature is a generated protobuf pattern, not a true immutability contract. + for (auto &arg : const_cast(msg).args) { + if (!arg.string_.empty()) { + const_cast(arg.string_.c_str())[arg.string_.size()] = '\0'; + } + } bool found = false; #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES // Register the call and get a unique server-generated action_call_id diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 03f3814bb9..2b4e9ea3cd 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -29,6 +29,10 @@ static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266 static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms #endif +// Extra byte reserved in rx_buf_ beyond the message size so protobuf +// StringRef fields can be null-terminated in-place after decode. +static constexpr uint16_t RX_BUF_NULL_TERMINATOR = 1; + // Maximum number of messages to batch in a single write operation // Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there) static constexpr size_t MAX_MESSAGES_PER_BATCH = 34; diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 2aad732f7f..3ae35e9be8 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -194,16 +194,21 @@ APIError APINoiseFrameHelper::try_read_frame_() { uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; // Check against size limits to prevent OOM: MAX_HANDSHAKE_SIZE for handshake, MAX_MESSAGE_SIZE for data - uint16_t limit = (state_ == State::DATA) ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE; + bool is_data = (state_ == State::DATA); + uint16_t limit = is_data ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE; if (msg_size > limit) { state_ = State::FAILED; HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, limit); - return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN; + return is_data ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN; } - // Reserve space for body - if (this->rx_buf_.size() != msg_size) { - this->rx_buf_.resize(msg_size); + // Reserve space for body (+ null terminator in DATA state so protobuf + // StringRef fields can be safely null-terminated in-place after decode. + // During handshake, rx_buf_.size() is used in prologue construction, so + // the buffer must be exactly msg_size to avoid prologue mismatch.) + uint16_t alloc_size = msg_size + (is_data ? RX_BUF_NULL_TERMINATOR : 0); + if (this->rx_buf_.size() != alloc_size) { + this->rx_buf_.resize(alloc_size); } if (rx_buf_len_ < msg_size) { @@ -407,7 +412,18 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { NoiseBuffer mbuf; noise_buffer_init(mbuf); - noise_buffer_set_inout(mbuf, this->rx_buf_.data(), this->rx_buf_.size(), this->rx_buf_.size()); + // read_packet() must only be called in DATA state; the extra + // RX_BUF_NULL_TERMINATOR byte is only allocated in DATA state + // (see try_read_frame_), so calling this during handshake would + // underflow the size calculation below. +#ifdef ESPHOME_DEBUG_API + assert(this->state_ == State::DATA); +#endif + // rx_buf_ has RX_BUF_NULL_TERMINATOR extra byte for null termination + // (only added in DATA state — see try_read_frame_), so subtract it + // to get the actual encrypted data size for decryption. + size_t encrypted_size = this->rx_buf_.size() - RX_BUF_NULL_TERMINATOR; + noise_buffer_set_inout(mbuf, this->rx_buf_.data(), encrypted_size, encrypted_size); int err = noise_cipherstate_decrypt(this->recv_cipher_, &mbuf); APIError decrypt_err = handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED); @@ -574,7 +590,9 @@ APIError APINoiseFrameHelper::init_handshake_() { } APIError APINoiseFrameHelper::check_handshake_finished_() { +#ifdef ESPHOME_DEBUG_API assert(state_ == State::HANDSHAKE); +#endif int action = noise_handshakestate_get_action(handshake_); if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 5069dbf68b..e2bb56e0ac 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -163,9 +163,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_() { } // header reading done - // Reserve space for body - if (this->rx_buf_.size() != this->rx_header_parsed_len_) { - this->rx_buf_.resize(this->rx_header_parsed_len_); + // Reserve space for body (+ null terminator so protobuf StringRef fields + // can be safely null-terminated in-place after decode) + if (this->rx_buf_.size() != this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR) { + this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR); } if (rx_buf_len_ < rx_header_parsed_len_) { diff --git a/esphome/components/api/user_services.cpp b/esphome/components/api/user_services.cpp index 9c2b4aa79a..28a43c656c 100644 --- a/esphome/components/api/user_services.cpp +++ b/esphome/components/api/user_services.cpp @@ -1,5 +1,6 @@ #include "user_services.h" #include "esphome/core/log.h" +#include "esphome/core/string_ref.h" namespace esphome::api { @@ -11,6 +12,8 @@ template<> int32_t get_execute_arg_value(const ExecuteServiceArgument & } template<> float get_execute_arg_value(const ExecuteServiceArgument &arg) { return arg.float_; } template<> std::string get_execute_arg_value(const ExecuteServiceArgument &arg) { return arg.string_; } +// Zero-copy StringRef version for YAML-generated services (string_ is null-terminated after decode) +template<> StringRef get_execute_arg_value(const ExecuteServiceArgument &arg) { return arg.string_; } // Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { @@ -61,6 +64,8 @@ template<> enums::ServiceArgType to_service_arg_type() { return enums::SER template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_INT; } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_FLOAT; } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_STRING; } +// Zero-copy StringRef version for YAML-generated services +template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_STRING; } // Legacy std::vector versions for external components using custom_api_device.h template<> enums::ServiceArgType to_service_arg_type>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; } diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index d2f143b97e..94816a0974 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -123,7 +123,9 @@ BUTTON_PRESS_SCHEMA = maybe_simple_id( ) -@automation.register_action("button.press", PressAction, BUTTON_PRESS_SCHEMA) +@automation.register_action( + "button.press", PressAction, BUTTON_PRESS_SCHEMA, synchronous=True +) async def button_press_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 648fe7decf..17095f41f6 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -248,25 +248,33 @@ COVER_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action("cover.open", OpenAction, COVER_ACTION_SCHEMA) +@automation.register_action( + "cover.open", OpenAction, COVER_ACTION_SCHEMA, synchronous=True +) async def cover_open_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("cover.close", CloseAction, COVER_ACTION_SCHEMA) +@automation.register_action( + "cover.close", CloseAction, COVER_ACTION_SCHEMA, synchronous=True +) async def cover_close_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("cover.stop", StopAction, COVER_ACTION_SCHEMA) +@automation.register_action( + "cover.stop", StopAction, COVER_ACTION_SCHEMA, synchronous=True +) async def cover_stop_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("cover.toggle", ToggleAction, COVER_ACTION_SCHEMA) +@automation.register_action( + "cover.toggle", ToggleAction, COVER_ACTION_SCHEMA, synchronous=True +) async def cover_toggle_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @@ -283,7 +291,9 @@ COVER_CONTROL_ACTION_SCHEMA = cv.Schema( ) -@automation.register_action("cover.control", ControlAction, COVER_CONTROL_ACTION_SCHEMA) +@automation.register_action( + "cover.control", ControlAction, COVER_CONTROL_ACTION_SCHEMA, synchronous=True +) async def cover_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 6010aa8ed4..e839df6aee 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -311,13 +311,17 @@ FAN_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action("fan.toggle", ToggleAction, FAN_ACTION_SCHEMA) +@automation.register_action( + "fan.toggle", ToggleAction, FAN_ACTION_SCHEMA, synchronous=True +) async def fan_toggle_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("fan.turn_off", TurnOffAction, FAN_ACTION_SCHEMA) +@automation.register_action( + "fan.turn_off", TurnOffAction, FAN_ACTION_SCHEMA, synchronous=True +) async def fan_turn_off_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @@ -336,6 +340,7 @@ async def fan_turn_off_to_code(config, action_id, template_arg, args): ), } ), + synchronous=True, ) async def fan_turn_on_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index fc400c5dd1..fe11a93a4b 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -102,6 +102,7 @@ async def to_code(config): cv.Required(CONF_VALUE): cv.templatable(cv.string_strict), } ), + synchronous=True, ) async def globals_set_to_code(config, action_id, template_arg, args): full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID]) diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index e5aa8fa0e9..89b2fc0fb2 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -51,6 +51,7 @@ from .types import ( ), } ), + synchronous=True, ) async def light_toggle_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -111,13 +112,13 @@ LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id( @automation.register_action( - "light.turn_off", LightControlAction, LIGHT_TURN_OFF_ACTION_SCHEMA + "light.turn_off", LightControlAction, LIGHT_TURN_OFF_ACTION_SCHEMA, synchronous=True ) @automation.register_action( - "light.turn_on", LightControlAction, LIGHT_TURN_ON_ACTION_SCHEMA + "light.turn_on", LightControlAction, LIGHT_TURN_ON_ACTION_SCHEMA, synchronous=True ) @automation.register_action( - "light.control", LightControlAction, LIGHT_CONTROL_ACTION_SCHEMA + "light.control", LightControlAction, LIGHT_CONTROL_ACTION_SCHEMA, synchronous=True ) async def light_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -193,7 +194,10 @@ LIGHT_DIM_RELATIVE_ACTION_SCHEMA = cv.Schema( @automation.register_action( - "light.dim_relative", DimRelativeAction, LIGHT_DIM_RELATIVE_ACTION_SCHEMA + "light.dim_relative", + DimRelativeAction, + LIGHT_DIM_RELATIVE_ACTION_SCHEMA, + synchronous=True, ) async def light_dim_relative_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 264197c175..026b8aaf24 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -519,7 +519,9 @@ LOGGER_LOG_ACTION_SCHEMA = cv.All( ) -@automation.register_action(CONF_LOGGER_LOG, LambdaAction, LOGGER_LOG_ACTION_SCHEMA) +@automation.register_action( + CONF_LOGGER_LOG, LambdaAction, LOGGER_LOG_ACTION_SCHEMA, synchronous=True +) async def logger_log_action_to_code(config, action_id, template_arg, args): esp_log = LOG_LEVEL_TO_ESP_LOG[config[CONF_LEVEL]] args_ = [cg.RawExpression(str(x)) for x in config[CONF_ARGS]] diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 44e8836487..c25c472038 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -492,7 +492,7 @@ MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema( @automation.register_action( - "mqtt.publish", MQTTPublishAction, MQTT_PUBLISH_ACTION_SCHEMA + "mqtt.publish", MQTTPublishAction, MQTT_PUBLISH_ACTION_SCHEMA, synchronous=True ) async def mqtt_publish_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -521,7 +521,10 @@ MQTT_PUBLISH_JSON_ACTION_SCHEMA = cv.Schema( @automation.register_action( - "mqtt.publish_json", MQTTPublishJsonAction, MQTT_PUBLISH_JSON_ACTION_SCHEMA + "mqtt.publish_json", + MQTTPublishJsonAction, + MQTT_PUBLISH_JSON_ACTION_SCHEMA, + synchronous=True, ) async def mqtt_publish_json_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index d12ec7463b..2238f2c037 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -352,6 +352,7 @@ OPERATION_BASE_SCHEMA = cv.Schema( cv.Required(CONF_VALUE): cv.templatable(cv.float_), } ), + synchronous=True, ) async def number_set_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -374,6 +375,7 @@ async def number_set_to_code(config, action_id, template_arg, args): } ) ), + synchronous=True, ) @automation.register_action( "number.decrement", @@ -388,6 +390,7 @@ async def number_set_to_code(config, action_id, template_arg, args): } ) ), + synchronous=True, ) @automation.register_action( "number.to_min", @@ -401,6 +404,7 @@ async def number_set_to_code(config, action_id, template_arg, args): } ) ), + synchronous=True, ) @automation.register_action( "number.to_max", @@ -414,6 +418,7 @@ async def number_set_to_code(config, action_id, template_arg, args): } ) ), + synchronous=True, ) @automation.register_action( "number.operation", @@ -426,6 +431,7 @@ async def number_set_to_code(config, action_id, template_arg, args): cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean), } ), + synchronous=True, ) async def number_to_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/output/__init__.py b/esphome/components/output/__init__.py index bde106b085..a4c960927b 100644 --- a/esphome/components/output/__init__.py +++ b/esphome/components/output/__init__.py @@ -74,14 +74,16 @@ BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action("output.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA) +@automation.register_action( + "output.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA, synchronous=True +) async def output_turn_on_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_action( - "output.turn_off", TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA + "output.turn_off", TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA, synchronous=True ) async def output_turn_off_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -97,6 +99,7 @@ async def output_turn_off_to_code(config, action_id, template_arg, args): cv.Required(CONF_LEVEL): cv.templatable(cv.percentage), } ), + synchronous=True, ) async def output_set_level_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index 8d69981db0..369cefad91 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -160,6 +160,7 @@ async def to_code(config): cv.Optional(validate_parameter_name): cv.templatable(cv.valid), }, ), + synchronous=True, ) async def script_execute_action_to_code(config, action_id, template_arg, args): def convert(type: str): @@ -208,6 +209,7 @@ async def script_execute_action_to_code(config, action_id, template_arg, args): "script.stop", ScriptStopAction, maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}), + synchronous=True, ) async def script_stop_action_to_code(config, action_id, template_arg, args): full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID]) diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index 84ad591ba1..c114b140a9 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -145,6 +145,7 @@ OPERATION_BASE_SCHEMA = cv.Schema( cv.Required(CONF_OPTION): cv.templatable(cv.string_strict), } ), + synchronous=True, ) async def select_set_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -162,6 +163,7 @@ async def select_set_to_code(config, action_id, template_arg, args): cv.Required(CONF_INDEX): cv.templatable(cv.positive_int), } ), + synchronous=True, ) async def select_set_index_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -217,6 +219,7 @@ async def select_is_to_code(config, condition_id, template_arg, args): cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean), } ), + synchronous=True, ) @automation.register_action( "select.next", @@ -229,6 +232,7 @@ async def select_is_to_code(config, condition_id, template_arg, args): } ) ), + synchronous=True, ) @automation.register_action( "select.previous", @@ -243,6 +247,7 @@ async def select_is_to_code(config, condition_id, template_arg, args): } ) ), + synchronous=True, ) @automation.register_action( "select.first", @@ -254,6 +259,7 @@ async def select_is_to_code(config, condition_id, template_arg, args): } ) ), + synchronous=True, ) @automation.register_action( "select.last", @@ -265,6 +271,7 @@ async def select_is_to_code(config, condition_id, template_arg, args): } ) ), + synchronous=True, ) async def select_operation_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index cfc5e2b6e8..6f1be7d53d 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -204,7 +204,7 @@ SWITCH_CONTROL_ACTION_SCHEMA = automation.maybe_simple_id( @automation.register_action( - "switch.control", ControlAction, SWITCH_CONTROL_ACTION_SCHEMA + "switch.control", ControlAction, SWITCH_CONTROL_ACTION_SCHEMA, synchronous=True ) async def switch_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -214,9 +214,15 @@ async def switch_control_to_code(config, action_id, template_arg, args): return var -@automation.register_action("switch.toggle", ToggleAction, SWITCH_ACTION_SCHEMA) -@automation.register_action("switch.turn_off", TurnOffAction, SWITCH_ACTION_SCHEMA) -@automation.register_action("switch.turn_on", TurnOnAction, SWITCH_ACTION_SCHEMA) +@automation.register_action( + "switch.toggle", ToggleAction, SWITCH_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "switch.turn_off", TurnOffAction, SWITCH_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "switch.turn_on", TurnOnAction, SWITCH_ACTION_SCHEMA, synchronous=True +) async def switch_toggle_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/text/__init__.py b/esphome/components/text/__init__.py index 9ceea0dfdf..61f7119cad 100644 --- a/esphome/components/text/__init__.py +++ b/esphome/components/text/__init__.py @@ -164,6 +164,7 @@ OPERATION_BASE_SCHEMA = cv.Schema( cv.Required(CONF_VALUE): cv.templatable(cv.string_strict), } ), + synchronous=True, ) async def text_set_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index d502c4d27f..6047202753 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -81,6 +81,20 @@ class StringRef { operator std::string() const { return str(); } + /// Compare (compatible with std::string::compare) + int compare(const StringRef &other) const { + int result = std::memcmp(base_, other.base_, std::min(len_, other.len_)); + if (result != 0) + return result; + if (len_ < other.len_) + return -1; + if (len_ > other.len_) + return 1; + return 0; + } + int compare(const char *s) const { return compare(StringRef(s)); } + int compare(const std::string &s) const { return compare(StringRef(s)); } + /// Find first occurrence of substring, returns std::string::npos if not found. /// Note: Requires the underlying string to be null-terminated. size_type find(const char *s, size_type pos = 0) const { diff --git a/esphome/util.py b/esphome/util.py index 7b896de27e..686aa74306 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -24,11 +24,14 @@ class RegistryEntry: fun: Callable[..., Any], type_id: "MockObjClass", schema: "Schema", + *, + synchronous: bool = False, ): self.name = name self.fun = fun self.type_id = type_id self.raw_schema = schema + self.synchronous = synchronous @property def coroutine_fun(self): @@ -49,9 +52,18 @@ class Registry(dict[str, RegistryEntry]): self.base_schema = base_schema or {} self.type_id_key = type_id_key - def register(self, name: str, type_id: "MockObjClass", schema: "Schema"): + def register( + self, + name: str, + type_id: "MockObjClass", + schema: "Schema", + *, + synchronous: bool = False, + ): def decorator(fun: Callable[..., Any]): - self[name] = RegistryEntry(name, fun, type_id, schema) + self[name] = RegistryEntry( + name, fun, type_id, schema, synchronous=synchronous + ) return fun return decorator diff --git a/tests/unit_tests/test_automation.py b/tests/unit_tests/test_automation.py new file mode 100644 index 0000000000..61fef8201d --- /dev/null +++ b/tests/unit_tests/test_automation.py @@ -0,0 +1,177 @@ +"""Tests for esphome.automation module.""" + +from collections.abc import Generator +from unittest.mock import patch + +import pytest + +from esphome.automation import has_non_synchronous_actions +from esphome.util import RegistryEntry + + +def _make_registry(non_synchronous_actions: set[str]) -> dict[str, RegistryEntry]: + """Create a mock ACTION_REGISTRY with specified non-synchronous actions. + + Uses the default synchronous=False, matching the real registry behavior. + """ + registry: dict[str, RegistryEntry] = {} + for name in non_synchronous_actions: + registry[name] = RegistryEntry(name, lambda: None, None, None) + return registry + + +@pytest.fixture +def mock_registry() -> Generator[dict[str, RegistryEntry]]: + """Fixture that patches ACTION_REGISTRY with delay, wait_until, script.wait as non-synchronous.""" + registry: dict[str, RegistryEntry] = _make_registry( + {"delay", "wait_until", "script.wait"} + ) + registry["logger.log"] = RegistryEntry( + "logger.log", lambda: None, None, None, synchronous=True + ) + with patch("esphome.automation.ACTION_REGISTRY", registry): + yield registry + + +def test_has_non_synchronous_actions_empty_list( + mock_registry: dict[str, RegistryEntry], +) -> None: + assert has_non_synchronous_actions([]) is False + + +def test_has_non_synchronous_actions_empty_dict( + mock_registry: dict[str, RegistryEntry], +) -> None: + assert has_non_synchronous_actions({}) is False + + +def test_has_non_synchronous_actions_non_dict_non_list( + mock_registry: dict[str, RegistryEntry], +) -> None: + assert has_non_synchronous_actions("string") is False + assert has_non_synchronous_actions(42) is False + assert has_non_synchronous_actions(None) is False + + +def test_has_non_synchronous_actions_delay( + mock_registry: dict[str, RegistryEntry], +) -> None: + assert has_non_synchronous_actions([{"delay": "1s"}]) is True + + +def test_has_non_synchronous_actions_wait_until( + mock_registry: dict[str, RegistryEntry], +) -> None: + assert has_non_synchronous_actions([{"wait_until": {"condition": {}}}]) is True + + +def test_has_non_synchronous_actions_script_wait( + mock_registry: dict[str, RegistryEntry], +) -> None: + assert has_non_synchronous_actions([{"script.wait": "script_id"}]) is True + + +def test_has_non_synchronous_actions_synchronous( + mock_registry: dict[str, RegistryEntry], +) -> None: + assert has_non_synchronous_actions([{"logger.log": "hello"}]) is False + + +def test_has_non_synchronous_actions_unknown_not_in_registry( + mock_registry: dict[str, RegistryEntry], +) -> None: + """Unknown actions not in registry are not flagged (only registered actions count).""" + assert has_non_synchronous_actions([{"unknown.action": "value"}]) is False + + +def test_has_non_synchronous_actions_default_non_synchronous( + mock_registry: dict[str, RegistryEntry], +) -> None: + """Actions registered without explicit synchronous=True default to non-synchronous.""" + mock_registry["some.action"] = RegistryEntry( + "some.action", lambda: None, None, None + ) + assert has_non_synchronous_actions([{"some.action": "value"}]) is True + + +def test_has_non_synchronous_actions_nested_in_then( + mock_registry: dict[str, RegistryEntry], +) -> None: + """Non-synchronous action nested inside a synchronous action's then block.""" + actions: list[dict[str, object]] = [ + { + "logger.log": "first", + "then": [{"delay": "1s"}], + } + ] + assert has_non_synchronous_actions(actions) is True + + +def test_has_non_synchronous_actions_deeply_nested( + mock_registry: dict[str, RegistryEntry], +) -> None: + """Non-synchronous action deeply nested in action structure.""" + actions: list[dict[str, object]] = [ + { + "if": { + "then": [ + {"logger.log": "hello"}, + {"delay": "500ms"}, + ] + } + } + ] + assert has_non_synchronous_actions(actions) is True + + +def test_has_non_synchronous_actions_none_in_nested( + mock_registry: dict[str, RegistryEntry], +) -> None: + """No non-synchronous actions even with nesting.""" + actions: list[dict[str, object]] = [ + { + "if": { + "then": [ + {"logger.log": "hello"}, + ] + } + } + ] + assert has_non_synchronous_actions(actions) is False + + +def test_has_non_synchronous_actions_multiple_one_non_synchronous( + mock_registry: dict[str, RegistryEntry], +) -> None: + assert ( + has_non_synchronous_actions( + [ + {"logger.log": "first"}, + {"delay": "1s"}, + {"logger.log": "second"}, + ] + ) + is True + ) + + +def test_has_non_synchronous_actions_multiple_all_synchronous( + mock_registry: dict[str, RegistryEntry], +) -> None: + assert ( + has_non_synchronous_actions( + [ + {"logger.log": "first"}, + {"logger.log": "second"}, + ] + ) + is False + ) + + +def test_has_non_synchronous_actions_dict_input( + mock_registry: dict[str, RegistryEntry], +) -> None: + """Direct dict input (single action).""" + assert has_non_synchronous_actions({"delay": "1s"}) is True + assert has_non_synchronous_actions({"logger.log": "hello"}) is False From 3e7424b307b5763bba217d5f0d5d18130b67be4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Mar 2026 15:22:55 -1000 Subject: [PATCH 290/609] [preferences] Reduce heap churn with small inline buffer optimization (#13259) --- esphome/components/esp32/preferences.cpp | 33 ++++----- esphome/components/libretiny/preferences.cpp | 33 ++++----- esphome/core/helpers.h | 72 ++++++++++++++++++++ 3 files changed, 96 insertions(+), 42 deletions(-) diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 8d6fdc86f6..a3ef10b21f 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -19,16 +19,7 @@ static constexpr size_t KEY_BUFFER_SIZE = 12; struct NVSData { uint32_t key; - std::unique_ptr data; - size_t len; - - void set_data(const uint8_t *src, size_t size) { - if (!this->data || this->len != size) { - this->data = std::make_unique(size); - this->len = size; - } - memcpy(this->data.get(), src, size); - } + SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.) }; static std::vector s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -41,14 +32,14 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { // try find in pending saves and update that for (auto &obj : s_pending_save) { if (obj.key == this->key) { - obj.set_data(data, len); + obj.data.set(data, len); return true; } } NVSData save{}; save.key = this->key; - save.set_data(data, len); - s_pending_save.emplace_back(std::move(save)); + save.data.set(data, len); + s_pending_save.push_back(std::move(save)); ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len); return true; } @@ -56,11 +47,11 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { // try find in pending saves and load from that for (auto &obj : s_pending_save) { if (obj.key == this->key) { - if (obj.len != len) { + if (obj.data.size() != len) { // size mismatch return false; } - memcpy(data, obj.data.get(), len); + memcpy(data, obj.data.data(), len); return true; } } @@ -133,10 +124,10 @@ class ESP32Preferences : public ESPPreferences { snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str); if (this->is_changed_(this->nvs_handle, save, key_str)) { - esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len); - ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len); + esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size()); + ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size()); if (err != 0) { - ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.len, esp_err_to_name(err)); + ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err)); failed++; last_err = err; last_key = save.key; @@ -144,7 +135,7 @@ class ESP32Preferences : public ESPPreferences { } written++; } else { - ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len); + ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size()); cached++; } } @@ -176,7 +167,7 @@ class ESP32Preferences : public ESPPreferences { return true; } // Check size first before allocating memory - if (actual_len != to_save.len) { + if (actual_len != to_save.data.size()) { return true; } // Most preferences are small, use stack buffer with heap fallback for large ones @@ -186,7 +177,7 @@ class ESP32Preferences : public ESPPreferences { ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err)); return true; } - return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0; + return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0; } bool reset() override { diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index 740c1a233a..1c101136e1 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -17,16 +17,7 @@ static constexpr size_t KEY_BUFFER_SIZE = 12; struct NVSData { uint32_t key; - std::unique_ptr data; - size_t len; - - void set_data(const uint8_t *src, size_t size) { - if (!this->data || this->len != size) { - this->data = std::make_unique(size); - this->len = size; - } - memcpy(this->data.get(), src, size); - } + SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.) }; static std::vector s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -41,14 +32,14 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend { // try find in pending saves and update that for (auto &obj : s_pending_save) { if (obj.key == this->key) { - obj.set_data(data, len); + obj.data.set(data, len); return true; } } NVSData save{}; save.key = this->key; - save.set_data(data, len); - s_pending_save.emplace_back(std::move(save)); + save.data.set(data, len); + s_pending_save.push_back(std::move(save)); ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len); return true; } @@ -57,11 +48,11 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend { // try find in pending saves and load from that for (auto &obj : s_pending_save) { if (obj.key == this->key) { - if (obj.len != len) { + if (obj.data.size() != len) { // size mismatch return false; } - memcpy(data, obj.data.get(), len); + memcpy(data, obj.data.data(), len); return true; } } @@ -122,11 +113,11 @@ class LibreTinyPreferences : public ESPPreferences { snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str); if (this->is_changed_(&this->db, save, key_str)) { - ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len); - fdb_blob_make(&this->blob, save.data.get(), save.len); + ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size()); + fdb_blob_make(&this->blob, save.data.data(), save.data.size()); fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob); if (err != FDB_NO_ERR) { - ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.len, err); + ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.data.size(), err); failed++; last_err = err; last_key = save.key; @@ -134,7 +125,7 @@ class LibreTinyPreferences : public ESPPreferences { } written++; } else { - ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len); + ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size()); cached++; } } @@ -159,7 +150,7 @@ class LibreTinyPreferences : public ESPPreferences { } // Check size first - if different, data has changed - if (kv.value_len != to_save.len) { + if (kv.value_len != to_save.data.size()) { return true; } @@ -173,7 +164,7 @@ class LibreTinyPreferences : public ESPPreferences { } // Compare the actual data - return memcmp(to_save.data.get(), stored_data.get(), kv.value_len) != 0; + return memcmp(to_save.data.data(), stored_data.get(), kv.value_len) != 0; } bool reset() override { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index b606e68df3..65d590a5e6 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -133,6 +133,78 @@ template class ConstVector { size_t size_; }; +/// Small buffer optimization - stores data inline when small, heap-allocates for large data +/// This avoids heap fragmentation for common small allocations while supporting arbitrary sizes. +/// Memory management is encapsulated - callers just use set() and data(). +template class SmallInlineBuffer { + public: + SmallInlineBuffer() = default; + ~SmallInlineBuffer() { + if (!this->is_inline_()) + delete[] this->heap_; + } + + // Move constructor + SmallInlineBuffer(SmallInlineBuffer &&other) noexcept : len_(other.len_) { + if (other.is_inline_()) { + memcpy(this->inline_, other.inline_, this->len_); + } else { + this->heap_ = other.heap_; + other.heap_ = nullptr; + } + other.len_ = 0; + } + + // Move assignment + SmallInlineBuffer &operator=(SmallInlineBuffer &&other) noexcept { + if (this != &other) { + if (!this->is_inline_()) + delete[] this->heap_; + this->len_ = other.len_; + if (other.is_inline_()) { + memcpy(this->inline_, other.inline_, this->len_); + } else { + this->heap_ = other.heap_; + other.heap_ = nullptr; + } + other.len_ = 0; + } + return *this; + } + + // Disable copy (would need deep copy of heap data) + SmallInlineBuffer(const SmallInlineBuffer &) = delete; + SmallInlineBuffer &operator=(const SmallInlineBuffer &) = delete; + + /// Set buffer contents, allocating heap if needed + void set(const uint8_t *src, size_t size) { + // Free existing heap allocation if switching from heap to inline or different heap size + if (!this->is_inline_() && (size <= InlineSize || size != this->len_)) { + delete[] this->heap_; + this->heap_ = nullptr; // Defensive: prevent use-after-free if logic changes + } + // Allocate new heap buffer if needed + if (size > InlineSize && (this->is_inline_() || size != this->len_)) { + this->heap_ = new uint8_t[size]; // NOLINT(cppcoreguidelines-owning-memory) + } + this->len_ = size; + memcpy(this->data(), src, size); + } + + uint8_t *data() { return this->is_inline_() ? this->inline_ : this->heap_; } + const uint8_t *data() const { return this->is_inline_() ? this->inline_ : this->heap_; } + size_t size() const { return this->len_; } + + protected: + bool is_inline_() const { return this->len_ <= InlineSize; } + + size_t len_{0}; + union { + uint8_t inline_[InlineSize]{}; // Zero-init ensures clean initial state + uint8_t *heap_; + }; +}; + /// Minimal static vector - saves memory by avoiding std::vector overhead template class StaticVector { public: From 0e18e4461e8d82df75b3acbb61c7dcc8c9993311 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Mar 2026 16:52:53 -1000 Subject: [PATCH 291/609] [time,api] Send pre-parsed timezone struct over protobuf (#14233) Co-authored-by: Claude Opus 4.6 --- esphome/components/api/api.proto | 28 ++++++++++ esphome/components/api/api_connection.cpp | 25 ++++++++- esphome/components/api/api_pb2.cpp | 54 ++++++++++++++++++ esphome/components/api/api_pb2.h | 38 ++++++++++++- esphome/components/api/api_pb2_dump.cpp | 39 +++++++++++++ esphome/components/time/__init__.py | 68 +++++++++++++++++++++-- 6 files changed, 245 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index d7f32cd8d1..802e3e3ae2 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -834,6 +834,33 @@ message GetTimeRequest { option (source) = SOURCE_SERVER; } +enum DSTRuleType { + DST_RULE_TYPE_NONE = 0; + DST_RULE_TYPE_MONTH_WEEK_DAY = 1; + DST_RULE_TYPE_JULIAN_NO_LEAP = 2; + DST_RULE_TYPE_DAY_OF_YEAR = 3; +} + +message DSTRule { + option (source) = SOURCE_CLIENT; + + sint32 time_seconds = 1; + uint32 day = 2; + DSTRuleType type = 3; + uint32 month = 4; + uint32 week = 5; + uint32 day_of_week = 6; +} + +message ParsedTimezone { + option (source) = SOURCE_CLIENT; + + sint32 std_offset_seconds = 1; + sint32 dst_offset_seconds = 2; + DSTRule dst_start = 3; + DSTRule dst_end = 4; +} + message GetTimeResponse { option (id) = 37; option (source) = SOURCE_CLIENT; @@ -841,6 +868,7 @@ message GetTimeResponse { fixed32 epoch_seconds = 1; string timezone = 2; + ParsedTimezone parsed_timezone = 3; } // ==================== USER-DEFINES SERVICES ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 215af611db..90287ec2dd 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1123,7 +1123,30 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) { homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds); #ifdef USE_TIME_TIMEZONE if (!value.timezone.empty()) { - homeassistant::global_homeassistant_time->set_timezone(value.timezone.c_str(), value.timezone.size()); + // Check if the sender provided pre-parsed timezone data. + // If std_offset is non-zero or DST rules are present, the parsed data was populated. + // For UTC (all zeros), string parsing produces the same result, so the fallback is equivalent. + const auto &pt = value.parsed_timezone; + if (pt.std_offset_seconds != 0 || pt.dst_start.type != enums::DST_RULE_TYPE_NONE) { + time::ParsedTimezone tz{}; + tz.std_offset_seconds = pt.std_offset_seconds; + tz.dst_offset_seconds = pt.dst_offset_seconds; + tz.dst_start.time_seconds = pt.dst_start.time_seconds; + tz.dst_start.day = static_cast(pt.dst_start.day); + tz.dst_start.type = static_cast(pt.dst_start.type); + tz.dst_start.month = static_cast(pt.dst_start.month); + tz.dst_start.week = static_cast(pt.dst_start.week); + tz.dst_start.day_of_week = static_cast(pt.dst_start.day_of_week); + tz.dst_end.time_seconds = pt.dst_end.time_seconds; + tz.dst_end.day = static_cast(pt.dst_end.day); + tz.dst_end.type = static_cast(pt.dst_end.type); + tz.dst_end.month = static_cast(pt.dst_end.month); + tz.dst_end.week = static_cast(pt.dst_end.week); + tz.dst_end.day_of_week = static_cast(pt.dst_end.day_of_week); + time::set_global_tz(tz); + } else { + homeassistant::global_homeassistant_time->set_timezone(value.timezone.c_str(), value.timezone.size()); + } } #endif } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 5c50a8aa5b..9e74d5ddc7 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -954,12 +954,66 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel return true; } #endif +bool DSTRule::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->time_seconds = value.as_sint32(); + break; + case 2: + this->day = value.as_uint32(); + break; + case 3: + this->type = static_cast(value.as_uint32()); + break; + case 4: + this->month = value.as_uint32(); + break; + case 5: + this->week = value.as_uint32(); + break; + case 6: + this->day_of_week = value.as_uint32(); + break; + default: + return false; + } + return true; +} +bool ParsedTimezone::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->std_offset_seconds = value.as_sint32(); + break; + case 2: + this->dst_offset_seconds = value.as_sint32(); + break; + default: + return false; + } + return true; +} +bool ParsedTimezone::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 3: + value.decode_to_message(this->dst_start); + break; + case 4: + value.decode_to_message(this->dst_end); + break; + default: + return false; + } + return true; +} bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 2: { this->timezone = StringRef(reinterpret_cast(value.data()), value.size()); break; } + case 3: + value.decode_to_message(this->parsed_timezone); + break; default: return false; } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 22dc3de995..033b789c18 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -63,6 +63,12 @@ enum LogLevel : uint32_t { LOG_LEVEL_VERBOSE = 6, LOG_LEVEL_VERY_VERBOSE = 7, }; +enum DSTRuleType : uint32_t { + DST_RULE_TYPE_NONE = 0, + DST_RULE_TYPE_MONTH_WEEK_DAY = 1, + DST_RULE_TYPE_JULIAN_NO_LEAP = 2, + DST_RULE_TYPE_DAY_OF_YEAR = 3, +}; #ifdef USE_API_USER_DEFINED_ACTIONS enum ServiceArgType : uint32_t { SERVICE_ARG_TYPE_BOOL = 0, @@ -1116,15 +1122,45 @@ class GetTimeRequest final : public ProtoMessage { protected: }; +class DSTRule final : public ProtoDecodableMessage { + public: + int32_t time_seconds{0}; + uint32_t day{0}; + enums::DSTRuleType type{}; + uint32_t month{0}; + uint32_t week{0}; + uint32_t day_of_week{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class ParsedTimezone final : public ProtoDecodableMessage { + public: + int32_t std_offset_seconds{0}; + int32_t dst_offset_seconds{0}; + DSTRule dst_start{}; + DSTRule dst_end{}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; class GetTimeResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 37; - static constexpr uint8_t ESTIMATED_SIZE = 14; + static constexpr uint8_t ESTIMATED_SIZE = 31; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "get_time_response"; } #endif uint32_t epoch_seconds{0}; StringRef timezone{}; + ParsedTimezone parsed_timezone{}; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 52d2486410..4eec42e936 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -208,6 +208,20 @@ template<> const char *proto_enum_to_string(enums::LogLevel val return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(enums::DSTRuleType value) { + switch (value) { + case enums::DST_RULE_TYPE_NONE: + return "DST_RULE_TYPE_NONE"; + case enums::DST_RULE_TYPE_MONTH_WEEK_DAY: + return "DST_RULE_TYPE_MONTH_WEEK_DAY"; + case enums::DST_RULE_TYPE_JULIAN_NO_LEAP: + return "DST_RULE_TYPE_JULIAN_NO_LEAP"; + case enums::DST_RULE_TYPE_DAY_OF_YEAR: + return "DST_RULE_TYPE_DAY_OF_YEAR"; + default: + return "UNKNOWN"; + } +} #ifdef USE_API_USER_DEFINED_ACTIONS template<> const char *proto_enum_to_string(enums::ServiceArgType value) { switch (value) { @@ -1254,10 +1268,35 @@ const char *GetTimeRequest::dump_to(DumpBuffer &out) const { out.append("GetTimeRequest {}"); return out.c_str(); } +const char *DSTRule::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "DSTRule"); + dump_field(out, "time_seconds", this->time_seconds); + dump_field(out, "day", this->day); + dump_field(out, "type", static_cast(this->type)); + dump_field(out, "month", this->month); + dump_field(out, "week", this->week); + dump_field(out, "day_of_week", this->day_of_week); + return out.c_str(); +} +const char *ParsedTimezone::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "ParsedTimezone"); + dump_field(out, "std_offset_seconds", this->std_offset_seconds); + dump_field(out, "dst_offset_seconds", this->dst_offset_seconds); + out.append(" dst_start: "); + this->dst_start.dump_to(out); + out.append("\n"); + out.append(" dst_end: "); + this->dst_end.dump_to(out); + out.append("\n"); + return out.c_str(); +} const char *GetTimeResponse::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "GetTimeResponse"); dump_field(out, "epoch_seconds", this->epoch_seconds); dump_field(out, "timezone", this->timezone); + out.append(" parsed_timezone: "); + this->parsed_timezone.dump_to(out); + out.append("\n"); return out.c_str(); } #ifdef USE_API_USER_DEFINED_ACTIONS diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index a20d79b857..7ffa408db9 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -1,6 +1,10 @@ from importlib import resources import logging +from aioesphomeapi.posix_tz import ( + DSTRuleType as PyDSTRuleType, + parse_posix_tz as parse_posix_tz_python, +) import tzlocal from esphome import automation @@ -39,6 +43,19 @@ CronTrigger = time_ns.class_("CronTrigger", automation.Trigger.template(), cg.Co SyncTrigger = time_ns.class_("SyncTrigger", automation.Trigger.template(), cg.Component) TimeHasTimeCondition = time_ns.class_("TimeHasTimeCondition", Condition) +# C++ types for pre-parsed timezone struct generation +DSTRuleType_cpp = time_ns.enum("DSTRuleType", is_class=True) +DSTRule_cpp = time_ns.struct("DSTRule") +ParsedTimezone_cpp = time_ns.struct("ParsedTimezone") + +# Map Python DSTRuleType enum values to C++ enum expressions +_DST_RULE_TYPE_MAP = { + PyDSTRuleType.NONE: DSTRuleType_cpp.NONE, + PyDSTRuleType.MONTH_WEEK_DAY: DSTRuleType_cpp.MONTH_WEEK_DAY, + PyDSTRuleType.JULIAN_NO_LEAP: DSTRuleType_cpp.JULIAN_NO_LEAP, + PyDSTRuleType.DAY_OF_YEAR: DSTRuleType_cpp.DAY_OF_YEAR, +} + def _load_tzdata(iana_key: str) -> bytes | None: # From https://tzdata.readthedocs.io/en/latest/#examples @@ -260,11 +277,17 @@ def validate_tz(value: str) -> str: value = cv.string_strict(value) tzfile = _load_tzdata(value) - if tzfile is None: - # Not a IANA key, probably a TZ string - return value + if tzfile is not None: + value = _extract_tz_string(tzfile) - return _extract_tz_string(tzfile) + # Validate that the POSIX TZ string is parseable (skip empty strings) + if value: + try: + parse_posix_tz_python(value) + except ValueError as e: + raise cv.Invalid(f"Invalid POSIX timezone string '{value}': {e}") from e + + return value TIME_SCHEMA = cv.Schema( @@ -305,11 +328,46 @@ TIME_SCHEMA = cv.Schema( ).extend(cv.polling_component_schema("15min")) +def _emit_dst_rule_fields(prefix, rule): + """Emit field-by-field assignments for a DSTRule to avoid rodata struct blob.""" + cg.add(cg.RawExpression(f"{prefix}.time_seconds = {rule.time_seconds}")) + cg.add(cg.RawExpression(f"{prefix}.day = {rule.day}")) + cg.add(cg.RawExpression(f"{prefix}.type = {_DST_RULE_TYPE_MAP[rule.type]}")) + cg.add(cg.RawExpression(f"{prefix}.month = {rule.month}")) + cg.add(cg.RawExpression(f"{prefix}.week = {rule.week}")) + cg.add(cg.RawExpression(f"{prefix}.day_of_week = {rule.day_of_week}")) + + +def _emit_parsed_timezone_fields(parsed): + """Emit field-by-field assignments for a local ParsedTimezone, then set_global_tz(). + + Uses individual assignments on a stack variable instead of a struct initializer + to keep constants as immediate operands in instructions (.irom0.text/flash) + rather than a const blob in .rodata (which maps to RAM on ESP8266). + Wrapped in a scope block to allow multiple time platforms in the same build. + """ + cg.add(cg.RawStatement("{")) + cg.add(cg.RawExpression("time::ParsedTimezone tz{}")) + cg.add(cg.RawExpression(f"tz.std_offset_seconds = {parsed.std_offset_seconds}")) + cg.add(cg.RawExpression(f"tz.dst_offset_seconds = {parsed.dst_offset_seconds}")) + _emit_dst_rule_fields("tz.dst_start", parsed.dst_start) + _emit_dst_rule_fields("tz.dst_end", parsed.dst_end) + cg.add(time_ns.set_global_tz(cg.RawExpression("tz"))) + cg.add(cg.RawStatement("}")) + + async def setup_time_core_(time_var, config): if timezone := config.get(CONF_TIMEZONE): - cg.add(time_var.set_timezone(timezone)) cg.add_define("USE_TIME_TIMEZONE") + if CORE.is_host: + # Host platform needs setenv("TZ")/tzset() for libc compatibility + cg.add(time_var.set_timezone(timezone)) + else: + # Embedded: pre-parse at codegen time, emit struct directly + parsed = parse_posix_tz_python(timezone) + _emit_parsed_timezone_fields(parsed) + for conf in config.get(CONF_ON_TIME, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], time_var) From 073ca63f60bb23a7c84106eb57a5ce06388b1295 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Mon, 2 Mar 2026 04:44:02 +0100 Subject: [PATCH 292/609] [rtttl] improve comments Part 2 (#13971) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/rtttl/rtttl.cpp | 189 ++++++++++++++++------------- esphome/components/rtttl/rtttl.h | 31 ++--- 2 files changed, 119 insertions(+), 101 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index ab95067f45..4ccfc539ea 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -8,18 +8,26 @@ namespace esphome::rtttl { static const char *const TAG = "rtttl"; -// These values can also be found as constants in the Tone library (Tone.h) -static const uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, - 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047, - 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, - 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951}; +static constexpr uint8_t SONG_NAME_LENGTH_LIMIT = 64; +static constexpr uint8_t SEMITONES_IN_OCTAVE = 12; -#if defined(USE_OUTPUT) || defined(USE_SPEAKER) -static const uint32_t DOUBLE_NOTE_GAP_MS = 10; -#endif // USE_OUTPUT || USE_SPEAKER +static constexpr uint8_t MIN_OCTAVE = 4; +static constexpr uint8_t MAX_OCTAVE = 7; + +static constexpr uint8_t DEFAULT_BPM = 63; // Default beats per minute + +// These values can also be found as constants in the Tone library (Tone.h) +static constexpr uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, + 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047, + 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, + 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951}; +static constexpr uint8_t NOTES_COUNT = static_cast(sizeof(NOTES) / sizeof(NOTES[0])); + +static constexpr uint8_t REPEATING_NOTE_GAP_MS = 10; #ifdef USE_SPEAKER -static const size_t SAMPLE_BUFFER_SIZE = 2048; +static constexpr uint16_t SAMPLE_BUFFER_SIZE = 2048; +static constexpr uint16_t SAMPLE_RATE = 16000; struct SpeakerSample { int8_t left{0}; @@ -27,7 +35,7 @@ struct SpeakerSample { }; inline double deg2rad(double degrees) { - static const double PI_ON_180 = 4.0 * atan(1.0) / 180.0; + static constexpr double PI_ON_180 = M_PI / 180.0; return degrees * PI_ON_180; } #endif // USE_SPEAKER @@ -85,7 +93,7 @@ void Rtttl::loop() { } #ifdef USE_OUTPUT - if (this->output_ != nullptr && millis() - this->last_note_ < this->note_duration_) { + if (this->output_ != nullptr && millis() - this->last_note_start_time_ < this->note_duration_) { return; } #endif // USE_OUTPUT @@ -113,36 +121,34 @@ void Rtttl::loop() { } if (this->samples_sent_ != this->samples_count_) { SpeakerSample sample[SAMPLE_BUFFER_SIZE + 2]; - int x = 0; + uint16_t sample_index = 0; double rem = 0.0; while (true) { - // Try and send out the remainder of the existing note, one per loop() - - if (this->samples_per_wave_ != 0 && this->samples_sent_ >= this->samples_gap_) { // Play note// + // Try and send out the remainder of the existing note, one per `loop()` + if (this->samples_per_wave_ != 0 && this->samples_sent_ >= this->samples_gap_) { // Play note rem = ((this->samples_sent_ << 10) % this->samples_per_wave_) * (360.0 / this->samples_per_wave_); - int16_t val = (127 * this->gain_) * sin(deg2rad(rem)); // 16bit = 49152 - - sample[x].left = val; - sample[x].right = val; + int8_t val = (127 * this->gain_) * sin(deg2rad(rem)); + sample[sample_index].left = val; + sample[sample_index].right = val; } else { - sample[x].left = 0; - sample[x].right = 0; + sample[sample_index].left = 0; + sample[sample_index].right = 0; } - if (static_cast(x) >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) { + if (sample_index >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) { break; } this->samples_sent_++; - x++; + sample_index++; } - if (x > 0) { - size_t bytes_to_send = x * sizeof(SpeakerSample); + if (sample_index > 0) { + size_t bytes_to_send = sample_index * sizeof(SpeakerSample); size_t send = this->speaker_->play((uint8_t *) (&sample), bytes_to_send); if (send != bytes_to_send) { - this->samples_sent_ -= (x - (send / sizeof(SpeakerSample))); + this->samples_sent_ -= (sample_index - (send / sizeof(SpeakerSample))); } return; } @@ -155,83 +161,84 @@ void Rtttl::loop() { return; } - // align to note: most rtttl's out there does not add and space after the ',' separator but just in case... + // Align to note: most rtttl's out there does not add any space after the ',' separator but just in case while (this->rtttl_[this->position_] == ',' || this->rtttl_[this->position_] == ' ') { this->position_++; } - // first, get note duration, if available - uint8_t num = this->get_integer_(); + // First, get note duration, if available + uint8_t note_denominator = this->get_integer_(); - if (num) { - this->note_duration_ = this->wholenote_ / num; + if (note_denominator) { + this->note_duration_ = this->wholenote_duration_ / note_denominator; } else { - this->note_duration_ = - this->wholenote_ / this->default_duration_; // we will need to check if we are a dotted note after + // We will need to check if we are a dotted note after + this->note_duration_ = this->wholenote_duration_ / this->default_note_denominator_; } - uint8_t note = note_index_from_char(this->rtttl_[this->position_]); + uint8_t note_index_in_octave = note_index_from_char(this->rtttl_[this->position_]); this->position_++; - // now, get optional '#' sharp + // Now, get optional '#' sharp if (this->rtttl_[this->position_] == '#') { - note++; + note_index_in_octave++; this->position_++; } - // now, get scale + // Now, get scale uint8_t scale = this->get_integer_(); if (scale == 0) { scale = this->default_octave_; } - if (scale < 4 || scale > 7) { - ESP_LOGE(TAG, "Octave must be between 4 and 7 (it is %d)", scale); + if (scale < MIN_OCTAVE || scale > MAX_OCTAVE) { + ESP_LOGE(TAG, "Octave must be between %d and %d (it is %d)", MIN_OCTAVE, MAX_OCTAVE, scale); this->finish_(); return; } - // now, get optional '.' dotted note + // Now, get optional '.' dotted note if (this->rtttl_[this->position_] == '.') { - this->note_duration_ += this->note_duration_ / 2; + this->note_duration_ += this->note_duration_ / 2; // Duration +50% this->position_++; } - // Now play the note bool need_note_gap = false; - if (note) { - auto note_index = (scale - 4) * 12 + note; - if (note_index < 0 || note_index >= (int) (sizeof(NOTES) / sizeof(NOTES[0]))) { - ESP_LOGE(TAG, "Note out of range (note: %d, scale: %d, index: %d, max: %d)", note, scale, note_index, - (int) (sizeof(NOTES) / sizeof(NOTES[0]))); + + // Now play the note + if (note_index_in_octave == 0) { + this->output_freq_ = 0; + ESP_LOGVV(TAG, "Waiting: %dms", this->note_duration_); + } else { + uint8_t note_index = (scale - MIN_OCTAVE) * SEMITONES_IN_OCTAVE + note_index_in_octave; + if (note_index >= NOTES_COUNT) { + ESP_LOGE(TAG, "Note out of range (note: %d, scale: %d, index: %d, max: %d)", note_index_in_octave, scale, + note_index, NOTES_COUNT); this->finish_(); return; } - auto freq = NOTES[note_index]; + uint16_t freq = NOTES[note_index]; need_note_gap = freq == this->output_freq_; // Add small silence gap between same note this->output_freq_ = freq; - ESP_LOGVV(TAG, "playing note: %d for %dms", note, this->note_duration_); - } else { - ESP_LOGVV(TAG, "waiting: %dms", this->note_duration_); - this->output_freq_ = 0; + ESP_LOGVV(TAG, "Playing note: %d for %dms", note_index_in_octave, this->note_duration_); } #ifdef USE_OUTPUT if (this->output_ != nullptr) { - if (need_note_gap && this->note_duration_ > DOUBLE_NOTE_GAP_MS) { + if (this->output_freq_ == 0) { this->output_->set_level(0.0); - delay(DOUBLE_NOTE_GAP_MS); - this->note_duration_ -= DOUBLE_NOTE_GAP_MS; - } - if (this->output_freq_ != 0) { + } else { + if (need_note_gap && this->note_duration_ > REPEATING_NOTE_GAP_MS) { + this->output_->set_level(0.0); + delay(REPEATING_NOTE_GAP_MS); + this->note_duration_ -= REPEATING_NOTE_GAP_MS; + } this->output_->update_frequency(this->output_freq_); this->output_->set_level(this->gain_); - } else { - this->output_->set_level(0.0); } } #endif // USE_OUTPUT @@ -241,28 +248,26 @@ void Rtttl::loop() { this->samples_sent_ = 0; this->samples_gap_ = 0; this->samples_per_wave_ = 0; - this->samples_count_ = (this->sample_rate_ * this->note_duration_) / 1000; + this->samples_count_ = (SAMPLE_RATE * this->note_duration_) / 1000; if (need_note_gap) { - this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1000; + this->samples_gap_ = (SAMPLE_RATE * REPEATING_NOTE_GAP_MS) / 1000; } if (this->output_freq_ != 0) { - // make sure there is enough samples to add a full last sinus. - - uint16_t samples_wish = this->samples_count_; - this->samples_per_wave_ = (this->sample_rate_ << 10) / this->output_freq_; + // Make sure there is enough samples to add a full last sinus. + uint32_t samples_wish = this->samples_count_; + this->samples_per_wave_ = (SAMPLE_RATE << 10) / this->output_freq_; uint16_t division = ((this->samples_count_ << 10) / this->samples_per_wave_) + 1; - this->samples_count_ = (division * this->samples_per_wave_); - this->samples_count_ = this->samples_count_ >> 10; - ESP_LOGVV(TAG, "- Calc play time: wish: %d gets: %d (div: %d spw: %d)", samples_wish, this->samples_count_, - division, this->samples_per_wave_); + this->samples_count_ = (division * this->samples_per_wave_) >> 10; + ESP_LOGVV(TAG, "Calc play time: wish: %" PRIu32 " gets: %" PRIu32 " (div: %d spw: %" PRIu32 ")", samples_wish, + this->samples_count_, division, this->samples_per_wave_); } // Convert from frequency in Hz to high and low samples in fixed point } #endif // USE_SPEAKER - this->last_note_ = millis(); + this->last_note_start_time_ = millis(); } void Rtttl::play(std::string rtttl) { @@ -275,25 +280,28 @@ void Rtttl::play(std::string rtttl) { this->rtttl_ = std::move(rtttl); - this->default_duration_ = 4; - this->default_octave_ = 6; + this->default_note_denominator_ = DEFAULT_NOTE_DENOMINATOR; + this->default_octave_ = DEFAULT_OCTAVE; this->note_duration_ = 0; - int bpm = 63; - uint16_t num; + uint16_t bpm = DEFAULT_BPM; + uint16_t num; // Used for: default note-denominator, default octave, BPM // Get name this->position_ = this->rtttl_.find(':'); - // it's somewhat documented to be up to 10 characters but let's be a bit flexible here - if (this->position_ == std::string::npos || this->position_ > 15) { + if (this->position_ == std::string::npos) { ESP_LOGE(TAG, "Unable to determine name; missing ':'"); return; } - + if (this->position_ >= SONG_NAME_LENGTH_LIMIT) { + ESP_LOGE(TAG, "Name is too long: length=%u, limit=%u", static_cast(this->position_), + static_cast(SONG_NAME_LENGTH_LIMIT)); + return; + } ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str()); - // get default duration + // Get default duration this->position_ = this->rtttl_.find("d=", this->position_); if (this->position_ == std::string::npos) { ESP_LOGE(TAG, "Missing 'd='"); @@ -301,11 +309,14 @@ void Rtttl::play(std::string rtttl) { } this->position_ += 2; num = this->get_integer_(); - if (num > 0) { - this->default_duration_ = num; + if (num == 1 || num == 2 || num == 4 || num == 8 || num == 16 || num == 32) { + this->default_note_denominator_ = num; + } else { + ESP_LOGE(TAG, "Invalid default duration: %d", num); + return; } - // get default octave + // Get default octave this->position_ = this->rtttl_.find("o=", this->position_); if (this->position_ == std::string::npos) { ESP_LOGE(TAG, "Missing 'o="); @@ -313,11 +324,14 @@ void Rtttl::play(std::string rtttl) { } this->position_ += 2; num = this->get_integer_(); - if (num >= 3 && num <= 7) { + if (num >= MIN_OCTAVE && num <= MAX_OCTAVE) { this->default_octave_ = num; + } else { + ESP_LOGE(TAG, "Invalid default octave: %d", num); + return; } - // get BPM + // Get BPM this->position_ = this->rtttl_.find("b=", this->position_); if (this->position_ == std::string::npos) { ESP_LOGE(TAG, "Missing b="); @@ -325,8 +339,11 @@ void Rtttl::play(std::string rtttl) { } this->position_ += 2; num = this->get_integer_(); - if (num != 0) { + if (num >= 4) { // Below 4 is not realistic and would cause a integer overflow bpm = num; + } else { + ESP_LOGE(TAG, "Invalid BPM: %d", num); + return; } this->position_ = this->rtttl_.find(':', this->position_); @@ -337,10 +354,10 @@ void Rtttl::play(std::string rtttl) { this->position_++; // BPM usually expresses the number of quarter notes per minute - this->wholenote_ = 60 * 1000L * 4 / bpm; // this is the time for whole note (in milliseconds) + this->wholenote_duration_ = 60 * 1000L * 4 / bpm; // This is the time for whole note (in milliseconds) this->output_freq_ = 0; - this->last_note_ = millis(); + this->last_note_start_time_ = millis(); this->note_duration_ = 1; #ifdef USE_OUTPUT diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index 4d4a652c51..e37cccae9e 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -13,6 +13,10 @@ namespace esphome::rtttl { +inline constexpr uint8_t DEFAULT_NOTE_DENOMINATOR = 4; // Default note-denominator (quarter note) +inline constexpr uint8_t DEFAULT_OCTAVE = + 6; // Default octave for a note (see: `MIN_OCTAVE`, `MAX_OCTAVE` in `rtttl.cpp`) + enum class State : uint8_t { STOPPED = 0, INIT, @@ -67,19 +71,18 @@ class Rtttl : public Component { std::string rtttl_{""}; /// The current position in the RTTTL string. size_t position_{0}; - /// The duration of a whole note in milliseconds. - uint16_t wholenote_; /// The default duration of a note (e.g. 4 for a quarter note). - uint16_t default_duration_; + uint8_t default_note_denominator_{DEFAULT_NOTE_DENOMINATOR}; /// The default octave for a note. - uint16_t default_octave_; - /// The time the last note was started. - uint32_t last_note_; + uint8_t default_octave_{DEFAULT_OCTAVE}; /// The duration of the current note in milliseconds. - uint16_t note_duration_; - + uint16_t note_duration_{0}; + /// The duration of a whole note in milliseconds. + uint16_t wholenote_duration_; + /// The time in milliseconds since microcontroller boot when the last note was started. + uint32_t last_note_start_time_; /// The frequency of the current note in Hz. - uint32_t output_freq_; + uint32_t output_freq_{0}; /// The gain of the output. float gain_{0.6f}; /// The current state of the RTTTL player. @@ -93,16 +96,14 @@ class Rtttl : public Component { #ifdef USE_SPEAKER /// The speaker to write the sound to. speaker::Speaker *speaker_{nullptr}; - /// The sample rate of the speaker. - int sample_rate_{16000}; /// The number of samples for one full cycle of a note's waveform, in Q10 fixed-point format. - int samples_per_wave_{0}; + uint32_t samples_per_wave_{0}; /// The number of samples sent. - int samples_sent_{0}; + uint32_t samples_sent_{0}; /// The total number of samples to send. - int samples_count_{0}; + uint32_t samples_count_{0}; /// The number of samples for the gap between notes. - int samples_gap_{0}; + uint32_t samples_gap_{0}; #endif // USE_SPEAKER /// The callback to call when playback is finished. From 6d3d8970a67dfeed99e48e36388b319b40f3ad79 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Mon, 2 Mar 2026 04:47:02 +0100 Subject: [PATCH 293/609] [openthread] Disable default enabled OT diag code (#14399) --- esphome/components/openthread/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 89d335c574..dde4987181 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -53,6 +53,9 @@ def set_sdkconfig_options(config): # There is a conflict if the logger's uart also uses the default UART, which is seen as a watchdog failure on "ot_cli" add_idf_sdkconfig_option("CONFIG_OPENTHREAD_CLI", False) + # Diag unused, if needed for lab/cert/etc tests then enable separately + add_idf_sdkconfig_option("CONFIG_OPENTHREAD_DIAG", False) + add_idf_sdkconfig_option("CONFIG_OPENTHREAD_ENABLED", True) if tlv := config.get(CONF_TLV): From f68a3ed15d441a90cd96415af05cc69ad2ab5411 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Mar 2026 18:09:00 -1000 Subject: [PATCH 294/609] [api] Remove virtual destructor from ProtoMessage (#14393) --- esphome/components/api/api_pb2.h | 6 +++--- esphome/components/api/proto.h | 7 ++++++- script/api_protobuf/api_protobuf.py | 5 ++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 033b789c18..a97f6c0a76 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -322,7 +322,6 @@ enum ZWaveProxyRequestType : uint32_t { class InfoResponseProtoMessage : public ProtoMessage { public: - ~InfoResponseProtoMessage() override = default; StringRef object_id{}; uint32_t key{0}; StringRef name{}; @@ -336,28 +335,29 @@ class InfoResponseProtoMessage : public ProtoMessage { #endif protected: + ~InfoResponseProtoMessage() = default; }; class StateResponseProtoMessage : public ProtoMessage { public: - ~StateResponseProtoMessage() override = default; uint32_t key{0}; #ifdef USE_DEVICES uint32_t device_id{0}; #endif protected: + ~StateResponseProtoMessage() = default; }; class CommandProtoMessage : public ProtoDecodableMessage { public: - ~CommandProtoMessage() override = default; uint32_t key{0}; #ifdef USE_DEVICES uint32_t device_id{0}; #endif protected: + ~CommandProtoMessage() = default; }; class HelloRequest final : public ProtoDecodableMessage { public: diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index c34f7744e6..750fff0810 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -452,7 +452,6 @@ class DumpBuffer { class ProtoMessage { public: - virtual ~ProtoMessage() = default; // Default implementation for messages with no fields virtual void encode(ProtoWriteBuffer &buffer) const {} // Default implementation for messages with no fields @@ -463,6 +462,11 @@ class ProtoMessage { virtual const char *dump_to(DumpBuffer &out) const = 0; virtual const char *message_name() const { return "unknown"; } #endif + + protected: + // Non-virtual: messages are never deleted polymorphically. + // Protected prevents accidental `delete base_ptr` (compile error). + ~ProtoMessage() = default; }; // Base class for messages that support decoding @@ -482,6 +486,7 @@ class ProtoDecodableMessage : public ProtoMessage { static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id); protected: + ~ProtoDecodableMessage() = default; virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; } virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; } virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 350947a8d6..9c9cda4d36 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2473,9 +2473,6 @@ def build_base_class( out = f"class {base_class_name} : public {parent_class} {{\n" out += " public:\n" - # Add destructor with override - public_content.insert(0, f"~{base_class_name}() override = default;") - # Base classes don't implement encode/decode/calculate_size # Derived classes handle these with their specific field numbers cpp = "" @@ -2483,6 +2480,8 @@ def build_base_class( out += indent("\n".join(public_content)) + "\n" out += "\n" out += " protected:\n" + # Non-virtual protected destructor prevents accidental polymorphic deletion + protected_content.insert(0, f"~{base_class_name}() = default;") out += indent("\n".join(protected_content)) if protected_content: out += "\n" From 80a2acca4f576b5c43fa0a87b5752a68e1d85b6c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Mar 2026 18:19:32 -1000 Subject: [PATCH 295/609] [ld2410] Add UART mock integration test for LD2410 component (#14377) --- .../external_components/uart_mock/__init__.py | 86 ++++ .../uart_mock/uart_mock.cpp | 159 +++++++ .../external_components/uart_mock/uart_mock.h | 78 ++++ .../fixtures/uart_mock_ld2410.yaml | 145 +++++++ .../uart_mock_ld2410_engineering.yaml | 156 +++++++ tests/integration/test_uart_mock_ld2410.py | 392 ++++++++++++++++++ 6 files changed, 1016 insertions(+) create mode 100644 tests/integration/fixtures/external_components/uart_mock/__init__.py create mode 100644 tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp create mode 100644 tests/integration/fixtures/external_components/uart_mock/uart_mock.h create mode 100644 tests/integration/fixtures/uart_mock_ld2410.yaml create mode 100644 tests/integration/fixtures/uart_mock_ld2410_engineering.yaml create mode 100644 tests/integration/test_uart_mock_ld2410.py diff --git a/tests/integration/fixtures/external_components/uart_mock/__init__.py b/tests/integration/fixtures/external_components/uart_mock/__init__.py new file mode 100644 index 0000000000..5bd879ab12 --- /dev/null +++ b/tests/integration/fixtures/external_components/uart_mock/__init__.py @@ -0,0 +1,86 @@ +import esphome.codegen as cg +from esphome.components import uart +from esphome.components.uart import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS +import esphome.config_validation as cv +from esphome.const import CONF_BAUD_RATE, CONF_DATA, CONF_DELAY, CONF_ID, CONF_INTERVAL + +CODEOWNERS = ["@esphome/tests"] +MULTI_CONF = True + +uart_mock_ns = cg.esphome_ns.namespace("uart_mock") +MockUartComponent = uart_mock_ns.class_( + "MockUartComponent", uart.UARTComponent, cg.Component +) + +CONF_INJECTIONS = "injections" +CONF_RESPONSES = "responses" +CONF_INJECT_RX = "inject_rx" +CONF_EXPECT_TX = "expect_tx" +CONF_PERIODIC_RX = "periodic_rx" + +UART_PARITY_OPTIONS = { + "NONE": uart.UARTParityOptions.UART_CONFIG_PARITY_NONE, + "EVEN": uart.UARTParityOptions.UART_CONFIG_PARITY_EVEN, + "ODD": uart.UARTParityOptions.UART_CONFIG_PARITY_ODD, +} + +INJECTION_SCHEMA = cv.Schema( + { + cv.Required(CONF_INJECT_RX): [cv.hex_uint8_t], + cv.Optional(CONF_DELAY, default="0ms"): cv.positive_time_period_milliseconds, + } +) + +RESPONSE_SCHEMA = cv.Schema( + { + cv.Required(CONF_EXPECT_TX): [cv.hex_uint8_t], + cv.Required(CONF_INJECT_RX): [cv.hex_uint8_t], + } +) + +PERIODIC_RX_SCHEMA = cv.Schema( + { + cv.Required(CONF_DATA): [cv.hex_uint8_t], + cv.Required(CONF_INTERVAL): cv.positive_time_period_milliseconds, + } +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(MockUartComponent), + cv.Required(CONF_BAUD_RATE): cv.int_range(min=1), + cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), + cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8), + cv.Optional(CONF_PARITY, default="NONE"): cv.enum( + UART_PARITY_OPTIONS, upper=True + ), + cv.Optional(CONF_INJECTIONS, default=[]): cv.ensure_list(INJECTION_SCHEMA), + cv.Optional(CONF_RESPONSES, default=[]): cv.ensure_list(RESPONSE_SCHEMA), + cv.Optional(CONF_PERIODIC_RX, default=[]): cv.ensure_list(PERIODIC_RX_SCHEMA), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + cg.add(var.set_baud_rate(config[CONF_BAUD_RATE])) + cg.add(var.set_stop_bits(config[CONF_STOP_BITS])) + cg.add(var.set_data_bits(config[CONF_DATA_BITS])) + cg.add(var.set_parity(config[CONF_PARITY])) + + for injection in config[CONF_INJECTIONS]: + rx_data = injection[CONF_INJECT_RX] + delay_ms = injection[CONF_DELAY] + cg.add(var.add_injection(rx_data, delay_ms)) + + for response in config[CONF_RESPONSES]: + tx_data = response[CONF_EXPECT_TX] + rx_data = response[CONF_INJECT_RX] + cg.add(var.add_response(tx_data, rx_data)) + + for periodic in config[CONF_PERIODIC_RX]: + data = periodic[CONF_DATA] + interval = periodic[CONF_INTERVAL] + cg.add(var.add_periodic_rx(data, interval)) diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp new file mode 100644 index 0000000000..a49d51ba10 --- /dev/null +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -0,0 +1,159 @@ +// Host-only test component — do not copy to production code. +// See uart_mock.h for details. + +#include "uart_mock.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::uart_mock { + +static const char *const TAG = "uart_mock"; + +void MockUartComponent::setup() { + ESP_LOGI(TAG, "Mock UART initialized with %zu injections, %zu responses, %zu periodic", this->injections_.size(), + this->responses_.size(), this->periodic_rx_.size()); +} + +void MockUartComponent::loop() { + uint32_t now = App.get_loop_component_start_time(); + + // Initialize scenario start time on first loop() call, after all components have + // finished setup(). This prevents injection delays from being consumed during setup. + if (!this->loop_started_) { + this->loop_started_ = true; + this->scenario_start_ms_ = now; + this->cumulative_delay_ms_ = 0; + ESP_LOGD(TAG, "Scenario started at %u ms", now); + } + + // Process at most ONE timed injection per loop iteration. + // This ensures each injection is in a separate loop cycle, giving the consuming + // component (e.g., LD2410) a chance to process each batch independently. + if (this->injection_index_ < this->injections_.size()) { + auto &injection = this->injections_[this->injection_index_]; + uint32_t target_time = this->scenario_start_ms_ + this->cumulative_delay_ms_ + injection.delay_ms; + if (now >= target_time) { + ESP_LOGD(TAG, "Injecting %zu RX bytes (injection %u)", injection.rx_data.size(), this->injection_index_); + this->inject_to_rx_buffer_(injection.rx_data); + this->cumulative_delay_ms_ += injection.delay_ms; + this->injection_index_++; + } + } + + // Process periodic RX + for (auto &periodic : this->periodic_rx_) { + if (now - periodic.last_inject_ms >= periodic.interval_ms) { + this->inject_to_rx_buffer_(periodic.data); + periodic.last_inject_ms = now; + } + } +} + +void MockUartComponent::dump_config() { + ESP_LOGCONFIG(TAG, + "Mock UART Component:\n" + " Baud Rate: %u\n" + " Injections: %zu\n" + " Responses: %zu\n" + " Periodic RX: %zu", + this->baud_rate_, this->injections_.size(), this->responses_.size(), this->periodic_rx_.size()); +} + +void MockUartComponent::write_array(const uint8_t *data, size_t len) { + this->tx_count_ += len; + this->tx_buffer_.insert(this->tx_buffer_.end(), data, data + len); + + // Log all TX data so tests can verify what the component sends + if (len > 0 && len <= 64) { + char hex_buf[format_hex_pretty_size(64)]; + ESP_LOGD(TAG, "TX %zu bytes: %s", len, format_hex_pretty_to(hex_buf, sizeof(hex_buf), data, len)); + } else if (len > 64) { + ESP_LOGD(TAG, "TX %zu bytes (too large to log)", len); + } + +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(uart::UART_DIRECTION_TX, data[i]); + } +#endif + + this->try_match_response_(); +} + +bool MockUartComponent::peek_byte(uint8_t *data) { + if (this->rx_buffer_.empty()) { + return false; + } + *data = this->rx_buffer_.front(); + return true; +} + +bool MockUartComponent::read_array(uint8_t *data, size_t len) { + if (this->rx_buffer_.size() < len) { + return false; + } + for (size_t i = 0; i < len; i++) { + data[i] = this->rx_buffer_.front(); + this->rx_buffer_.pop_front(); + } + this->rx_count_ += len; + +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(uart::UART_DIRECTION_RX, data[i]); + } +#endif + + return true; +} + +size_t MockUartComponent::available() { return this->rx_buffer_.size(); } + +void MockUartComponent::flush() { + // Nothing to flush in mock +} + +void MockUartComponent::add_injection(const std::vector &rx_data, uint32_t delay_ms) { + this->injections_.push_back({rx_data, delay_ms}); +} + +void MockUartComponent::add_response(const std::vector &expect_tx, const std::vector &inject_rx) { + this->responses_.push_back({expect_tx, inject_rx}); +} + +void MockUartComponent::add_periodic_rx(const std::vector &data, uint32_t interval_ms) { + this->periodic_rx_.push_back({data, interval_ms, 0}); +} + +void MockUartComponent::try_match_response_() { + for (auto &response : this->responses_) { + if (this->tx_buffer_.size() < response.expect_tx.size()) { + continue; + } + // Check if tx_buffer_ ends with expect_tx + size_t offset = this->tx_buffer_.size() - response.expect_tx.size(); + if (std::equal(response.expect_tx.begin(), response.expect_tx.end(), this->tx_buffer_.begin() + offset)) { + ESP_LOGD(TAG, "TX match found, injecting %zu RX bytes", response.inject_rx.size()); + this->inject_to_rx_buffer_(response.inject_rx); + this->tx_buffer_.clear(); + return; + } + } +} + +void MockUartComponent::inject_to_rx_buffer_(const std::vector &data) { + // Log injected RX data so tests can see what's being fed to the component + if (!data.empty() && data.size() <= 64) { + char hex_buf[format_hex_pretty_size(64)]; + ESP_LOGD(TAG, "RX inject %zu bytes: %s", data.size(), + format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size())); + } else if (data.size() > 64) { + ESP_LOGD(TAG, "RX inject %zu bytes (too large to log inline)", data.size()); + } + for (uint8_t byte : data) { + this->rx_buffer_.push_back(byte); + } +} + +} // namespace esphome::uart_mock diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h new file mode 100644 index 0000000000..00f4bf2e2b --- /dev/null +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h @@ -0,0 +1,78 @@ +#pragma once + +// ============================================================================ +// HOST-ONLY TEST COMPONENT — DO NOT COPY TO PRODUCTION CODE +// +// This component runs exclusively on the host platform for integration testing. +// It intentionally uses std::vector, std::deque, and dynamic allocation which +// would be inappropriate for production embedded components. Do not use this +// code as a reference for writing ESPHome components targeting real hardware. +// ============================================================================ + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart_component.h" +#include +#include + +namespace esphome::uart_mock { + +class MockUartComponent : public uart::UARTComponent, public Component { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::BUS; } + + // UARTComponent interface + void write_array(const uint8_t *data, size_t len) override; + bool peek_byte(uint8_t *data) override; + bool read_array(uint8_t *data, size_t len) override; + size_t available() override; + void flush() override; + + // Scenario configuration - called from generated code + void add_injection(const std::vector &rx_data, uint32_t delay_ms); + void add_response(const std::vector &expect_tx, const std::vector &inject_rx); + void add_periodic_rx(const std::vector &data, uint32_t interval_ms); + + protected: + void check_logger_conflict() override {} + void try_match_response_(); + void inject_to_rx_buffer_(const std::vector &data); + + // Timed injections + struct Injection { + std::vector rx_data; + uint32_t delay_ms; + }; + std::vector injections_; + uint32_t injection_index_{0}; + uint32_t scenario_start_ms_{0}; + uint32_t cumulative_delay_ms_{0}; + bool loop_started_{false}; + + // TX-triggered responses + struct Response { + std::vector expect_tx; + std::vector inject_rx; + }; + std::vector responses_; + std::vector tx_buffer_; + + // RX buffer + std::deque rx_buffer_; + + // Periodic RX + struct PeriodicRx { + std::vector data; + uint32_t interval_ms; + uint32_t last_inject_ms{0}; + }; + std::vector periodic_rx_; + + // Observability + uint32_t tx_count_{0}; + uint32_t rx_count_{0}; +}; + +} // namespace esphome::uart_mock diff --git a/tests/integration/fixtures/uart_mock_ld2410.yaml b/tests/integration/fixtures/uart_mock_ld2410.yaml new file mode 100644 index 0000000000..9a81468263 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_ld2410.yaml @@ -0,0 +1,145 @@ +esphome: + name: uart-mock-ld2410-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy ld2410's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + id: mock_uart + baud_rate: 256000 + injections: + # Phase 1 (t=100ms): Valid LD2410 normal mode data frame - happy path + # The buffer is clean at this point, so this frame should parse correctly. + # Moving target: 100cm, energy 50 + # Still target: 120cm, energy 25 + # Detection distance: 300cm + # Target state: 0x03 (moving + still) + # + # Note: LD2410's two_byte_to_int() uses signed char, so low bytes must be + # <=127 to produce correct values. Values >127 wrap negative. + # + # Frame layout (24 bytes): + # [0-3] F4 F3 F2 F1 = data frame header + # [4-5] 0D 00 = length 13 + # [6] 02 = data type (normal) + # [7] AA = data header marker + # [8] 03 = target states (moving+still) + # [9-10] 64 00 = moving distance 100 (0x0064) + # [11] 32 = moving energy 50 + # [12-13] 78 00 = still distance 120 (0x0078) + # [14] 19 = still energy 25 + # [15-16] 2C 01 = detection distance 300 (0x012C) + # [17] 00 = padding + # [18] 55 = data footer marker + # [19] 00 = CRC/check + # [20-23] F8 F7 F6 F5 = data frame footer + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x0D, 0x00, + 0x02, 0xAA, + 0x03, + 0x64, 0x00, + 0x32, + 0x78, 0x00, + 0x19, + 0x2C, 0x01, + 0x00, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 2 (t=200ms): Garbage bytes - corrupt the buffer + # These random bytes will accumulate in the LD2410's internal buffer. + # No footer will be found, so the buffer just grows. + - delay: 100ms + inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22] + + # Phase 3 (t=300ms): Truncated frame (header + partial data, no footer) + # More bytes accumulating in the buffer without a footer match. + - delay: 100ms + inject_rx: [0xF4, 0xF3, 0xF2, 0xF1, 0x0D, 0x00, 0x02, 0xAA] + + # Phase 4 (t=500ms): Overflow - inject 85 bytes of 0xFF (MAX_LINE_LENGTH=50) + # Buffer has 15 bytes from phases 2+3. + # Overflow math: need 35 bytes to trigger first overflow (pos 15->49), + # then 50 more to trigger second overflow (pos 0->49). Total = 85 bytes. + # After two overflows, buffer_pos_ = 0 with a completely clean buffer. + # The LD2410 logs "Max command length exceeded" at each overflow. + - delay: 200ms + inject_rx: + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ] + + # Phase 5 (t=600ms): Valid frame after overflow - recovery test + # Buffer was reset by overflow. This valid frame should parse correctly. + # Moving target: 50cm, energy 100 + # Still target: 75cm, energy 80 + # Detection distance: 127cm + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x0D, 0x00, + 0x02, 0xAA, + 0x03, + 0x32, 0x00, + 0x64, + 0x4B, 0x00, + 0x50, + 0x7F, 0x00, + 0x00, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + +ld2410: + id: ld2410_dev + uart_id: mock_uart + +sensor: + - platform: ld2410 + ld2410_id: ld2410_dev + moving_distance: + name: "Moving Distance" + still_distance: + name: "Still Distance" + moving_energy: + name: "Moving Energy" + still_energy: + name: "Still Energy" + detection_distance: + name: "Detection Distance" + +binary_sensor: + - platform: ld2410 + ld2410_id: ld2410_dev + has_target: + name: "Has Target" + has_moving_target: + name: "Has Moving Target" + has_still_target: + name: "Has Still Target" diff --git a/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml b/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml new file mode 100644 index 0000000000..3b730fc1f8 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml @@ -0,0 +1,156 @@ +esphome: + name: uart-mock-ld2410-eng-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy ld2410's DEPENDENCIES = ["uart"] +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + id: mock_uart + baud_rate: 256000 + injections: + # Phase 1 (t=100ms): Valid LD2410 engineering mode data frame + # Captured from a real Screek Human Presence Sensor 1U with LD2410 firmware 2.4.x + # + # Engineering mode frame layout (45 bytes): + # [0-3] F4 F3 F2 F1 = data frame header + # [4-5] 23 00 = length 35 + # [6] 01 = data type (engineering mode) + # [7] AA = data header marker + # [8] 03 = target states (moving+still) + # [9-10] 1E 00 = moving distance 30 (0x001E) + # [11] 64 = moving energy 100 + # [12-13] 1E 00 = still distance 30 (0x001E) + # [14] 64 = still energy 100 + # [15-16] 00 00 = detection distance 0 + # [17] 08 = max moving distance gate + # [18] 08 = max still distance gate + # [19-27] gate moving energies (gates 0-8) + # [28-36] gate still energies (gates 0-8) + # [37] 57 = light sensor value 87 + # [38] 01 = out pin presence (HIGH) + # [39] 55 = data footer marker + # [40] 00 = check + # [41-44] F8 F7 F6 F5 = data frame footer + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x23, 0x00, + 0x01, 0xAA, + 0x03, + 0x1E, 0x00, + 0x64, + 0x1E, 0x00, + 0x64, + 0x00, 0x00, + 0x08, 0x08, + 0x64, 0x41, 0x06, 0x0E, 0x2B, 0x16, 0x03, 0x03, 0x07, + 0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, + 0x57, 0x01, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 2 (t=200ms): Second engineering mode frame with different values + # Real capture: moving at 73cm, still at 30cm, detection at 33cm + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x23, 0x00, + 0x01, 0xAA, + 0x03, + 0x49, 0x00, + 0x64, + 0x1E, 0x00, + 0x64, + 0x21, 0x00, + 0x08, 0x08, + 0x11, 0x64, 0x05, 0x29, 0x39, 0x10, 0x03, 0x11, 0x0E, + 0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, + 0x57, 0x01, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 3 (t=300ms): Frame with still target at 291cm (multi-byte distance) + # This tests the two_byte_to_int function with high byte > 0 + # Note: low byte 0x2F < 0x80 so it avoids the signed char bug + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x23, 0x00, + 0x01, 0xAA, + 0x03, + 0x2F, 0x00, + 0x36, + 0x23, 0x01, + 0x64, + 0x21, 0x00, + 0x08, 0x08, + 0x2F, 0x36, 0x09, 0x0D, 0x15, 0x0B, 0x06, 0x06, 0x08, + 0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x5A, 0x3D, + 0x57, 0x01, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + +ld2410: + id: ld2410_dev + uart_id: mock_uart + +sensor: + - platform: ld2410 + ld2410_id: ld2410_dev + moving_distance: + name: "Moving Distance" + still_distance: + name: "Still Distance" + moving_energy: + name: "Moving Energy" + still_energy: + name: "Still Energy" + detection_distance: + name: "Detection Distance" + light: + name: "Light" + g0: + move_energy: + name: "Gate 0 Move Energy" + still_energy: + name: "Gate 0 Still Energy" + g1: + move_energy: + name: "Gate 1 Move Energy" + still_energy: + name: "Gate 1 Still Energy" + g2: + move_energy: + name: "Gate 2 Move Energy" + still_energy: + name: "Gate 2 Still Energy" + +binary_sensor: + - platform: ld2410 + ld2410_id: ld2410_dev + has_target: + name: "Has Target" + has_moving_target: + name: "Has Moving Target" + has_still_target: + name: "Has Still Target" + out_pin_presence_status: + name: "Out Pin Presence" diff --git a/tests/integration/test_uart_mock_ld2410.py b/tests/integration/test_uart_mock_ld2410.py new file mode 100644 index 0000000000..e01d6ff8e8 --- /dev/null +++ b/tests/integration/test_uart_mock_ld2410.py @@ -0,0 +1,392 @@ +"""Integration test for LD2410 component with mock UART. + +Tests: +test_uart_mock_ld2410 (normal mode): + 1. Happy path - valid data frame publishes correct sensor values + 2. Garbage resilience - random bytes don't crash the component + 3. Truncated frame handling - partial frame doesn't corrupt state + 4. Buffer overflow recovery - overflow resets the parser + 5. Post-overflow parsing - next valid frame after overflow is parsed correctly + 6. TX logging - verifies LD2410 sends expected setup commands + +test_uart_mock_ld2410_engineering (engineering mode): + 1. Engineering mode frames with per-gate energy data and light sensor + 2. Multi-byte still distance (291cm) using high byte > 0 + 3. Out pin presence binary sensor + 4. Gate energy sensor values from real device captures +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path + +from aioesphomeapi import ( + BinarySensorInfo, + BinarySensorState, + EntityState, + SensorInfo, + SensorState, +) +import pytest + +from .state_utils import InitialStateHelper, build_key_to_entity_mapping, find_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_uart_mock_ld2410( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test LD2410 data parsing with happy path, garbage, overflow, and recovery.""" + # Replace external component path placeholder + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track overflow warning in logs + overflow_seen = loop.create_future() + + # Track TX data logged by the mock for assertions + tx_log_lines: list[str] = [] + + def line_callback(line: str) -> None: + if "Max command length exceeded" in line and not overflow_seen.done(): + overflow_seen.set_result(True) + # Capture all TX log lines from uart_mock + if "uart_mock" in line and "TX " in line: + tx_log_lines.append(line) + + # Track sensor state updates (after initial state is swallowed) + sensor_states: dict[str, list[float]] = { + "moving_distance": [], + "still_distance": [], + "moving_energy": [], + "still_energy": [], + "detection_distance": [], + } + binary_states: dict[str, list[bool]] = { + "has_target": [], + "has_moving_target": [], + "has_still_target": [], + } + + # Signal when we see recovery frame values + recovery_received = loop.create_future() + + def on_state(state: EntityState) -> None: + if isinstance(state, SensorState) and not state.missing_state: + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in sensor_states: + sensor_states[sensor_name].append(state.state) + # Check if this is the recovery frame (moving_distance = 50) + if ( + sensor_name == "moving_distance" + and state.state == pytest.approx(50.0) + and not recovery_received.done() + ): + recovery_received.set_result(True) + elif isinstance(state, BinarySensorState): + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in binary_states: + binary_states[sensor_name].append(state.state) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + + # Build key mappings for all sensor types + all_names = list(sensor_states.keys()) + list(binary_states.keys()) + key_to_sensor = build_key_to_entity_mapping(entities, all_names) + + # Set up initial state helper + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Phase 1 values are in the initial states (swallowed by InitialStateHelper). + # Verify them via initial_states dict. + moving_dist_entity = find_entity(entities, "moving_distance", SensorInfo) + assert moving_dist_entity is not None + initial_moving = initial_state_helper.initial_states.get(moving_dist_entity.key) + assert initial_moving is not None and isinstance(initial_moving, SensorState) + assert initial_moving.state == pytest.approx(100.0), ( + f"Initial moving distance should be 100, got {initial_moving.state}" + ) + + still_dist_entity = find_entity(entities, "still_distance", SensorInfo) + assert still_dist_entity is not None + initial_still = initial_state_helper.initial_states.get(still_dist_entity.key) + assert initial_still is not None and isinstance(initial_still, SensorState) + assert initial_still.state == pytest.approx(120.0), ( + f"Initial still distance should be 120, got {initial_still.state}" + ) + + moving_energy_entity = find_entity(entities, "moving_energy", SensorInfo) + assert moving_energy_entity is not None + initial_me = initial_state_helper.initial_states.get(moving_energy_entity.key) + assert initial_me is not None and isinstance(initial_me, SensorState) + assert initial_me.state == pytest.approx(50.0), ( + f"Initial moving energy should be 50, got {initial_me.state}" + ) + + still_energy_entity = find_entity(entities, "still_energy", SensorInfo) + assert still_energy_entity is not None + initial_se = initial_state_helper.initial_states.get(still_energy_entity.key) + assert initial_se is not None and isinstance(initial_se, SensorState) + assert initial_se.state == pytest.approx(25.0), ( + f"Initial still energy should be 25, got {initial_se.state}" + ) + + detect_dist_entity = find_entity(entities, "detection_distance", SensorInfo) + assert detect_dist_entity is not None + initial_dd = initial_state_helper.initial_states.get(detect_dist_entity.key) + assert initial_dd is not None and isinstance(initial_dd, SensorState) + assert initial_dd.state == pytest.approx(300.0), ( + f"Initial detection distance should be 300, got {initial_dd.state}" + ) + + # Wait for the recovery frame (Phase 5) to be parsed + # This proves the component survived garbage + truncated + overflow + try: + await asyncio.wait_for(recovery_received, timeout=15.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for recovery frame. Received sensor states:\n" + f" moving_distance: {sensor_states['moving_distance']}\n" + f" still_distance: {sensor_states['still_distance']}\n" + f" moving_energy: {sensor_states['moving_energy']}\n" + f" still_energy: {sensor_states['still_energy']}\n" + f" detection_distance: {sensor_states['detection_distance']}" + ) + + # Verify overflow warning was logged + assert overflow_seen.done(), ( + "Expected 'Max command length exceeded' warning in logs" + ) + + # Verify LD2410 sent setup commands (TX logging) + # LD2410 sends 7 commands during setup: FF (config on), A0 (version), + # A5 (MAC), AB (distance res), AE (light), 61 (params), FE (config off) + assert len(tx_log_lines) > 0, "Expected TX log lines from uart_mock" + tx_data = " ".join(tx_log_lines) + # Verify command frame header appears (FD:FC:FB:FA) + assert "FD:FC:FB:FA" in tx_data, ( + "Expected LD2410 command frame header FD:FC:FB:FA in TX log" + ) + # Verify command frame footer appears (04:03:02:01) + assert "04:03:02:01" in tx_data, ( + "Expected LD2410 command frame footer 04:03:02:01 in TX log" + ) + + # Recovery frame values (Phase 5, after overflow) + assert len(sensor_states["moving_distance"]) >= 1, ( + f"Expected recovery moving_distance, got: {sensor_states['moving_distance']}" + ) + # Find the recovery value (moving_distance = 50) + recovery_values = [ + v for v in sensor_states["moving_distance"] if v == pytest.approx(50.0) + ] + assert len(recovery_values) >= 1, ( + f"Expected moving_distance=50 in recovery, got: {sensor_states['moving_distance']}" + ) + + # Recovery frame: moving=50, still=75, energy=100/80, detect=127 + recovery_idx = next( + i + for i, v in enumerate(sensor_states["moving_distance"]) + if v == pytest.approx(50.0) + ) + assert sensor_states["still_distance"][recovery_idx] == pytest.approx(75.0), ( + f"Recovery still distance should be 75, got {sensor_states['still_distance'][recovery_idx]}" + ) + assert sensor_states["moving_energy"][recovery_idx] == pytest.approx(100.0), ( + f"Recovery moving energy should be 100, got {sensor_states['moving_energy'][recovery_idx]}" + ) + assert sensor_states["still_energy"][recovery_idx] == pytest.approx(80.0), ( + f"Recovery still energy should be 80, got {sensor_states['still_energy'][recovery_idx]}" + ) + assert sensor_states["detection_distance"][recovery_idx] == pytest.approx( + 127.0 + ), ( + f"Recovery detection distance should be 127, got {sensor_states['detection_distance'][recovery_idx]}" + ) + + # Verify binary sensors detected targets + # Binary sensors could be in initial states or forwarded states + has_target_entity = find_entity(entities, "has_target", BinarySensorInfo) + assert has_target_entity is not None + initial_ht = initial_state_helper.initial_states.get(has_target_entity.key) + assert initial_ht is not None and isinstance(initial_ht, BinarySensorState) + assert initial_ht.state is True, "Has target should be True" + + has_moving_entity = find_entity(entities, "has_moving_target", BinarySensorInfo) + assert has_moving_entity is not None + initial_hm = initial_state_helper.initial_states.get(has_moving_entity.key) + assert initial_hm is not None and isinstance(initial_hm, BinarySensorState) + assert initial_hm.state is True, "Has moving target should be True" + + has_still_entity = find_entity(entities, "has_still_target", BinarySensorInfo) + assert has_still_entity is not None + initial_hs = initial_state_helper.initial_states.get(has_still_entity.key) + assert initial_hs is not None and isinstance(initial_hs, BinarySensorState) + assert initial_hs.state is True, "Has still target should be True" + + +@pytest.mark.asyncio +async def test_uart_mock_ld2410_engineering( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test LD2410 engineering mode with per-gate energy, light, and multi-byte distance.""" + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track sensor state updates (after initial state is swallowed) + sensor_states: dict[str, list[float]] = { + "moving_distance": [], + "still_distance": [], + "moving_energy": [], + "still_energy": [], + "detection_distance": [], + "light": [], + "gate_0_move_energy": [], + "gate_1_move_energy": [], + "gate_2_move_energy": [], + "gate_0_still_energy": [], + "gate_1_still_energy": [], + "gate_2_still_energy": [], + } + binary_states: dict[str, list[bool]] = { + "has_target": [], + "has_moving_target": [], + "has_still_target": [], + "out_pin_presence": [], + } + + # Signal when we see Phase 3 frame (still_distance = 291) + phase3_received = loop.create_future() + + def on_state(state: EntityState) -> None: + if isinstance(state, SensorState) and not state.missing_state: + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in sensor_states: + sensor_states[sensor_name].append(state.state) + if ( + sensor_name == "still_distance" + and state.state == pytest.approx(291.0) + and not phase3_received.done() + ): + phase3_received.set_result(True) + elif isinstance(state, BinarySensorState): + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in binary_states: + binary_states[sensor_name].append(state.state) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + + all_names = list(sensor_states.keys()) + list(binary_states.keys()) + key_to_sensor = build_key_to_entity_mapping(entities, all_names) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Phase 1 initial values (engineering mode frame): + # moving=30, energy=100, still=30, energy=100, detect=0 + moving_dist_entity = find_entity(entities, "moving_distance", SensorInfo) + assert moving_dist_entity is not None + initial_moving = initial_state_helper.initial_states.get(moving_dist_entity.key) + assert initial_moving is not None and isinstance(initial_moving, SensorState) + assert initial_moving.state == pytest.approx(30.0), ( + f"Initial moving distance should be 30, got {initial_moving.state}" + ) + + still_dist_entity = find_entity(entities, "still_distance", SensorInfo) + assert still_dist_entity is not None + initial_still = initial_state_helper.initial_states.get(still_dist_entity.key) + assert initial_still is not None and isinstance(initial_still, SensorState) + assert initial_still.state == pytest.approx(30.0), ( + f"Initial still distance should be 30, got {initial_still.state}" + ) + + # Verify engineering mode sensors from initial state + # Gate 0 moving energy = 0x64 = 100 + gate0_move_entity = find_entity(entities, "gate_0_move_energy", SensorInfo) + assert gate0_move_entity is not None + initial_g0m = initial_state_helper.initial_states.get(gate0_move_entity.key) + assert initial_g0m is not None and isinstance(initial_g0m, SensorState) + assert initial_g0m.state == pytest.approx(100.0), ( + f"Gate 0 move energy should be 100, got {initial_g0m.state}" + ) + + # Gate 1 moving energy = 0x41 = 65 + gate1_move_entity = find_entity(entities, "gate_1_move_energy", SensorInfo) + assert gate1_move_entity is not None + initial_g1m = initial_state_helper.initial_states.get(gate1_move_entity.key) + assert initial_g1m is not None and isinstance(initial_g1m, SensorState) + assert initial_g1m.state == pytest.approx(65.0), ( + f"Gate 1 move energy should be 65, got {initial_g1m.state}" + ) + + # Light sensor = 0x57 = 87 + light_entity = find_entity(entities, "light", SensorInfo) + assert light_entity is not None + initial_light = initial_state_helper.initial_states.get(light_entity.key) + assert initial_light is not None and isinstance(initial_light, SensorState) + assert initial_light.state == pytest.approx(87.0), ( + f"Light sensor should be 87, got {initial_light.state}" + ) + + # Out pin presence = 0x01 = True + out_pin_entity = find_entity(entities, "out_pin_presence", BinarySensorInfo) + assert out_pin_entity is not None + initial_out = initial_state_helper.initial_states.get(out_pin_entity.key) + assert initial_out is not None and isinstance(initial_out, BinarySensorState) + assert initial_out.state is True, "Out pin presence should be True" + + # Wait for Phase 3 frame (still_distance = 291cm, multi-byte) + try: + await asyncio.wait_for(phase3_received, timeout=15.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 3 frame. Received sensor states:\n" + f" still_distance: {sensor_states['still_distance']}\n" + f" moving_distance: {sensor_states['moving_distance']}" + ) + + # Phase 3: still distance = 0x0123 = 291cm (multi-byte distance test) + phase3_still = [ + v for v in sensor_states["still_distance"] if v == pytest.approx(291.0) + ] + assert len(phase3_still) >= 1, ( + f"Expected still_distance=291, got: {sensor_states['still_distance']}" + ) From 9d4357c619ee94ae9a934276525e1bfa0ac99b8b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Mar 2026 18:20:14 -1000 Subject: [PATCH 296/609] [core] Wake main loop from ISR in enable_loop_soon_any_context() (#14383) --- esphome/components/socket/__init__.py | 7 ++++--- esphome/core/application.h | 8 +++++++- esphome/core/component.cpp | 8 +++++++- esphome/core/lwip_fast_select.c | 24 +++++++++++++++++++----- esphome/core/lwip_fast_select.h | 7 +++++++ 5 files changed, 44 insertions(+), 10 deletions(-) diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index a83648979c..08cf3ea33c 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -189,9 +189,10 @@ async def to_code(config): cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS") cg.add_define("USE_SOCKET_SELECT_SUPPORT") # ESP32 and LibreTiny both have LwIP >= 2.1.3 with lwip_socket_dbg_get_socket() - # and FreeRTOS task notifications — enable fast select to bypass lwip_select() - if CORE.is_esp32 or CORE.is_libretiny: - cg.add_define("USE_LWIP_FAST_SELECT") + # and FreeRTOS task notifications — enable fast select to bypass lwip_select(). + # Only when not using lwip_tcp, which does not provide select() support. + if (CORE.is_esp32 or CORE.is_libretiny) and impl != IMPLEMENTATION_LWIP_TCP: + cg.add_build_flag("-DUSE_LWIP_FAST_SELECT") def FILTER_SOURCE_FILES() -> list[str]: diff --git a/esphome/core/application.h b/esphome/core/application.h index 13e0f63885..d2345e2b0b 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -501,7 +501,7 @@ class Application { void wake_loop_threadsafe(); #endif -#if defined(USE_WAKE_LOOP_THREADSAFE) && defined(USE_LWIP_FAST_SELECT) +#ifdef USE_LWIP_FAST_SELECT /// Wake the main event loop from an ISR. /// Uses vTaskNotifyGiveFromISR() — <1 us, ISR-safe. /// Only available on platforms with fast select (ESP32, LibreTiny). @@ -509,6 +509,12 @@ class Application { static void IRAM_ATTR wake_loop_isrsafe(int *px_higher_priority_task_woken) { esphome_lwip_wake_main_loop_from_isr(px_higher_priority_task_woken); } + +#ifdef USE_ESP32 + /// Wake the main event loop from any context (ISR, thread, or main loop). + /// Detects the calling context and uses the appropriate FreeRTOS API. + static void IRAM_ATTR wake_loop_any_context() { esphome_lwip_wake_main_loop_any_context(); } +#endif #endif #endif diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index fd4e0d2984..5afd901da2 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -315,7 +315,7 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { // This method is thread and ISR-safe because: // 1. Only performs simple assignments to volatile variables (atomic on all platforms) // 2. No read-modify-write operations that could be interrupted - // 3. No memory allocation, object construction, or function calls + // 3. No memory allocation or object construction; on ESP32 the only call (wake_loop_any_context) is ISR-safe // 4. IRAM_ATTR ensures code is in IRAM, not flash (required for ISR execution) // 5. Components are never destroyed, so no use-after-free concerns // 6. App is guaranteed to be initialized before any ISR could fire @@ -323,6 +323,12 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { // 8. Race condition with main loop is handled by clearing flag before processing this->pending_enable_loop_ = true; App.has_pending_enable_loop_requests_ = true; +#if defined(USE_LWIP_FAST_SELECT) && defined(USE_ESP32) + // Wake the main loop if sleeping in ulTaskNotifyTake(). Without this, + // the main loop would not wake until the select timeout expires (~16ms). + // Uses xPortInIsrContext() to choose the correct FreeRTOS notify API. + Application::wake_loop_any_context(); +#endif } void Component::reset_to_construction_state() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index da0f1f337a..989f66e9be 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -105,10 +105,9 @@ // critical sections). Multiple concurrent xTaskNotifyGive calls are safe — // the notification count simply increments. -// USE_ESP32 and USE_LIBRETINY are compiler -D flags, so they are always visible in this .c file. -// Feature macros like USE_LWIP_FAST_SELECT may come from generated headers that are not included here, -// so this implementation is enabled based on platform flags instead of USE_LWIP_FAST_SELECT. -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +// USE_LWIP_FAST_SELECT is set via -D build flag (not cg.add_define) so it is +// visible in both .c and .cpp translation units. +#ifdef USE_LWIP_FAST_SELECT // LwIP headers must come first — they define netconn_callback, struct lwip_sock, etc. #include @@ -239,4 +238,19 @@ void IRAM_ATTR esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task } } -#endif // defined(USE_ESP32) || defined(USE_LIBRETINY) +// Wake the main loop from any context (ISR, thread, or main loop). +// ESP32-only: uses xPortInIsrContext() to detect ISR context. +// LibreTiny is excluded because it lacks IRAM_ATTR support needed for ISR-safe paths. +#ifdef USE_ESP32 +void IRAM_ATTR esphome_lwip_wake_main_loop_any_context(void) { + if (xPortInIsrContext()) { + int px_higher_priority_task_woken = 0; + esphome_lwip_wake_main_loop_from_isr(&px_higher_priority_task_woken); + portYIELD_FROM_ISR(px_higher_priority_task_woken); + } else { + esphome_lwip_wake_main_loop(); + } +} +#endif + +#endif // USE_LWIP_FAST_SELECT diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h index b7a70a8f9f..6fce34fd76 100644 --- a/esphome/core/lwip_fast_select.h +++ b/esphome/core/lwip_fast_select.h @@ -33,6 +33,13 @@ void esphome_lwip_wake_main_loop(void); /// @param px_higher_priority_task_woken Set to pdTRUE if a context switch is needed. void esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken); +/// Wake the main loop task from any context (ISR, thread, or main loop). +/// ESP32-only: uses xPortInIsrContext() to detect ISR context. +/// LibreTiny lacks IRAM_ATTR support needed for ISR-safe paths. +#ifdef USE_ESP32 +void esphome_lwip_wake_main_loop_any_context(void); +#endif + #ifdef __cplusplus } #endif From 590ee81f7a13356c75d5cafa2436771fe4b80f53 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Mon, 2 Mar 2026 07:16:36 +0100 Subject: [PATCH 297/609] [openthread] Disable default enabled OT console build (#14390) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/openthread/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index dde4987181..5861c3db3f 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -52,6 +52,8 @@ def set_sdkconfig_options(config): # There is a conflict if the logger's uart also uses the default UART, which is seen as a watchdog failure on "ot_cli" add_idf_sdkconfig_option("CONFIG_OPENTHREAD_CLI", False) + # Console is the transport layer for CLI; disable it too since CLI is disabled + add_idf_sdkconfig_option("CONFIG_OPENTHREAD_CONSOLE_ENABLE", False) # Diag unused, if needed for lab/cert/etc tests then enable separately add_idf_sdkconfig_option("CONFIG_OPENTHREAD_DIAG", False) From 3160457ca620adfbe48980af20d89a5c062462ee Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Mon, 2 Mar 2026 00:51:27 -0800 Subject: [PATCH 298/609] Create integration tests for modbus (#14395) Co-authored-by: J. Nick Koston --- .../external_components/uart_mock/__init__.py | 83 +++++++++- .../uart_mock/automation.h | 51 ++++++ .../uart_mock/uart_mock.cpp | 25 ++- .../external_components/uart_mock/uart_mock.h | 10 +- .../fixtures/uart_mock_modbus.yaml | 40 +++++ .../fixtures/uart_mock_modbus_timing.yaml | 54 +++++++ tests/integration/test_uart_mock_modbus.py | 153 ++++++++++++++++++ 7 files changed, 409 insertions(+), 7 deletions(-) create mode 100644 tests/integration/fixtures/external_components/uart_mock/automation.h create mode 100644 tests/integration/fixtures/uart_mock_modbus.yaml create mode 100644 tests/integration/fixtures/uart_mock_modbus_timing.yaml create mode 100644 tests/integration/test_uart_mock_modbus.py diff --git a/tests/integration/fixtures/external_components/uart_mock/__init__.py b/tests/integration/fixtures/external_components/uart_mock/__init__.py index 5bd879ab12..dea8c38551 100644 --- a/tests/integration/fixtures/external_components/uart_mock/__init__.py +++ b/tests/integration/fixtures/external_components/uart_mock/__init__.py @@ -1,8 +1,28 @@ +from esphome import automation import esphome.codegen as cg from esphome.components import uart -from esphome.components.uart import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS +from esphome.components.uart import ( + CONF_RX_FULL_THRESHOLD, + CONF_RX_TIMEOUT, + debug_to_code, + maybe_empty_debug, + validate_raw_data, +) import esphome.config_validation as cv -from esphome.const import CONF_BAUD_RATE, CONF_DATA, CONF_DELAY, CONF_ID, CONF_INTERVAL +from esphome.const import ( + CONF_BAUD_RATE, + CONF_DATA, + CONF_DATA_BITS, + CONF_DEBUG, + CONF_DELAY, + CONF_ID, + CONF_INTERVAL, + CONF_PARITY, + CONF_RX_BUFFER_SIZE, + CONF_STOP_BITS, + CONF_TRIGGER_ID, +) +from esphome.core import ID CODEOWNERS = ["@esphome/tests"] MULTI_CONF = True @@ -11,12 +31,20 @@ uart_mock_ns = cg.esphome_ns.namespace("uart_mock") MockUartComponent = uart_mock_ns.class_( "MockUartComponent", uart.UARTComponent, cg.Component ) +MockUartInjectRXAction = uart_mock_ns.class_( + "MockUartInjectRXAction", automation.Action +) +MockUartTXTrigger = uart_mock_ns.class_( + "MockUartTXTrigger", + automation.Trigger.template(cg.std_vector.template(cg.uint8)), +) CONF_INJECTIONS = "injections" CONF_RESPONSES = "responses" CONF_INJECT_RX = "inject_rx" CONF_EXPECT_TX = "expect_tx" CONF_PERIODIC_RX = "periodic_rx" +CONF_ON_TX = "on_tx" UART_PARITY_OPTIONS = { "NONE": uart.UARTParityOptions.UART_CONFIG_PARITY_NONE, @@ -31,6 +59,15 @@ INJECTION_SCHEMA = cv.Schema( } ) +CONFIG_INJECT_RX_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(MockUartComponent), + cv.Required("data"): cv.templatable(validate_raw_data), + }, + key=CONF_DATA, +) + + RESPONSE_SCHEMA = cv.Schema( { cv.Required(CONF_EXPECT_TX): [cv.hex_uint8_t], @@ -49,6 +86,9 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(MockUartComponent), cv.Required(CONF_BAUD_RATE): cv.int_range(min=1), + cv.Optional(CONF_RX_BUFFER_SIZE, default=256): cv.validate_bytes, + cv.Optional(CONF_RX_FULL_THRESHOLD, default=10): cv.int_range(min=1, max=120), + cv.Optional(CONF_RX_TIMEOUT, default=2): cv.int_range(min=0, max=92), cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8), cv.Optional(CONF_PARITY, default="NONE"): cv.enum( @@ -57,15 +97,45 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_INJECTIONS, default=[]): cv.ensure_list(INJECTION_SCHEMA), cv.Optional(CONF_RESPONSES, default=[]): cv.ensure_list(RESPONSE_SCHEMA), cv.Optional(CONF_PERIODIC_RX, default=[]): cv.ensure_list(PERIODIC_RX_SCHEMA), + cv.Optional(CONF_ON_TX): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MockUartTXTrigger), + } + ), + cv.Optional(CONF_DEBUG): maybe_empty_debug, } ).extend(cv.COMPONENT_SCHEMA) +@automation.register_action( + "uart_mock.inject_rx", MockUartInjectRXAction, CONFIG_INJECT_RX_SCHEMA +) +async def inject_rx_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + data = config[CONF_DATA] + if isinstance(data, bytes): + data = list(data) + + if cg.is_template(data): + templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) + cg.add(var.set_data_template(templ)) + else: + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) + return var + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) cg.add(var.set_baud_rate(config[CONF_BAUD_RATE])) + cg.add(var.set_rx_buffer_size(config[CONF_RX_BUFFER_SIZE])) + cg.add(var.set_rx_full_threshold(config[CONF_RX_FULL_THRESHOLD])) + cg.add(var.set_rx_timeout(config[CONF_RX_TIMEOUT])) cg.add(var.set_stop_bits(config[CONF_STOP_BITS])) cg.add(var.set_data_bits(config[CONF_DATA_BITS])) cg.add(var.set_parity(config[CONF_PARITY])) @@ -84,3 +154,12 @@ async def to_code(config): data = periodic[CONF_DATA] interval = periodic[CONF_INTERVAL] cg.add(var.add_periodic_rx(data, interval)) + + for conf in config.get(CONF_ON_TX, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(cg.std_vector.template(cg.uint8), "data")], conf + ) + + if CONF_DEBUG in config: + await debug_to_code(config[CONF_DEBUG], var) diff --git a/tests/integration/fixtures/external_components/uart_mock/automation.h b/tests/integration/fixtures/external_components/uart_mock/automation.h new file mode 100644 index 0000000000..83a057d3a0 --- /dev/null +++ b/tests/integration/fixtures/external_components/uart_mock/automation.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "uart_mock.h" + +namespace esphome::uart_mock { + +// This pattern is similar to UARTWriteAction but calls inject_rx instead of write_array, and is parented to VirtualUART +// instead of UARTComponent +template class MockUartInjectRXAction : public Action, public Parented { + public: + void set_data_template(std::vector (*func)(Ts...)) { + // Stateless lambdas (generated by ESPHome) implicitly convert to function pointers + this->code_.func = func; + this->len_ = -1; // Sentinel value indicates template mode + } + + // Store pointer to static data in flash (no RAM copy) + void set_data_static(const uint8_t *data, size_t len) { + this->code_.data = data; + this->len_ = len; // Length >= 0 indicates static mode + } + + void play(const Ts &...x) override { + if (this->len_ >= 0) { + // Static mode: use pointer and length + this->parent_->inject_to_rx_buffer(this->code_.data, static_cast(this->len_)); + } else { + // Template mode: call function + auto val = this->code_.func(x...); + this->parent_->inject_to_rx_buffer(val); + } + } + + protected: + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Code { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } code_; +}; + +class MockUartTXTrigger : public Trigger> { + public: + explicit MockUartTXTrigger(MockUartComponent *parent) { + parent->set_tx_hook([this](std::vector data) { this->trigger(data); }); + } +}; + +} // namespace esphome::uart_mock diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp index a49d51ba10..a4a1c41234 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -35,7 +35,7 @@ void MockUartComponent::loop() { uint32_t target_time = this->scenario_start_ms_ + this->cumulative_delay_ms_ + injection.delay_ms; if (now >= target_time) { ESP_LOGD(TAG, "Injecting %zu RX bytes (injection %u)", injection.rx_data.size(), this->injection_index_); - this->inject_to_rx_buffer_(injection.rx_data); + this->inject_to_rx_buffer(injection.rx_data); this->cumulative_delay_ms_ += injection.delay_ms; this->injection_index_++; } @@ -44,7 +44,7 @@ void MockUartComponent::loop() { // Process periodic RX for (auto &periodic : this->periodic_rx_) { if (now - periodic.last_inject_ms >= periodic.interval_ms) { - this->inject_to_rx_buffer_(periodic.data); + this->inject_to_rx_buffer(periodic.data); periodic.last_inject_ms = now; } } @@ -79,6 +79,12 @@ void MockUartComponent::write_array(const uint8_t *data, size_t len) { #endif this->try_match_response_(); + + // This directly calls a tx_hook (lambda) as an alternative to the simpler match_response mechanism. + if (this->tx_hook_) { + std::vector buf(data, data + len); + this->tx_hook_(buf); + } } bool MockUartComponent::peek_byte(uint8_t *data) { @@ -114,6 +120,12 @@ void MockUartComponent::flush() { // Nothing to flush in mock } +void MockUartComponent::set_rx_full_threshold(size_t rx_full_threshold) { + this->rx_full_threshold_ = rx_full_threshold; +} + +void MockUartComponent::set_rx_timeout(size_t rx_timeout) { this->rx_timeout_ = rx_timeout; } + void MockUartComponent::add_injection(const std::vector &rx_data, uint32_t delay_ms) { this->injections_.push_back({rx_data, delay_ms}); } @@ -135,14 +147,19 @@ void MockUartComponent::try_match_response_() { size_t offset = this->tx_buffer_.size() - response.expect_tx.size(); if (std::equal(response.expect_tx.begin(), response.expect_tx.end(), this->tx_buffer_.begin() + offset)) { ESP_LOGD(TAG, "TX match found, injecting %zu RX bytes", response.inject_rx.size()); - this->inject_to_rx_buffer_(response.inject_rx); + this->inject_to_rx_buffer(response.inject_rx); this->tx_buffer_.clear(); return; } } } -void MockUartComponent::inject_to_rx_buffer_(const std::vector &data) { +void MockUartComponent::inject_to_rx_buffer(const uint8_t *data, size_t len) { + std::vector vec(data, data + len); + this->inject_to_rx_buffer(vec); +} + +void MockUartComponent::inject_to_rx_buffer(const std::vector &data) { // Log injected RX data so tests can see what's being fed to the component if (!data.empty() && data.size() <= 64) { char hex_buf[format_hex_pretty_size(64)]; diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h index 00f4bf2e2b..5bbc3c1bf6 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h @@ -29,16 +29,21 @@ class MockUartComponent : public uart::UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; void flush() override; + void set_rx_full_threshold(size_t rx_full_threshold) override; + void set_rx_timeout(size_t rx_timeout) override; // Scenario configuration - called from generated code void add_injection(const std::vector &rx_data, uint32_t delay_ms); void add_response(const std::vector &expect_tx, const std::vector &inject_rx); void add_periodic_rx(const std::vector &data, uint32_t interval_ms); + void set_tx_hook(std::function &)> &&cb) { this->tx_hook_ = std::move(cb); } + void inject_to_rx_buffer(const std::vector &data); + void inject_to_rx_buffer(const uint8_t *data, size_t len); + protected: void check_logger_conflict() override {} void try_match_response_(); - void inject_to_rx_buffer_(const std::vector &data); // Timed injections struct Injection { @@ -73,6 +78,9 @@ class MockUartComponent : public uart::UARTComponent, public Component { // Observability uint32_t tx_count_{0}; uint32_t rx_count_{0}; + + // Direct TX hook for tests that want to bypass the response-matching logic + std::function &)> tx_hook_; }; } // namespace esphome::uart_mock diff --git a/tests/integration/fixtures/uart_mock_modbus.yaml b/tests/integration/fixtures/uart_mock_modbus.yaml new file mode 100644 index 0000000000..89b9b91861 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_modbus.yaml @@ -0,0 +1,40 @@ +esphome: + name: uart-mock-modbus-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + - id: virtual_uart_dev + baud_rate: 9600 + rx_full_threshold: 120 + rx_timeout: 2 + debug: + responses: + - expect_tx: [0x01, 0x03, 0x00, 0x03, 0x00, 0x01, 0x74, 0x0A] # Read holding register 1 on device 1 + inject_rx: [0x01, 0x03, 0x02, 0x01, 0x03, 0xF9, 0xD5] # Return value 0x0103 (hex) = 259 (dec) + +modbus: + uart_id: virtual_uart_dev + +modbus_controller: + address: 1 + +sensor: + - platform: modbus_controller + name: "basic_register" + address: 0x03 + register_type: holding diff --git a/tests/integration/fixtures/uart_mock_modbus_timing.yaml b/tests/integration/fixtures/uart_mock_modbus_timing.yaml new file mode 100644 index 0000000000..cd485ca394 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_modbus_timing.yaml @@ -0,0 +1,54 @@ +esphome: + name: uart-mock-modbus-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + - id: virtual_uart_dev + baud_rate: 9600 + rx_full_threshold: 120 + rx_timeout: 2 + debug: + on_tx: + - then: + - if: + condition: #Read 80 input registers on device 2, starting at address 0 (SDM meter request) + lambda: "return data == std::vector({0x02,0x04,0x00,0x00,0x00,0x50,0xF0,0x05});" + then: + - uart_mock.inject_rx: # Good response from SDM meter with CRC. Voltage is 243V + !lambda return {0x02,0x04,0xA0,0x43,0x73,0x19,0x9A,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3F,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; + - delay: 120ms # Simulate some delay for UART buffer to fill before next part of response (1ms = 1 byte at 9600 baud) + - uart_mock.inject_rx: # Rest of response (including CRC) + !lambda return{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x42,0x6F,0xCC,0xCD,0x43,0x7C,0xB8,0x10,0x3D,0x38,0x51,0xEC, + 0x43,0x81,0x1B,0xE7,0x3B,0x03,0x12,0x6F,0x50,0x1B}; + +modbus: + uart_id: virtual_uart_dev + +sensor: + - platform: sdm_meter + address: 2 + phase_a: + voltage: + name: sdm_voltage diff --git a/tests/integration/test_uart_mock_modbus.py b/tests/integration/test_uart_mock_modbus.py new file mode 100644 index 0000000000..309cb56dc9 --- /dev/null +++ b/tests/integration/test_uart_mock_modbus.py @@ -0,0 +1,153 @@ +"""Integration test for modbus component with virtual UART. + +Tests: +test_uart_mock_modbus : + 1. Read a single register and parse successfully (basic_register) + 2. Read multiple registers from SDM meter and parse successfully (sdm_voltage), with some intermediate delay to simulate UART buffer time. + +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path + +from aioesphomeapi import EntityState, SensorState +import pytest + +from .state_utils import InitialStateHelper, build_key_to_entity_mapping +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_uart_mock_modbus( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test basic modbus data parsing.""" + # Replace external component path placeholder + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track sensor state updates (after initial state is swallowed) + sensor_states: dict[str, list[float]] = { + "basic_register": [], + } + + basic_register_changed = loop.create_future() + + def on_state(state: EntityState) -> None: + if isinstance(state, SensorState) and not state.missing_state: + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in sensor_states: + sensor_states[sensor_name].append(state.state) + if ( + sensor_name == "basic_register" + and state.state == 259.0 + and not basic_register_changed.done() + ): + basic_register_changed.set_result(True) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + + # Build key mappings for all sensor types + all_names = list(sensor_states.keys()) + key_to_sensor = build_key_to_entity_mapping(entities, all_names) + + # Set up initial state helper + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Wait for basic register to be updated with successful parse + try: + await asyncio.wait_for(basic_register_changed, timeout=15.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Basic Register change. Received sensor states:\n" + f" basic_register: {sensor_states['basic_register']}\n" + ) + + +@pytest.mark.asyncio +@pytest.mark.xfail( + reason="There is a bug in UART which will timeout for long responses." +) +async def test_uart_mock_modbus_timing( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test basic modbus data parsing.""" + # Replace external component path placeholder + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track sensor state updates (after initial state is swallowed) + sensor_states: dict[str, list[float]] = { + "sdm_voltage": [], + } + + voltage_changed = loop.create_future() + + def on_state(state: EntityState) -> None: + if isinstance(state, SensorState) and not state.missing_state: + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in sensor_states: + sensor_states[sensor_name].append(state.state) + # Check if this is a good voltage reading (243V) + if ( + sensor_name == "sdm_voltage" + and state.state > 200.0 + and not voltage_changed.done() + ): + voltage_changed.set_result(True) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + + # Build key mappings for all sensor types + all_names = list(sensor_states.keys()) + key_to_sensor = build_key_to_entity_mapping(entities, all_names) + + # Set up initial state helper + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Wait for voltage to be updated with successful parse + try: + await asyncio.wait_for(voltage_changed, timeout=15.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for SDM voltage change. Received sensor states:\n" + f" sdm_voltage: {sensor_states['sdm_voltage']}\n" + ) From 77a7cbcffd4b8ec1fa3bc77b32f9ef06dc96761d Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:41:20 -0500 Subject: [PATCH 299/609] Use cached files on network errors in external_files (#14055) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jesserockz <3060199+jesserockz@users.noreply.github.com> --- esphome/external_files.py | 15 +++++++-- tests/unit_tests/test_external_files.py | 44 +++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/esphome/external_files.py b/esphome/external_files.py index 80b54ebb2f..72a3f33fdc 100644 --- a/esphome/external_files.py +++ b/esphome/external_files.py @@ -55,10 +55,12 @@ def has_remote_file_changed(url: str, local_file_path: Path) -> bool: _LOGGER.debug("has_remote_file_changed: File modified") return True except requests.exceptions.RequestException as e: - raise cv.Invalid( - f"Could not check if {url} has changed, please check if file exists " - f"({e})" + _LOGGER.warning( + "Could not check if %s has changed due to network error (%s), using cached file", + url, + e, ) + return False _LOGGER.debug("has_remote_file_changed: File doesn't exists at %s", local_file_path) return True @@ -98,6 +100,13 @@ def download_content(url: str, path: Path, timeout=NETWORK_TIMEOUT) -> bytes: ) req.raise_for_status() except requests.exceptions.RequestException as e: + if path.exists(): + _LOGGER.warning( + "Could not download from %s due to network error (%s), using cached file", + url, + e, + ) + return path.read_bytes() raise cv.Invalid(f"Could not download from {url}: {e}") path.parent.mkdir(parents=True, exist_ok=True) diff --git a/tests/unit_tests/test_external_files.py b/tests/unit_tests/test_external_files.py index 05e0bd3523..a319fae83d 100644 --- a/tests/unit_tests/test_external_files.py +++ b/tests/unit_tests/test_external_files.py @@ -144,16 +144,16 @@ def test_has_remote_file_changed_no_local_file(setup_core: Path) -> None: def test_has_remote_file_changed_network_error( mock_head: MagicMock, setup_core: Path ) -> None: - """Test has_remote_file_changed handles network errors gracefully.""" + """Test has_remote_file_changed returns False on network error when file is cached.""" test_file = setup_core / "cached.txt" test_file.write_text("cached content") mock_head.side_effect = requests.exceptions.RequestException("Network error") url = "https://example.com/file.txt" + result = external_files.has_remote_file_changed(url, test_file) - with pytest.raises(Invalid, match="Could not check if.*Network error"): - external_files.has_remote_file_changed(url, test_file) + assert result is False @patch("esphome.external_files.requests.head") @@ -198,3 +198,41 @@ def test_is_file_recent_handles_float_seconds(setup_core: Path) -> None: result = external_files.is_file_recent(test_file, refresh) assert result is True + + +@patch("esphome.external_files.requests.get") +@patch("esphome.external_files.has_remote_file_changed") +def test_download_content_with_network_error_uses_cache( + mock_has_changed: MagicMock, mock_get: MagicMock, setup_core: Path +) -> None: + """Test download_content uses cached file when network fails.""" + test_file = setup_core / "cached.txt" + cached_content = b"cached content" + test_file.write_bytes(cached_content) + + # Simulate file has changed, so it tries to download + mock_has_changed.return_value = True + mock_get.side_effect = requests.exceptions.RequestException("Network error") + + url = "https://example.com/file.txt" + result = external_files.download_content(url, test_file) + + assert result == cached_content + + +@patch("esphome.external_files.requests.get") +@patch("esphome.external_files.has_remote_file_changed") +def test_download_content_with_network_error_no_cache_fails( + mock_has_changed: MagicMock, mock_get: MagicMock, setup_core: Path +) -> None: + """Test download_content raises error when network fails and no cache exists.""" + test_file = setup_core / "nonexistent.txt" + + # Simulate file has changed (doesn't exist), so it tries to download + mock_has_changed.return_value = True + mock_get.side_effect = requests.exceptions.RequestException("Network error") + + url = "https://example.com/file.txt" + + with pytest.raises(Invalid, match="Could not download from.*Network error"): + external_files.download_content(url, test_file) From 1c5fd8bbd46fca739ec2bf7743f87ea433c98ad6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 06:56:51 -1000 Subject: [PATCH 300/609] [core] Move millis_64 rollover tracking out of Scheduler (#14360) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32/__init__.py | 1 + esphome/components/esp8266/core.cpp | 4 +- esphome/components/host/__init__.py | 1 + esphome/components/libretiny/core.cpp | 4 +- esphome/components/rp2040/__init__.py | 1 + esphome/components/zephyr/__init__.py | 1 + esphome/core/config.py | 6 + esphome/core/defines.h | 5 + esphome/core/scheduler.cpp | 182 +--------------------- esphome/core/scheduler.h | 47 +----- esphome/core/time_64.cpp | 207 ++++++++++++++++++++++++++ esphome/core/time_64.h | 24 +++ 12 files changed, 257 insertions(+), 226 deletions(-) create mode 100644 esphome/core/time_64.cpp create mode 100644 esphome/core/time_64.h diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index dd9e394fd2..a14d3af69e 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1438,6 +1438,7 @@ async def to_code(config): cg.set_cpp_standard("gnu++20") cg.add_build_flag("-DUSE_ESP32") + cg.add_define("USE_NATIVE_64BIT_TIME") cg.add_build_flag("-Wl,-z,noexecstack") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) variant = config[CONF_VARIANT] diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 497e99b61f..7136cf41cb 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -3,7 +3,7 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" -#include "esphome/core/application.h" +#include "esphome/core/time_64.h" #include "esphome/core/helpers.h" #include "preferences.h" #include @@ -17,7 +17,7 @@ namespace esphome { void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); } +uint64_t millis_64() { return Millis64Impl::compute(::millis()); } void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index ba05e497c8..8adbfb02ec 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -41,6 +41,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): cg.add_build_flag("-DUSE_HOST") + cg.add_define("USE_NATIVE_64BIT_TIME") cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) cg.add_build_flag("-std=gnu++20") cg.add_define("ESPHOME_BOARD", "host") diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index 6cbc81938d..88952692a7 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -3,7 +3,7 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" -#include "esphome/core/application.h" +#include "esphome/core/time_64.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -14,7 +14,7 @@ namespace esphome { void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); } +uint64_t millis_64() { return Millis64Impl::compute(::millis()); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void HOT delay(uint32_t ms) { ::delay(ms); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 23f12e651f..ea269a47c5 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -169,6 +169,7 @@ async def to_code(config): cg.add_platformio_option("lib_compat_mode", "strict") cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_build_flag("-DUSE_RP2040") + cg.add_define("USE_NATIVE_64BIT_TIME") cg.set_cpp_standard("gnu++20") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "RP2040") diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index 43d5cebebb..4cc71bddca 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -112,6 +112,7 @@ def add_extra_script(stage: str, filename: str, path: Path) -> None: def zephyr_to_code(config): cg.add_build_flag("-DUSE_ZEPHYR") + cg.add_define("USE_NATIVE_64BIT_TIME") cg.set_cpp_standard("gnu++20") # build is done by west so bypass board checking in platformio cg.add_platformio_option("boards_dir", CORE.relative_build_path("boards")) diff --git a/esphome/core/config.py b/esphome/core/config.py index 215432835a..593b402230 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -650,6 +650,12 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, + "time_64.cpp": { + PlatformFramework.ESP8266_ARDUINO, + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, # Note: lock_free_queue.h and event_pool.h are header files and don't need to be filtered # as they are only included when needed by the preprocessor } diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 181425c162..1f2597dd98 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -178,6 +178,11 @@ #define USE_I2S_LEGACY #endif +// Platforms with native 64-bit time sources (no rollover tracking needed) +#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_ZEPHYR) || defined(USE_RP2040) +#define USE_NATIVE_64BIT_TIME +#endif + // ESP32-specific feature flags #ifdef USE_ESP32 #define USE_MQTT_IDF_ENQUEUE diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 2c10e7e2da..ca560e8250 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -9,7 +9,6 @@ #include #include #include -#include namespace esphome { @@ -28,10 +27,6 @@ static constexpr size_t MAX_POOL_SIZE = 5; // Set to 5 to match the pool size - when we have as many cancelled items as our // pool can hold, it's time to clean up and recycle them. static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5; -#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) -// Half the 32-bit range - used to detect rollovers vs normal time progression -static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; -#endif // max delay to start an interval sequence static constexpr uint32_t MAX_INTERVAL_DELAY = 5000; @@ -152,9 +147,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type return; } - // Get fresh 64-bit timestamp BEFORE taking lock - const uint64_t now_64 = millis_64(); - // Take lock early to protect scheduler_item_pool_ access LockGuard guard{this->lock_}; @@ -181,6 +173,9 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type } else #endif /* not ESPHOME_THREAD_SINGLE */ { + // Only non-defer items need a timestamp for scheduling + const uint64_t now_64 = millis_64(); + // Type-specific setup if (type == SchedulerItem::INTERVAL) { item->interval = delay; @@ -475,19 +470,8 @@ void HOT Scheduler::call(uint32_t now) { if (now_64 - last_print > 2000) { last_print = now_64; std::vector old_items; -#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) && \ - defined(ESPHOME_THREAD_MULTI_ATOMICS) - const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); - const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); - ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), - this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg); -#elif !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) - ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), - this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_); -#else ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64, this->items_.size(), this->scheduler_item_pool_.size(), now_64); -#endif // Cleanup before debug output this->cleanup_(); while (!this->items_.empty()) { @@ -715,166 +699,6 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type return total_cancelled > 0; } -#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) -uint64_t Scheduler::millis_64_impl_(uint32_t now) { - // THREAD SAFETY NOTE: - // This function has three implementations, based on the precompiler flags - // - ESPHOME_THREAD_SINGLE - Runs on single-threaded platforms (ESP8266, RP2040, etc.) - // - ESPHOME_THREAD_MULTI_NO_ATOMICS - Runs on multi-threaded platforms without atomics (LibreTiny BK72xx) - // - ESPHOME_THREAD_MULTI_ATOMICS - Runs on multi-threaded platforms with atomics (ESP32, HOST, LibreTiny - // RTL87xx/LN882x, etc.) - // - // Make sure all changes are synchronized if you edit this function. - // - // IMPORTANT: Always pass fresh millis() values to this function. The implementation - // handles out-of-order timestamps between threads, but minimizing time differences - // helps maintain accuracy. - // - -#ifdef ESPHOME_THREAD_SINGLE - // This is the single core implementation. - // - // Single-core platforms have no concurrency, so this is a simple implementation - // that just tracks 32-bit rollover (every 49.7 days) without any locking or atomics. - - uint16_t major = this->millis_major_; - uint32_t last = this->last_millis_; - - // Check for rollover - if (now < last && (last - now) > HALF_MAX_UINT32) { - this->millis_major_++; - major++; - this->last_millis_ = now; -#ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); -#endif /* ESPHOME_DEBUG_SCHEDULER */ - } else if (now > last) { - // Only update if time moved forward - this->last_millis_ = now; - } - - // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time - return now + (static_cast(major) << 32); - -#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) - // This is the multi core no atomics implementation. - // - // Without atomics, this implementation uses locks more aggressively: - // 1. Always locks when near the rollover boundary (within 10 seconds) - // 2. Always locks when detecting a large backwards jump - // 3. Updates without lock in normal forward progression (accepting minor races) - // This is less efficient but necessary without atomic operations. - uint16_t major = this->millis_major_; - uint32_t last = this->last_millis_; - - // Define a safe window around the rollover point (10 seconds) - // This covers any reasonable scheduler delays or thread preemption - static constexpr uint32_t ROLLOVER_WINDOW = 10000; // 10 seconds in milliseconds - - // Check if we're near the rollover boundary (close to std::numeric_limits::max() or just past 0) - bool near_rollover = (last > (std::numeric_limits::max() - ROLLOVER_WINDOW)) || (now < ROLLOVER_WINDOW); - - if (near_rollover || (now < last && (last - now) > HALF_MAX_UINT32)) { - // Near rollover or detected a rollover - need lock for safety - LockGuard guard{this->lock_}; - // Re-read with lock held - last = this->last_millis_; - - if (now < last && (last - now) > HALF_MAX_UINT32) { - // True rollover detected (happens every ~49.7 days) - this->millis_major_++; - major++; -#ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); -#endif /* ESPHOME_DEBUG_SCHEDULER */ - } - // Update last_millis_ while holding lock - this->last_millis_ = now; - } else if (now > last) { - // Normal case: Not near rollover and time moved forward - // Update without lock. While this may cause minor races (microseconds of - // backwards time movement), they're acceptable because: - // 1. The scheduler operates at millisecond resolution, not microsecond - // 2. We've already prevented the critical rollover race condition - // 3. Any backwards movement is orders of magnitude smaller than scheduler delays - this->last_millis_ = now; - } - // If now <= last and we're not near rollover, don't update - // This minimizes backwards time movement - - // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time - return now + (static_cast(major) << 32); - -#elif defined(ESPHOME_THREAD_MULTI_ATOMICS) - // This is the multi core with atomics implementation. - // - // Uses atomic operations with acquire/release semantics to ensure coherent - // reads of millis_major_ and last_millis_ across cores. Features: - // 1. Epoch-coherency retry loop to handle concurrent updates - // 2. Lock only taken for actual rollover detection and update - // 3. Lock-free CAS updates for normal forward time progression - // 4. Memory ordering ensures cores see consistent time values - - for (;;) { - uint16_t major = this->millis_major_.load(std::memory_order_acquire); - - /* - * Acquire so that if we later decide **not** to take the lock we still - * observe a `millis_major_` value coherent with the loaded `last_millis_`. - * The acquire load ensures any later read of `millis_major_` sees its - * corresponding increment. - */ - uint32_t last = this->last_millis_.load(std::memory_order_acquire); - - // If we might be near a rollover (large backwards jump), take the lock for the entire operation - // This ensures rollover detection and last_millis_ update are atomic together - if (now < last && (last - now) > HALF_MAX_UINT32) { - // Potential rollover - need lock for atomic rollover detection + update - LockGuard guard{this->lock_}; - // Re-read with lock held; mutex already provides ordering - last = this->last_millis_.load(std::memory_order_relaxed); - - if (now < last && (last - now) > HALF_MAX_UINT32) { - // True rollover detected (happens every ~49.7 days) - this->millis_major_.fetch_add(1, std::memory_order_relaxed); - major++; -#ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); -#endif /* ESPHOME_DEBUG_SCHEDULER */ - } - /* - * Update last_millis_ while holding the lock to prevent races - * Publish the new low-word *after* bumping `millis_major_` (done above) - * so readers never see a mismatched pair. - */ - this->last_millis_.store(now, std::memory_order_release); - } else { - // Normal case: Try lock-free update, but only allow forward movement within same epoch - // This prevents accidentally moving backwards across a rollover boundary - while (now > last && (now - last) < HALF_MAX_UINT32) { - if (this->last_millis_.compare_exchange_weak(last, now, - std::memory_order_release, // success - std::memory_order_relaxed)) { // failure - break; - } - // CAS failure means no data was published; relaxed is fine - // last is automatically updated by compare_exchange_weak if it fails - } - } - uint16_t major_end = this->millis_major_.load(std::memory_order_relaxed); - if (major_end == major) - return now + (static_cast(major) << 32); - } - // Unreachable - the loop always returns when major_end == major - __builtin_unreachable(); - -#else -#error \ - "No platform threading model defined. One of ESPHOME_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_MULTI_ATOMICS must be defined." -#endif -} -#endif // !USE_ESP32 && !USE_HOST && !USE_ZEPHYR && !USE_RP2040 - bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) { // High bits are almost always equal (change only on 32-bit rollover ~49 days) // Optimize for common case: check low bits first when high bits are equal diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index d52cf5147d..cefbdd1b22 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -12,6 +12,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include "esphome/core/time_64.h" namespace esphome { @@ -284,23 +285,16 @@ class Scheduler { bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id); // Extend a 32-bit millis() value to 64-bit. Use when the caller already has a fresh now. - // On ESP32, Host, Zephyr, and RP2040, ignores now and uses the native 64-bit time source via millis_64(). + // On platforms with native 64-bit time, ignores now and uses millis_64() directly. // On other platforms, extends now to 64-bit using rollover tracking. uint64_t millis_64_from_(uint32_t now) { -#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_ZEPHYR) || defined(USE_RP2040) +#ifdef USE_NATIVE_64BIT_TIME (void) now; return millis_64(); #else - return this->millis_64_impl_(now); + return Millis64Impl::compute(now); #endif } - -#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) - // On platforms without native 64-bit time, millis_64() HAL function delegates to this - // method which tracks 32-bit millis() rollover using millis_major_ and last_millis_. - friend uint64_t millis_64(); - uint64_t millis_64_impl_(uint32_t now); -#endif // Cleanup logically deleted items from the scheduler // Returns the number of items remaining after cleanup // IMPORTANT: This method should only be called from the main thread (loop task). @@ -566,39 +560,6 @@ class Scheduler { // can stall the entire system, causing timing issues and dropped events for any components that need // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52) std::vector scheduler_item_pool_; - -#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) - // On platforms with native 64-bit time (ESP32, Host, Zephyr, RP2040), no rollover tracking needed. - // On other platforms, these fields track 32-bit millis() rollover for millis_64_impl_(). -#ifdef ESPHOME_THREAD_MULTI_ATOMICS - /* - * Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates - * - * MEMORY-ORDERING NOTE - * -------------------- - * `last_millis_` and `millis_major_` form a single 64-bit timestamp split in half. - * Writers publish `last_millis_` with memory_order_release and readers use - * memory_order_acquire. This ensures that once a reader sees the new low word, - * it also observes the corresponding increment of `millis_major_`. - */ - std::atomic last_millis_{0}; -#else /* not ESPHOME_THREAD_MULTI_ATOMICS */ - // Platforms without atomic support or single-threaded platforms - uint32_t last_millis_{0}; -#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ - - /* - * Upper 16 bits of the 64-bit millis counter. Incremented only while holding - * `lock_`; read concurrently. Atomic (relaxed) avoids a formal data race. - * Ordering relative to `last_millis_` is provided by its release store and the - * corresponding acquire loads. - */ -#ifdef ESPHOME_THREAD_MULTI_ATOMICS - std::atomic millis_major_{0}; -#else /* not ESPHOME_THREAD_MULTI_ATOMICS */ - uint16_t millis_major_{0}; -#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ -#endif /* !USE_ESP32 && !USE_HOST && !USE_ZEPHYR && !USE_RP2040 */ }; } // namespace esphome diff --git a/esphome/core/time_64.cpp b/esphome/core/time_64.cpp new file mode 100644 index 0000000000..db5df25eb9 --- /dev/null +++ b/esphome/core/time_64.cpp @@ -0,0 +1,207 @@ +#include "esphome/core/defines.h" + +#ifndef USE_NATIVE_64BIT_TIME + +#include "time_64.h" + +#include "esphome/core/helpers.h" +#ifdef ESPHOME_DEBUG_SCHEDULER +#include "esphome/core/log.h" +#include +#endif +#ifdef ESPHOME_THREAD_MULTI_ATOMICS +#include +#endif +#include + +namespace esphome { + +#ifdef ESPHOME_DEBUG_SCHEDULER +static const char *const TAG = "time_64"; +#endif + +uint64_t Millis64Impl::compute(uint32_t now) { + // Half the 32-bit range - used to detect rollovers vs normal time progression + static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; + + // State variables for rollover tracking - static to persist across calls +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + // Mutex for rollover serialization (taken only every ~49.7 days). + // A spinlock would be smaller (~1 byte vs ~80-100 bytes) but is unsafe on + // preemptive single-core RTOS platforms due to priority inversion: a high-priority + // task spinning would prevent the lock holder from running to release it. + static Mutex lock; + /* + * Multi-threaded platforms with atomic support: last_millis needs atomic for lock-free updates. + * Writers publish last_millis with memory_order_release and readers use memory_order_acquire. + * This ensures that once a reader sees the new low word, it also observes the corresponding + * increment of millis_major. + */ + static std::atomic last_millis{0}; + /* + * Upper 16 bits of the 64-bit millis counter. Incremented only while holding lock; + * read concurrently. Atomic (relaxed) avoids a formal data race. Ordering relative + * to last_millis is provided by its release store and the corresponding acquire loads. + */ + static std::atomic millis_major{0}; +#elif !defined(ESPHOME_THREAD_SINGLE) /* ESPHOME_THREAD_MULTI_NO_ATOMICS */ + static Mutex lock; + static uint32_t last_millis{0}; + static uint16_t millis_major{0}; +#else /* ESPHOME_THREAD_SINGLE */ + static uint32_t last_millis{0}; + static uint16_t millis_major{0}; +#endif + + // THREAD SAFETY NOTE: + // This function has three implementations, based on the precompiler flags + // - ESPHOME_THREAD_SINGLE - Runs on single-threaded platforms (ESP8266, etc.) + // - ESPHOME_THREAD_MULTI_NO_ATOMICS - Runs on multi-threaded platforms without atomics (LibreTiny BK72xx) + // - ESPHOME_THREAD_MULTI_ATOMICS - Runs on multi-threaded platforms with atomics (LibreTiny RTL87xx/LN882x, etc.) + // + // Make sure all changes are synchronized if you edit this function. + // + // IMPORTANT: Always pass fresh millis() values to this function. The implementation + // handles out-of-order timestamps between threads, but minimizing time differences + // helps maintain accuracy. + +#ifdef ESPHOME_THREAD_SINGLE + // Single-core platforms have no concurrency, so this is a simple implementation + // that just tracks 32-bit rollover (every 49.7 days) without any locking or atomics. + + uint16_t major = millis_major; + uint32_t last = last_millis; + + // Check for rollover + if (now < last && (last - now) > HALF_MAX_UINT32) { + millis_major++; + major++; + last_millis = now; +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); +#endif /* ESPHOME_DEBUG_SCHEDULER */ + } else if (now > last) { + // Only update if time moved forward + last_millis = now; + } + + // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time + return now + (static_cast(major) << 32); + +#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) + // Without atomics, this implementation uses locks more aggressively: + // 1. Always locks when near the rollover boundary (within 10 seconds) + // 2. Always locks when detecting a large backwards jump + // 3. Updates without lock in normal forward progression (accepting minor races) + // This is less efficient but necessary without atomic operations. + uint16_t major = millis_major; + uint32_t last = last_millis; + + // Define a safe window around the rollover point (10 seconds) + // This covers any reasonable scheduler delays or thread preemption + static constexpr uint32_t ROLLOVER_WINDOW = 10000; // 10 seconds in milliseconds + + // Check if we're near the rollover boundary (close to std::numeric_limits::max() or just past 0) + bool near_rollover = (last > (std::numeric_limits::max() - ROLLOVER_WINDOW)) || (now < ROLLOVER_WINDOW); + + if (near_rollover || (now < last && (last - now) > HALF_MAX_UINT32)) { + // Near rollover or detected a rollover - need lock for safety + LockGuard guard{lock}; + // Re-read with lock held + last = last_millis; + + if (now < last && (last - now) > HALF_MAX_UINT32) { + // True rollover detected (happens every ~49.7 days) + millis_major++; + major++; +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); +#endif /* ESPHOME_DEBUG_SCHEDULER */ + } + // Update last_millis while holding lock + last_millis = now; + } else if (now > last) { + // Normal case: Not near rollover and time moved forward + // Update without lock. While this may cause minor races (microseconds of + // backwards time movement), they're acceptable because: + // 1. The scheduler operates at millisecond resolution, not microsecond + // 2. We've already prevented the critical rollover race condition + // 3. Any backwards movement is orders of magnitude smaller than scheduler delays + last_millis = now; + } + // If now <= last and we're not near rollover, don't update + // This minimizes backwards time movement + + // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time + return now + (static_cast(major) << 32); + +#elif defined(ESPHOME_THREAD_MULTI_ATOMICS) + // Uses atomic operations with acquire/release semantics to ensure coherent + // reads of millis_major and last_millis across cores. Features: + // 1. Epoch-coherency retry loop to handle concurrent updates + // 2. Lock only taken for actual rollover detection and update + // 3. Lock-free CAS updates for normal forward time progression + // 4. Memory ordering ensures cores see consistent time values + + for (;;) { + uint16_t major = millis_major.load(std::memory_order_acquire); + + /* + * Acquire so that if we later decide **not** to take the lock we still + * observe a millis_major value coherent with the loaded last_millis. + * The acquire load ensures any later read of millis_major sees its + * corresponding increment. + */ + uint32_t last = last_millis.load(std::memory_order_acquire); + + // If we might be near a rollover (large backwards jump), take the lock + // This ensures rollover detection and last_millis update are atomic together + if (now < last && (last - now) > HALF_MAX_UINT32) { + // Potential rollover - need lock for atomic rollover detection + update + LockGuard guard{lock}; + // Re-read with lock held; mutex already provides ordering + last = last_millis.load(std::memory_order_relaxed); + + if (now < last && (last - now) > HALF_MAX_UINT32) { + // True rollover detected (happens every ~49.7 days) + millis_major.fetch_add(1, std::memory_order_relaxed); + major++; +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); +#endif /* ESPHOME_DEBUG_SCHEDULER */ + } + /* + * Update last_millis while holding the lock to prevent races. + * Publish the new low-word *after* bumping millis_major (done above) + * so readers never see a mismatched pair. + */ + last_millis.store(now, std::memory_order_release); + } else { + // Normal case: Try lock-free update, but only allow forward movement within same epoch + // This prevents accidentally moving backwards across a rollover boundary + while (now > last && (now - last) < HALF_MAX_UINT32) { + if (last_millis.compare_exchange_weak(last, now, + std::memory_order_release, // success + std::memory_order_relaxed)) { // failure + break; + } + // CAS failure means no data was published; relaxed is fine + // last is automatically updated by compare_exchange_weak if it fails + } + } + uint16_t major_end = millis_major.load(std::memory_order_relaxed); + if (major_end == major) + return now + (static_cast(major) << 32); + } + // Unreachable - the loop always returns when major_end == major + __builtin_unreachable(); + +#else +#error \ + "No platform threading model defined. One of ESPHOME_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_MULTI_ATOMICS must be defined." +#endif +} + +} // namespace esphome + +#endif // !USE_NATIVE_64BIT_TIME diff --git a/esphome/core/time_64.h b/esphome/core/time_64.h new file mode 100644 index 0000000000..42d4b041e5 --- /dev/null +++ b/esphome/core/time_64.h @@ -0,0 +1,24 @@ +#pragma once +#include "esphome/core/defines.h" + +#ifndef USE_NATIVE_64BIT_TIME + +#include + +namespace esphome { + +class Scheduler; + +/// Extends 32-bit millis() to 64-bit using rollover tracking. +/// Access restricted to platform HAL (millis_64()) and Scheduler. +/// All other code should call millis_64() from hal.h instead. +class Millis64Impl { + friend uint64_t millis_64(); + friend class Scheduler; + + static uint64_t compute(uint32_t now); +}; + +} // namespace esphome + +#endif // !USE_NATIVE_64BIT_TIME From 82da4935b603624bf90a1429309e3d23d749bd46 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 06:57:08 -1000 Subject: [PATCH 301/609] [core] Auto-wrap static strings in PROGMEM on ESP8266 via TemplatableValue (#13885) --- esphome/codegen.py | 1 + esphome/components/api/__init__.py | 34 +++++--- .../components/api/homeassistant_service.h | 46 ++++++++++- esphome/core/automation.h | 56 +++++++++++-- esphome/cpp_generator.py | 26 +++++++ tests/unit_tests/test_cpp_generator.py | 78 +++++++++++++++++++ 6 files changed, 223 insertions(+), 18 deletions(-) diff --git a/esphome/codegen.py b/esphome/codegen.py index c5283f4967..30e3135360 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -11,6 +11,7 @@ from esphome.cpp_generator import ( # noqa: F401 ArrayInitializer, Expression, + FlashStringLiteral, LineComment, LogStringLiteral, MockObj, diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index d7b6bec357..dd99862cc2 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -535,24 +535,31 @@ async def homeassistant_service_to_code( cg.add_define("USE_API_HOMEASSISTANT_SERVICES") serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, False) - templ = await cg.templatable(config[CONF_ACTION], args, None) + templ = await cg.templatable(config[CONF_ACTION], args, cg.std_string) cg.add(var.set_service(templ)) # Initialize FixedVectors with exact sizes from config cg.add(var.init_data(len(config[CONF_DATA]))) for key, value in config[CONF_DATA].items(): + # output_type=None because lambdas can return non-string types (int, + # float, char*) that TemplatableStringValue converts via to_string. + # Static strings are manually wrapped for PROGMEM on ESP8266. templ = await cg.templatable(value, args, None) - cg.add(var.add_data(key, templ)) + if isinstance(templ, str): + templ = cg.FlashStringLiteral(templ) + cg.add(var.add_data(cg.FlashStringLiteral(key), templ)) cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE]))) for key, value in config[CONF_DATA_TEMPLATE].items(): templ = await cg.templatable(value, args, None) - cg.add(var.add_data_template(key, templ)) + if isinstance(templ, str): + templ = cg.FlashStringLiteral(templ) + cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ)) cg.add(var.init_variables(len(config[CONF_VARIABLES]))) for key, value in config[CONF_VARIABLES].items(): templ = await cg.templatable(value, args, None) - cg.add(var.add_variable(key, templ)) + cg.add(var.add_variable(cg.FlashStringLiteral(key), templ)) if on_error := config.get(CONF_ON_ERROR): cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES") @@ -621,24 +628,31 @@ async def homeassistant_event_to_code(config, action_id, template_arg, args): cg.add_define("USE_API_HOMEASSISTANT_SERVICES") serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, True) - templ = await cg.templatable(config[CONF_EVENT], args, None) + templ = await cg.templatable(config[CONF_EVENT], args, cg.std_string) cg.add(var.set_service(templ)) # Initialize FixedVectors with exact sizes from config cg.add(var.init_data(len(config[CONF_DATA]))) for key, value in config[CONF_DATA].items(): + # output_type=None because lambdas can return non-string types (int, + # float, char*) that TemplatableStringValue converts via to_string. + # Static strings are manually wrapped for PROGMEM on ESP8266. templ = await cg.templatable(value, args, None) - cg.add(var.add_data(key, templ)) + if isinstance(templ, str): + templ = cg.FlashStringLiteral(templ) + cg.add(var.add_data(cg.FlashStringLiteral(key), templ)) cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE]))) for key, value in config[CONF_DATA_TEMPLATE].items(): templ = await cg.templatable(value, args, None) - cg.add(var.add_data_template(key, templ)) + if isinstance(templ, str): + templ = cg.FlashStringLiteral(templ) + cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ)) cg.add(var.init_variables(len(config[CONF_VARIABLES]))) for key, value in config[CONF_VARIABLES].items(): templ = await cg.templatable(value, args, None) - cg.add(var.add_variable(key, templ)) + cg.add(var.add_variable(cg.FlashStringLiteral(key), templ)) return var @@ -662,11 +676,11 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg cg.add_define("USE_API_HOMEASSISTANT_SERVICES") serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, True) - cg.add(var.set_service("esphome.tag_scanned")) + cg.add(var.set_service(cg.FlashStringLiteral("esphome.tag_scanned"))) # Initialize FixedVector with exact size (1 data field) cg.add(var.init_data(1)) templ = await cg.templatable(config[CONF_TAG], args, cg.std_string) - cg.add(var.add_data("tag_id", templ)) + cg.add(var.add_data(cg.FlashStringLiteral("tag_id"), templ)) return var diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 340699e1a6..9d14061d07 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -130,6 +130,20 @@ template class HomeAssistantServiceCallAction : public Actionadd_kv_(this->variables_, key, std::forward(value)); } +#ifdef USE_ESP8266 + // On ESP8266, ESPHOME_F() returns __FlashStringHelper* (PROGMEM pointer). + // Store as const char* — populate_service_map copies from PROGMEM at play() time. + template void add_data(const __FlashStringHelper *key, V &&value) { + this->add_kv_(this->data_, reinterpret_cast(key), std::forward(value)); + } + template void add_data_template(const __FlashStringHelper *key, V &&value) { + this->add_kv_(this->data_template_, reinterpret_cast(key), std::forward(value)); + } + template void add_variable(const __FlashStringHelper *key, V &&value) { + this->add_kv_(this->variables_, reinterpret_cast(key), std::forward(value)); + } +#endif + #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES template void set_response_template(T response_template) { this->response_template_ = response_template; @@ -221,7 +235,32 @@ template class HomeAssistantServiceCallAction : public Action class HomeAssistantServiceCallAction : public Action #include @@ -56,6 +57,16 @@ template class TemplatableValue { this->static_str_ = str; } +#ifdef USE_ESP8266 + // On ESP8266, __FlashStringHelper* is a distinct type from const char*. + // ESPHOME_F(s) expands to F(s) which returns __FlashStringHelper* pointing to PROGMEM. + // Store as FLASH_STRING — value()/is_empty()/ref_or_copy_to() use _P functions + // to access the PROGMEM pointer safely. + TemplatableValue(const __FlashStringHelper *str) requires std::same_as : type_(FLASH_STRING) { + this->static_str_ = reinterpret_cast(str); + } +#endif + template TemplatableValue(F value) requires(!std::invocable) : type_(VALUE) { if constexpr (USE_HEAP_STORAGE) { this->value_ = new T(std::move(value)); @@ -89,7 +100,7 @@ template class TemplatableValue { this->f_ = new std::function(*other.f_); } else if (this->type_ == STATELESS_LAMBDA) { this->stateless_f_ = other.stateless_f_; - } else if (this->type_ == STATIC_STRING) { + } else if (this->type_ == STATIC_STRING || this->type_ == FLASH_STRING) { this->static_str_ = other.static_str_; } } @@ -108,7 +119,7 @@ template class TemplatableValue { other.f_ = nullptr; } else if (this->type_ == STATELESS_LAMBDA) { this->stateless_f_ = other.stateless_f_; - } else if (this->type_ == STATIC_STRING) { + } else if (this->type_ == STATIC_STRING || this->type_ == FLASH_STRING) { this->static_str_ = other.static_str_; } other.type_ = NONE; @@ -141,7 +152,7 @@ template class TemplatableValue { } else if (this->type_ == LAMBDA) { delete this->f_; } - // STATELESS_LAMBDA/STATIC_STRING/NONE: no cleanup needed (pointers, not heap-allocated) + // STATELESS_LAMBDA/STATIC_STRING/FLASH_STRING/NONE: no cleanup needed (pointers, not heap-allocated) } bool has_value() const { return this->type_ != NONE; } @@ -165,6 +176,17 @@ template class TemplatableValue { return std::string(this->static_str_); } __builtin_unreachable(); +#ifdef USE_ESP8266 + case FLASH_STRING: + // PROGMEM pointer — must use _P functions to access on ESP8266 + if constexpr (std::same_as) { + size_t len = strlen_P(this->static_str_); + std::string result(len, '\0'); + memcpy_P(result.data(), this->static_str_, len); + return result; + } + __builtin_unreachable(); +#endif case NONE: default: return T{}; @@ -186,9 +208,12 @@ template class TemplatableValue { } /// Check if this holds a static string (const char* stored without allocation) + /// The pointer is always directly readable (RAM or flash-mapped). + /// Returns false for FLASH_STRING (PROGMEM on ESP8266, requires _P functions). bool is_static_string() const { return this->type_ == STATIC_STRING; } /// Get the static string pointer (only valid if is_static_string() returns true) + /// The pointer is always directly readable — FLASH_STRING uses a separate type. const char *get_static_string() const { return this->static_str_; } /// Check if the string value is empty without allocating (for std::string specialization). @@ -200,6 +225,12 @@ template class TemplatableValue { return true; case STATIC_STRING: return this->static_str_ == nullptr || this->static_str_[0] == '\0'; +#ifdef USE_ESP8266 + case FLASH_STRING: + // PROGMEM pointer — must use progmem_read_byte on ESP8266 + return this->static_str_ == nullptr || + progmem_read_byte(reinterpret_cast(this->static_str_)) == '\0'; +#endif case VALUE: return this->value_->empty(); default: // LAMBDA/STATELESS_LAMBDA - must call value() @@ -209,8 +240,9 @@ template class TemplatableValue { /// Get a StringRef to the string value without heap allocation when possible. /// For STATIC_STRING/VALUE, returns reference to existing data (no allocation). + /// For FLASH_STRING (ESP8266 PROGMEM), copies to provided buffer via _P functions. /// For LAMBDA/STATELESS_LAMBDA, calls value(), copies to provided buffer, returns ref to buffer. - /// @param lambda_buf Buffer used only for lambda case (must remain valid while StringRef is used). + /// @param lambda_buf Buffer used only for copy cases (must remain valid while StringRef is used). /// @param lambda_buf_size Size of the buffer. /// @return StringRef pointing to the string data. StringRef ref_or_copy_to(char *lambda_buf, size_t lambda_buf_size) const requires std::same_as { @@ -221,6 +253,19 @@ template class TemplatableValue { if (this->static_str_ == nullptr) return StringRef(); return StringRef(this->static_str_, strlen(this->static_str_)); +#ifdef USE_ESP8266 + case FLASH_STRING: + if (this->static_str_ == nullptr) + return StringRef(); + { + // PROGMEM pointer — copy to buffer via _P functions + size_t len = strlen_P(this->static_str_); + size_t copy_len = std::min(len, lambda_buf_size - 1); + memcpy_P(lambda_buf, this->static_str_, copy_len); + lambda_buf[copy_len] = '\0'; + return StringRef(lambda_buf, copy_len); + } +#endif case VALUE: return StringRef(this->value_->data(), this->value_->size()); default: { // LAMBDA/STATELESS_LAMBDA - must call value() and copy @@ -239,6 +284,7 @@ template class TemplatableValue { LAMBDA, STATELESS_LAMBDA, STATIC_STRING, // For const char* when T is std::string - avoids heap allocation + FLASH_STRING, // PROGMEM pointer on ESP8266; never set on other platforms } type_; // For std::string, use heap pointer to minimize union size (4 bytes vs 12+). // For other types, store value inline as before. @@ -247,7 +293,7 @@ template class TemplatableValue { ValueStorage value_; // T for inline storage, T* for heap storage std::function *f_; T (*stateless_f_)(X...); - const char *static_str_; // For STATIC_STRING type + const char *static_str_; // For STATIC_STRING and FLASH_STRING types }; }; diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index fe666bdd6e..5457485d25 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -247,6 +247,23 @@ class LogStringLiteral(Literal): return f"LOG_STR({cpp_string_escape(self.string)})" +class FlashStringLiteral(Literal): + """A string literal wrapped in ESPHOME_F() for PROGMEM storage on ESP8266. + + On ESP8266, ESPHOME_F(s) expands to F(s) which stores the string in flash (PROGMEM). + On other platforms, ESPHOME_F(s) expands to plain s (no-op). + """ + + __slots__ = ("string",) + + def __init__(self, string: str) -> None: + super().__init__() + self.string = string + + def __str__(self) -> str: + return f"ESPHOME_F({cpp_string_escape(self.string)})" + + class IntLiteral(Literal): __slots__ = ("i",) @@ -761,6 +778,15 @@ async def templatable( if is_template(value): return await process_lambda(value, args, return_type=output_type) if to_exp is None: + # Automatically wrap static strings in ESPHOME_F() for PROGMEM storage on ESP8266. + # On other platforms ESPHOME_F() is a no-op returning const char*. + # Lazy import to avoid circular dependency (cpp_generator <-> cpp_types). + # Identity check (is) avoids brittle string comparison. + if isinstance(value, str) and output_type is not None: + from esphome.cpp_types import std_string + + if output_type is std_string: + return FlashStringLiteral(value) return value if isinstance(to_exp, dict): return to_exp[value] diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 049d21027f..bdc31cdef8 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -248,6 +248,12 @@ class TestLiterals: (cg.FloatLiteral(4.2), "4.2f"), (cg.FloatLiteral(1.23456789), "1.23456789f"), (cg.FloatLiteral(math.nan), "NAN"), + (cg.FlashStringLiteral("hello"), 'ESPHOME_F("hello")'), + (cg.FlashStringLiteral(""), 'ESPHOME_F("")'), + ( + cg.FlashStringLiteral('quote"here'), + 'ESPHOME_F("quote\\042here")', + ), ), ) def test_str__simple(self, target: cg.Literal, expected: str): @@ -624,3 +630,75 @@ class TestProcessLambda: # Test invalid tuple format (single element) with pytest.raises(AssertionError): await cg.process_lambda(lambda_obj, [(int,)]) + + +@pytest.mark.asyncio +async def test_templatable__string_with_std_string_returns_flash_literal() -> None: + """Static string with std::string output_type returns FlashStringLiteral.""" + result = await cg.templatable("hello", [], ct.std_string) + + assert isinstance(result, cg.FlashStringLiteral) + assert str(result) == 'ESPHOME_F("hello")' + + +@pytest.mark.asyncio +async def test_templatable__empty_string_with_std_string() -> None: + """Empty static string with std::string output_type returns FlashStringLiteral.""" + result = await cg.templatable("", [], ct.std_string) + + assert isinstance(result, cg.FlashStringLiteral) + assert str(result) == 'ESPHOME_F("")' + + +@pytest.mark.asyncio +async def test_templatable__string_with_none_output_type() -> None: + """Static string with output_type=None returns raw string (no wrapping).""" + result = await cg.templatable("hello", [], None) + + assert isinstance(result, str) + assert result == "hello" + + +@pytest.mark.asyncio +async def test_templatable__int_with_std_string() -> None: + """Non-string value with std::string output_type returns raw value.""" + result = await cg.templatable(42, [], ct.std_string) + + assert result == 42 + + +@pytest.mark.asyncio +async def test_templatable__string_with_non_string_output_type() -> None: + """Static string with non-std::string output_type returns raw string.""" + result = await cg.templatable("hello", [], ct.bool_) + + assert isinstance(result, str) + assert result == "hello" + + +@pytest.mark.asyncio +async def test_templatable__with_to_exp_callable() -> None: + """When to_exp is provided, it is applied to non-template values.""" + result = await cg.templatable(42, [], None, to_exp=lambda x: x * 2) + + assert result == 84 + + +@pytest.mark.asyncio +async def test_templatable__with_to_exp_dict() -> None: + """When to_exp is a dict, value is looked up.""" + mapping: dict[str, int] = {"on": 1, "off": 0} + result = await cg.templatable("on", [], None, to_exp=mapping) + + assert result == 1 + + +@pytest.mark.asyncio +async def test_templatable__lambda_with_std_string() -> None: + """Lambda value returns LambdaExpression, not FlashStringLiteral.""" + from esphome.core import Lambda + + lambda_obj = Lambda('return "hello";') + result = await cg.templatable(lambda_obj, [], ct.std_string) + + assert isinstance(result, cg.LambdaExpression) From 00242443e1e67366712840b9fe1b47831d2e9fea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 06:57:23 -1000 Subject: [PATCH 302/609] [light] Replace powf gamma with pre-computed lookup tables (LUT) (#14123) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32/core.cpp | 1 + esphome/components/esp8266/core.cpp | 3 + esphome/components/host/core.cpp | 1 + esphome/components/libretiny/core.cpp | 1 + esphome/components/light/__init__.py | 40 +++++++++- esphome/components/light/addressable_light.h | 4 +- .../light/addressable_light_wrapper.h | 9 +-- .../components/light/esp_color_correction.cpp | 36 ++++----- .../components/light/esp_color_correction.h | 45 +++++++---- esphome/components/light/light_call.cpp | 5 +- esphome/components/light/light_color_values.h | 48 +++++------- esphome/components/light/light_state.cpp | 78 ++++++++++++++++--- esphome/components/light/light_state.h | 26 ++++++- esphome/components/lvgl/light/lvgl_light.h | 2 +- esphome/components/rgb/rgb_light_output.h | 2 +- esphome/components/rgbw/rgbw_light_output.h | 2 +- esphome/components/rp2040/core.cpp | 1 + esphome/components/zephyr/core.cpp | 1 + esphome/core/defines.h | 1 + esphome/core/hal.h | 1 + esphome/core/helpers.h | 4 + 21 files changed, 228 insertions(+), 83 deletions(-) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index b9ae871abf..59b791da40 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -48,6 +48,7 @@ void arch_init() { void HOT arch_feed_wdt() { esp_task_wdt_reset(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { uint32_t freq = 0; diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 7136cf41cb..b665124d66 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -34,6 +34,9 @@ void HOT arch_feed_wdt() { system_soft_wdt_feed(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT } +uint16_t progmem_read_uint16(const uint16_t *addr) { + return pgm_read_word(addr); // NOLINT +} uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { return F_CPU; } diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index 9af85bec58..cb2b2e19d7 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -59,6 +59,7 @@ void HOT arch_feed_wdt() { } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { struct timespec spec; clock_gettime(CLOCK_MONOTONIC, &spec); diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index 88952692a7..74b33a30a0 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -36,6 +36,7 @@ void HOT arch_feed_wdt() { lt_wdt_feed(); } uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } } // namespace esphome diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index f1089ad64f..40382bbda7 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass, field import enum import esphome.automation as auto @@ -37,7 +38,7 @@ from esphome.const import ( CONF_WEB_SERVER, CONF_WHITE, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, HexInt, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -66,6 +67,40 @@ from .types import ( # noqa CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True +DOMAIN = "light" + + +@dataclass +class LightData: + gamma_tables: dict = field(default_factory=dict) # gamma_value -> fwd_arr + + +def _get_data() -> LightData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = LightData() + return CORE.data[DOMAIN] + + +def _get_or_create_gamma_table(gamma_correct): + data = _get_data() + if gamma_correct in data.gamma_tables: + return data.gamma_tables[gamma_correct] + + if gamma_correct > 0: + forward = [ + HexInt(min(65535, int(round((i / 255.0) ** gamma_correct * 65535)))) + for i in range(256) + ] + else: + forward = [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)] + + gamma_str = f"{gamma_correct}".replace(".", "_") + fwd_id = ID(f"gamma_{gamma_str}_fwd", is_declaration=True, type=cg.uint16) + fwd_arr = cg.progmem_array(fwd_id, forward) + data.gamma_tables[gamma_correct] = fwd_arr + return fwd_arr + + LightRestoreMode = light_ns.enum("LightRestoreMode") RESTORE_MODES = { "RESTORE_DEFAULT_OFF": LightRestoreMode.LIGHT_RESTORE_DEFAULT_OFF, @@ -239,6 +274,9 @@ async def setup_light_core_(light_var, output_var, config): cg.add(light_var.set_flash_transition_length(flash_transition_length)) if (gamma_correct := config.get(CONF_GAMMA_CORRECT)) is not None: cg.add(light_var.set_gamma_correct(gamma_correct)) + fwd_arr = _get_or_create_gamma_table(gamma_correct) + cg.add(light_var.set_gamma_table(fwd_arr)) + cg.add_define("USE_LIGHT_GAMMA_LUT") effects = await cg.build_registry_list( EFFECTS_REGISTRY, config.get(CONF_EFFECTS, []) ) diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index fcaf07f578..17cdb7d6f6 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -66,7 +66,9 @@ class AddressableLight : public LightOutput, public Component { Color(to_uint8_scale(red), to_uint8_scale(green), to_uint8_scale(blue), to_uint8_scale(white))); } void setup_state(LightState *state) override { - this->correction_.calculate_gamma_table(state->get_gamma_correct()); +#ifdef USE_LIGHT_GAMMA_LUT + this->correction_.set_gamma_table(state->get_gamma_table()); +#endif this->state_parent_ = state; } void update_state(LightState *state) override; diff --git a/esphome/components/light/addressable_light_wrapper.h b/esphome/components/light/addressable_light_wrapper.h index cd83482248..cc6aa57905 100644 --- a/esphome/components/light/addressable_light_wrapper.h +++ b/esphome/components/light/addressable_light_wrapper.h @@ -74,11 +74,10 @@ class AddressableLightWrapper : public light::AddressableLight { return; } - float gamma = this->light_state_->get_gamma_correct(); - float r = gamma_uncorrect(this->wrapper_state_[0] / 255.0f, gamma); - float g = gamma_uncorrect(this->wrapper_state_[1] / 255.0f, gamma); - float b = gamma_uncorrect(this->wrapper_state_[2] / 255.0f, gamma); - float w = gamma_uncorrect(this->wrapper_state_[3] / 255.0f, gamma); + float r = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[0] / 255.0f); + float g = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[1] / 255.0f); + float b = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[2] / 255.0f); + float w = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[3] / 255.0f); auto call = this->light_state_->make_call(); diff --git a/esphome/components/light/esp_color_correction.cpp b/esphome/components/light/esp_color_correction.cpp index 1b511a94b2..9d731a2bd5 100644 --- a/esphome/components/light/esp_color_correction.cpp +++ b/esphome/components/light/esp_color_correction.cpp @@ -1,25 +1,25 @@ #include "esp_color_correction.h" -#include "light_color_values.h" -#include "esphome/core/log.h" namespace esphome::light { -void ESPColorCorrection::calculate_gamma_table(float gamma) { - for (uint16_t i = 0; i < 256; i++) { - // corrected = val ^ gamma - auto corrected = to_uint8_scale(gamma_correct(i / 255.0f, gamma)); - this->gamma_table_[i] = corrected; - } - if (gamma == 0.0f) { - for (uint16_t i = 0; i < 256; i++) - this->gamma_reverse_table_[i] = i; - return; - } - for (uint16_t i = 0; i < 256; i++) { - // val = corrected ^ (1/gamma) - auto uncorrected = to_uint8_scale(powf(i / 255.0f, 1.0f / gamma)); - this->gamma_reverse_table_[i] = uncorrected; - } +uint8_t ESPColorCorrection::gamma_correct_(uint8_t value) const { + if (this->gamma_table_ == nullptr) + return value; + return static_cast((progmem_read_uint16(&this->gamma_table_[value]) + 128) / 257); +} + +uint8_t ESPColorCorrection::gamma_uncorrect_(uint8_t value) const { + if (this->gamma_table_ == nullptr) + return value; + if (value == 0) + return 0; + uint16_t target = value * 257; // Scale 0-255 to 0-65535 + uint8_t lo = gamma_table_reverse_search(this->gamma_table_, target); + if (lo >= 255) + return 255; + uint16_t a = progmem_read_uint16(&this->gamma_table_[lo]); + uint16_t b = progmem_read_uint16(&this->gamma_table_[lo + 1]); + return (target - a <= b - target) ? lo : lo + 1; } } // namespace esphome::light diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index d275e045b7..48ecc46364 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -1,15 +1,30 @@ #pragma once #include "esphome/core/color.h" +#include "esphome/core/hal.h" namespace esphome::light { +/// Binary search a monotonically increasing uint16[256] PROGMEM table. +/// Returns the largest index where table[index] <= target. +inline uint8_t gamma_table_reverse_search(const uint16_t *table, uint16_t target) { + uint8_t lo = 0, hi = 255; + while (lo < hi) { + uint8_t mid = (lo + hi + 1) / 2; + if (progmem_read_uint16(&table[mid]) <= target) { + lo = mid; + } else { + hi = mid - 1; + } + } + return lo; +} + class ESPColorCorrection { public: - ESPColorCorrection() : max_brightness_(255, 255, 255, 255) {} void set_max_brightness(const Color &max_brightness) { this->max_brightness_ = max_brightness; } void set_local_brightness(uint8_t local_brightness) { this->local_brightness_ = local_brightness; } - void calculate_gamma_table(float gamma); + void set_gamma_table(const uint16_t *table) { this->gamma_table_ = table; } inline Color color_correct(Color color) const ESPHOME_ALWAYS_INLINE { // corrected = (uncorrected * max_brightness * local_brightness) ^ gamma return Color(this->color_correct_red(color.red), this->color_correct_green(color.green), @@ -17,19 +32,19 @@ class ESPColorCorrection { } inline uint8_t color_correct_red(uint8_t red) const ESPHOME_ALWAYS_INLINE { uint8_t res = esp_scale8_twice(red, this->max_brightness_.red, this->local_brightness_); - return this->gamma_table_[res]; + return this->gamma_correct_(res); } inline uint8_t color_correct_green(uint8_t green) const ESPHOME_ALWAYS_INLINE { uint8_t res = esp_scale8_twice(green, this->max_brightness_.green, this->local_brightness_); - return this->gamma_table_[res]; + return this->gamma_correct_(res); } inline uint8_t color_correct_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE { uint8_t res = esp_scale8_twice(blue, this->max_brightness_.blue, this->local_brightness_); - return this->gamma_table_[res]; + return this->gamma_correct_(res); } inline uint8_t color_correct_white(uint8_t white) const ESPHOME_ALWAYS_INLINE { uint8_t res = esp_scale8_twice(white, this->max_brightness_.white, this->local_brightness_); - return this->gamma_table_[res]; + return this->gamma_correct_(res); } inline Color color_uncorrect(Color color) const ESPHOME_ALWAYS_INLINE { // uncorrected = corrected^(1/gamma) / (max_brightness * local_brightness) @@ -39,36 +54,40 @@ class ESPColorCorrection { inline uint8_t color_uncorrect_red(uint8_t red) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.red == 0 || this->local_brightness_ == 0) return 0; - uint16_t uncorrected = this->gamma_reverse_table_[red] * 255UL; + uint16_t uncorrected = this->gamma_uncorrect_(red) * 255UL; uint16_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_; return (uint8_t) std::min(res, uint16_t(255)); } inline uint8_t color_uncorrect_green(uint8_t green) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.green == 0 || this->local_brightness_ == 0) return 0; - uint16_t uncorrected = this->gamma_reverse_table_[green] * 255UL; + uint16_t uncorrected = this->gamma_uncorrect_(green) * 255UL; uint16_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_; return (uint8_t) std::min(res, uint16_t(255)); } inline uint8_t color_uncorrect_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.blue == 0 || this->local_brightness_ == 0) return 0; - uint16_t uncorrected = this->gamma_reverse_table_[blue] * 255UL; + uint16_t uncorrected = this->gamma_uncorrect_(blue) * 255UL; uint16_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_; return (uint8_t) std::min(res, uint16_t(255)); } inline uint8_t color_uncorrect_white(uint8_t white) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.white == 0 || this->local_brightness_ == 0) return 0; - uint16_t uncorrected = this->gamma_reverse_table_[white] * 255UL; + uint16_t uncorrected = this->gamma_uncorrect_(white) * 255UL; uint16_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_; return (uint8_t) std::min(res, uint16_t(255)); } protected: - uint8_t gamma_table_[256]; - uint8_t gamma_reverse_table_[256]; - Color max_brightness_; + /// Forward gamma: read uint16 PROGMEM table, convert to uint8 + uint8_t gamma_correct_(uint8_t value) const; + /// Reverse gamma: binary search the forward PROGMEM table + uint8_t gamma_uncorrect_(uint8_t value) const; + + const uint16_t *gamma_table_{nullptr}; + Color max_brightness_{255, 255, 255, 255}; uint8_t local_brightness_{255}; }; diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 0291b2c3c6..14cd0e92f6 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -389,9 +389,8 @@ void LightCall::transform_parameters_() { const float ww_fraction = (color_temp - min_mireds) / range; const float cw_fraction = 1.0f - ww_fraction; const float max_cw_ww = std::max(ww_fraction, cw_fraction); - const float gamma = this->parent_->get_gamma_correct(); - this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, gamma); - this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, gamma); + this->cold_white_ = this->parent_->gamma_uncorrect_lut(cw_fraction / max_cw_ww); + this->warm_white_ = this->parent_->gamma_uncorrect_lut(ww_fraction / max_cw_ww); this->set_flag_(FLAG_HAS_COLD_WHITE); this->set_flag_(FLAG_HAS_WARM_WHITE); } diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index dc23263312..3a9ca8c8c2 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -111,60 +111,54 @@ class LightColorValues { } } - // Note that method signature of as_* methods is kept as-is for compatibility reasons, so not all parameters - // are always used or necessary. Methods will be deprecated later. - /// Convert these light color values to a binary representation and write them to binary. void as_binary(bool *binary) const { *binary = this->state_ == 1.0f; } /// Convert these light color values to a brightness-only representation and write them to brightness. - void as_brightness(float *brightness, float gamma = 0) const { - *brightness = gamma_correct(this->state_ * this->brightness_, gamma); - } + void as_brightness(float *brightness) const { *brightness = this->state_ * this->brightness_; } /// Convert these light color values to an RGB representation and write them to red, green, blue. - void as_rgb(float *red, float *green, float *blue, float gamma = 0, bool color_interlock = false) const { + void as_rgb(float *red, float *green, float *blue) const { if (this->color_mode_ & ColorCapability::RGB) { float brightness = this->state_ * this->brightness_ * this->color_brightness_; - *red = gamma_correct(brightness * this->red_, gamma); - *green = gamma_correct(brightness * this->green_, gamma); - *blue = gamma_correct(brightness * this->blue_, gamma); + *red = brightness * this->red_; + *green = brightness * this->green_; + *blue = brightness * this->blue_; } else { *red = *green = *blue = 0; } } /// Convert these light color values to an RGBW representation and write them to red, green, blue, white. - void as_rgbw(float *red, float *green, float *blue, float *white, float gamma = 0, - bool color_interlock = false) const { - this->as_rgb(red, green, blue, gamma); + void as_rgbw(float *red, float *green, float *blue, float *white) const { + this->as_rgb(red, green, blue); if (this->color_mode_ & ColorCapability::WHITE) { - *white = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); + *white = this->state_ * this->brightness_ * this->white_; } else { *white = 0; } } /// Convert these light color values to an RGBWW representation with the given parameters. - void as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, float gamma = 0, + void as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, bool constant_brightness = false) const { - this->as_rgb(red, green, blue, gamma); - this->as_cwww(cold_white, warm_white, gamma, constant_brightness); + this->as_rgb(red, green, blue); + this->as_cwww(cold_white, warm_white, constant_brightness); } /// Convert these light color values to an RGB+CT+BR representation with the given parameters. void as_rgbct(float color_temperature_cw, float color_temperature_ww, float *red, float *green, float *blue, - float *color_temperature, float *white_brightness, float gamma = 0) const { - this->as_rgb(red, green, blue, gamma); - this->as_ct(color_temperature_cw, color_temperature_ww, color_temperature, white_brightness, gamma); + float *color_temperature, float *white_brightness) const { + this->as_rgb(red, green, blue); + this->as_ct(color_temperature_cw, color_temperature_ww, color_temperature, white_brightness); } /// Convert these light color values to an CWWW representation with the given parameters. - void as_cwww(float *cold_white, float *warm_white, float gamma = 0, bool constant_brightness = false) const { + void as_cwww(float *cold_white, float *warm_white, bool constant_brightness = false) const { if (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) { - const float cw_level = gamma_correct(this->cold_white_, gamma); - const float ww_level = gamma_correct(this->warm_white_, gamma); - const float white_level = gamma_correct(this->state_ * this->brightness_, gamma); + const float cw_level = this->cold_white_; + const float ww_level = this->warm_white_; + const float white_level = this->state_ * this->brightness_; if (!constant_brightness) { *cold_white = white_level * cw_level; *warm_white = white_level * ww_level; @@ -184,13 +178,13 @@ class LightColorValues { } /// Convert these light color values to a CT+BR representation with the given parameters. - void as_ct(float color_temperature_cw, float color_temperature_ww, float *color_temperature, float *white_brightness, - float gamma = 0) const { + void as_ct(float color_temperature_cw, float color_temperature_ww, float *color_temperature, + float *white_brightness) const { const float white_level = this->color_mode_ & ColorCapability::RGB ? this->white_ : 1; if (this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) { *color_temperature = (this->color_temperature_ - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); - *white_brightness = gamma_correct(this->state_ * this->brightness_ * white_level, gamma); + *white_brightness = this->state_ * this->brightness_ * white_level; } else { // Probably won't get here but put this here anyway. *white_brightness = 0; } diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index ed86bf58da..161092532a 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -1,4 +1,5 @@ #include "light_state.h" +#include "esp_color_correction.h" #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" @@ -204,33 +205,90 @@ void LightState::add_effects(const std::initializer_list &effects void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); } void LightState::current_values_as_brightness(float *brightness) { - this->current_values.as_brightness(brightness, this->gamma_correct_); + this->current_values.as_brightness(brightness); + *brightness = this->gamma_correct_lut(*brightness); } -void LightState::current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock) { - this->current_values.as_rgb(red, green, blue, this->gamma_correct_, false); +void LightState::current_values_as_rgb(float *red, float *green, float *blue) { + this->current_values.as_rgb(red, green, blue); + *red = this->gamma_correct_lut(*red); + *green = this->gamma_correct_lut(*green); + *blue = this->gamma_correct_lut(*blue); } -void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock) { - this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, false); +void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white) { + this->current_values.as_rgbw(red, green, blue, white); + *red = this->gamma_correct_lut(*red); + *green = this->gamma_correct_lut(*green); + *blue = this->gamma_correct_lut(*blue); + *white = this->gamma_correct_lut(*white); } void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, bool constant_brightness) { - this->current_values.as_rgbww(red, green, blue, cold_white, warm_white, this->gamma_correct_, constant_brightness); + this->current_values.as_rgbww(red, green, blue, cold_white, warm_white, constant_brightness); + *red = this->gamma_correct_lut(*red); + *green = this->gamma_correct_lut(*green); + *blue = this->gamma_correct_lut(*blue); + *cold_white = this->gamma_correct_lut(*cold_white); + *warm_white = this->gamma_correct_lut(*warm_white); } void LightState::current_values_as_rgbct(float *red, float *green, float *blue, float *color_temperature, float *white_brightness) { auto traits = this->get_traits(); this->current_values.as_rgbct(traits.get_min_mireds(), traits.get_max_mireds(), red, green, blue, color_temperature, - white_brightness, this->gamma_correct_); + white_brightness); + *red = this->gamma_correct_lut(*red); + *green = this->gamma_correct_lut(*green); + *blue = this->gamma_correct_lut(*blue); + *white_brightness = this->gamma_correct_lut(*white_brightness); } void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) { - this->current_values.as_cwww(cold_white, warm_white, this->gamma_correct_, constant_brightness); + this->current_values.as_cwww(cold_white, warm_white, constant_brightness); + *cold_white = this->gamma_correct_lut(*cold_white); + *warm_white = this->gamma_correct_lut(*warm_white); } void LightState::current_values_as_ct(float *color_temperature, float *white_brightness) { auto traits = this->get_traits(); - this->current_values.as_ct(traits.get_min_mireds(), traits.get_max_mireds(), color_temperature, white_brightness, - this->gamma_correct_); + this->current_values.as_ct(traits.get_min_mireds(), traits.get_max_mireds(), color_temperature, white_brightness); + *white_brightness = this->gamma_correct_lut(*white_brightness); } +#ifdef USE_LIGHT_GAMMA_LUT +float LightState::gamma_correct_lut(float value) const { + if (value <= 0.0f) + return 0.0f; + if (value >= 1.0f) + return 1.0f; + if (this->gamma_table_ == nullptr) + return value; + float scaled = value * 255.0f; + auto idx = static_cast(scaled); + if (idx >= 255) + return progmem_read_uint16(&this->gamma_table_[255]) / 65535.0f; + float frac = scaled - idx; + float a = progmem_read_uint16(&this->gamma_table_[idx]); + float b = progmem_read_uint16(&this->gamma_table_[idx + 1]); + return (a + frac * (b - a)) / 65535.0f; +} +float LightState::gamma_uncorrect_lut(float value) const { + if (value <= 0.0f) + return 0.0f; + if (value >= 1.0f) + return 1.0f; + if (this->gamma_table_ == nullptr) + return value; + uint16_t target = static_cast(value * 65535.0f); + uint8_t lo = gamma_table_reverse_search(this->gamma_table_, target); + if (lo >= 255) + return 1.0f; + // Interpolate between lo and lo+1 + uint16_t a = progmem_read_uint16(&this->gamma_table_[lo]); + uint16_t b = progmem_read_uint16(&this->gamma_table_[lo + 1]); + if (b == a) + return lo / 255.0f; + float frac = static_cast(target - a) / static_cast(b - a); + return (lo + frac) / 255.0f; +} +#endif // USE_LIGHT_GAMMA_LUT + bool LightState::is_transformer_active() { return this->is_transformer_active_; } void LightState::start_effect_(uint32_t effect_index) { diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 83b9226d03..528f5bd911 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -11,6 +11,7 @@ #include "light_traits.h" #include "light_transformer.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include #include @@ -166,6 +167,23 @@ class LightState : public EntityBase, public Component { void set_gamma_correct(float gamma_correct); float get_gamma_correct() const { return this->gamma_correct_; } +#ifdef USE_LIGHT_GAMMA_LUT + /// Set pre-computed gamma forward lookup table (256-entry uint16 PROGMEM array) + void set_gamma_table(const uint16_t *forward) { this->gamma_table_ = forward; } + + /// Get the forward gamma lookup table + const uint16_t *get_gamma_table() const { return this->gamma_table_; } + + /// Apply gamma correction using the pre-computed forward LUT + float gamma_correct_lut(float value) const; + /// Reverse gamma correction by binary-searching the forward LUT + float gamma_uncorrect_lut(float value) const; +#else + /// No gamma LUT — passthrough + float gamma_correct_lut(float value) const { return value; } + float gamma_uncorrect_lut(float value) const { return value; } +#endif // USE_LIGHT_GAMMA_LUT + /// Set the restore mode of this light void set_restore_mode(LightRestoreMode restore_mode); @@ -224,9 +242,9 @@ class LightState : public EntityBase, public Component { void current_values_as_brightness(float *brightness); - void current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock = false); + void current_values_as_rgb(float *red, float *green, float *blue); - void current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock = false); + void current_values_as_rgbw(float *red, float *green, float *blue, float *white); void current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, bool constant_brightness = false); @@ -297,6 +315,10 @@ class LightState : public EntityBase, public Component { uint32_t flash_transition_length_{}; /// Gamma correction factor for the light. float gamma_correct_{}; +#ifdef USE_LIGHT_GAMMA_LUT + const uint16_t *gamma_table_{nullptr}; +#endif // USE_LIGHT_GAMMA_LUT + /// Whether the light value should be written in the next cycle. bool next_write_{true}; // for effects, true if a transformer (transition) is active. diff --git a/esphome/components/lvgl/light/lvgl_light.h b/esphome/components/lvgl/light/lvgl_light.h index 50ae4c5327..569f9a03c0 100644 --- a/esphome/components/lvgl/light/lvgl_light.h +++ b/esphome/components/lvgl/light/lvgl_light.h @@ -16,7 +16,7 @@ class LVLight : public light::LightOutput { } void write_state(light::LightState *state) override { float red, green, blue; - state->current_values_as_rgb(&red, &green, &blue, false); + state->current_values_as_rgb(&red, &green, &blue); auto color = lv_color_make(red * 255, green * 255, blue * 255); if (this->obj_ != nullptr) { this->set_value_(color); diff --git a/esphome/components/rgb/rgb_light_output.h b/esphome/components/rgb/rgb_light_output.h index ef53c8042d..783187667a 100644 --- a/esphome/components/rgb/rgb_light_output.h +++ b/esphome/components/rgb/rgb_light_output.h @@ -20,7 +20,7 @@ class RGBLightOutput : public light::LightOutput { } void write_state(light::LightState *state) override { float red, green, blue; - state->current_values_as_rgb(&red, &green, &blue, false); + state->current_values_as_rgb(&red, &green, &blue); this->red_->set_level(red); this->green_->set_level(green); this->blue_->set_level(blue); diff --git a/esphome/components/rgbw/rgbw_light_output.h b/esphome/components/rgbw/rgbw_light_output.h index a2ab17b75d..140726a43c 100644 --- a/esphome/components/rgbw/rgbw_light_output.h +++ b/esphome/components/rgbw/rgbw_light_output.h @@ -25,7 +25,7 @@ class RGBWLightOutput : public light::LightOutput { } void write_state(light::LightState *state) override { float red, green, blue, white; - state->current_values_as_rgbw(&red, &green, &blue, &white, this->color_interlock_); + state->current_values_as_rgbw(&red, &green, &blue, &white); this->red_->set_level(red); this->green_->set_level(green); this->blue_->set_level(blue); diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index 8b86de4be1..6386d53292 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -34,6 +34,7 @@ void HOT arch_feed_wdt() { watchdog_update(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT } +uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } uint32_t HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); } uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); } diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index f0772a4422..cf3ea70245 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -60,6 +60,7 @@ void arch_restart() { sys_reboot(SYS_REBOOT_COLD); } uint32_t arch_get_cpu_cycle_count() { return k_cycle_get_32(); } uint32_t arch_get_cpu_freq_hz() { return sys_clock_hw_cycles_per_sec(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } Mutex::Mutex() { auto *mutex = new k_mutex(); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 1f2597dd98..8c78afa7d4 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -61,6 +61,7 @@ #define USE_IR_RF #define USE_JSON #define USE_LIGHT +#define USE_LIGHT_GAMMA_LUT #define USE_LOCK #define USE_LOGGER #define USE_LOGGER_LEVEL_LISTENERS diff --git a/esphome/core/hal.h b/esphome/core/hal.h index fa5ca646f2..ef45be629d 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -42,5 +42,6 @@ void arch_feed_wdt(); uint32_t arch_get_cpu_cycle_count(); uint32_t arch_get_cpu_freq_hz(); uint8_t progmem_read_byte(const uint8_t *addr); +uint16_t progmem_read_uint16(const uint16_t *addr); } // namespace esphome diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 65d590a5e6..c68cb549bb 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1437,8 +1437,12 @@ bool base64_decode_int32_vector(const std::string &base64, std::vector ///@{ /// Applies gamma correction of \p gamma to \p value. +// Remove before 2026.9.0 +ESPDEPRECATED("Use LightState::gamma_correct_lut() instead. Removed in 2026.9.0.", "2026.3.0") float gamma_correct(float value, float gamma); /// Reverts gamma correction of \p gamma to \p value. +// Remove before 2026.9.0 +ESPDEPRECATED("Use LightState::gamma_uncorrect_lut() instead. Removed in 2026.9.0.", "2026.3.0") float gamma_uncorrect(float value, float gamma); /// Convert \p red, \p green and \p blue (all 0-1) values to \p hue (0-360), \p saturation (0-1) and \p value (0-1). From 54f410901fa09bd025bc370be3753346c2430b9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 06:57:39 -1000 Subject: [PATCH 303/609] [web_server] Avoid temporary std::string allocations in request parameter parsing (#14366) Co-authored-by: Claude Opus 4.6 --- .../alarm_control_panel_call.cpp | 9 ++++- .../alarm_control_panel_call.h | 3 +- esphome/components/api/api_connection.cpp | 4 +- esphome/components/climate/climate.cpp | 18 ++++++--- esphome/components/climate/climate.h | 4 ++ esphome/components/text/text_call.cpp | 5 +++ esphome/components/text/text_call.h | 1 + .../components/water_heater/water_heater.cpp | 21 +++++----- .../components/water_heater/water_heater.h | 3 +- esphome/components/web_server/web_server.cpp | 38 ++++++++++++++----- esphome/components/web_server/web_server.h | 8 ++-- 11 files changed, 82 insertions(+), 32 deletions(-) diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp b/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp index ba58ee3904..0c43cd555a 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp @@ -12,7 +12,14 @@ AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code) { if (code != nullptr) { - this->code_ = std::string(code); + return this->set_code(code, strlen(code)); + } + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code, size_t len) { + if (code != nullptr) { + this->code_ = std::string(code, len); } return *this; } diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_call.h b/esphome/components/alarm_control_panel/alarm_control_panel_call.h index 58764ea166..6e39a0a413 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel_call.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel_call.h @@ -15,7 +15,8 @@ class AlarmControlPanelCall { AlarmControlPanelCall(AlarmControlPanel *parent); AlarmControlPanelCall &set_code(const char *code); - AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str()); } + AlarmControlPanelCall &set_code(const char *code, size_t len); + AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str(), code.size()); } AlarmControlPanelCall &arm_away(); AlarmControlPanelCall &arm_home(); AlarmControlPanelCall &arm_night(); diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 90287ec2dd..738dd1ef05 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -889,7 +889,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co } void APIConnection::on_text_command_request(const TextCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(text::Text, text, text) - call.set_value(msg.state); + call.set_value(msg.state.c_str(), msg.state.size()); call.perform(); } #endif @@ -1360,7 +1360,7 @@ void APIConnection::on_alarm_control_panel_command_request(const AlarmControlPan call.pending(); break; } - call.set_code(msg.code); + call.set_code(msg.code.c_str(), msg.code.size()); call.perform(); } #endif diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index ba6de4ff61..43d25effa3 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -173,14 +173,17 @@ ClimateCall &ClimateCall::set_mode(ClimateMode mode) { return *this; } -ClimateCall &ClimateCall::set_mode(const std::string &mode) { +ClimateCall &ClimateCall::set_mode(const std::string &mode) { return this->set_mode(mode.c_str(), mode.size()); } + +ClimateCall &ClimateCall::set_mode(const char *mode, size_t len) { + StringRef mode_ref(mode, len); for (const auto &mode_entry : CLIMATE_MODES_BY_STR) { - if (str_equals_case_insensitive(mode, mode_entry.str)) { + if (str_equals_case_insensitive(mode_ref, mode_entry.str)) { this->set_mode(static_cast(mode_entry.value)); return *this; } } - ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str()); + ESP_LOGW(TAG, "'%s' - Unrecognized mode %.*s", this->parent_->get_name().c_str(), (int) len, mode); return *this; } @@ -266,13 +269,18 @@ ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) { } ClimateCall &ClimateCall::set_swing_mode(const std::string &swing_mode) { + return this->set_swing_mode(swing_mode.c_str(), swing_mode.size()); +} + +ClimateCall &ClimateCall::set_swing_mode(const char *swing_mode, size_t len) { + StringRef mode_ref(swing_mode, len); for (const auto &mode_entry : CLIMATE_SWING_MODES_BY_STR) { - if (str_equals_case_insensitive(swing_mode, mode_entry.str)) { + if (str_equals_case_insensitive(mode_ref, mode_entry.str)) { this->set_swing_mode(static_cast(mode_entry.value)); return *this; } } - ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str()); + ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %.*s", this->parent_->get_name().c_str(), (int) len, swing_mode); return *this; } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 6fac254502..aa9ca91bc2 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -41,6 +41,8 @@ class ClimateCall { ClimateCall &set_mode(optional mode); /// Set the mode of the climate device based on a string. ClimateCall &set_mode(const std::string &mode); + /// Set the mode of the climate device based on a C string. + ClimateCall &set_mode(const char *mode, size_t len); /// Set the target temperature of the climate device. ClimateCall &set_target_temperature(float target_temperature); /// Set the target temperature of the climate device. @@ -87,6 +89,8 @@ class ClimateCall { ClimateCall &set_swing_mode(optional swing_mode); /// Set the swing mode of the climate device based on a string. ClimateCall &set_swing_mode(const std::string &swing_mode); + /// Set the swing mode of the climate device based on a C string. + ClimateCall &set_swing_mode(const char *swing_mode, size_t len); /// Set the preset of the climate device. ClimateCall &set_preset(ClimatePreset preset); /// Set the preset of the climate device. diff --git a/esphome/components/text/text_call.cpp b/esphome/components/text/text_call.cpp index 8a1630c5ca..b7aed098c7 100644 --- a/esphome/components/text/text_call.cpp +++ b/esphome/components/text/text_call.cpp @@ -11,6 +11,11 @@ TextCall &TextCall::set_value(const std::string &value) { return *this; } +TextCall &TextCall::set_value(const char *value, size_t len) { + this->value_ = std::string(value, len); + return *this; +} + void TextCall::validate_() { const auto *name = this->parent_->get_name().c_str(); diff --git a/esphome/components/text/text_call.h b/esphome/components/text/text_call.h index 532fae34b2..5a2b257ab2 100644 --- a/esphome/components/text/text_call.h +++ b/esphome/components/text/text_call.h @@ -13,6 +13,7 @@ class TextCall { void perform(); TextCall &set_value(const std::string &value); + TextCall &set_value(const char *value, size_t len); protected: Text *const parent_; diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index 9d7ae0cbc0..3989230d2d 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -5,6 +5,7 @@ #include "esphome/core/progmem.h" #include +#include namespace esphome::water_heater { @@ -23,23 +24,25 @@ WaterHeaterCall &WaterHeaterCall::set_mode(WaterHeaterMode mode) { return *this; } -WaterHeaterCall &WaterHeaterCall::set_mode(const char *mode) { - if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("OFF")) == 0) { +WaterHeaterCall &WaterHeaterCall::set_mode(const char *mode) { return this->set_mode(mode, strlen(mode)); } + +WaterHeaterCall &WaterHeaterCall::set_mode(const char *mode, size_t len) { + if (len == 3 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("OFF"), 3) == 0) { this->set_mode(WATER_HEATER_MODE_OFF); - } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("ECO")) == 0) { + } else if (len == 3 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("ECO"), 3) == 0) { this->set_mode(WATER_HEATER_MODE_ECO); - } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("ELECTRIC")) == 0) { + } else if (len == 8 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("ELECTRIC"), 8) == 0) { this->set_mode(WATER_HEATER_MODE_ELECTRIC); - } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("PERFORMANCE")) == 0) { + } else if (len == 11 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("PERFORMANCE"), 11) == 0) { this->set_mode(WATER_HEATER_MODE_PERFORMANCE); - } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("HIGH_DEMAND")) == 0) { + } else if (len == 11 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("HIGH_DEMAND"), 11) == 0) { this->set_mode(WATER_HEATER_MODE_HIGH_DEMAND); - } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("HEAT_PUMP")) == 0) { + } else if (len == 9 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("HEAT_PUMP"), 9) == 0) { this->set_mode(WATER_HEATER_MODE_HEAT_PUMP); - } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("GAS")) == 0) { + } else if (len == 3 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("GAS"), 3) == 0) { this->set_mode(WATER_HEATER_MODE_GAS); } else { - ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode); + ESP_LOGW(TAG, "'%s' - Unrecognized mode %.*s", this->parent_->get_name().c_str(), (int) len, mode); } return *this; } diff --git a/esphome/components/water_heater/water_heater.h b/esphome/components/water_heater/water_heater.h index 070ae99575..a1e1ca10a6 100644 --- a/esphome/components/water_heater/water_heater.h +++ b/esphome/components/water_heater/water_heater.h @@ -76,7 +76,8 @@ class WaterHeaterCall { WaterHeaterCall &set_mode(WaterHeaterMode mode); WaterHeaterCall &set_mode(const char *mode); - WaterHeaterCall &set_mode(const std::string &mode) { return this->set_mode(mode.c_str()); } + WaterHeaterCall &set_mode(const char *mode, size_t len); + WaterHeaterCall &set_mode(const std::string &mode) { return this->set_mode(mode.c_str(), mode.size()); } WaterHeaterCall &set_target_temperature(float temperature); WaterHeaterCall &set_target_temperature_low(float temperature); WaterHeaterCall &set_target_temperature_high(float temperature); diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 4824e33dcd..47e427c0d1 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -969,7 +969,9 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa parse_light_param_uint_(request, ESPHOME_F("transition"), call, &decltype(call)::set_transition_length, 1000); if (is_on) { - parse_string_param_(request, ESPHOME_F("effect"), call, &decltype(call)::set_effect); + parse_cstr_param_( + request, ESPHOME_F("effect"), call, + static_cast(&decltype(call)::set_effect)); } DEFER_ACTION(call, call.perform()); @@ -1368,7 +1370,9 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat } auto call = obj->make_call(); - parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value); + parse_cstr_param_( + request, ESPHOME_F("value"), call, + static_cast(&decltype(call)::set_value)); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1426,7 +1430,9 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM } auto call = obj->make_call(); - parse_string_param_(request, ESPHOME_F("option"), call, &decltype(call)::set_option); + parse_cstr_param_( + request, ESPHOME_F("option"), call, + static_cast(&decltype(call)::set_option)); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1487,10 +1493,18 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url auto call = obj->make_call(); // Parse string mode parameters - parse_string_param_(request, ESPHOME_F("mode"), call, &decltype(call)::set_mode); - parse_string_param_(request, ESPHOME_F("fan_mode"), call, &decltype(call)::set_fan_mode); - parse_string_param_(request, ESPHOME_F("swing_mode"), call, &decltype(call)::set_swing_mode); - parse_string_param_(request, ESPHOME_F("preset"), call, &decltype(call)::set_preset); + parse_cstr_param_( + request, ESPHOME_F("mode"), call, + static_cast(&decltype(call)::set_mode)); + parse_cstr_param_(request, ESPHOME_F("fan_mode"), call, + static_cast( + &decltype(call)::set_fan_mode)); + parse_cstr_param_(request, ESPHOME_F("swing_mode"), call, + static_cast( + &decltype(call)::set_swing_mode)); + parse_cstr_param_(request, ESPHOME_F("preset"), call, + static_cast( + &decltype(call)::set_preset)); // Parse temperature parameters // static_cast needed to disambiguate overloaded setters (float vs optional) @@ -1804,7 +1818,10 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques } auto call = obj->make_call(); - parse_string_param_(request, ESPHOME_F("code"), call, &decltype(call)::set_code); + parse_cstr_param_( + request, ESPHOME_F("code"), call, + static_cast(&decltype(call)::set_code)); // Lookup table for alarm control panel methods static const struct { @@ -1892,7 +1909,10 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons water_heater::WaterHeaterCall &base_call = call; // Parse mode parameter - parse_string_param_(request, ESPHOME_F("mode"), base_call, &water_heater::WaterHeaterCall::set_mode); + parse_cstr_param_( + request, ESPHOME_F("mode"), base_call, + static_cast( + &water_heater::WaterHeaterCall::set_mode)); // Parse temperature parameters parse_num_param_(request, ESPHOME_F("target_temperature"), base_call, diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 64c492f82b..6152dfbfd3 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -533,13 +533,13 @@ class WebServer final : public Controller, public Component, public AsyncWebHand } } - // Generic helper to parse and apply a string parameter + // Generic helper to parse and apply a string parameter using const char* setter (avoids std::string allocation) template - void parse_string_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, - Ret (T::*setter)(const std::string &)) { + void parse_cstr_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, + Ret (T::*setter)(const char *, size_t)) { if (request->hasArg(param_name)) { const auto &value = request->arg(param_name); - (call.*setter)(std::string(value.c_str(), value.length())); + (call.*setter)(value.c_str(), value.length()); } } From f278250740330b6b4e990ae19b0bc91d846ec924 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 06:57:59 -1000 Subject: [PATCH 304/609] [core] Deduplicate ControllerRegistry notify dispatch loop (#14394) --- esphome/core/controller_registry.cpp | 21 +++++++++++++-------- esphome/core/controller_registry.h | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/esphome/core/controller_registry.cpp b/esphome/core/controller_registry.cpp index 13b505e8e9..255efa86ba 100644 --- a/esphome/core/controller_registry.cpp +++ b/esphome/core/controller_registry.cpp @@ -10,21 +10,26 @@ StaticVector ControllerRegistry::controll void ControllerRegistry::register_controller(Controller *controller) { controllers.push_back(controller); } +void ControllerRegistry::notify(void *obj, DispatchFunc dispatch) { + for (auto *controller : controllers) { + dispatch(controller, obj); + } +} + // Macro for standard registry notification dispatch - calls on__update() +// Each wrapper passes a small trampoline lambda that calls the correct virtual method. +// NOLINTBEGIN(bugprone-macro-parentheses) #define CONTROLLER_REGISTRY_NOTIFY(entity_type, entity_name) \ - void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ - for (auto *controller : controllers) { \ - controller->on_##entity_name##_update(obj); \ - } \ + void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { \ + notify(obj, [](Controller *c, void *o) { c->on_##entity_name##_update(static_cast(o)); }); \ } // Macro for entities where controller method has no "_update" suffix (Event, Update) #define CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(entity_type, entity_name) \ - void ControllerRegistry::notify_##entity_name(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ - for (auto *controller : controllers) { \ - controller->on_##entity_name(obj); \ - } \ + void ControllerRegistry::notify_##entity_name(entity_type *obj) { \ + notify(obj, [](Controller *c, void *o) { c->on_##entity_name(static_cast(o)); }); \ } +// NOLINTEND(bugprone-macro-parentheses) #ifdef USE_BINARY_SENSOR CONTROLLER_REGISTRY_NOTIFY(binary_sensor::BinarySensor, binary_sensor) diff --git a/esphome/core/controller_registry.h b/esphome/core/controller_registry.h index d6452d8827..15e3b4ba83 100644 --- a/esphome/core/controller_registry.h +++ b/esphome/core/controller_registry.h @@ -247,6 +247,21 @@ class ControllerRegistry { #endif protected: + /** Type-erased dispatch function pointer. + * + * Each notify method passes a small trampoline that calls the + * correct virtual method on Controller. The shared notify() loop + * iterates controllers once, calling the trampoline for each. + */ + using DispatchFunc = void (*)(Controller *, void *); + + /** Shared dispatch loop - iterates controllers and calls dispatch for each. + * + * Marked noinline to ensure only one copy of the loop exists in flash, + * rather than being duplicated into each notify_*_update wrapper. + */ + static void __attribute__((noinline)) notify(void *obj, DispatchFunc dispatch); + static StaticVector controllers; }; From 39572d9628d80ecf5f74f99f54c536ca26b357c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 06:58:15 -1000 Subject: [PATCH 305/609] [light] Resolve effect names to indices at codegen time (#14265) --- esphome/components/light/automation.h | 2 +- esphome/components/light/automation.py | 50 ++++++++++++++++++++++++-- esphome/components/light/light_state.h | 15 ++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index c90d71c5df..2854bc62d9 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -41,7 +41,7 @@ template class LightControlAction : public Action { TEMPLATABLE_VALUE(float, color_temperature) TEMPLATABLE_VALUE(float, cold_white) TEMPLATABLE_VALUE(float, warm_white) - TEMPLATABLE_VALUE(std::string, effect) + TEMPLATABLE_VALUE(uint32_t, effect) void play(const Ts &...x) override { auto call = this->parent_->make_call(); diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 89b2fc0fb2..08fd26a937 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -10,12 +10,14 @@ from esphome.const import ( CONF_COLOR_MODE, CONF_COLOR_TEMPERATURE, CONF_EFFECT, + CONF_EFFECTS, CONF_FLASH_LENGTH, CONF_GREEN, CONF_ID, CONF_LIMIT_MODE, CONF_MAX_BRIGHTNESS, CONF_MIN_BRIGHTNESS, + CONF_NAME, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_RED, @@ -24,6 +26,9 @@ from esphome.const import ( CONF_WARM_WHITE, CONF_WHITE, ) +from esphome.core import CORE, Lambda +from esphome.cpp_generator import LambdaExpression +from esphome.types import ConfigType from .types import ( COLOR_MODES, @@ -111,6 +116,26 @@ LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id( ) +def _resolve_effect_index(config: ConfigType) -> int: + """Resolve a static effect name to its 1-based index at codegen time. + + Effect index 0 means "None" (no effect). Effects are 1-indexed matching + the C++ convention in LightState. + """ + original_name = config[CONF_EFFECT] + effect_name = original_name.lower() + if effect_name == "none": + return 0 + light_id = config[CONF_ID] + light_path = CORE.config.get_path_for_id(light_id)[:-1] + light_config = CORE.config.get_config_for_path(light_path) + for i, effect_conf in enumerate(light_config.get(CONF_EFFECTS, [])): + key = next(iter(effect_conf)) + if effect_conf[key][CONF_NAME].lower() == effect_name: + return i + 1 + raise ValueError(f"Effect '{original_name}' not found in light '{light_id}'") + + @automation.register_action( "light.turn_off", LightControlAction, LIGHT_TURN_OFF_ACTION_SCHEMA, synchronous=True ) @@ -165,8 +190,29 @@ async def light_control_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(config[CONF_WARM_WHITE], args, float) cg.add(var.set_warm_white(template_)) if CONF_EFFECT in config: - template_ = await cg.templatable(config[CONF_EFFECT], args, cg.std_string) - cg.add(var.set_effect(template_)) + if isinstance(config[CONF_EFFECT], Lambda): + # Lambda returns a string — wrap in a C++ lambda that resolves + # the effect name to its uint32_t index at runtime + inner_lambda = await cg.process_lambda( + config[CONF_EFFECT], args, return_type=cg.std_string + ) + fwd_args = ", ".join(n for _, n in args) + # capture="" is correct: paren is a global variable name + # string-interpolated into the body at codegen time, not a + # C++ runtime capture. + wrapper = LambdaExpression( + f"auto __effect_s = ({inner_lambda})({fwd_args});\n" + f"return {paren}->get_effect_index(" + f"__effect_s.c_str(), __effect_s.size());", + args, + capture="", + return_type=cg.uint32, + ) + cg.add(var.set_effect(wrapper)) + else: + # Static string — resolve effect name to index at codegen time + effect_index = _resolve_effect_index(config) + cg.add(var.set_effect(effect_index)) return var diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 528f5bd911..b8d72cc832 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -13,6 +13,7 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include "esphome/core/progmem.h" #include #include @@ -218,6 +219,20 @@ class LightState : public EntityBase, public Component { return 0; // Effect not found } + /// Get effect index by name (const char* overload, avoids std::string construction). + uint32_t get_effect_index(const char *name, size_t len) const { + if (len == 4 && ESPHOME_strncasecmp_P(name, ESPHOME_PSTR("none"), 4) == 0) { + return 0; + } + StringRef ref(name, len); + for (size_t i = 0; i < this->effects_.size(); i++) { + if (str_equals_case_insensitive(ref, this->effects_[i]->get_name())) { + return i + 1; + } + } + return 0; + } + /// Get effect by index. Returns nullptr if index is invalid. LightEffect *get_effect_by_index(uint32_t index) const { if (index == 0 || index > this->effects_.size()) { From 585e195044af6a7415bf56108d4073c60a4a3e3b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 06:58:31 -1000 Subject: [PATCH 306/609] [socket] Devirtualize socket abstraction layer (#14398) --- esphome/components/api/api_server.h | 2 +- .../captive_portal/dns_server_esp32_idf.h | 2 +- esphome/components/esphome/ota/ota_esphome.h | 2 +- .../components/socket/bsd_sockets_impl.cpp | 172 +-- esphome/components/socket/bsd_sockets_impl.h | 114 ++ esphome/components/socket/headers.h | 17 + .../components/socket/lwip_raw_tcp_impl.cpp | 1150 ++++++++--------- esphome/components/socket/lwip_raw_tcp_impl.h | 200 +++ .../components/socket/lwip_sockets_impl.cpp | 139 +- esphome/components/socket/lwip_sockets_impl.h | 80 ++ esphome/components/socket/socket.cpp | 32 +- esphome/components/socket/socket.h | 110 +- esphome/core/application.h | 9 +- 13 files changed, 1153 insertions(+), 876 deletions(-) create mode 100644 esphome/components/socket/bsd_sockets_impl.h create mode 100644 esphome/components/socket/lwip_raw_tcp_impl.h create mode 100644 esphome/components/socket/lwip_sockets_impl.h diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 3abf68358c..6eff2005f8 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -257,7 +257,7 @@ class APIServer : public Component, } void socket_failed_(const LogString *msg); // Pointers and pointer-like types first (4 bytes each) - socket::Socket *socket_{nullptr}; + socket::ListenSocket *socket_{nullptr}; #ifdef USE_API_CLIENT_CONNECTED_TRIGGER Trigger client_connected_trigger_; #endif diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.h b/esphome/components/captive_portal/dns_server_esp32_idf.h index f8e4cfec84..b30856c204 100644 --- a/esphome/components/captive_portal/dns_server_esp32_idf.h +++ b/esphome/components/captive_portal/dns_server_esp32_idf.h @@ -22,7 +22,7 @@ class DNSServer { } static constexpr size_t DNS_BUFFER_SIZE = 192; - socket::Socket *socket_{nullptr}; + socket::ListenSocket *socket_{nullptr}; network::IPAddress server_ip_; uint8_t buffer_[DNS_BUFFER_SIZE]; }; diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 53715cfe6a..08edacad92 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -84,7 +84,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { std::unique_ptr auth_buf_; #endif // USE_OTA_PASSWORD - socket::Socket *server_{nullptr}; + socket::ListenSocket *server_{nullptr}; std::unique_ptr client_; std::unique_ptr backend_; diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index c96713f376..92ecfc692b 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -1,135 +1,81 @@ -#include "socket.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" +#include "socket.h" #ifdef USE_SOCKET_IMPL_BSD_SOCKETS #include #include "esphome/core/application.h" -#ifdef USE_ESP32 -#include -#include -#endif - namespace esphome::socket { -class BSDSocketImpl final : public Socket { - public: - BSDSocketImpl(int fd, bool monitor_loop = false) { - this->fd_ = fd; - // Register new socket with the application for select() if monitoring requested - if (monitor_loop && this->fd_ >= 0) { - // Only set loop_monitored_ to true if registration succeeds - this->loop_monitored_ = App.register_socket_fd(this->fd_); - } - } - ~BSDSocketImpl() override { - if (!this->closed_) { - this->close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall) - } - } - int connect(const struct sockaddr *addr, socklen_t addrlen) override { return ::connect(this->fd_, addr, addrlen); } - std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { - int fd = ::accept(this->fd_, addr, addrlen); - if (fd == -1) - return {}; - return make_unique(fd, false); - } - std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override { - int fd = ::accept(this->fd_, addr, addrlen); - if (fd == -1) - return {}; - return make_unique(fd, true); +BSDSocketImpl::BSDSocketImpl(int fd, bool monitor_loop) { + this->fd_ = fd; + // Register new socket with the application for select() if monitoring requested + if (monitor_loop && this->fd_ >= 0) { + // Only set loop_monitored_ to true if registration succeeds + this->loop_monitored_ = App.register_socket_fd(this->fd_); } +} - int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); } - int close() override { - if (!this->closed_) { - // Unregister from select() before closing if monitored - if (this->loop_monitored_) { - App.unregister_socket_fd(this->fd_); - } - int ret = ::close(this->fd_); - this->closed_ = true; - return ret; +BSDSocketImpl::~BSDSocketImpl() { + if (!this->closed_) { + this->close(); + } +} + +int BSDSocketImpl::close() { + if (!this->closed_) { + // Unregister from select() before closing if monitored + if (this->loop_monitored_) { + App.unregister_socket_fd(this->fd_); } + int ret = ::close(this->fd_); + this->closed_ = true; + return ret; + } + return 0; +} + +int BSDSocketImpl::setblocking(bool blocking) { + int fl = ::fcntl(this->fd_, F_GETFL, 0); + if (blocking) { + fl &= ~O_NONBLOCK; + } else { + fl |= O_NONBLOCK; + } + ::fcntl(this->fd_, F_SETFL, fl); + return 0; +} + +bool BSDSocketImpl::ready() const { return socket_ready_fd(this->fd_, this->loop_monitored_); } + +size_t BSDSocketImpl::getpeername_to(std::span buf) { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + if (this->getpeername(reinterpret_cast(&storage), &len) != 0) { + buf[0] = '\0'; return 0; } - int shutdown(int how) override { return ::shutdown(this->fd_, how); } + return format_sockaddr_to(reinterpret_cast(&storage), len, buf); +} - int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { - return ::getpeername(this->fd_, addr, addrlen); - } - int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { - return ::getsockname(this->fd_, addr, addrlen); - } - int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { - return ::getsockopt(this->fd_, level, optname, optval, optlen); - } - int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override { - return ::setsockopt(this->fd_, level, optname, optval, optlen); - } - int listen(int backlog) override { return ::listen(this->fd_, backlog); } - ssize_t read(void *buf, size_t len) override { -#ifdef USE_ESP32 - return ::lwip_read(this->fd_, buf, len); -#else - return ::read(this->fd_, buf, len); -#endif - } - ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) override { -#if defined(USE_ESP32) || defined(USE_HOST) - return ::recvfrom(this->fd_, buf, len, 0, addr, addr_len); -#else - return ::lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len); -#endif - } - ssize_t readv(const struct iovec *iov, int iovcnt) override { -#if defined(USE_ESP32) - return ::lwip_readv(this->fd_, iov, iovcnt); -#else - return ::readv(this->fd_, iov, iovcnt); -#endif - } - ssize_t write(const void *buf, size_t len) override { -#ifdef USE_ESP32 - return ::lwip_write(this->fd_, buf, len); -#else - return ::write(this->fd_, buf, len); -#endif - } - ssize_t send(void *buf, size_t len, int flags) { return ::send(this->fd_, buf, len, flags); } - ssize_t writev(const struct iovec *iov, int iovcnt) override { -#if defined(USE_ESP32) - return ::lwip_writev(this->fd_, iov, iovcnt); -#else - return ::writev(this->fd_, iov, iovcnt); -#endif - } - - ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override { - return ::sendto(this->fd_, buf, len, flags, to, tolen); // NOLINT(readability-suspicious-call-argument) - } - - int setblocking(bool blocking) override { - int fl = ::fcntl(this->fd_, F_GETFL, 0); - if (blocking) { - fl &= ~O_NONBLOCK; - } else { - fl |= O_NONBLOCK; - } - ::fcntl(this->fd_, F_SETFL, fl); +size_t BSDSocketImpl::getsockname_to(std::span buf) { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + if (this->getsockname(reinterpret_cast(&storage), &len) != 0) { + buf[0] = '\0'; return 0; } -}; + return format_sockaddr_to(reinterpret_cast(&storage), len, buf); +} // Helper to create a socket with optional monitoring -static std::unique_ptr create_socket(int domain, int type, int protocol, bool loop_monitored = false) { +static std::unique_ptr create_socket(int domain, int type, int protocol, bool loop_monitored = false) { int ret = ::socket(domain, type, protocol); if (ret == -1) return nullptr; - return std::unique_ptr{new BSDSocketImpl(ret, loop_monitored)}; + return make_unique(ret, loop_monitored); } std::unique_ptr socket(int domain, int type, int protocol) { @@ -140,6 +86,14 @@ std::unique_ptr socket_loop_monitored(int domain, int type, int protocol return create_socket(domain, type, protocol, true); } +std::unique_ptr socket_listen(int domain, int type, int protocol) { + return create_socket(domain, type, protocol, false); +} + +std::unique_ptr socket_listen_loop_monitored(int domain, int type, int protocol) { + return create_socket(domain, type, protocol, true); +} + } // namespace esphome::socket #endif // USE_SOCKET_IMPL_BSD_SOCKETS diff --git a/esphome/components/socket/bsd_sockets_impl.h b/esphome/components/socket/bsd_sockets_impl.h new file mode 100644 index 0000000000..edee39f315 --- /dev/null +++ b/esphome/components/socket/bsd_sockets_impl.h @@ -0,0 +1,114 @@ +#pragma once +#include "esphome/core/defines.h" + +#ifdef USE_SOCKET_IMPL_BSD_SOCKETS + +#include +#include + +#include "esphome/core/helpers.h" +#include "headers.h" + +#ifdef USE_ESP32 +#include +#endif + +namespace esphome::socket { + +class BSDSocketImpl { + public: + BSDSocketImpl(int fd, bool monitor_loop = false); + ~BSDSocketImpl(); + BSDSocketImpl(const BSDSocketImpl &) = delete; + BSDSocketImpl &operator=(const BSDSocketImpl &) = delete; + + int connect(const struct sockaddr *addr, socklen_t addrlen) { return ::connect(this->fd_, addr, addrlen); } + std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) { + int fd = ::accept(this->fd_, addr, addrlen); + if (fd == -1) + return {}; + return make_unique(fd, false); + } + std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) { + int fd = ::accept(this->fd_, addr, addrlen); + if (fd == -1) + return {}; + return make_unique(fd, true); + } + + int bind(const struct sockaddr *addr, socklen_t addrlen) { return ::bind(this->fd_, addr, addrlen); } + int close(); + int shutdown(int how) { return ::shutdown(this->fd_, how); } + + int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return ::getpeername(this->fd_, addr, addrlen); } + int getsockname(struct sockaddr *addr, socklen_t *addrlen) { return ::getsockname(this->fd_, addr, addrlen); } + + /// Format peer address into a fixed-size buffer (no heap allocation) + size_t getpeername_to(std::span buf); + /// Format local address into a fixed-size buffer (no heap allocation) + size_t getsockname_to(std::span buf); + + int getsockopt(int level, int optname, void *optval, socklen_t *optlen) { + return ::getsockopt(this->fd_, level, optname, optval, optlen); + } + int setsockopt(int level, int optname, const void *optval, socklen_t optlen) { + return ::setsockopt(this->fd_, level, optname, optval, optlen); + } + int listen(int backlog) { return ::listen(this->fd_, backlog); } + ssize_t read(void *buf, size_t len) { +#ifdef USE_ESP32 + return ::lwip_read(this->fd_, buf, len); +#else + return ::read(this->fd_, buf, len); +#endif + } + ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) { +#if defined(USE_ESP32) || defined(USE_HOST) + return ::recvfrom(this->fd_, buf, len, 0, addr, addr_len); +#else + return ::lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len); +#endif + } + ssize_t readv(const struct iovec *iov, int iovcnt) { +#if defined(USE_ESP32) + return ::lwip_readv(this->fd_, iov, iovcnt); +#else + return ::readv(this->fd_, iov, iovcnt); +#endif + } + ssize_t write(const void *buf, size_t len) { +#ifdef USE_ESP32 + return ::lwip_write(this->fd_, buf, len); +#else + return ::write(this->fd_, buf, len); +#endif + } + ssize_t send(void *buf, size_t len, int flags) { return ::send(this->fd_, buf, len, flags); } + ssize_t writev(const struct iovec *iov, int iovcnt) { +#if defined(USE_ESP32) + return ::lwip_writev(this->fd_, iov, iovcnt); +#else + return ::writev(this->fd_, iov, iovcnt); +#endif + } + + ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) { + return ::sendto(this->fd_, buf, len, flags, to, tolen); // NOLINT(readability-suspicious-call-argument) + } + + int setblocking(bool blocking); + int loop() { return 0; } + + bool ready() const; + + int get_fd() const { return this->fd_; } + + protected: + int fd_{-1}; + bool closed_{false}; + bool loop_monitored_{false}; +}; + +} // namespace esphome::socket + +#endif // USE_SOCKET_IMPL_BSD_SOCKETS diff --git a/esphome/components/socket/headers.h b/esphome/components/socket/headers.h index 032892072d..16e4d23d3b 100644 --- a/esphome/components/socket/headers.h +++ b/esphome/components/socket/headers.h @@ -183,3 +183,20 @@ using socklen_t = uint32_t; #endif #endif // USE_SOCKET_IMPL_BSD_SOCKETS + +#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) + +namespace esphome::socket { + +// Maximum length for formatted socket address string (IP address without port) +// IPv4: "255.255.255.255" = 15 chars + null = 16 +// IPv6: full address = 45 chars + null = 46 +#if USE_NETWORK_IPV6 +static constexpr size_t SOCKADDR_STR_LEN = 46; // INET6_ADDRSTRLEN +#else +static constexpr size_t SOCKADDR_STR_LEN = 16; // INET_ADDRSTRLEN +#endif + +} // namespace esphome::socket + +#endif diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 6e95f5bc7a..6556979fe0 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -3,13 +3,8 @@ #ifdef USE_SOCKET_IMPL_LWIP_TCP -#include "lwip/ip.h" -#include "lwip/netif.h" -#include "lwip/opt.h" -#include "lwip/tcp.h" #include #include -#include #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -56,628 +51,596 @@ static const char *const TAG = "socket.lwip"; #define LWIP_LOG(msg, ...) #endif -class LWIPRawImpl : public Socket { - public: - LWIPRawImpl(sa_family_t family, struct tcp_pcb *pcb) : pcb_(pcb), family_(family) {} - ~LWIPRawImpl() override { - if (pcb_ != nullptr) { - LWIP_LOG("tcp_abort(%p)", pcb_); - tcp_abort(pcb_); - pcb_ = nullptr; - } - } +// ---- LWIPRawCommon methods ---- - void init() { - LWIP_LOG("init(%p)", pcb_); - tcp_arg(pcb_, this); - tcp_recv(pcb_, LWIPRawImpl::s_recv_fn); - tcp_err(pcb_, LWIPRawImpl::s_err_fn); +LWIPRawCommon::~LWIPRawCommon() { + if (this->pcb_ != nullptr) { + LWIP_LOG("tcp_abort(%p)", this->pcb_); + tcp_abort(this->pcb_); + this->pcb_ = nullptr; } +} - std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { - // Non-listening sockets return error +int LWIPRawCommon::bind(const struct sockaddr *name, socklen_t addrlen) { + if (this->pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (name == nullptr) { errno = EINVAL; - return nullptr; + return 0; } - int bind(const struct sockaddr *name, socklen_t addrlen) final { - if (pcb_ == nullptr) { - errno = EBADF; - return -1; - } - if (name == nullptr) { - errno = EINVAL; - return 0; - } - ip_addr_t ip; - in_port_t port; + ip_addr_t ip; + in_port_t port; #if LWIP_IPV6 - if (family_ == AF_INET) { - if (addrlen < sizeof(sockaddr_in)) { - errno = EINVAL; - return -1; - } - auto *addr4 = reinterpret_cast(name); - port = ntohs(addr4->sin_port); - ip.type = IPADDR_TYPE_V4; - ip.u_addr.ip4.addr = addr4->sin_addr.s_addr; - LWIP_LOG("tcp_bind(%p ip=%s port=%u)", pcb_, ip4addr_ntoa(&ip.u_addr.ip4), port); - } else if (family_ == AF_INET6) { - if (addrlen < sizeof(sockaddr_in6)) { - errno = EINVAL; - return -1; - } - auto *addr6 = reinterpret_cast(name); - port = ntohs(addr6->sin6_port); - ip.type = IPADDR_TYPE_ANY; - memcpy(&ip.u_addr.ip6.addr, &addr6->sin6_addr.un.u8_addr, 16); - LWIP_LOG("tcp_bind(%p ip=%s port=%u)", pcb_, ip6addr_ntoa(&ip.u_addr.ip6), port); - } else { - errno = EINVAL; - return -1; - } -#else - if (family_ != AF_INET) { + if (this->family_ == AF_INET) { + if (addrlen < sizeof(sockaddr_in)) { errno = EINVAL; return -1; } auto *addr4 = reinterpret_cast(name); port = ntohs(addr4->sin_port); - ip.addr = addr4->sin_addr.s_addr; - LWIP_LOG("tcp_bind(%p ip=%u port=%u)", pcb_, ip.addr, port); + ip.type = IPADDR_TYPE_V4; + ip.u_addr.ip4.addr = addr4->sin_addr.s_addr; + LWIP_LOG("tcp_bind(%p ip=%s port=%u)", this->pcb_, ip4addr_ntoa(&ip.u_addr.ip4), port); + } else if (this->family_ == AF_INET6) { + if (addrlen < sizeof(sockaddr_in6)) { + errno = EINVAL; + return -1; + } + auto *addr6 = reinterpret_cast(name); + port = ntohs(addr6->sin6_port); + ip.type = IPADDR_TYPE_ANY; + memcpy(&ip.u_addr.ip6.addr, &addr6->sin6_addr.un.u8_addr, 16); + LWIP_LOG("tcp_bind(%p ip=%s port=%u)", this->pcb_, ip6addr_ntoa(&ip.u_addr.ip6), port); + } else { + errno = EINVAL; + return -1; + } +#else + if (this->family_ != AF_INET) { + errno = EINVAL; + return -1; + } + auto *addr4 = reinterpret_cast(name); + port = ntohs(addr4->sin_port); + ip.addr = addr4->sin_addr.s_addr; + LWIP_LOG("tcp_bind(%p ip=%u port=%u)", this->pcb_, ip.addr, port); #endif - err_t err = tcp_bind(pcb_, &ip, port); - if (err == ERR_USE) { - LWIP_LOG(" -> err ERR_USE"); - errno = EADDRINUSE; - return -1; - } - if (err == ERR_VAL) { - LWIP_LOG(" -> err ERR_VAL"); + err_t err = tcp_bind(this->pcb_, &ip, port); + if (err == ERR_USE) { + LWIP_LOG(" -> err ERR_USE"); + errno = EADDRINUSE; + return -1; + } + if (err == ERR_VAL) { + LWIP_LOG(" -> err ERR_VAL"); + errno = EINVAL; + return -1; + } + if (err != ERR_OK) { + LWIP_LOG(" -> err %d", err); + errno = EIO; + return -1; + } + return 0; +} + +int LWIPRawCommon::close() { + if (this->pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + LWIP_LOG("tcp_close(%p)", this->pcb_); + err_t err = tcp_close(this->pcb_); + if (err != ERR_OK) { + LWIP_LOG(" -> err %d", err); + tcp_abort(this->pcb_); + this->pcb_ = nullptr; + errno = err == ERR_MEM ? ENOMEM : EIO; + return -1; + } + this->pcb_ = nullptr; + return 0; +} + +int LWIPRawCommon::shutdown(int how) { + if (this->pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + bool shut_rx = false, shut_tx = false; + if (how == SHUT_RD) { + shut_rx = true; + } else if (how == SHUT_WR) { + shut_tx = true; + } else if (how == SHUT_RDWR) { + shut_rx = shut_tx = true; + } else { + errno = EINVAL; + return -1; + } + LWIP_LOG("tcp_shutdown(%p shut_rx=%d shut_tx=%d)", this->pcb_, shut_rx ? 1 : 0, shut_tx ? 1 : 0); + err_t err = tcp_shutdown(this->pcb_, shut_rx, shut_tx); + if (err != ERR_OK) { + LWIP_LOG(" -> err %d", err); + errno = err == ERR_MEM ? ENOMEM : EIO; + return -1; + } + return 0; +} + +int LWIPRawCommon::getpeername(struct sockaddr *name, socklen_t *addrlen) { + if (this->pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (name == nullptr || addrlen == nullptr) { + errno = EINVAL; + return -1; + } + return this->ip2sockaddr_(&this->pcb_->remote_ip, this->pcb_->remote_port, name, addrlen); +} + +int LWIPRawCommon::getsockname(struct sockaddr *name, socklen_t *addrlen) { + if (this->pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (name == nullptr || addrlen == nullptr) { + errno = EINVAL; + return -1; + } + return this->ip2sockaddr_(&this->pcb_->local_ip, this->pcb_->local_port, name, addrlen); +} + +size_t LWIPRawCommon::getpeername_to(std::span buf) { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + if (this->getpeername(reinterpret_cast(&storage), &len) != 0) { + buf[0] = '\0'; + return 0; + } + return format_sockaddr_to(reinterpret_cast(&storage), len, buf); +} + +size_t LWIPRawCommon::getsockname_to(std::span buf) { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + if (this->getsockname(reinterpret_cast(&storage), &len) != 0) { + buf[0] = '\0'; + return 0; + } + return format_sockaddr_to(reinterpret_cast(&storage), len, buf); +} + +int LWIPRawCommon::getsockopt(int level, int optname, void *optval, socklen_t *optlen) { + if (this->pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (optlen == nullptr || optval == nullptr) { + errno = EINVAL; + return -1; + } + if (level == SOL_SOCKET && optname == SO_REUSEADDR) { + if (*optlen < 4) { errno = EINVAL; return -1; } - if (err != ERR_OK) { - LWIP_LOG(" -> err %d", err); - errno = EIO; - return -1; - } + // lwip doesn't seem to have this feature. Don't send an error + // to prevent warnings + *reinterpret_cast(optval) = 1; + *optlen = 4; return 0; } - int close() final { - if (pcb_ == nullptr) { - errno = ECONNRESET; + if (level == IPPROTO_TCP && optname == TCP_NODELAY) { + if (*optlen < 4) { + errno = EINVAL; return -1; } - LWIP_LOG("tcp_close(%p)", pcb_); - err_t err = tcp_close(pcb_); - if (err != ERR_OK) { - LWIP_LOG(" -> err %d", err); - tcp_abort(pcb_); - pcb_ = nullptr; - errno = err == ERR_MEM ? ENOMEM : EIO; - return -1; - } - pcb_ = nullptr; + *reinterpret_cast(optval) = this->nodelay_; + *optlen = 4; return 0; } - int shutdown(int how) final { - if (pcb_ == nullptr) { - errno = ECONNRESET; + + errno = EINVAL; + return -1; +} + +int LWIPRawCommon::setsockopt(int level, int optname, const void *optval, socklen_t optlen) { + if (this->pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (level == SOL_SOCKET && optname == SO_REUSEADDR) { + if (optlen != 4) { + errno = EINVAL; return -1; } - bool shut_rx = false, shut_tx = false; - if (how == SHUT_RD) { - shut_rx = true; - } else if (how == SHUT_WR) { - shut_tx = true; - } else if (how == SHUT_RDWR) { - shut_rx = shut_tx = true; + // lwip doesn't seem to have this feature. Don't send an error + // to prevent warnings + return 0; + } + if (level == IPPROTO_TCP && optname == TCP_NODELAY) { + if (optlen != 4) { + errno = EINVAL; + return -1; + } + int val = *reinterpret_cast(optval); + this->nodelay_ = val; + return 0; + } + + errno = EINVAL; + return -1; +} + +int LWIPRawCommon::ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen) { + if (this->family_ == AF_INET) { + if (*addrlen < sizeof(struct sockaddr_in)) { + errno = EINVAL; + return -1; + } + + struct sockaddr_in *addr = reinterpret_cast(name); + addr->sin_family = AF_INET; + *addrlen = addr->sin_len = sizeof(struct sockaddr_in); + addr->sin_port = port; + inet_addr_from_ip4addr(&addr->sin_addr, ip_2_ip4(ip)); + return 0; + } +#if LWIP_IPV6 + else if (this->family_ == AF_INET6) { + if (*addrlen < sizeof(struct sockaddr_in6)) { + errno = EINVAL; + return -1; + } + + struct sockaddr_in6 *addr = reinterpret_cast(name); + addr->sin6_family = AF_INET6; + *addrlen = addr->sin6_len = sizeof(struct sockaddr_in6); + addr->sin6_port = port; + + // AF_INET6 sockets are bound to IPv4 as well, so we may encounter IPv4 addresses that must be converted to IPv6. + if (IP_IS_V4(ip)) { + ip_addr_t mapped; + ip4_2_ipv4_mapped_ipv6(ip_2_ip6(&mapped), ip_2_ip4(ip)); + inet6_addr_from_ip6addr(&addr->sin6_addr, ip_2_ip6(&mapped)); } else { - errno = EINVAL; - return -1; - } - LWIP_LOG("tcp_shutdown(%p shut_rx=%d shut_tx=%d)", pcb_, shut_rx ? 1 : 0, shut_tx ? 1 : 0); - err_t err = tcp_shutdown(pcb_, shut_rx, shut_tx); - if (err != ERR_OK) { - LWIP_LOG(" -> err %d", err); - errno = err == ERR_MEM ? ENOMEM : EIO; - return -1; + inet6_addr_from_ip6addr(&addr->sin6_addr, ip_2_ip6(ip)); } return 0; } +#endif + return -1; +} - int getpeername(struct sockaddr *name, socklen_t *addrlen) final { - if (pcb_ == nullptr) { - errno = ECONNRESET; - return -1; - } - if (name == nullptr || addrlen == nullptr) { - errno = EINVAL; - return -1; - } - return this->ip2sockaddr_(&pcb_->remote_ip, pcb_->remote_port, name, addrlen); +// ---- LWIPRawImpl methods ---- + +LWIPRawImpl::~LWIPRawImpl() { + // Base class destructor handles pcb_ cleanup via tcp_abort +} + +void LWIPRawImpl::init() { + LWIP_LOG("init(%p)", this->pcb_); + tcp_arg(this->pcb_, this); + tcp_recv(this->pcb_, LWIPRawImpl::s_recv_fn); + tcp_err(this->pcb_, LWIPRawImpl::s_err_fn); +} + +void LWIPRawImpl::s_err_fn(void *arg, err_t err) { + // "If a connection is aborted because of an error, the application is alerted of this event by + // the err callback." + // pcb is already freed when this callback is called + // ERR_RST: connection was reset by remote host + // ERR_ABRT: aborted through tcp_abort or TCP timer + auto *arg_this = reinterpret_cast(arg); + ESP_LOGVV(TAG, "socket %p: err(err=%d)", arg_this, err); + arg_this->pcb_ = nullptr; +} + +err_t LWIPRawImpl::s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err) { + auto *arg_this = reinterpret_cast(arg); + return arg_this->recv_fn(pb, err); +} + +err_t LWIPRawImpl::recv_fn(struct pbuf *pb, err_t err) { + LWIP_LOG("recv(pb=%p err=%d)", pb, err); + if (err != 0) { + // "An error code if there has been an error receiving Only return ERR_ABRT if you have + // called tcp_abort from within the callback function!" + this->rx_closed_ = true; + return ERR_OK; } - int getsockname(struct sockaddr *name, socklen_t *addrlen) final { - if (pcb_ == nullptr) { - errno = ECONNRESET; - return -1; - } - if (name == nullptr || addrlen == nullptr) { - errno = EINVAL; - return -1; - } - return this->ip2sockaddr_(&pcb_->local_ip, pcb_->local_port, name, addrlen); + if (pb == nullptr) { + this->rx_closed_ = true; + return ERR_OK; } - int getsockopt(int level, int optname, void *optval, socklen_t *optlen) final { - if (pcb_ == nullptr) { - errno = ECONNRESET; - return -1; - } - if (optlen == nullptr || optval == nullptr) { - errno = EINVAL; - return -1; - } - if (level == SOL_SOCKET && optname == SO_REUSEADDR) { - if (*optlen < 4) { - errno = EINVAL; - return -1; - } + if (this->rx_buf_ == nullptr) { + // no need to copy because lwIP gave control of it to us + this->rx_buf_ = pb; + this->rx_buf_offset_ = 0; + } else { + pbuf_cat(this->rx_buf_, pb); + } +#ifdef USE_ESP8266 + // Wake the main loop immediately so it can process the received data. + socket_wake(); +#endif + return ERR_OK; +} - // lwip doesn't seem to have this feature. Don't send an error - // to prevent warnings - *reinterpret_cast(optval) = 1; - *optlen = 4; - return 0; - } - if (level == IPPROTO_TCP && optname == TCP_NODELAY) { - if (*optlen < 4) { - errno = EINVAL; - return -1; - } - *reinterpret_cast(optval) = nodelay_; - *optlen = 4; - return 0; - } - - errno = EINVAL; +ssize_t LWIPRawImpl::read(void *buf, size_t len) { + if (this->pcb_ == nullptr) { + errno = ECONNRESET; return -1; } - int setsockopt(int level, int optname, const void *optval, socklen_t optlen) final { - if (pcb_ == nullptr) { - errno = ECONNRESET; - return -1; - } - if (level == SOL_SOCKET && optname == SO_REUSEADDR) { - if (optlen != 4) { - errno = EINVAL; - return -1; - } - - // lwip doesn't seem to have this feature. Don't send an error - // to prevent warnings - return 0; - } - if (level == IPPROTO_TCP && optname == TCP_NODELAY) { - if (optlen != 4) { - errno = EINVAL; - return -1; - } - int val = *reinterpret_cast(optval); - nodelay_ = val; - return 0; - } - - errno = EINVAL; + if (this->rx_closed_ && this->rx_buf_ == nullptr) { + return 0; + } + if (len == 0) { + return 0; + } + if (this->rx_buf_ == nullptr) { + errno = EWOULDBLOCK; return -1; } - int listen(int backlog) override { - // Regular sockets can't be converted to listening - this shouldn't happen - // as listen() should only be called on sockets created for listening + + size_t read = 0; + uint8_t *buf8 = reinterpret_cast(buf); + while (len && this->rx_buf_ != nullptr) { + size_t pb_len = this->rx_buf_->len; + size_t pb_left = pb_len - this->rx_buf_offset_; + if (pb_left == 0) + break; + size_t copysize = std::min(len, pb_left); + memcpy(buf8, reinterpret_cast(this->rx_buf_->payload) + this->rx_buf_offset_, copysize); + + if (pb_left == copysize) { + // full pb copied, free it + if (this->rx_buf_->next == nullptr) { + // last buffer in chain + pbuf_free(this->rx_buf_); + this->rx_buf_ = nullptr; + this->rx_buf_offset_ = 0; + } else { + auto *old_buf = this->rx_buf_; + this->rx_buf_ = this->rx_buf_->next; + pbuf_ref(this->rx_buf_); + pbuf_free(old_buf); + this->rx_buf_offset_ = 0; + } + } else { + this->rx_buf_offset_ += copysize; + } + LWIP_LOG("tcp_recved(%p %u)", this->pcb_, copysize); + tcp_recved(this->pcb_, copysize); + + buf8 += copysize; + len -= copysize; + read += copysize; + } + + if (read == 0) { + errno = EWOULDBLOCK; + return -1; + } + + return read; +} + +ssize_t LWIPRawImpl::readv(const struct iovec *iov, int iovcnt) { + ssize_t ret = 0; + for (int i = 0; i < iovcnt; i++) { + ssize_t err = this->read(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); + if (err == -1) { + if (ret != 0) { + // if we already read some don't return an error + break; + } + return err; + } + ret += err; + if ((size_t) err != iov[i].iov_len) + break; + } + return ret; +} + +ssize_t LWIPRawImpl::internal_write_(const void *buf, size_t len) { + if (this->pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (len == 0) + return 0; + if (buf == nullptr) { + errno = EINVAL; + return 0; + } + auto space = tcp_sndbuf(this->pcb_); + if (space == 0) { + errno = EWOULDBLOCK; + return -1; + } + size_t to_send = std::min((size_t) space, len); + LWIP_LOG("tcp_write(%p buf=%p %u)", this->pcb_, buf, to_send); + err_t err = tcp_write(this->pcb_, buf, to_send, TCP_WRITE_FLAG_COPY); + if (err == ERR_MEM) { + LWIP_LOG(" -> err ERR_MEM"); + errno = EWOULDBLOCK; + return -1; + } + if (err != ERR_OK) { + LWIP_LOG(" -> err %d", err); + errno = ECONNRESET; + return -1; + } + return to_send; +} + +int LWIPRawImpl::internal_output_() { + LWIP_LOG("tcp_output(%p)", this->pcb_); + err_t err = tcp_output(this->pcb_); + if (err == ERR_ABRT) { + // sometimes lwip returns ERR_ABRT for no apparent reason + // the connection works fine afterwards, and back with ESPAsyncTCP we + // indirectly also ignored this error + // FIXME: figure out where this is returned and what it means in this context + LWIP_LOG(" -> err ERR_ABRT"); + return 0; + } + if (err != ERR_OK) { + LWIP_LOG(" -> err %d", err); + errno = ECONNRESET; + return -1; + } + return 0; +} + +ssize_t LWIPRawImpl::write(const void *buf, size_t len) { + ssize_t written = this->internal_write_(buf, len); + if (written == -1) + return -1; + if (written == 0) { + // no need to output if nothing written + return 0; + } + if (this->nodelay_) { + int err = this->internal_output_(); + if (err == -1) + return -1; + } + return written; +} + +ssize_t LWIPRawImpl::writev(const struct iovec *iov, int iovcnt) { + ssize_t written = 0; + for (int i = 0; i < iovcnt; i++) { + ssize_t err = this->internal_write_(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); + if (err == -1) { + if (written != 0) { + // if we already read some don't return an error + break; + } + return err; + } + written += err; + if ((size_t) err != iov[i].iov_len) + break; + } + if (written == 0) { + // no need to output if nothing written + return 0; + } + if (this->nodelay_) { + int err = this->internal_output_(); + if (err == -1) + return -1; + } + return written; +} + +// ---- LWIPRawListenImpl methods ---- + +LWIPRawListenImpl::~LWIPRawListenImpl() { + // Base class destructor handles pcb_ cleanup via tcp_abort +} + +void LWIPRawListenImpl::init() { + LWIP_LOG("init(%p)", this->pcb_); + tcp_arg(this->pcb_, this); + tcp_accept(this->pcb_, LWIPRawListenImpl::s_accept_fn); + tcp_err(this->pcb_, LWIPRawListenImpl::s_err_fn); +} + +void LWIPRawListenImpl::s_err_fn(void *arg, err_t err) { + auto *arg_this = reinterpret_cast(arg); + ESP_LOGVV(TAG, "socket %p: err(err=%d)", arg_this, err); + arg_this->pcb_ = nullptr; +} + +err_t LWIPRawListenImpl::s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) { + auto *arg_this = reinterpret_cast(arg); + return arg_this->accept_fn_(newpcb, err); +} + +std::unique_ptr LWIPRawListenImpl::accept(struct sockaddr *addr, socklen_t *addrlen) { + if (this->pcb_ == nullptr) { + errno = EBADF; + return nullptr; + } + if (this->accepted_socket_count_ == 0) { + errno = EWOULDBLOCK; + return nullptr; + } + // Take from front for FIFO ordering + std::unique_ptr sock = std::move(this->accepted_sockets_[0]); + // Shift remaining sockets forward + for (uint8_t i = 1; i < this->accepted_socket_count_; i++) { + this->accepted_sockets_[i - 1] = std::move(this->accepted_sockets_[i]); + } + this->accepted_socket_count_--; + LWIP_LOG("Connection accepted by application, queue size: %d", this->accepted_socket_count_); + if (addr != nullptr) { + sock->getpeername(addr, addrlen); + } + LWIP_LOG("accept(%p)", sock.get()); + return sock; +} + +int LWIPRawListenImpl::listen(int backlog) { + if (this->pcb_ == nullptr) { + errno = EBADF; + return -1; + } + LWIP_LOG("tcp_listen_with_backlog(%p backlog=%d)", this->pcb_, backlog); + struct tcp_pcb *listen_pcb = tcp_listen_with_backlog(this->pcb_, backlog); + if (listen_pcb == nullptr) { + tcp_abort(this->pcb_); + this->pcb_ = nullptr; errno = EOPNOTSUPP; return -1; } - ssize_t read(void *buf, size_t len) final { - if (pcb_ == nullptr) { - errno = ECONNRESET; - return -1; - } - if (rx_closed_ && rx_buf_ == nullptr) { - return 0; - } - if (len == 0) { - return 0; - } - if (rx_buf_ == nullptr) { - errno = EWOULDBLOCK; - return -1; - } + // tcp_listen reallocates the pcb, replace ours + this->pcb_ = listen_pcb; + // set callbacks on new pcb + LWIP_LOG("tcp_arg(%p)", this->pcb_); + tcp_arg(this->pcb_, this); + tcp_accept(this->pcb_, LWIPRawListenImpl::s_accept_fn); + return 0; +} - size_t read = 0; - uint8_t *buf8 = reinterpret_cast(buf); - while (len && rx_buf_ != nullptr) { - size_t pb_len = rx_buf_->len; - size_t pb_left = pb_len - rx_buf_offset_; - if (pb_left == 0) - break; - size_t copysize = std::min(len, pb_left); - memcpy(buf8, reinterpret_cast(rx_buf_->payload) + rx_buf_offset_, copysize); - - if (pb_left == copysize) { - // full pb copied, free it - if (rx_buf_->next == nullptr) { - // last buffer in chain - pbuf_free(rx_buf_); - rx_buf_ = nullptr; - rx_buf_offset_ = 0; - } else { - auto *old_buf = rx_buf_; - rx_buf_ = rx_buf_->next; - pbuf_ref(rx_buf_); - pbuf_free(old_buf); - rx_buf_offset_ = 0; - } - } else { - rx_buf_offset_ += copysize; - } - LWIP_LOG("tcp_recved(%p %u)", pcb_, copysize); - tcp_recved(pcb_, copysize); - - buf8 += copysize; - len -= copysize; - read += copysize; - } - - if (read == 0) { - errno = EWOULDBLOCK; - return -1; - } - - return read; - } - ssize_t readv(const struct iovec *iov, int iovcnt) final { - ssize_t ret = 0; - for (int i = 0; i < iovcnt; i++) { - ssize_t err = read(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); - if (err == -1) { - if (ret != 0) { - // if we already read some don't return an error - break; - } - return err; - } - ret += err; - if ((size_t) err != iov[i].iov_len) - break; - } - return ret; - } - - ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) final { - errno = ENOTSUP; - return -1; - } - - ssize_t internal_write(const void *buf, size_t len) { - if (pcb_ == nullptr) { - errno = ECONNRESET; - return -1; - } - if (len == 0) - return 0; - if (buf == nullptr) { - errno = EINVAL; - return 0; - } - auto space = tcp_sndbuf(pcb_); - if (space == 0) { - errno = EWOULDBLOCK; - return -1; - } - size_t to_send = std::min((size_t) space, len); - LWIP_LOG("tcp_write(%p buf=%p %u)", pcb_, buf, to_send); - err_t err = tcp_write(pcb_, buf, to_send, TCP_WRITE_FLAG_COPY); - if (err == ERR_MEM) { - LWIP_LOG(" -> err ERR_MEM"); - errno = EWOULDBLOCK; - return -1; - } - if (err != ERR_OK) { - LWIP_LOG(" -> err %d", err); - errno = ECONNRESET; - return -1; - } - return to_send; - } - int internal_output() { - LWIP_LOG("tcp_output(%p)", pcb_); - err_t err = tcp_output(pcb_); - if (err == ERR_ABRT) { - LWIP_LOG(" -> err ERR_ABRT"); - // sometimes lwip returns ERR_ABRT for no apparent reason - // the connection works fine afterwards, and back with ESPAsyncTCP we - // indirectly also ignored this error - // FIXME: figure out where this is returned and what it means in this context - return 0; - } - if (err != ERR_OK) { - LWIP_LOG(" -> err %d", err); - errno = ECONNRESET; - return -1; - } - return 0; - } - ssize_t write(const void *buf, size_t len) final { - ssize_t written = internal_write(buf, len); - if (written == -1) - return -1; - if (written == 0) { - // no need to output if nothing written - return 0; - } - if (nodelay_) { - int err = internal_output(); - if (err == -1) - return -1; - } - return written; - } - ssize_t writev(const struct iovec *iov, int iovcnt) final { - ssize_t written = 0; - for (int i = 0; i < iovcnt; i++) { - ssize_t err = internal_write(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); - if (err == -1) { - if (written != 0) { - // if we already read some don't return an error - break; - } - return err; - } - written += err; - if ((size_t) err != iov[i].iov_len) - break; - } - if (written == 0) { - // no need to output if nothing written - return 0; - } - if (nodelay_) { - int err = internal_output(); - if (err == -1) - return -1; - } - return written; - } - ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) final { - // return ::sendto(fd_, buf, len, flags, to, tolen); - errno = ENOSYS; - return -1; - } - bool ready() const override { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; } - - int setblocking(bool blocking) final { - if (pcb_ == nullptr) { - errno = ECONNRESET; - return -1; - } - if (blocking) { - // blocking operation not supported - errno = EINVAL; - return -1; - } - return 0; - } - - void err_fn(err_t err) { - LWIP_LOG("err(err=%d)", err); - // "If a connection is aborted because of an error, the application is alerted of this event by - // the err callback." - // pcb is already freed when this callback is called - // ERR_RST: connection was reset by remote host - // ERR_ABRT: aborted through tcp_abort or TCP timer - pcb_ = nullptr; - } - err_t recv_fn(struct pbuf *pb, err_t err) { - LWIP_LOG("recv(pb=%p err=%d)", pb, err); - if (err != 0) { - // "An error code if there has been an error receiving Only return ERR_ABRT if you have - // called tcp_abort from within the callback function!" - rx_closed_ = true; - return ERR_OK; - } - if (pb == nullptr) { - rx_closed_ = true; - return ERR_OK; - } - if (rx_buf_ == nullptr) { - // no need to copy because lwIP gave control of it to us - rx_buf_ = pb; - rx_buf_offset_ = 0; - } else { - pbuf_cat(rx_buf_, pb); - } -#ifdef USE_ESP8266 - // Wake the main loop immediately so it can process the received data. - socket_wake(); -#endif +err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) { + LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err); + if (err != ERR_OK || newpcb == nullptr) { + // "An error code if there has been an error accepting. Only return ERR_ABRT if you have + // called tcp_abort from within the callback function!" + // https://www.nongnu.org/lwip/2_1_x/tcp_8h.html#a00517abce6856d6c82f0efebdafb734d + // nothing to do here, we just don't push it to the queue return ERR_OK; } - - static void s_err_fn(void *arg, err_t err) { - LWIPRawImpl *arg_this = reinterpret_cast(arg); - arg_this->err_fn(err); + // Check if we've reached the maximum accept queue size + if (this->accepted_socket_count_ >= MAX_ACCEPTED_SOCKETS) { + LWIP_LOG("Rejecting connection, queue full (%d)", this->accepted_socket_count_); + // Abort the connection when queue is full + tcp_abort(newpcb); + // Must return ERR_ABRT since we called tcp_abort() + return ERR_ABRT; } - - static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err) { - LWIPRawImpl *arg_this = reinterpret_cast(arg); - return arg_this->recv_fn(pb, err); - } - - protected: - int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen) { - if (family_ == AF_INET) { - if (*addrlen < sizeof(struct sockaddr_in)) { - errno = EINVAL; - return -1; - } - - struct sockaddr_in *addr = reinterpret_cast(name); - addr->sin_family = AF_INET; - *addrlen = addr->sin_len = sizeof(struct sockaddr_in); - addr->sin_port = port; - inet_addr_from_ip4addr(&addr->sin_addr, ip_2_ip4(ip)); - return 0; - } -#if LWIP_IPV6 - else if (family_ == AF_INET6) { - if (*addrlen < sizeof(struct sockaddr_in6)) { - errno = EINVAL; - return -1; - } - - struct sockaddr_in6 *addr = reinterpret_cast(name); - addr->sin6_family = AF_INET6; - *addrlen = addr->sin6_len = sizeof(struct sockaddr_in6); - addr->sin6_port = port; - - // AF_INET6 sockets are bound to IPv4 as well, so we may encounter IPv4 addresses that must be converted to IPv6. - if (IP_IS_V4(ip)) { - ip_addr_t mapped; - ip4_2_ipv4_mapped_ipv6(ip_2_ip6(&mapped), ip_2_ip4(ip)); - inet6_addr_from_ip6addr(&addr->sin6_addr, ip_2_ip6(&mapped)); - } else { - inet6_addr_from_ip6addr(&addr->sin6_addr, ip_2_ip6(ip)); - } - return 0; - } -#endif - return -1; - } - - // Member ordering optimized to minimize padding on 32-bit systems - // Largest members first (4 bytes), then smaller members (1 byte each) - struct tcp_pcb *pcb_; - pbuf *rx_buf_ = nullptr; - size_t rx_buf_offset_ = 0; - bool rx_closed_ = false; - // don't use lwip nodelay flag, it sometimes causes reconnect - // instead use it for determining whether to call lwip_output - bool nodelay_ = false; - sa_family_t family_ = 0; -}; - -// Listening socket class - only allocates accept queue when needed (for bind+listen sockets) -// This saves 16 bytes (12 bytes array + 1 byte count + 3 bytes padding) for regular connected sockets on ESP8266/RP2040 -class LWIPRawListenImpl final : public LWIPRawImpl { - public: - LWIPRawListenImpl(sa_family_t family, struct tcp_pcb *pcb) : LWIPRawImpl(family, pcb) {} - - void init() { - LWIP_LOG("init(%p)", pcb_); - tcp_arg(pcb_, this); - tcp_accept(pcb_, LWIPRawListenImpl::s_accept_fn); - tcp_err(pcb_, LWIPRawImpl::s_err_fn); // Use base class error handler - } - - bool ready() const override { return this->accepted_socket_count_ > 0; } - - std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { - if (pcb_ == nullptr) { - errno = EBADF; - return nullptr; - } - if (accepted_socket_count_ == 0) { - errno = EWOULDBLOCK; - return nullptr; - } - // Take from front for FIFO ordering - std::unique_ptr sock = std::move(accepted_sockets_[0]); - // Shift remaining sockets forward - for (uint8_t i = 1; i < accepted_socket_count_; i++) { - accepted_sockets_[i - 1] = std::move(accepted_sockets_[i]); - } - accepted_socket_count_--; - LWIP_LOG("Connection accepted by application, queue size: %d", accepted_socket_count_); - if (addr != nullptr) { - sock->getpeername(addr, addrlen); - } - LWIP_LOG("accept(%p)", sock.get()); - return std::unique_ptr(std::move(sock)); - } - - int listen(int backlog) override { - if (pcb_ == nullptr) { - errno = EBADF; - return -1; - } - LWIP_LOG("tcp_listen_with_backlog(%p backlog=%d)", pcb_, backlog); - struct tcp_pcb *listen_pcb = tcp_listen_with_backlog(pcb_, backlog); - if (listen_pcb == nullptr) { - tcp_abort(pcb_); - pcb_ = nullptr; - errno = EOPNOTSUPP; - return -1; - } - // tcp_listen reallocates the pcb, replace ours - pcb_ = listen_pcb; - // set callbacks on new pcb - LWIP_LOG("tcp_arg(%p)", pcb_); - tcp_arg(pcb_, this); - tcp_accept(pcb_, LWIPRawListenImpl::s_accept_fn); - return 0; - } - - private: - err_t accept_fn_(struct tcp_pcb *newpcb, err_t err) { - LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err); - if (err != ERR_OK || newpcb == nullptr) { - // "An error code if there has been an error accepting. Only return ERR_ABRT if you have - // called tcp_abort from within the callback function!" - // https://www.nongnu.org/lwip/2_1_x/tcp_8h.html#a00517abce6856d6c82f0efebdafb734d - // nothing to do here, we just don't push it to the queue - return ERR_OK; - } - // Check if we've reached the maximum accept queue size - if (accepted_socket_count_ >= MAX_ACCEPTED_SOCKETS) { - LWIP_LOG("Rejecting connection, queue full (%d)", accepted_socket_count_); - // Abort the connection when queue is full - tcp_abort(newpcb); - // Must return ERR_ABRT since we called tcp_abort() - return ERR_ABRT; - } - auto sock = make_unique(family_, newpcb); - sock->init(); - accepted_sockets_[accepted_socket_count_++] = std::move(sock); - LWIP_LOG("Accepted connection, queue size: %d", accepted_socket_count_); + auto sock = make_unique(this->family_, newpcb); + sock->init(); + this->accepted_sockets_[this->accepted_socket_count_++] = std::move(sock); + LWIP_LOG("Accepted connection, queue size: %d", this->accepted_socket_count_); #ifdef USE_ESP8266 - // Wake the main loop immediately so it can accept the new connection. - socket_wake(); + // Wake the main loop immediately so it can accept the new connection. + socket_wake(); #endif - return ERR_OK; - } + return ERR_OK; +} - static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) { - LWIPRawListenImpl *arg_this = reinterpret_cast(arg); - return arg_this->accept_fn_(newpcb, err); - } - - // Accept queue - holds incoming connections briefly until the event loop calls accept() - // This is NOT a connection pool - just a temporary queue between LWIP callbacks and the main loop - // 3 slots is plenty since connections are pulled out quickly by the event loop - // - // Memory analysis: std::array<3> vs original std::queue implementation: - // - std::queue uses std::deque internally which on 32-bit systems needs: - // 24 bytes (deque object) + 32+ bytes (map array) + heap allocations - // Total: ~56+ bytes minimum, plus heap fragmentation - // - std::array<3>: 12 bytes fixed (3 pointers × 4 bytes) - // Saves ~44+ bytes RAM per listening socket + avoids ALL heap allocations - // Used on ESP8266 and RP2040 (platforms using LWIP_TCP implementation) - // - // By using a separate listening socket class, regular connected sockets save - // 16 bytes (12 bytes array + 1 byte count + 3 bytes padding) of memory overhead on 32-bit systems - static constexpr size_t MAX_ACCEPTED_SOCKETS = 3; - std::array, MAX_ACCEPTED_SOCKETS> accepted_sockets_; - uint8_t accepted_socket_count_ = 0; // Number of sockets currently in queue -}; +// ---- Factory functions ---- std::unique_ptr socket(int domain, int type, int protocol) { if (type != SOCK_STREAM) { @@ -688,9 +651,7 @@ std::unique_ptr socket(int domain, int type, int protocol) { auto *pcb = tcp_new(); if (pcb == nullptr) return nullptr; - // Create listening socket implementation since user sockets typically bind+listen - // Accepted connections are created directly as LWIPRawImpl in the accept callback - auto *sock = new LWIPRawListenImpl((sa_family_t) domain, pcb); // NOLINT(cppcoreguidelines-owning-memory) + auto *sock = new LWIPRawImpl((sa_family_t) domain, pcb); // NOLINT(cppcoreguidelines-owning-memory) sock->init(); return std::unique_ptr{sock}; } @@ -700,6 +661,25 @@ std::unique_ptr socket_loop_monitored(int domain, int type, int protocol return socket(domain, type, protocol); } +std::unique_ptr socket_listen(int domain, int type, int protocol) { + if (type != SOCK_STREAM) { + ESP_LOGE(TAG, "UDP sockets not supported on this platform, use WiFiUDP"); + errno = EPROTOTYPE; + return nullptr; + } + auto *pcb = tcp_new(); + if (pcb == nullptr) + return nullptr; + auto *sock = new LWIPRawListenImpl((sa_family_t) domain, pcb); // NOLINT(cppcoreguidelines-owning-memory) + sock->init(); + return std::unique_ptr{sock}; +} + +std::unique_ptr socket_listen_loop_monitored(int domain, int type, int protocol) { + // LWIPRawImpl doesn't use file descriptors, so monitoring is not applicable + return socket_listen(domain, type, protocol); +} + } // namespace esphome::socket #endif // USE_SOCKET_IMPL_LWIP_TCP diff --git a/esphome/components/socket/lwip_raw_tcp_impl.h b/esphome/components/socket/lwip_raw_tcp_impl.h new file mode 100644 index 0000000000..c171e0537f --- /dev/null +++ b/esphome/components/socket/lwip_raw_tcp_impl.h @@ -0,0 +1,200 @@ +#pragma once +#include "esphome/core/defines.h" + +#ifdef USE_SOCKET_IMPL_LWIP_TCP + +#include +#include +#include +#include +#include + +#include "esphome/core/helpers.h" +#include "headers.h" +#include "lwip/ip.h" +#include "lwip/netif.h" +#include "lwip/opt.h" +#include "lwip/tcp.h" + +namespace esphome::socket { + +// Forward declaration +class LWIPRawImpl; + +/// Non-virtual common base for LWIP raw TCP sockets. +/// Provides shared fields and methods for both connected and listening sockets. +/// No virtual methods — pure code sharing. +class LWIPRawCommon { + public: + LWIPRawCommon(sa_family_t family, struct tcp_pcb *pcb) : pcb_(pcb), family_(family) {} + ~LWIPRawCommon(); + LWIPRawCommon(const LWIPRawCommon &) = delete; + LWIPRawCommon &operator=(const LWIPRawCommon &) = delete; + + int bind(const struct sockaddr *name, socklen_t addrlen); + int close(); + int shutdown(int how); + + int getpeername(struct sockaddr *name, socklen_t *addrlen); + int getsockname(struct sockaddr *name, socklen_t *addrlen); + + /// Format peer address into a fixed-size buffer (no heap allocation) + size_t getpeername_to(std::span buf); + /// Format local address into a fixed-size buffer (no heap allocation) + size_t getsockname_to(std::span buf); + + int getsockopt(int level, int optname, void *optval, socklen_t *optlen); + int setsockopt(int level, int optname, const void *optval, socklen_t optlen); + + int get_fd() const { return -1; } + + protected: + int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen); + + // Member ordering optimized to minimize padding on 32-bit systems + struct tcp_pcb *pcb_; + // don't use lwip nodelay flag, it sometimes causes reconnect + // instead use it for determining whether to call lwip_output + bool nodelay_ = false; + sa_family_t family_ = 0; +}; + +/// Connected socket implementation for LWIP raw TCP. +/// No virtual methods — callers always use the concrete type. +class LWIPRawImpl : public LWIPRawCommon { + public: + using LWIPRawCommon::LWIPRawCommon; + ~LWIPRawImpl(); + + void init(); + + // Non-listening sockets return error + std::unique_ptr accept(struct sockaddr *, socklen_t *) { + errno = EINVAL; + return nullptr; + } + std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) { + return this->accept(addr, addrlen); + } + // Regular sockets can't be converted to listening - this shouldn't happen + // as listen() should only be called on sockets created for listening + int listen(int) { + errno = EOPNOTSUPP; + return -1; + } + ssize_t read(void *buf, size_t len); + ssize_t readv(const struct iovec *iov, int iovcnt); + ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *) { + errno = ENOTSUP; + return -1; + } + ssize_t write(const void *buf, size_t len); + ssize_t writev(const struct iovec *iov, int iovcnt); + ssize_t sendto(const void *, size_t, int, const struct sockaddr *, socklen_t) { + // return ::sendto(fd_, buf, len, flags, to, tolen); + errno = ENOSYS; + return -1; + } + bool ready() const { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; } + + int setblocking(bool blocking) { + if (this->pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (blocking) { + // blocking operation not supported + errno = EINVAL; + return -1; + } + return 0; + } + int loop() { return 0; } + + err_t recv_fn(struct pbuf *pb, err_t err); + + static void s_err_fn(void *arg, err_t err); + static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err); + + protected: + ssize_t internal_write_(const void *buf, size_t len); + int internal_output_(); + + pbuf *rx_buf_ = nullptr; + size_t rx_buf_offset_ = 0; + bool rx_closed_ = false; +}; + +/// Listening socket implementation for LWIP raw TCP. +/// Separate from LWIPRawImpl — no virtual dispatch needed. +class LWIPRawListenImpl : public LWIPRawCommon { + public: + using LWIPRawCommon::LWIPRawCommon; + ~LWIPRawListenImpl(); + + void init(); + + bool ready() const { return this->accepted_socket_count_ > 0; } + + std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen); + std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) { + return this->accept(addr, addrlen); + } + int listen(int backlog); + + // Listening sockets don't do I/O + ssize_t read(void *, size_t) { + errno = ENOTSUP; + return -1; + } + ssize_t write(const void *, size_t) { + errno = ENOTSUP; + return -1; + } + ssize_t readv(const struct iovec *, int) { + errno = ENOTSUP; + return -1; + } + ssize_t writev(const struct iovec *, int) { + errno = ENOTSUP; + return -1; + } + ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *) { + errno = ENOTSUP; + return -1; + } + ssize_t sendto(const void *, size_t, int, const struct sockaddr *, socklen_t) { + errno = ENOTSUP; + return -1; + } + int setblocking(bool) { return 0; } + int loop() { return 0; } + + static void s_err_fn(void *arg, err_t err); + + private: + err_t accept_fn_(struct tcp_pcb *newpcb, err_t err); + static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err); + + // Accept queue - holds incoming connections briefly until the event loop calls accept() + // This is NOT a connection pool - just a temporary queue between LWIP callbacks and the main loop + // 3 slots is plenty since connections are pulled out quickly by the event loop + // + // Memory analysis: std::array<3> vs original std::queue implementation: + // - std::queue uses std::deque internally which on 32-bit systems needs: + // 24 bytes (deque object) + 32+ bytes (map array) + heap allocations + // Total: ~56+ bytes minimum, plus heap fragmentation + // - std::array<3>: 12 bytes fixed (3 pointers × 4 bytes) + // Saves ~44+ bytes RAM per listening socket + avoids ALL heap allocations + // Used on ESP8266 and RP2040 (platforms using LWIP_TCP implementation) + // + // By using a separate listening socket class, regular connected sockets save + // 16 bytes (12 bytes array + 1 byte count + 3 bytes padding) of memory overhead on 32-bit systems + static constexpr size_t MAX_ACCEPTED_SOCKETS = 3; + std::array, MAX_ACCEPTED_SOCKETS> accepted_sockets_; + uint8_t accepted_socket_count_ = 0; // Number of sockets currently in queue +}; + +} // namespace esphome::socket + +#endif // USE_SOCKET_IMPL_LWIP_TCP diff --git a/esphome/components/socket/lwip_sockets_impl.cpp b/esphome/components/socket/lwip_sockets_impl.cpp index 79d68e085a..0322820ef4 100644 --- a/esphome/components/socket/lwip_sockets_impl.cpp +++ b/esphome/components/socket/lwip_sockets_impl.cpp @@ -1,6 +1,6 @@ -#include "socket.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" +#include "socket.h" #ifdef USE_SOCKET_IMPL_LWIP_SOCKETS @@ -9,94 +9,73 @@ namespace esphome::socket { -class LwIPSocketImpl final : public Socket { - public: - LwIPSocketImpl(int fd, bool monitor_loop = false) { - this->fd_ = fd; - // Register new socket with the application for select() if monitoring requested - if (monitor_loop && this->fd_ >= 0) { - // Only set loop_monitored_ to true if registration succeeds - this->loop_monitored_ = App.register_socket_fd(this->fd_); - } - } - ~LwIPSocketImpl() override { - if (!this->closed_) { - this->close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall) - } - } - int connect(const struct sockaddr *addr, socklen_t addrlen) override { - return lwip_connect(this->fd_, addr, addrlen); - } - std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { - int fd = lwip_accept(this->fd_, addr, addrlen); - if (fd == -1) - return {}; - return make_unique(fd, false); - } - std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override { - int fd = lwip_accept(this->fd_, addr, addrlen); - if (fd == -1) - return {}; - return make_unique(fd, true); +LwIPSocketImpl::LwIPSocketImpl(int fd, bool monitor_loop) { + this->fd_ = fd; + // Register new socket with the application for select() if monitoring requested + if (monitor_loop && this->fd_ >= 0) { + // Only set loop_monitored_ to true if registration succeeds + this->loop_monitored_ = App.register_socket_fd(this->fd_); } +} - int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); } - int close() override { - if (!this->closed_) { - // Unregister from select() before closing if monitored - if (this->loop_monitored_) { - App.unregister_socket_fd(this->fd_); - } - int ret = lwip_close(this->fd_); - this->closed_ = true; - return ret; +LwIPSocketImpl::~LwIPSocketImpl() { + if (!this->closed_) { + this->close(); + } +} + +int LwIPSocketImpl::close() { + if (!this->closed_) { + // Unregister from select() before closing if monitored + if (this->loop_monitored_) { + App.unregister_socket_fd(this->fd_); } + int ret = lwip_close(this->fd_); + this->closed_ = true; + return ret; + } + return 0; +} + +int LwIPSocketImpl::setblocking(bool blocking) { + int fl = lwip_fcntl(this->fd_, F_GETFL, 0); + if (blocking) { + fl &= ~O_NONBLOCK; + } else { + fl |= O_NONBLOCK; + } + lwip_fcntl(this->fd_, F_SETFL, fl); + return 0; +} + +bool LwIPSocketImpl::ready() const { return socket_ready_fd(this->fd_, this->loop_monitored_); } + +size_t LwIPSocketImpl::getpeername_to(std::span buf) { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + if (this->getpeername(reinterpret_cast(&storage), &len) != 0) { + buf[0] = '\0'; return 0; } - int shutdown(int how) override { return lwip_shutdown(this->fd_, how); } + return format_sockaddr_to(reinterpret_cast(&storage), len, buf); +} - int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { - return lwip_getpeername(this->fd_, addr, addrlen); - } - int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { - return lwip_getsockname(this->fd_, addr, addrlen); - } - int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { - return lwip_getsockopt(this->fd_, level, optname, optval, optlen); - } - int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override { - return lwip_setsockopt(this->fd_, level, optname, optval, optlen); - } - int listen(int backlog) override { return lwip_listen(this->fd_, backlog); } - ssize_t read(void *buf, size_t len) override { return lwip_read(this->fd_, buf, len); } - ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) override { - return lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len); - } - ssize_t readv(const struct iovec *iov, int iovcnt) override { return lwip_readv(this->fd_, iov, iovcnt); } - ssize_t write(const void *buf, size_t len) override { return lwip_write(this->fd_, buf, len); } - ssize_t send(void *buf, size_t len, int flags) { return lwip_send(this->fd_, buf, len, flags); } - ssize_t writev(const struct iovec *iov, int iovcnt) override { return lwip_writev(this->fd_, iov, iovcnt); } - ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override { - return lwip_sendto(this->fd_, buf, len, flags, to, tolen); - } - int setblocking(bool blocking) override { - int fl = lwip_fcntl(this->fd_, F_GETFL, 0); - if (blocking) { - fl &= ~O_NONBLOCK; - } else { - fl |= O_NONBLOCK; - } - lwip_fcntl(this->fd_, F_SETFL, fl); +size_t LwIPSocketImpl::getsockname_to(std::span buf) { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + if (this->getsockname(reinterpret_cast(&storage), &len) != 0) { + buf[0] = '\0'; return 0; } -}; + return format_sockaddr_to(reinterpret_cast(&storage), len, buf); +} // Helper to create a socket with optional monitoring -static std::unique_ptr create_socket(int domain, int type, int protocol, bool loop_monitored = false) { +static std::unique_ptr create_socket(int domain, int type, int protocol, bool loop_monitored = false) { int ret = lwip_socket(domain, type, protocol); if (ret == -1) return nullptr; - return std::unique_ptr{new LwIPSocketImpl(ret, loop_monitored)}; + return make_unique(ret, loop_monitored); } std::unique_ptr socket(int domain, int type, int protocol) { @@ -107,6 +86,14 @@ std::unique_ptr socket_loop_monitored(int domain, int type, int protocol return create_socket(domain, type, protocol, true); } +std::unique_ptr socket_listen(int domain, int type, int protocol) { + return create_socket(domain, type, protocol, false); +} + +std::unique_ptr socket_listen_loop_monitored(int domain, int type, int protocol) { + return create_socket(domain, type, protocol, true); +} + } // namespace esphome::socket #endif // USE_SOCKET_IMPL_LWIP_SOCKETS diff --git a/esphome/components/socket/lwip_sockets_impl.h b/esphome/components/socket/lwip_sockets_impl.h new file mode 100644 index 0000000000..2e319fcc4d --- /dev/null +++ b/esphome/components/socket/lwip_sockets_impl.h @@ -0,0 +1,80 @@ +#pragma once +#include "esphome/core/defines.h" + +#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS + +#include +#include + +#include "esphome/core/helpers.h" +#include "headers.h" + +namespace esphome::socket { + +class LwIPSocketImpl { + public: + LwIPSocketImpl(int fd, bool monitor_loop = false); + ~LwIPSocketImpl(); + LwIPSocketImpl(const LwIPSocketImpl &) = delete; + LwIPSocketImpl &operator=(const LwIPSocketImpl &) = delete; + + int connect(const struct sockaddr *addr, socklen_t addrlen) { return lwip_connect(this->fd_, addr, addrlen); } + std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) { + int fd = lwip_accept(this->fd_, addr, addrlen); + if (fd == -1) + return {}; + return make_unique(fd, false); + } + std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) { + int fd = lwip_accept(this->fd_, addr, addrlen); + if (fd == -1) + return {}; + return make_unique(fd, true); + } + + int bind(const struct sockaddr *addr, socklen_t addrlen) { return lwip_bind(this->fd_, addr, addrlen); } + int close(); + int shutdown(int how) { return lwip_shutdown(this->fd_, how); } + + int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return lwip_getpeername(this->fd_, addr, addrlen); } + int getsockname(struct sockaddr *addr, socklen_t *addrlen) { return lwip_getsockname(this->fd_, addr, addrlen); } + + /// Format peer address into a fixed-size buffer (no heap allocation) + size_t getpeername_to(std::span buf); + /// Format local address into a fixed-size buffer (no heap allocation) + size_t getsockname_to(std::span buf); + + int getsockopt(int level, int optname, void *optval, socklen_t *optlen) { + return lwip_getsockopt(this->fd_, level, optname, optval, optlen); + } + int setsockopt(int level, int optname, const void *optval, socklen_t optlen) { + return lwip_setsockopt(this->fd_, level, optname, optval, optlen); + } + int listen(int backlog) { return lwip_listen(this->fd_, backlog); } + ssize_t read(void *buf, size_t len) { return lwip_read(this->fd_, buf, len); } + ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) { + return lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len); + } + ssize_t readv(const struct iovec *iov, int iovcnt) { return lwip_readv(this->fd_, iov, iovcnt); } + ssize_t write(const void *buf, size_t len) { return lwip_write(this->fd_, buf, len); } + ssize_t send(void *buf, size_t len, int flags) { return lwip_send(this->fd_, buf, len, flags); } + ssize_t writev(const struct iovec *iov, int iovcnt) { return lwip_writev(this->fd_, iov, iovcnt); } + ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) { + return lwip_sendto(this->fd_, buf, len, flags, to, tolen); + } + int setblocking(bool blocking); + int loop() { return 0; } + + bool ready() const; + + int get_fd() const { return this->fd_; } + + protected: + int fd_{-1}; + bool closed_{false}; + bool loop_monitored_{false}; +}; + +} // namespace esphome::socket + +#endif // USE_SOCKET_IMPL_LWIP_SOCKETS diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index 6154c497e0..c04671c7ee 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -8,10 +8,10 @@ namespace esphome::socket { -Socket::~Socket() {} - #ifdef USE_SOCKET_SELECT_SUPPORT -bool Socket::ready() const { return !this->loop_monitored_ || App.is_socket_ready_(this->fd_); } +// Shared ready() implementation for fd-based socket implementations (BSD and LWIP sockets). +// Checks if the Application's select() loop has marked this fd as ready. +bool socket_ready_fd(int fd, bool loop_monitored) { return !loop_monitored || App.is_socket_ready_(fd); } #endif // Platform-specific inet_ntop wrappers @@ -81,26 +81,6 @@ size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::s return 0; } -size_t Socket::getpeername_to(std::span buf) { - struct sockaddr_storage storage; - socklen_t len = sizeof(storage); - if (this->getpeername(reinterpret_cast(&storage), &len) != 0) { - buf[0] = '\0'; - return 0; - } - return format_sockaddr_to(reinterpret_cast(&storage), len, buf); -} - -size_t Socket::getsockname_to(std::span buf) { - struct sockaddr_storage storage; - socklen_t len = sizeof(storage); - if (this->getsockname(reinterpret_cast(&storage), &len) != 0) { - buf[0] = '\0'; - return 0; - } - return format_sockaddr_to(reinterpret_cast(&storage), len, buf); -} - std::unique_ptr socket_ip(int type, int protocol) { #if USE_NETWORK_IPV6 return socket(AF_INET6, type, protocol); @@ -109,11 +89,11 @@ std::unique_ptr socket_ip(int type, int protocol) { #endif /* USE_NETWORK_IPV6 */ } -std::unique_ptr socket_ip_loop_monitored(int type, int protocol) { +std::unique_ptr socket_ip_loop_monitored(int type, int protocol) { #if USE_NETWORK_IPV6 - return socket_loop_monitored(AF_INET6, type, protocol); + return socket_listen_loop_monitored(AF_INET6, type, protocol); #else - return socket_loop_monitored(AF_INET, type, protocol); + return socket_listen_loop_monitored(AF_INET, type, protocol); #endif /* USE_NETWORK_IPV6 */ } diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index a771e2fe1a..86a4f0cba9 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -7,87 +7,41 @@ #include "headers.h" #if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) + +// Include only the active implementation's header. +// SOCKADDR_STR_LEN is defined in headers.h. +#ifdef USE_SOCKET_IMPL_BSD_SOCKETS +#include "bsd_sockets_impl.h" +#elif defined(USE_SOCKET_IMPL_LWIP_SOCKETS) +#include "lwip_sockets_impl.h" +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) +#include "lwip_raw_tcp_impl.h" +#endif + namespace esphome::socket { -// Maximum length for formatted socket address string (IP address without port) -// IPv4: "255.255.255.255" = 15 chars + null = 16 -// IPv6: full address = 45 chars + null = 46 -#if USE_NETWORK_IPV6 -static constexpr size_t SOCKADDR_STR_LEN = 46; // INET6_ADDRSTRLEN -#else -static constexpr size_t SOCKADDR_STR_LEN = 16; // INET_ADDRSTRLEN +// Type aliases — only one implementation is active per build. +// Socket is the concrete type for connected sockets. +// ListenSocket is the concrete type for listening/server sockets. +// On BSD and LWIP_SOCKETS, both aliases resolve to the same type. +// On LWIP_TCP, they are different types (no virtual dispatch between them). +#ifdef USE_SOCKET_IMPL_BSD_SOCKETS +using Socket = BSDSocketImpl; +using ListenSocket = BSDSocketImpl; +#elif defined(USE_SOCKET_IMPL_LWIP_SOCKETS) +using Socket = LwIPSocketImpl; +using ListenSocket = LwIPSocketImpl; +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) +using Socket = LWIPRawImpl; +using ListenSocket = LWIPRawListenImpl; #endif -class Socket { - public: - Socket() = default; - virtual ~Socket(); - Socket(const Socket &) = delete; - Socket &operator=(const Socket &) = delete; - - virtual std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) = 0; - /// Accept a connection and monitor it in the main loop - /// NOTE: This function is NOT thread-safe and must only be called from the main loop - virtual std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) { - return accept(addr, addrlen); // Default implementation for backward compatibility - } - virtual int bind(const struct sockaddr *addr, socklen_t addrlen) = 0; - virtual int close() = 0; - // not supported yet: - // virtual int connect(const std::string &address) = 0; -#if defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) - virtual int connect(const struct sockaddr *addr, socklen_t addrlen) = 0; -#endif - virtual int shutdown(int how) = 0; - - virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0; - virtual int getsockname(struct sockaddr *addr, socklen_t *addrlen) = 0; - - /// Format peer address into a fixed-size buffer (no heap allocation) - /// Non-virtual wrapper around getpeername() - can be optimized away if unused - /// Returns number of characters written (excluding null terminator), or 0 on error - size_t getpeername_to(std::span buf); - /// Format local address into a fixed-size buffer (no heap allocation) - /// Non-virtual wrapper around getsockname() - can be optimized away if unused - size_t getsockname_to(std::span buf); - virtual int getsockopt(int level, int optname, void *optval, socklen_t *optlen) = 0; - virtual int setsockopt(int level, int optname, const void *optval, socklen_t optlen) = 0; - virtual int listen(int backlog) = 0; - virtual ssize_t read(void *buf, size_t len) = 0; - virtual ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) = 0; - virtual ssize_t readv(const struct iovec *iov, int iovcnt) = 0; - virtual ssize_t write(const void *buf, size_t len) = 0; - virtual ssize_t writev(const struct iovec *iov, int iovcnt) = 0; - virtual ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) = 0; - - virtual int setblocking(bool blocking) = 0; - virtual int loop() { return 0; }; - - /// Get the underlying file descriptor (returns -1 if not supported) - /// Non-virtual: only one socket implementation is active per build. #ifdef USE_SOCKET_SELECT_SUPPORT - int get_fd() const { return this->fd_; } -#else - int get_fd() const { return -1; } +/// Shared ready() helper for fd-based socket implementations. +/// Checks if the Application's select() loop has marked this fd as ready. +bool socket_ready_fd(int fd, bool loop_monitored); #endif - /// Check if socket has data ready to read. Must only be called from the main loop thread. - /// For select()-based sockets: non-virtual, checks Application's select() results - /// For LWIP raw TCP sockets: virtual, checks internal buffer state -#ifdef USE_SOCKET_SELECT_SUPPORT - bool ready() const; -#else - virtual bool ready() const { return true; } -#endif - - protected: -#ifdef USE_SOCKET_SELECT_SUPPORT - int fd_{-1}; - bool closed_{false}; - bool loop_monitored_{false}; -#endif -}; - /// Create a socket of the given domain, type and protocol. std::unique_ptr socket(int domain, int type, int protocol); /// Create a socket in the newest available IP domain (IPv6 or IPv4) of the given type and protocol. @@ -100,7 +54,13 @@ std::unique_ptr socket_ip(int type, int protocol); /// NOTE: On ESP platforms, FD_SETSIZE is typically 10, limiting the number of monitored sockets. /// File descriptors >= FD_SETSIZE will not be monitored and will log an error. std::unique_ptr socket_loop_monitored(int domain, int type, int protocol); -std::unique_ptr socket_ip_loop_monitored(int type, int protocol); + +/// Create a listening socket of the given domain, type and protocol. +std::unique_ptr socket_listen(int domain, int type, int protocol); +/// Create a listening socket and monitor it for data in the main loop. +std::unique_ptr socket_listen_loop_monitored(int domain, int type, int protocol); +/// Create a listening socket in the newest available IP domain and monitor it. +std::unique_ptr socket_ip_loop_monitored(int type, int protocol); /// Set a sockaddr to the specified address and port for the IP version used by socket_ip(). /// @param addr Destination sockaddr structure diff --git a/esphome/core/application.h b/esphome/core/application.h index d2345e2b0b..659b60222d 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -105,7 +105,10 @@ #endif namespace esphome::socket { -class Socket; +#ifdef USE_SOCKET_SELECT_SUPPORT +/// Shared ready() helper for fd-based socket implementations. +bool socket_ready_fd(int fd, bool loop_monitored); // NOLINT(readability-redundant-declaration) +#endif } // namespace esphome::socket // Forward declarations for friend access from codegen-generated setup() @@ -520,7 +523,9 @@ class Application { protected: friend Component; - friend class socket::Socket; +#ifdef USE_SOCKET_SELECT_SUPPORT + friend bool socket::socket_ready_fd(int fd, bool loop_monitored); +#endif friend void ::setup(); friend void ::original_setup(); From a1d91ac779458545b0a6e5f014f57bc6d31d87ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 06:59:23 -1000 Subject: [PATCH 307/609] [core] Compile-time detection of loop() overrides (#14405) --- esphome/core/application.cpp | 19 +++++++-------- esphome/core/application.h | 10 +++++++- esphome/core/component.cpp | 12 ---------- esphome/core/component.h | 7 ++++-- esphome/core/config.py | 36 ++++++++++++++++++++++++++++ esphome/cpp_helpers.py | 5 ++++ tests/unit_tests/test_cpp_helpers.py | 12 ++++++++-- 7 files changed, 73 insertions(+), 28 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index c977fd66b3..26cd670629 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -79,7 +79,12 @@ static void insertion_sort_by_priority(Iterator first, Iterator last) { } } -void Application::register_component_(Component *comp) { this->components_.push_back(comp); } +void Application::register_component_impl_(Component *comp, bool has_loop) { + if (has_loop) { + comp->component_state_ |= COMPONENT_HAS_LOOP; + } + this->components_.push_back(comp); +} void Application::setup() { ESP_LOGI(TAG, "Running through setup()"); ESP_LOGV(TAG, "Sorting components by setup priority"); @@ -382,16 +387,8 @@ void Application::teardown_components(uint32_t timeout_ms) { } void Application::calculate_looping_components_() { - // Count total components that need looping - size_t total_looping = 0; - for (auto *obj : this->components_) { - if (obj->has_overridden_loop()) { - total_looping++; - } - } - - // Initialize FixedVector with exact size - no reallocation possible - this->looping_components_.init(total_looping); + // FixedVector capacity was pre-initialized by codegen with the exact count + // of components that override loop(), computed at C++ compile time. // Add all components with loop override that aren't already LOOP_DONE // Some components (like logger) may call disable_loop() during initialization diff --git a/esphome/core/application.h b/esphome/core/application.h index 659b60222d..44e8de7ee9 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "esphome/core/component.h" #include "esphome/core/defines.h" @@ -542,7 +543,14 @@ class Application { #endif #endif - void register_component_(Component *comp); + /// Register a component, detecting loop() override at compile time. + /// The template resolves &T::loop vs &Component::loop as a constexpr bool + /// and forwards it to register_component_impl_ which stores it in component_state_. + template void register_component_(T *comp) { + this->register_component_impl_(comp, !std::is_same_v); + } + + void register_component_impl_(Component *comp, bool has_loop); void calculate_looping_components_(); void add_looping_components_by_state_(bool match_loop_done); diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 5afd901da2..a71aa8b3a3 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -496,18 +496,6 @@ void Component::set_setup_priority(float priority) { } #endif -bool Component::has_overridden_loop() const { -#if defined(USE_HOST) || defined(CLANG_TIDY) - return true; -#else -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wpmf-conversions" - bool loop_overridden = (void *) (this->*(&Component::loop)) != (void *) (&Component::loop); -#pragma GCC diagnostic pop - return loop_overridden; -#endif -} - PollingComponent::PollingComponent(uint32_t update_interval) : update_interval_(update_interval) {} void PollingComponent::call_setup() { diff --git a/esphome/core/component.h b/esphome/core/component.h index 6b920da290..d8102ea670 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -76,6 +76,8 @@ inline constexpr uint8_t STATUS_LED_MASK = 0x18; inline constexpr uint8_t STATUS_LED_OK = 0x00; inline constexpr uint8_t STATUS_LED_WARNING = 0x08; inline constexpr uint8_t STATUS_LED_ERROR = 0x10; +// Component loop override flag uses bit 5 (set at registration time) +inline constexpr uint8_t COMPONENT_HAS_LOOP = 0x20; // Remove before 2026.8.0 enum class RetryResult { DONE, RETRY }; @@ -271,7 +273,7 @@ class Component { */ void status_momentary_error(const char *name, uint32_t length = 5000); - bool has_overridden_loop() const; + bool has_overridden_loop() const { return (this->component_state_ & COMPONENT_HAS_LOOP) != 0; } /** Set where this component was loaded from for some debug messages. * @@ -510,7 +512,8 @@ class Component { /// Bits 0-2: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED, 0x04=LOOP_DONE) /// Bit 3: STATUS_LED_WARNING /// Bit 4: STATUS_LED_ERROR - /// Bits 5-7: Unused - reserved for future expansion + /// Bit 5: Has overridden loop() (set at registration time) + /// Bits 6-7: Unused - reserved for future expansion uint8_t component_state_{0x00}; volatile bool pending_enable_loop_{false}; ///< ISR-safe flag for enable_loop_soon_any_context }; diff --git a/esphome/core/config.py b/esphome/core/config.py index 593b402230..3835fd3875 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections import Counter import logging import os from pathlib import Path @@ -504,6 +505,40 @@ async def _add_controller_registry_define() -> None: cg.add_define("CONTROLLER_REGISTRY_MAX", controller_count) +@coroutine_with_priority(CoroPriority.FINAL) +async def _add_looping_components() -> None: + # Emit a constexpr that computes the looping component count at C++ compile time + # and pre-init the FixedVector with the exact capacity. Uses std::is_same_v to + # detect loop() overrides. The constexpr goes in main.cpp's global section where + # all component types are in scope. calculate_looping_components_() then skips + # the counting pass and only does the two population passes. + entries = CORE.data.get("looping_component_entries", []) + if not entries: + return + + # Build constexpr sum for the exact count, deduplicating by type + type_counts = Counter(entries) + terms = [ + f"({count} * !std::is_same_v)" + for cpp_type, count in type_counts.items() + ] + constexpr_expr = " + \\\n ".join(terms) + cg.add_global( + cg.RawStatement( + f"static constexpr size_t ESPHOME_LOOPING_COMPONENT_COUNT = \\\n" + f" {constexpr_expr};" + ) + ) + + # Pre-init FixedVector with exact capacity so calculate_looping_components_() + # can skip the counting pass + cg.add( + cg.RawExpression( + "App.looping_components_.init(ESPHOME_LOOPING_COMPONENT_COUNT)" + ) + ) + + @coroutine_with_priority(CoroPriority.CORE) async def to_code(config: ConfigType) -> None: cg.add_global(cg.global_ns.namespace("esphome").using) @@ -527,6 +562,7 @@ async def to_code(config: ConfigType) -> None: CORE.add_job(_add_platform_defines) CORE.add_job(_add_controller_registry_define) + CORE.add_job(_add_looping_components) CORE.add_job(_add_automations, config) diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index b673eaa7e1..8f8c693140 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -80,6 +80,11 @@ async def register_component(var, config): add(var.set_component_source(LogStringLiteral(name))) add(App.register_component_(var)) + + # Collect C++ type for compile-time looping component count + comp_entries = CORE.data.setdefault("looping_component_entries", []) + comp_entries.append(str(var.base.type)) + return var diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index 82ded409c7..5b6eed156f 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -14,7 +14,11 @@ async def test_gpio_pin_expression__conf_is_none(monkeypatch): @pytest.mark.asyncio async def test_register_component(monkeypatch): - var = Mock(base="foo.bar") + base_mock = Mock() + base_mock.__str__ = lambda self: "foo.bar" + base_mock.type = Mock() + base_mock.type.__str__ = lambda self: "foo::Bar" + var = Mock(base=base_mock) app_mock = Mock(register_component_=Mock(return_value=var)) monkeypatch.setattr(ch, "App", app_mock) @@ -46,7 +50,11 @@ async def test_register_component__no_component_id(monkeypatch): @pytest.mark.asyncio async def test_register_component__with_setup_priority(monkeypatch): - var = Mock(base="foo.bar") + base_mock = Mock() + base_mock.__str__ = lambda self: "foo.bar" + base_mock.type = Mock() + base_mock.type.__str__ = lambda self: "foo::Bar" + var = Mock(base=base_mock) app_mock = Mock(register_component_=Mock(return_value=var)) monkeypatch.setattr(ch, "App", app_mock) From b9b1af1c3dd11e2d41c0b88542637a6ceaec5ecd Mon Sep 17 00:00:00 2001 From: netixx Date: Mon, 2 Mar 2026 18:38:28 +0100 Subject: [PATCH 308/609] [mcp23016] Fix register access to use 16-bit paired transactions (#13676) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/mcp23016/mcp23016.cpp | 71 +++++++++--------------- esphome/components/mcp23016/mcp23016.h | 15 +++-- 2 files changed, 33 insertions(+), 53 deletions(-) diff --git a/esphome/components/mcp23016/mcp23016.cpp b/esphome/components/mcp23016/mcp23016.cpp index 56b2ecf9f4..fbdb6903b8 100644 --- a/esphome/components/mcp23016/mcp23016.cpp +++ b/esphome/components/mcp23016/mcp23016.cpp @@ -8,90 +8,71 @@ namespace mcp23016 { static const char *const TAG = "mcp23016"; void MCP23016::setup() { - uint8_t iocon; - if (!this->read_reg_(MCP23016_IOCON0, &iocon)) { + uint16_t iocon; + // MCP23016 registers operate as paired 16-bit registers. Addressing the + // odd register (e.g. IOCON1) reads/writes that register first, then wraps + // to the even register (IOCON0) in the same pair. Starting from the odd + // address gives the correct byte order for 1 << pin mapping: + // high byte = port 1 (pins 8-15), low byte = port 0 (pins 0-7). + if (!this->read_reg_(MCP23016_IOCON1, &iocon)) { this->mark_failed(); return; } // Read current output register state - this->read_reg_(MCP23016_OLAT0, &this->olat_0_); - this->read_reg_(MCP23016_OLAT1, &this->olat_1_); + this->read_reg_(MCP23016_OLAT1, &this->olat_); // all pins input - this->write_reg_(MCP23016_IODIR0, 0xFF); - this->write_reg_(MCP23016_IODIR1, 0xFF); + this->write_reg_(MCP23016_IODIR1, 0xFFFF); } void MCP23016::loop() { // Invalidate cache at the start of each loop this->reset_pin_cache_(); } -bool MCP23016::digital_read_hw(uint8_t pin) { - uint8_t reg_addr = pin < 8 ? MCP23016_GP0 : MCP23016_GP1; - uint8_t value = 0; - if (!this->read_reg_(reg_addr, &value)) { - return false; - } - - // Update the appropriate part of input_mask_ - if (pin < 8) { - this->input_mask_ = (this->input_mask_ & 0xFF00) | value; - } else { - this->input_mask_ = (this->input_mask_ & 0x00FF) | (uint16_t(value) << 8); - } - return true; -} +bool MCP23016::digital_read_hw(uint8_t pin) { return this->read_reg_(MCP23016_GP1, &this->input_mask_); } bool MCP23016::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); } -void MCP23016::digital_write_hw(uint8_t pin, bool value) { - uint8_t reg_addr = pin < 8 ? MCP23016_OLAT0 : MCP23016_OLAT1; - this->update_reg_(pin, value, reg_addr); -} +void MCP23016::digital_write_hw(uint8_t pin, bool value) { this->update_reg_(pin, value, MCP23016_OLAT1); } void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) { - uint8_t iodir = pin < 8 ? MCP23016_IODIR0 : MCP23016_IODIR1; if (flags == gpio::FLAG_INPUT) { - this->update_reg_(pin, true, iodir); + this->update_reg_(pin, true, MCP23016_IODIR1); } else if (flags == gpio::FLAG_OUTPUT) { - this->update_reg_(pin, false, iodir); + this->update_reg_(pin, false, MCP23016_IODIR1); } } -float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; } -bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) { +float MCP23016::get_setup_priority() const { return setup_priority::IO; } +bool MCP23016::read_reg_(uint8_t reg, uint16_t *value) { if (this->is_failed()) return false; - return this->read_byte(reg, value); + return this->read_byte_16(reg, value); } -bool MCP23016::write_reg_(uint8_t reg, uint8_t value) { +bool MCP23016::write_reg_(uint8_t reg, uint16_t value) { if (this->is_failed()) return false; - return this->write_byte(reg, value); + return this->write_byte_16(reg, value); } void MCP23016::update_reg_(uint8_t pin, bool pin_value, uint8_t reg_addr) { - uint8_t bit = pin % 8; - uint8_t reg_value = 0; - if (reg_addr == MCP23016_OLAT0) { - reg_value = this->olat_0_; - } else if (reg_addr == MCP23016_OLAT1) { - reg_value = this->olat_1_; + uint16_t reg_value = 0; + + if (reg_addr == MCP23016_OLAT1) { + reg_value = this->olat_; } else { this->read_reg_(reg_addr, ®_value); } if (pin_value) { - reg_value |= 1 << bit; + reg_value |= 1 << pin; } else { - reg_value &= ~(1 << bit); + reg_value &= ~(1 << pin); } this->write_reg_(reg_addr, reg_value); - if (reg_addr == MCP23016_OLAT0) { - this->olat_0_ = reg_value; - } else if (reg_addr == MCP23016_OLAT1) { - this->olat_1_ = reg_value; + if (reg_addr == MCP23016_OLAT1) { + this->olat_ = reg_value; } } diff --git a/esphome/components/mcp23016/mcp23016.h b/esphome/components/mcp23016/mcp23016.h index c2bc885c95..494bc9c197 100644 --- a/esphome/components/mcp23016/mcp23016.h +++ b/esphome/components/mcp23016/mcp23016.h @@ -19,13 +19,13 @@ enum MCP23016GPIORegisters { // 1 side MCP23016_GP1 = 0x01, MCP23016_OLAT1 = 0x03, - MCP23016_IPOL1 = 0x04, + MCP23016_IPOL1 = 0x05, MCP23016_IODIR1 = 0x07, - MCP23016_INTCAP1 = 0x08, + MCP23016_INTCAP1 = 0x09, MCP23016_IOCON1 = 0x0B, }; -class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::CachedGpioExpander { +class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::CachedGpioExpander { public: MCP23016() = default; @@ -42,16 +42,15 @@ class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander:: void digital_write_hw(uint8_t pin, bool value) override; // read a given register - bool read_reg_(uint8_t reg, uint8_t *value); + bool read_reg_(uint8_t reg, uint16_t *value); // write a value to a given register - bool write_reg_(uint8_t reg, uint8_t value); + bool write_reg_(uint8_t reg, uint16_t value); // update registers with given pin value. void update_reg_(uint8_t pin, bool pin_value, uint8_t reg_a); - uint8_t olat_0_{0x00}; - uint8_t olat_1_{0x00}; + uint16_t olat_{0x0000}; // Cache for input values (16-bit combined for both banks) - uint16_t input_mask_{0x00}; + uint16_t input_mask_{0x0000}; }; class MCP23016GPIOPin : public GPIOPin { From 2fa244715d21e30338702c0ea017884f338efd03 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 08:54:54 -1000 Subject: [PATCH 309/609] [socket] Fix pre-existing bugs found during socket devirtualization review (#14404) --- esphome/components/socket/bsd_sockets_impl.h | 2 +- .../components/socket/lwip_raw_tcp_impl.cpp | 21 +++++++++++++++++-- esphome/components/socket/lwip_sockets_impl.h | 2 +- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/esphome/components/socket/bsd_sockets_impl.h b/esphome/components/socket/bsd_sockets_impl.h index edee39f315..d9ed9dc567 100644 --- a/esphome/components/socket/bsd_sockets_impl.h +++ b/esphome/components/socket/bsd_sockets_impl.h @@ -83,7 +83,7 @@ class BSDSocketImpl { return ::write(this->fd_, buf, len); #endif } - ssize_t send(void *buf, size_t len, int flags) { return ::send(this->fd_, buf, len, flags); } + ssize_t send(const void *buf, size_t len, int flags) { return ::send(this->fd_, buf, len, flags); } ssize_t writev(const struct iovec *iov, int iovcnt) { #if defined(USE_ESP32) return ::lwip_writev(this->fd_, iov, iovcnt); diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 6556979fe0..430356592f 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -68,7 +68,7 @@ int LWIPRawCommon::bind(const struct sockaddr *name, socklen_t addrlen) { } if (name == nullptr) { errno = EINVAL; - return 0; + return -1; } ip_addr_t ip; in_port_t port; @@ -319,6 +319,13 @@ int LWIPRawCommon::ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *n // ---- LWIPRawImpl methods ---- LWIPRawImpl::~LWIPRawImpl() { + // Free any received pbufs that LWIP transferred ownership of via recv_fn. + // tcp_abort() in the base destructor won't free these since LWIP considers + // ownership transferred once the recv callback accepts them. + if (this->rx_buf_ != nullptr) { + pbuf_free(this->rx_buf_); + this->rx_buf_ = nullptr; + } // Base class destructor handles pcb_ cleanup via tcp_abort } @@ -545,7 +552,14 @@ ssize_t LWIPRawImpl::writev(const struct iovec *iov, int iovcnt) { // ---- LWIPRawListenImpl methods ---- LWIPRawListenImpl::~LWIPRawListenImpl() { - // Base class destructor handles pcb_ cleanup via tcp_abort + // Listen PCBs must use tcp_close(), not tcp_abort(). + // tcp_abandon() asserts pcb->state != LISTEN and would access + // fields that don't exist in the smaller tcp_pcb_listen struct. + // Close here and null pcb_ so the base destructor skips tcp_abort. + if (this->pcb_ != nullptr) { + tcp_close(this->pcb_); + this->pcb_ = nullptr; + } } void LWIPRawListenImpl::init() { @@ -609,6 +623,9 @@ int LWIPRawListenImpl::listen(int backlog) { LWIP_LOG("tcp_arg(%p)", this->pcb_); tcp_arg(this->pcb_, this); tcp_accept(this->pcb_, LWIPRawListenImpl::s_accept_fn); + // Note: tcp_err() is NOT re-registered here. tcp_listen_with_backlog() converts the + // full tcp_pcb to a smaller tcp_pcb_listen struct that lacks the errf field. + // Calling tcp_err() on a listen PCB writes past the struct boundary (undefined behavior). return 0; } diff --git a/esphome/components/socket/lwip_sockets_impl.h b/esphome/components/socket/lwip_sockets_impl.h index 2e319fcc4d..d6699aded2 100644 --- a/esphome/components/socket/lwip_sockets_impl.h +++ b/esphome/components/socket/lwip_sockets_impl.h @@ -57,7 +57,7 @@ class LwIPSocketImpl { } ssize_t readv(const struct iovec *iov, int iovcnt) { return lwip_readv(this->fd_, iov, iovcnt); } ssize_t write(const void *buf, size_t len) { return lwip_write(this->fd_, buf, len); } - ssize_t send(void *buf, size_t len, int flags) { return lwip_send(this->fd_, buf, len, flags); } + ssize_t send(const void *buf, size_t len, int flags) { return lwip_send(this->fd_, buf, len, flags); } ssize_t writev(const struct iovec *iov, int iovcnt) { return lwip_writev(this->fd_, iov, iovcnt); } ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) { return lwip_sendto(this->fd_, buf, len, flags, to, tolen); From cb232d828879b365bfa116ff3b4a7546c1e6ce62 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 09:11:47 -1000 Subject: [PATCH 310/609] [core] Fix compile-time loop() detection for multiple inheritance (#14411) --- esphome/core/application.h | 13 ++++++++++--- esphome/core/config.py | 3 ++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 44e8de7ee9..13fd0180ab 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -118,6 +118,14 @@ void original_setup(); // NOLINT(readability-redundant-declaration) - used by c namespace esphome { +/// SFINAE helper: detects whether T overrides Component::loop(). +/// When &T::loop is ambiguous (multiple inheritance with separate loop() methods), +/// the ambiguity itself proves an override exists, so the true_type default is correct. +template struct HasLoopOverride : std::true_type {}; +template +struct HasLoopOverride> + : std::bool_constant> {}; + // Teardown timeout constant (in milliseconds) // For reboots, it's more important to shut down quickly than disconnect cleanly // since we're not entering deep sleep. The only consequence of not shutting down @@ -544,10 +552,9 @@ class Application { #endif /// Register a component, detecting loop() override at compile time. - /// The template resolves &T::loop vs &Component::loop as a constexpr bool - /// and forwards it to register_component_impl_ which stores it in component_state_. + /// Uses HasLoopOverride which handles ambiguous &T::loop from multiple inheritance. template void register_component_(T *comp) { - this->register_component_impl_(comp, !std::is_same_v); + this->register_component_impl_(comp, HasLoopOverride::value); } void register_component_impl_(Component *comp, bool has_loop); diff --git a/esphome/core/config.py b/esphome/core/config.py index 3835fd3875..9411949bb9 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -517,9 +517,10 @@ async def _add_looping_components() -> None: return # Build constexpr sum for the exact count, deduplicating by type + # Uses HasLoopOverride which handles ambiguous &T::loop from multiple inheritance type_counts = Counter(entries) terms = [ - f"({count} * !std::is_same_v)" + f"({count} * HasLoopOverride<{cpp_type}>::value)" for cpp_type, count in type_counts.items() ] constexpr_expr = " + \\\n ".join(terms) From 38f671a9231348919a2ffd779ebf070906cbdef9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:52:06 -0500 Subject: [PATCH 311/609] [uart] Fix flow_control_pin inverted flag ignored on ESP-IDF (#14410) Co-authored-by: Claude Opus 4.6 --- esphome/components/uart/uart_component_esp_idf.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 631db1e5c7..544404448b 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -157,6 +157,9 @@ void IDFUARTComponent::load_settings(bool dump_config) { if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) { invert |= UART_SIGNAL_RXD_INV; } + if (this->flow_control_pin_ != nullptr && this->flow_control_pin_->is_inverted()) { + invert |= UART_SIGNAL_RTS_INV; + } err = uart_set_line_inverse(this->uart_num_, invert); if (err != ESP_OK) { From d1de50c0e513431943607f678fc3f661aa605dd0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 11:11:04 -1000 Subject: [PATCH 312/609] [core] Add ESP8266 support to wake_loop_any_context() (#14392) --- esphome/components/socket/lwip_raw_tcp_impl.cpp | 2 +- esphome/components/socket/socket.h | 5 +++-- esphome/core/application.h | 12 +++++++++++- esphome/core/component.cpp | 9 +++++---- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 430356592f..d697bd47a5 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -36,7 +36,7 @@ void socket_delay(uint32_t ms) { esp_delay(ms, []() { return !s_socket_woke; }); } -void socket_wake() { +void IRAM_ATTR socket_wake() { s_socket_woke = true; esp_schedule(); } diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 86a4f0cba9..546d278260 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -86,8 +86,9 @@ size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::s /// On ESP8266, lwip callbacks set a flag and call esp_schedule() to wake the delay. void socket_delay(uint32_t ms); -/// Called by lwip callbacks to signal socket activity and wake delay. -void socket_wake(); +/// Signal socket/IO activity and wake the main loop from esp_delay() early. +/// ISR-safe: uses IRAM_ATTR internally and only sets a volatile flag + esp_schedule(). +void socket_wake(); // NOLINT(readability-redundant-declaration) #endif } // namespace esphome::socket diff --git a/esphome/core/application.h b/esphome/core/application.h index 13fd0180ab..63d59c555e 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -34,7 +34,11 @@ #endif #endif #endif // USE_SOCKET_SELECT_SUPPORT - +#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) +namespace esphome::socket { +void socket_wake(); // NOLINT(readability-redundant-declaration) +} // namespace esphome::socket +#endif #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" #endif @@ -530,6 +534,12 @@ class Application { #endif #endif +#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) + /// Wake the main event loop from any context (ISR, thread, or main loop). + /// On ESP8266: sets the socket wake flag and calls esp_schedule() to exit esp_delay() early. + static void IRAM_ATTR wake_loop_any_context() { socket::socket_wake(); } +#endif + protected: friend Component; #ifdef USE_SOCKET_SELECT_SUPPORT diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index a71aa8b3a3..53cb50a44c 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -323,10 +323,11 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { // 8. Race condition with main loop is handled by clearing flag before processing this->pending_enable_loop_ = true; App.has_pending_enable_loop_requests_ = true; -#if defined(USE_LWIP_FAST_SELECT) && defined(USE_ESP32) - // Wake the main loop if sleeping in ulTaskNotifyTake(). Without this, - // the main loop would not wake until the select timeout expires (~16ms). - // Uses xPortInIsrContext() to choose the correct FreeRTOS notify API. +#if (defined(USE_LWIP_FAST_SELECT) && defined(USE_ESP32)) || (defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP)) + // Wake the main loop from sleep. Without this, the main loop would not + // wake until the select/delay timeout expires (~16ms). + // ESP32: uses xPortInIsrContext() to choose the correct FreeRTOS notify API. + // ESP8266: sets socket wake flag and calls esp_schedule() to exit esp_delay() early. Application::wake_loop_any_context(); #endif } From 3615a7b90c13f2bb100d5ea3241795aec5c7512b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 11:42:25 -1000 Subject: [PATCH 313/609] [core] Eliminate __udivdi3 in millis() on ESP32 and RP2040 (#14409) --- esphome/components/esp32/core.cpp | 4 +- esphome/components/rp2040/core.cpp | 4 +- esphome/core/helpers.h | 38 ++++++++++++ .../fixtures/micros_to_millis.yaml | 61 +++++++++++++++++++ tests/integration/test_micros_to_millis.py | 46 ++++++++++++++ 5 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 tests/integration/fixtures/micros_to_millis.yaml create mode 100644 tests/integration/test_micros_to_millis.py diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 59b791da40..7ebbba609e 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -22,8 +22,8 @@ extern "C" __attribute__((weak)) void initArduino() {} namespace esphome { void HOT yield() { vPortYield(); } -uint32_t IRAM_ATTR HOT millis() { return (uint32_t) (esp_timer_get_time() / 1000ULL); } -uint64_t HOT millis_64() { return static_cast(esp_timer_get_time()) / 1000ULL; } +uint32_t IRAM_ATTR HOT millis() { return micros_to_millis(static_cast(esp_timer_get_time())); } +uint64_t HOT millis_64() { return micros_to_millis(static_cast(esp_timer_get_time())); } void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index 6386d53292..a15ee7e263 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -11,8 +11,8 @@ namespace esphome { void HOT yield() { ::yield(); } -uint64_t millis_64() { return time_us_64() / 1000ULL; } -uint32_t HOT millis() { return static_cast(millis_64()); } +uint64_t millis_64() { return micros_to_millis(time_us_64()); } +uint32_t HOT millis() { return micros_to_millis(time_us_64()); } void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t HOT micros() { return ::micros(); } void HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index c68cb549bb..ae505a2d8a 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -599,6 +599,44 @@ template constexpr uint32_t fnv1a_hash_extend(uint32_t hash, T constexpr uint32_t fnv1a_hash(const char *str) { return fnv1a_hash_extend(FNV1_OFFSET_BASIS, str); } inline uint32_t fnv1a_hash(const std::string &str) { return fnv1a_hash(str.c_str()); } +/// Convert a 64-bit microsecond count to milliseconds without calling +/// __udivdi3 (software 64-bit divide, ~1200 ns on Xtensa @ 240 MHz). +/// +/// Returns uint32_t by default (for millis()), or uint64_t when requested +/// (for millis_64()). The only difference is whether hi * Q is truncated +/// to 32 bits or widened to 64. +/// +/// On 32-bit targets, GCC does not optimize 64-bit constant division into a +/// multiply-by-reciprocal. Since 1000 = 8 * 125, we first right-shift by 3 +/// (free divide-by-8), then use the Euclidean division identity to decompose +/// the remaining 64-bit divide-by-125 into a single 32-bit division: +/// +/// floor(us / 1000) = floor(floor(us / 8) / 125) [exact for integers] +/// 2^32 = Q * 125 + R (34359738 * 125 + 46) +/// (hi * 2^32 + lo) / 125 = hi * Q + (hi * R + lo) / 125 +/// +/// GCC optimizes the remaining 32-bit "/ 125U" into a multiply-by-reciprocal +/// (mulhu + shift), so no division instruction is emitted. +/// +/// Safe for us up to ~3.2e18 (~101,700 years of microseconds). +/// +/// See: https://en.wikipedia.org/wiki/Euclidean_division +/// See: https://ridiculousfish.com/blog/posts/labor-of-division-episode-iii.html +template inline constexpr ESPHOME_ALWAYS_INLINE ReturnT micros_to_millis(uint64_t us) { + constexpr uint32_t d = 125U; + constexpr uint32_t q = static_cast((1ULL << 32) / d); // 34359738 + constexpr uint32_t r = static_cast((1ULL << 32) % d); // 46 + // 1000 = 8 * 125; divide-by-8 is a free shift + uint64_t x = us >> 3; + uint32_t lo = static_cast(x); + uint32_t hi = static_cast(x >> 32); + // Combine remainder term: hi * (2^32 % 125) + lo + uint32_t adj = hi * r + lo; + // If adj overflowed, the true value is 2^32 + adj; apply the identity again + // static_cast(hi) widens to 64-bit when ReturnT=uint64_t, preserving upper bits of hi*q + return static_cast(hi) * q + (adj < lo ? (adj + r) / d + q : adj / d); +} + /// Return a random 32-bit unsigned integer. uint32_t random_uint32(); /// Return a random float between 0 and 1. diff --git a/tests/integration/fixtures/micros_to_millis.yaml b/tests/integration/fixtures/micros_to_millis.yaml new file mode 100644 index 0000000000..d11808c43a --- /dev/null +++ b/tests/integration/fixtures/micros_to_millis.yaml @@ -0,0 +1,61 @@ +esphome: + name: micros-to-millis-test + platformio_options: + build_flags: + - "-DDEBUG" + on_boot: + - lambda: |- + using esphome::micros_to_millis; + const char *TAG = "MTM"; + int pass = 0, fail = 0; + + auto check = [&](const char *name, uint64_t us) { + uint32_t got = micros_to_millis(us); + uint32_t want = (uint32_t)(us / 1000ULL); + if (got == want) { pass++; } + else { ESP_LOGE(TAG, "%s FAILED: got=%u want=%u", name, got, want); fail++; } + }; + + // Basic values + check("zero", 0); + check("below_1ms", 999); + check("exactly_1ms", 1000); + check("above_1ms", 1001); + + // Shift boundary (1000 = 8 * 125, exercises the >>3 shift) + check("shift_7999", 7999); + check("shift_8000", 8000); + check("shift_8001", 8001); + + // 32-bit boundary + check("u32max_minus1", 0xFFFFFFFEULL); + check("u32max", 0xFFFFFFFFULL); + check("u32max_plus1", 0x100000000ULL); + + // Realistic uptimes + check("30_days", 2592000000000ULL); + check("1_year", 31536000000000ULL); + + // Carry path: construct x = us>>3 with specific hi/lo that trigger adj overflow + { uint64_t x = (603ULL << 32) | 0xFFFFFFFFU; check("carry_603", x << 3); } + { uint64_t x = (5000ULL << 32) | 0xFFFFFFFFU; check("carry_5000", x << 3); } + + // Carry boundary: exact transition where adj overflows (hi=1000, R=46) + { + uint32_t hi = 1000; + uint32_t thr = 0xFFFFFFFFU - hi * 46U; + uint64_t h = (uint64_t)hi << 32; + check("carry_before", (h | (thr - 1)) << 3); + check("carry_at", (h | thr) << 3); + check("carry_after", (h | (thr + 1)) << 3); + } + + // Mod-8 variations (exercises the >>3 truncation) + for (int i = 0; i < 8; i++) { check("mod8", 2592000000000ULL + i); } + + if (fail == 0) { ESP_LOGI(TAG, "ALL_PASSED %d tests", pass); } + else { ESP_LOGE(TAG, "%d FAILED out of %d", fail, pass + fail); } + +host: +api: +logger: diff --git a/tests/integration/test_micros_to_millis.py b/tests/integration/test_micros_to_millis.py new file mode 100644 index 0000000000..9960d6b017 --- /dev/null +++ b/tests/integration/test_micros_to_millis.py @@ -0,0 +1,46 @@ +"""Integration test for micros_to_millis Euclidean decomposition.""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_micros_to_millis( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that micros_to_millis matches reference uint64 division.""" + + all_passed = asyncio.Event() + failures: list[str] = [] + + def on_log_line(line: str) -> None: + clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) + if "ALL_PASSED" in clean_line: + all_passed.set() + elif "FAILED" in clean_line and "[MTM" in clean_line: + failures.append(clean_line) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "micros-to-millis-test" + + try: + await asyncio.wait_for(all_passed.wait(), timeout=2.0) + except TimeoutError: + if failures: + pytest.fail(f"micros_to_millis failures: {failures}") + pytest.fail("micros_to_millis test timed out") + + assert not failures, f"micros_to_millis failures: {failures}" From 5510b45f3bfa45e3204de607e0a3815b42623606 Mon Sep 17 00:00:00 2001 From: Lino Schmidt <72667500+LinoSchmidt@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:43:06 +0100 Subject: [PATCH 314/609] [const] Move CONF_WATCHDOG (#14415) --- esphome/components/as5600/__init__.py | 2 +- esphome/components/as5600/sensor/__init__.py | 1 - esphome/const.py | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/as5600/__init__.py b/esphome/components/as5600/__init__.py index acb1c4d9db..b141329e94 100644 --- a/esphome/components/as5600/__init__.py +++ b/esphome/components/as5600/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_ID, CONF_POWER_MODE, CONF_RANGE, + CONF_WATCHDOG, ) CODEOWNERS = ["@ammmze"] @@ -57,7 +58,6 @@ FAST_FILTER = { CONF_RAW_ANGLE = "raw_angle" CONF_RAW_POSITION = "raw_position" -CONF_WATCHDOG = "watchdog" CONF_SLOW_FILTER = "slow_filter" CONF_FAST_FILTER = "fast_filter" CONF_START_POSITION = "start_position" diff --git a/esphome/components/as5600/sensor/__init__.py b/esphome/components/as5600/sensor/__init__.py index 1491852e07..e84733a484 100644 --- a/esphome/components/as5600/sensor/__init__.py +++ b/esphome/components/as5600/sensor/__init__.py @@ -23,7 +23,6 @@ AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingCompone CONF_RAW_ANGLE = "raw_angle" CONF_RAW_POSITION = "raw_position" -CONF_WATCHDOG = "watchdog" CONF_SLOW_FILTER = "slow_filter" CONF_FAST_FILTER = "fast_filter" CONF_PWM_FREQUENCY = "pwm_frequency" diff --git a/esphome/const.py b/esphome/const.py index 7262a106d8..bbd85ca66b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1094,6 +1094,7 @@ CONF_WAND_ID = "wand_id" CONF_WARM_WHITE = "warm_white" CONF_WARM_WHITE_COLOR_TEMPERATURE = "warm_white_color_temperature" CONF_WARMUP_TIME = "warmup_time" +CONF_WATCHDOG = "watchdog" CONF_WATCHDOG_THRESHOLD = "watchdog_threshold" CONF_WATCHDOG_TIMEOUT = "watchdog_timeout" CONF_WATER_HEATER = "water_heater" From 727fa073777cb758dd580fd9ea31a664fc58133f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:44:53 -1000 Subject: [PATCH 315/609] Bump github/codeql-action from 4.32.4 to 4.32.5 (#14416) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5d7c32eaa9..4bd018b5c9 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 + uses: github/codeql-action/init@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 + uses: github/codeql-action/analyze@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5 with: category: "/language:${{matrix.language}}" From 2e623fd6c3f83870ddeac768b855cbf3ba51e517 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 11:48:50 -1000 Subject: [PATCH 316/609] [tests] Fix flaky log assertion race in oversized payload tests (#14414) --- tests/integration/test_oversized_payloads.py | 69 ++++++++++++-------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/tests/integration/test_oversized_payloads.py b/tests/integration/test_oversized_payloads.py index 8bf890261a..be488347aa 100644 --- a/tests/integration/test_oversized_payloads.py +++ b/tests/integration/test_oversized_payloads.py @@ -17,10 +17,10 @@ async def test_oversized_payload_plaintext( ) -> None: """Test that oversized payloads (>32768 bytes) from client cause disconnection without crashing.""" process_exited = False - helper_log_found = False + helper_log_event = asyncio.Event() def check_logs(line: str) -> None: - nonlocal process_exited, helper_log_found + nonlocal process_exited # Check for signs that the process exited/crashed if "Segmentation fault" in line or "core dumped" in line: process_exited = True @@ -30,7 +30,7 @@ async def test_oversized_payload_plaintext( and "Bad packet: message size" in line and "exceeds maximum" in line ): - helper_log_found = True + helper_log_event.set() async with run_compiled(yaml_config, line_callback=check_logs): async with api_client_connected_with_disconnect() as (client, disconnect_event): @@ -54,10 +54,13 @@ async def test_oversized_payload_plaintext( # After disconnection, verify process didn't crash assert not process_exited, "ESPHome process should not crash" - # Verify we saw the expected HELPER_LOG message - assert helper_log_found, ( - "Expected to see HELPER_LOG about message size exceeding maximum" - ) + # Wait for the expected log message (may arrive after disconnect event) + try: + await asyncio.wait_for(helper_log_event.wait(), timeout=2.0) + except TimeoutError: + pytest.fail( + "Expected to see HELPER_LOG about message size exceeding maximum" + ) # Try to reconnect to verify the process is still running async with api_client_connected_with_disconnect() as (client2, _): @@ -77,10 +80,10 @@ async def test_oversized_protobuf_message_id_plaintext( This tests the message type limit - message IDs must fit in a uint16_t (0-65535). """ process_exited = False - helper_log_found = False + helper_log_event = asyncio.Event() def check_logs(line: str) -> None: - nonlocal process_exited, helper_log_found + nonlocal process_exited # Check for signs that the process exited/crashed if "Segmentation fault" in line or "core dumped" in line: process_exited = True @@ -90,7 +93,7 @@ async def test_oversized_protobuf_message_id_plaintext( and "Bad packet: message type" in line and "exceeds maximum" in line ): - helper_log_found = True + helper_log_event.set() async with run_compiled(yaml_config, line_callback=check_logs): async with api_client_connected_with_disconnect() as (client, disconnect_event): @@ -114,10 +117,13 @@ async def test_oversized_protobuf_message_id_plaintext( # After disconnection, verify process didn't crash assert not process_exited, "ESPHome process should not crash" - # Verify we saw the expected HELPER_LOG message - assert helper_log_found, ( - "Expected to see HELPER_LOG about message type exceeding maximum" - ) + # Wait for the expected log message (may arrive after disconnect event) + try: + await asyncio.wait_for(helper_log_event.wait(), timeout=2.0) + except TimeoutError: + pytest.fail( + "Expected to see HELPER_LOG about message type exceeding maximum" + ) # Try to reconnect to verify the process is still running async with api_client_connected_with_disconnect() as (client2, _): @@ -135,10 +141,10 @@ async def test_oversized_payload_noise( """Test that oversized payloads from client cause disconnection without crashing with noise encryption.""" noise_key = "N4Yle5YirwZhPiHHsdZLdOA73ndj/84veVaLhTvxCuU=" process_exited = False - helper_log_found = False + helper_log_event = asyncio.Event() def check_logs(line: str) -> None: - nonlocal process_exited, helper_log_found + nonlocal process_exited # Check for signs that the process exited/crashed if "Segmentation fault" in line or "core dumped" in line: process_exited = True @@ -149,7 +155,7 @@ async def test_oversized_payload_noise( and "Bad packet: message size" in line and "exceeds maximum" in line ): - helper_log_found = True + helper_log_event.set() async with run_compiled(yaml_config, line_callback=check_logs): async with api_client_connected_with_disconnect(noise_psk=noise_key) as ( @@ -177,10 +183,13 @@ async def test_oversized_payload_noise( # After disconnection, verify process didn't crash assert not process_exited, "ESPHome process should not crash" - # Verify we saw the expected HELPER_LOG message - assert helper_log_found, ( - "Expected to see HELPER_LOG about message size exceeding maximum" - ) + # Wait for the expected log message (may arrive after disconnect event) + try: + await asyncio.wait_for(helper_log_event.wait(), timeout=2.0) + except TimeoutError: + pytest.fail( + "Expected to see HELPER_LOG about message size exceeding maximum" + ) # Try to reconnect to verify the process is still running async with api_client_connected_with_disconnect(noise_psk=noise_key) as ( @@ -274,10 +283,10 @@ async def test_noise_corrupt_encrypted_frame( """ noise_key = "N4Yle5YirwZhPiHHsdZLdOA73ndj/84veVaLhTvxCuU=" process_exited = False - cipherstate_failed = False + cipherstate_event = asyncio.Event() def check_logs(line: str) -> None: - nonlocal process_exited, cipherstate_failed + nonlocal process_exited # Check for signs that the process exited/crashed if "Segmentation fault" in line or "core dumped" in line: process_exited = True @@ -290,7 +299,7 @@ async def test_noise_corrupt_encrypted_frame( "[W][api.connection" in line and "Reading failed CIPHERSTATE_DECRYPT_FAILED" in line ): - cipherstate_failed = True + cipherstate_event.set() async with run_compiled(yaml_config, line_callback=check_logs): async with api_client_connected_with_disconnect(noise_psk=noise_key) as ( @@ -326,10 +335,14 @@ async def test_noise_corrupt_encrypted_frame( assert not process_exited, ( "ESPHome process should not crash on corrupt encrypted frames" ) - # Verify we saw the expected log message about decryption failure - assert cipherstate_failed, ( - "Expected to see log about noise_cipherstate_decrypt failure or CIPHERSTATE_DECRYPT_FAILED" - ) + # Wait for the expected log message (may arrive after disconnect event) + try: + await asyncio.wait_for(cipherstate_event.wait(), timeout=2.0) + except TimeoutError: + pytest.fail( + "Expected to see log about noise_cipherstate_decrypt failure" + " or CIPHERSTATE_DECRYPT_FAILED" + ) # Verify we can still reconnect after handling the corrupt frame async with api_client_connected_with_disconnect(noise_psk=noise_key) as ( From 7a87348855aa7d49dc77926868717e0343bdb47a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:49:14 -0500 Subject: [PATCH 317/609] [ci] Skip PR title check for dependabot PRs (#14418) Co-authored-by: Claude Opus 4.6 --- .github/workflows/pr-title-check.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml index f23c2c870e..198b9a6b25 100644 --- a/.github/workflows/pr-title-check.yml +++ b/.github/workflows/pr-title-check.yml @@ -26,14 +26,19 @@ jobs: } = require('./.github/scripts/detect-tags.js'); const title = context.payload.pull_request.title; + const author = context.payload.pull_request.user.login; + + // Skip bot PRs (e.g. dependabot) - they have their own title format + if (author === 'dependabot[bot]') { + return; + } // Block titles starting with "word:" or "word(scope):" patterns const commitStylePattern = /^\w+(\(.*?\))?[!]?\s*:/; if (commitStylePattern.test(title)) { core.setFailed( `PR title should not start with a "prefix:" style format.\n` + - `Please use the format: [component] Brief description\n` + - `Example: [pn532] Add health checking and auto-reset` + `Please use the format: [component] Brief description\n` ); return; } From 97d713ee6477c003a8241b0452c1bcf2d6557289 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 2 Mar 2026 19:16:38 -0600 Subject: [PATCH 318/609] [media_source] Add new Media Source platform component (#14417) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/media_source/__init__.py | 40 +++++ .../components/media_source/media_source.h | 159 ++++++++++++++++++ esphome/core/defines.h | 1 + 4 files changed, 201 insertions(+) create mode 100644 esphome/components/media_source/__init__.py create mode 100644 esphome/components/media_source/media_source.h diff --git a/CODEOWNERS b/CODEOWNERS index 4c97b7f99d..21bee125c6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -316,6 +316,7 @@ esphome/components/mcp9808/* @k7hpn esphome/components/md5/* @esphome/core esphome/components/mdns/* @esphome/core esphome/components/media_player/* @jesserockz +esphome/components/media_source/* @kahrendt esphome/components/micro_wake_word/* @jesserockz @kahrendt esphome/components/micronova/* @edenhaus @jorre05 esphome/components/microphone/* @jesserockz @kahrendt diff --git a/esphome/components/media_source/__init__.py b/esphome/components/media_source/__init__.py new file mode 100644 index 0000000000..43256db4af --- /dev/null +++ b/esphome/components/media_source/__init__.py @@ -0,0 +1,40 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID +from esphome.core import CORE +from esphome.coroutine import CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObjClass + +CODEOWNERS = ["@kahrendt"] + +AUTO_LOAD = ["audio"] + +IS_PLATFORM_COMPONENT = True + +media_source_ns = cg.esphome_ns.namespace("media_source") + +MediaSource = media_source_ns.class_("MediaSource") + + +async def register_media_source(var, config): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + CORE.register_platform_component("media_source", var) + return var + + +_MEDIA_SOURCE_SCHEMA = cv.Schema({}) + + +def media_source_schema( + class_: MockObjClass, +) -> cv.Schema: + schema = {cv.GenerateID(CONF_ID): cv.declare_id(class_)} + + return _MEDIA_SOURCE_SCHEMA.extend(schema) + + +@coroutine_with_priority(CoroPriority.CORE) +async def to_code(config): + cg.add_global(media_source_ns.using) + cg.add_define("USE_MEDIA_SOURCE") diff --git a/esphome/components/media_source/media_source.h b/esphome/components/media_source/media_source.h new file mode 100644 index 0000000000..688c27134f --- /dev/null +++ b/esphome/components/media_source/media_source.h @@ -0,0 +1,159 @@ +#pragma once + +#include "esphome/components/audio/audio.h" +#include "esphome/core/helpers.h" + +#include +#include + +namespace esphome::media_source { + +enum class MediaSourceState : uint8_t { + IDLE, // Not playing, ready to accept play_uri + PLAYING, // Currently playing media + PAUSED, // Playback paused, can be resumed + ERROR, // Error occurred during playback; sources are responsible for logging their own error details +}; + +/// @brief Commands that are sent from the orchestrator to a media source +enum class MediaSourceCommand : uint8_t { + // All sources should support these basic commands. + PLAY, + PAUSE, + STOP, + + // Only sources with internal playlists will handle these; simple sources should ignore them. + NEXT, + PREVIOUS, + CLEAR_PLAYLIST, + REPEAT_ALL, + REPEAT_ONE, + REPEAT_OFF, + SHUFFLE, + UNSHUFFLE, +}; + +/// @brief Callbacks from a MediaSource to its orchestrator +class MediaSourceListener { + public: + virtual ~MediaSourceListener() = default; + + // Callbacks that all sources use to send data and state changes to the orchestrator. + /// @brief Send audio data to the listener + virtual size_t write_audio(const uint8_t *data, size_t length, uint32_t timeout_ms, + const audio::AudioStreamInfo &stream_info) = 0; + /// @brief Notify listener of state changes + virtual void report_state(MediaSourceState state) = 0; + + // Callbacks from smart sources requesting the orchestrator to change volume, mute, or start a new URI. + // Simple sources never invoke these. + /// @brief Request the orchestrator to change volume + virtual void request_volume(float volume) {} + /// @brief Request the orchestrator to change mute state + virtual void request_mute(bool is_muted) {} + /// @brief Request the orchestrator to play a new URI + virtual void request_play_uri(const std::string &uri) {} +}; + +/// @brief Abstract base class for media sources +/// MediaSource provides audio data to an orchestrator via the MediaSourceListener interface. It also receives commands +/// from the orchestrator to control playback. +class MediaSource { + public: + virtual ~MediaSource() = default; + + // === Playback Control === + + /// @brief Start playing the given URI + /// Sources should validate the URI and state, returning false if the source is busy. + /// The orchestrator is responsible for stopping active sources before starting a new one. + /// @param uri URI to play; e.g., "http://stream_url" + /// @return true if playback started successfully, false otherwise + virtual bool play_uri(const std::string &uri) = 0; + + /// @brief Handle playback commands (pause, stop, next, etc.) + /// @param command Command to execute + virtual void handle_command(MediaSourceCommand command) = 0; + + /// @brief Whether this source manages its own playlist internally + /// Smart sources that handle next/previous/repeat/shuffle should override this to return true. + virtual bool has_internal_playlist() const { return false; } + + // === State Access === + + /// @brief Get current playback state (must only be called from the main loop) + /// @return Current state of this source + MediaSourceState get_state() const { return this->state_; } + + // === URI Matching === + + /// @brief Check if this source can handle the given URI + /// Each source must override this to match its supported URI scheme(s). + /// @param uri URI to check + /// @return true if this source can handle the URI + virtual bool can_handle(const std::string &uri) const = 0; + + // === Listener: Source -> Orchestrator === + + /// @brief Set the listener that receives callbacks from this source + /// @param listener Pointer to the MediaSourceListener implementation + void set_listener(MediaSourceListener *listener) { this->listener_ = listener; } + + /// @brief Check if a listener has been registered + bool has_listener() const { return this->listener_ != nullptr; } + + /// @brief Write audio data to the listener + /// @param data Pointer to audio data buffer (not modified by this method) + /// @param length Number of bytes to write + /// @param timeout_ms Milliseconds to wait if the listener can't accept data immediately + /// @param stream_info Audio stream format information + /// @return Number of bytes written, or 0 if no listener is set + size_t write_output(const uint8_t *data, size_t length, uint32_t timeout_ms, + const audio::AudioStreamInfo &stream_info) { + if (this->listener_ != nullptr) { + return this->listener_->write_audio(data, length, timeout_ms, stream_info); + } + return 0; + } + + // === Callbacks: Orchestrator -> Source === + + /// @brief Notify the source that volume changed + /// Simple sources ignore this. Override for smart sources that track volume state. + /// @param volume New volume level (0.0 to 1.0) + virtual void notify_volume_changed(float volume) {} + + /// @brief Notify the source that mute state changed + /// Simple sources ignore this. Override for smart sources that track mute state. + /// @param is_muted New mute state + virtual void notify_mute_changed(bool is_muted) {} + + /// @brief Notify the source about audio that has been played + /// Called when the speaker reports that audio frames have been written to the DAC. + /// Sources can override this to track playback progress for synchronization. + /// @param frames Number of audio frames that were played + /// @param timestamp System time in microseconds when the frames finished writing to the DAC + virtual void notify_audio_played(uint32_t frames, int64_t timestamp) {} + + protected: + /// @brief Update state and notify listener (must only be called from the main loop) + /// This is the only way to change state_, ensuring listener notifications always fire. + /// Sources running FreeRTOS tasks should signal via event groups and call this from loop(). + /// @param state New state to set + void set_state_(MediaSourceState state) { + if (this->state_ != state) { + this->state_ = state; + if (this->listener_ != nullptr) { + this->listener_->report_state(state); + } + } + } + + private: + // Private to enforce the invariant that listener notifications always fire on state changes. + // All state transitions must go through set_state_() which couples the update with notification. + MediaSourceState state_{MediaSourceState::IDLE}; + MediaSourceListener *listener_{nullptr}; +}; + +} // namespace esphome::media_source diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 8c78afa7d4..7fbc5a0b53 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -106,6 +106,7 @@ #define MDNS_DYNAMIC_TXT_COUNT 2 #define SNTP_SERVER_COUNT 3 #define USE_MEDIA_PLAYER +#define USE_MEDIA_SOURCE #define USE_NEXTION_TFT_UPLOAD #define USE_NUMBER #define USE_OUTPUT From c77241940b701e8a8c5709374340c80b75e7e7c6 Mon Sep 17 00:00:00 2001 From: melak Date: Tue, 3 Mar 2026 02:24:00 +0100 Subject: [PATCH 319/609] [lps22] Add support for the LPS22DF variant (#14397) --- esphome/components/lps22/lps22.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/lps22/lps22.cpp b/esphome/components/lps22/lps22.cpp index 7fc5774b08..592b7faaf0 100644 --- a/esphome/components/lps22/lps22.cpp +++ b/esphome/components/lps22/lps22.cpp @@ -8,6 +8,7 @@ static constexpr const char *const TAG = "lps22"; static constexpr uint8_t WHO_AM_I = 0x0F; static constexpr uint8_t LPS22HB_ID = 0xB1; static constexpr uint8_t LPS22HH_ID = 0xB3; +static constexpr uint8_t LPS22DF_ID = 0xB4; static constexpr uint8_t CTRL_REG2 = 0x11; static constexpr uint8_t CTRL_REG2_ONE_SHOT_MASK = 0b1; static constexpr uint8_t STATUS = 0x27; @@ -24,8 +25,8 @@ static constexpr float TEMPERATURE_SCALE = 0.01f; void LPS22Component::setup() { uint8_t value = 0x00; this->read_register(WHO_AM_I, &value, 1); - if (value != LPS22HB_ID && value != LPS22HH_ID) { - ESP_LOGW(TAG, "device IDs as %02x, which isn't a known LPS22HB or LPS22HH ID", value); + if (value != LPS22HB_ID && value != LPS22HH_ID && value != LPS22DF_ID) { + ESP_LOGW(TAG, "device IDs as %02x, which isn't a known LPS22HB/HH/DF ID", value); this->mark_failed(); } } From ae49b673218a9c5857c6cc8670a8fa4185d6c8ee Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Mon, 2 Mar 2026 18:47:40 -0700 Subject: [PATCH 320/609] [ld2450] Clear all related sensors when a target is not being tracked (#13602) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/ld2450/ld2450.cpp | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 583918e5f5..eb17cc7de7 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -474,15 +474,12 @@ void LD2450Component::handle_periodic_data_() { is_moving = false; // tx is used for further calculations, so always needs to be populated tx = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]); - SAFE_PUBLISH_SENSOR(this->move_x_sensors_[index], tx); // Y start = TARGET_Y + index * 8; ty = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]); - SAFE_PUBLISH_SENSOR(this->move_y_sensors_[index], ty); // RESOLUTION start = TARGET_RESOLUTION + index * 8; res = (this->buffer_data_[start + 1] << 8) | this->buffer_data_[start]; - SAFE_PUBLISH_SENSOR(this->move_resolution_sensors_[index], res); #endif // SPEED start = TARGET_SPEED + index * 8; @@ -491,9 +488,6 @@ void LD2450Component::handle_periodic_data_() { is_moving = true; moving_target_count++; } -#ifdef USE_SENSOR - SAFE_PUBLISH_SENSOR(this->move_speed_sensors_[index], ts); -#endif // DISTANCE // Optimized: use already decoded tx and ty values, replace pow() with multiplication int32_t x_squared = (int32_t) tx * tx; @@ -503,10 +497,23 @@ void LD2450Component::handle_periodic_data_() { target_count++; } #ifdef USE_SENSOR - SAFE_PUBLISH_SENSOR(this->move_distance_sensors_[index], td); - // ANGLE - atan2f computes angle from Y axis directly, no sqrt/division needed - angle = atan2f(static_cast(-tx), static_cast(ty)) * (180.0f / std::numbers::pi_v); - SAFE_PUBLISH_SENSOR(this->move_angle_sensors_[index], angle); + if (td == 0) { + SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_x_sensors_[index]); + SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_y_sensors_[index]); + SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_resolution_sensors_[index]); + SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_speed_sensors_[index]); + SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_distance_sensors_[index]); + SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_angle_sensors_[index]); + } else { + SAFE_PUBLISH_SENSOR(this->move_x_sensors_[index], tx); + SAFE_PUBLISH_SENSOR(this->move_y_sensors_[index], ty); + SAFE_PUBLISH_SENSOR(this->move_resolution_sensors_[index], res); + SAFE_PUBLISH_SENSOR(this->move_speed_sensors_[index], ts); + SAFE_PUBLISH_SENSOR(this->move_distance_sensors_[index], td); + // ANGLE - atan2f computes angle from Y axis directly, no sqrt/division needed + angle = atan2f(static_cast(-tx), static_cast(ty)) * (180.0f / std::numbers::pi_v); + SAFE_PUBLISH_SENSOR(this->move_angle_sensors_[index], angle); + } #endif #ifdef USE_TEXT_SENSOR // DIRECTION From db15b94cd7b0a2f323572ca66c172246c28752a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 17:17:20 -1000 Subject: [PATCH 321/609] [core] Inline HighFrequencyLoopRequester::is_high_frequency() (#14423) --- esphome/core/helpers.cpp | 1 - esphome/core/helpers.h | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 6d801e7ebc..c75799fe57 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -794,7 +794,6 @@ void HighFrequencyLoopRequester::stop() { num_requests--; this->started_ = false; } -bool HighFrequencyLoopRequester::is_high_frequency() { return num_requests > 0; } std::string get_mac_address() { uint8_t mac[6]; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index ae505a2d8a..187b383f65 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1732,7 +1732,7 @@ class HighFrequencyLoopRequester { void stop(); /// Check whether the loop is running continuously. - static bool is_high_frequency(); + static bool is_high_frequency() { return num_requests > 0; } protected: bool started_{false}; From 60d66ca2dcdc59a351c51a070c5fd326d84d2fda Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Tue, 3 Mar 2026 05:28:01 +0100 Subject: [PATCH 322/609] [openthread] Add tx power option (#14200) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/openthread/__init__.py | 30 ++++++++++++++++++- esphome/components/openthread/openthread.cpp | 3 ++ esphome/components/openthread/openthread.h | 2 ++ .../components/openthread/openthread_esp.cpp | 6 ++++ .../openthread/test.esp32-c6-idf.yaml | 1 + 5 files changed, 41 insertions(+), 1 deletion(-) diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 5861c3db3f..5c64cf31dc 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -4,13 +4,20 @@ from esphome.components.esp32 import ( VARIANT_ESP32C6, VARIANT_ESP32H2, add_idf_sdkconfig_option, + get_esp32_variant, include_builtin_idf_component, only_on_variant, require_vfs_select, ) from esphome.components.mdns import MDNSComponent, enable_mdns_storage import esphome.config_validation as cv -from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID, CONF_USE_ADDRESS +from esphome.const import ( + CONF_CHANNEL, + CONF_ENABLE_IPV6, + CONF_ID, + CONF_OUTPUT_POWER, + CONF_USE_ADDRESS, +) from esphome.core import CORE, TimePeriodMilliseconds import esphome.final_validate as fv from esphome.types import ConfigType @@ -45,6 +52,20 @@ CONF_DEVICE_TYPES = [ ] +def _validate_txpower(value): + if CORE.is_esp32: + variant = get_esp32_variant() + + # HW limits: Datasheet section "802.15.4 RF Transmitter (TX) Characteristics" + # Further regulatory/soft limit may apply, e.g. by region + if variant in (VARIANT_ESP32C6, VARIANT_ESP32C5): + return cv.int_range(min=-15, max=20)(value) + if variant == VARIANT_ESP32H2: + return cv.int_range(min=-24, max=20)(value) + + return value # Unsupported, fail later with clear error + + def set_sdkconfig_options(config): # and expose options for using SPI/UART RCPs add_idf_sdkconfig_option("CONFIG_IEEE802154_ENABLED", True) @@ -155,6 +176,10 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_TLV): cv.string_strict, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, cv.Optional(CONF_POLL_PERIOD): cv.positive_time_period_milliseconds, + cv.Optional(CONF_OUTPUT_POWER): cv.All( + cv.decibel, + _validate_txpower, + ), } ).extend(_CONNECTION_SCHEMA), cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), @@ -197,4 +222,7 @@ async def to_code(config): cg.add(srp.set_mdns(mdns_component)) await cg.register_component(srp, config) + if (output_power := config.get(CONF_OUTPUT_POWER)) is not None: + cg.add(ot.set_output_power(output_power)) + set_sdkconfig_options(config) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index d22a14aeae..92897a7e96 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -43,6 +43,9 @@ void OpenThreadComponent::dump_config() { ESP_LOGCONFIG(TAG, " Device is configured as Minimal End Device (MED)"); } #endif + if (this->output_power_.has_value()) { + ESP_LOGCONFIG(TAG, " Output power: %" PRId8 "dBm", *this->output_power_); + } } bool OpenThreadComponent::is_connected() { diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 9e429f289b..728847afa5 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -38,6 +38,7 @@ class OpenThreadComponent : public Component { #if CONFIG_OPENTHREAD_MTD void set_poll_period(uint32_t poll_period) { this->poll_period_ = poll_period; } #endif + void set_output_power(int8_t output_power) { this->output_power_ = output_power; } protected: std::optional get_omr_address_(InstanceLock &lock); @@ -45,6 +46,7 @@ class OpenThreadComponent : public Component { #if CONFIG_OPENTHREAD_MTD uint32_t poll_period_{0}; #endif + std::optional output_power_{}; bool teardown_started_{false}; bool teardown_complete_{false}; diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 9dd68a1ccc..2af78b729f 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -135,6 +135,12 @@ void OpenThreadComponent::ot_main() { TRUEFALSE(link_mode_config.mRxOnWhenIdle)); #endif + if (this->output_power_.has_value()) { + if (const auto err = otPlatRadioSetTransmitPower(instance, *this->output_power_); err != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set power: %s", otThreadErrorToString(err)); + } + } + // Run the main loop #if CONFIG_OPENTHREAD_CLI esp_openthread_cli_create_task(); diff --git a/tests/components/openthread/test.esp32-c6-idf.yaml b/tests/components/openthread/test.esp32-c6-idf.yaml index 9df63b2f29..77abc433c1 100644 --- a/tests/components/openthread/test.esp32-c6-idf.yaml +++ b/tests/components/openthread/test.esp32-c6-idf.yaml @@ -13,3 +13,4 @@ openthread: force_dataset: true use_address: open-thread-test.local poll_period: 20sec + output_power: 1dBm From 903c67c99499ebdfb60c68816a3cd61114f44897 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 3 Mar 2026 22:16:54 +1300 Subject: [PATCH 323/609] Revert "[wifi] Revert cyw43_wifi_link_status change for RP2040" This reverts commit 1b5bf2c84875977030d073228d31153fe7af64e5. --- esphome/components/wifi/wifi_component_pico_w.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index ad07e1ff25..270425d8c2 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -121,7 +121,12 @@ const char *get_disconnect_reason_str(uint8_t reason) { } WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { - int status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA); + // Use cyw43_wifi_link_status instead of cyw43_tcpip_link_status because the Arduino + // framework's __wrap_cyw43_cb_tcpip_init is a no-op — the SDK's internal netif + // (cyw43_state.netif[]) is never initialized. cyw43_tcpip_link_status checks that netif's + // flags and would only fall through to cyw43_wifi_link_status when the flags aren't set. + // Using cyw43_wifi_link_status directly gives us the actual WiFi radio join state. + int status = cyw43_wifi_link_status(&cyw43_state, CYW43_ITF_STA); switch (status) { case CYW43_LINK_JOIN: // WiFi joined, check if we have an IP address via the Arduino framework's WiFi class From cfde0613bbed38f9ce3d86affcde5fd0b300e972 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 3 Mar 2026 23:53:18 +1100 Subject: [PATCH 324/609] [const][uart][usb_uart][weikai][core] Move constants to components/const (#14430) --- esphome/components/const/__init__.py | 3 +++ esphome/components/uart/__init__.py | 4 +--- esphome/components/usb_uart/__init__.py | 4 +--- esphome/components/weikai/__init__.py | 4 +--- esphome/const.py | 3 --- .../fixtures/external_components/uart_mock/__init__.py | 4 +--- 6 files changed, 7 insertions(+), 15 deletions(-) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 3201db5dfd..059bf3f26a 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -8,14 +8,17 @@ BYTE_ORDER_BIG = "big_endian" CONF_COLOR_DEPTH = "color_depth" CONF_CRC_ENABLE = "crc_enable" +CONF_DATA_BITS = "data_bits" CONF_DRAW_ROUNDING = "draw_rounding" CONF_ENABLED = "enabled" CONF_IGNORE_NOT_FOUND = "ignore_not_found" CONF_ON_PACKET = "on_packet" CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" +CONF_PARITY = "parity" CONF_REQUEST_HEADERS = "request_headers" CONF_ROWS = "rows" +CONF_STOP_BITS = "stop_bits" CONF_USE_PSRAM = "use_psram" ICON_CURRENT_DC = "mdi:current-dc" diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 69db4b44aa..3bc4263b31 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -4,6 +4,7 @@ import re from esphome import automation, pins import esphome.codegen as cg +from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( @@ -11,7 +12,6 @@ from esphome.const import ( CONF_BAUD_RATE, CONF_BYTES, CONF_DATA, - CONF_DATA_BITS, CONF_DEBUG, CONF_DELIMITER, CONF_DIRECTION, @@ -21,12 +21,10 @@ from esphome.const import ( CONF_ID, CONF_LAMBDA, CONF_NUMBER, - CONF_PARITY, CONF_PORT, CONF_RX_BUFFER_SIZE, CONF_RX_PIN, CONF_SEQUENCE, - CONF_STOP_BITS, CONF_TIMEOUT, CONF_TRIGGER_ID, CONF_TX_PIN, diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index cc69c0cb5a..f0ee53d028 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import socket +from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS from esphome.components.uart import UARTComponent from esphome.components.usb_host import register_usb_client, usb_device_schema import esphome.config_validation as cv @@ -7,12 +8,9 @@ from esphome.const import ( CONF_BAUD_RATE, CONF_BUFFER_SIZE, CONF_CHANNELS, - CONF_DATA_BITS, CONF_DEBUG, CONF_DUMMY_RECEIVER, CONF_ID, - CONF_PARITY, - CONF_STOP_BITS, ) from esphome.cpp_types import Component diff --git a/esphome/components/weikai/__init__.py b/esphome/components/weikai/__init__.py index 66cd4ce12a..bc80f167ef 100644 --- a/esphome/components/weikai/__init__.py +++ b/esphome/components/weikai/__init__.py @@ -1,18 +1,16 @@ import esphome.codegen as cg from esphome.components import uart +from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS import esphome.config_validation as cv from esphome.const import ( CONF_BAUD_RATE, CONF_CHANNEL, - CONF_DATA_BITS, CONF_ID, CONF_INPUT, CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_OUTPUT, - CONF_PARITY, - CONF_STOP_BITS, ) CODEOWNERS = ["@DrCoolZic"] diff --git a/esphome/const.py b/esphome/const.py index bbd85ca66b..d5625f6a54 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -280,7 +280,6 @@ CONF_CUSTOM_PRESETS = "custom_presets" CONF_CYCLE = "cycle" CONF_DALLAS_ID = "dallas_id" CONF_DATA = "data" -CONF_DATA_BITS = "data_bits" CONF_DATA_PIN = "data_pin" CONF_DATA_PINS = "data_pins" CONF_DATA_RATE = "data_rate" @@ -760,7 +759,6 @@ CONF_PAGE_ID = "page_id" CONF_PAGES = "pages" CONF_PANASONIC = "panasonic" CONF_PARAMETERS = "parameters" -CONF_PARITY = "parity" CONF_PASSWORD = "password" CONF_PATH = "path" CONF_PATTERN = "pattern" @@ -963,7 +961,6 @@ CONF_STEP_PIN = "step_pin" CONF_STILL_THRESHOLD = "still_threshold" CONF_STOP = "stop" CONF_STOP_ACTION = "stop_action" -CONF_STOP_BITS = "stop_bits" CONF_STORE_BASELINE = "store_baseline" CONF_SUBNET = "subnet" CONF_SUBSCRIBE_QOS = "subscribe_qos" diff --git a/tests/integration/fixtures/external_components/uart_mock/__init__.py b/tests/integration/fixtures/external_components/uart_mock/__init__.py index dea8c38551..8deab4c21e 100644 --- a/tests/integration/fixtures/external_components/uart_mock/__init__.py +++ b/tests/integration/fixtures/external_components/uart_mock/__init__.py @@ -1,6 +1,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import uart +from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS from esphome.components.uart import ( CONF_RX_FULL_THRESHOLD, CONF_RX_TIMEOUT, @@ -12,14 +13,11 @@ import esphome.config_validation as cv from esphome.const import ( CONF_BAUD_RATE, CONF_DATA, - CONF_DATA_BITS, CONF_DEBUG, CONF_DELAY, CONF_ID, CONF_INTERVAL, - CONF_PARITY, CONF_RX_BUFFER_SIZE, - CONF_STOP_BITS, CONF_TRIGGER_ID, ) from esphome.core import ID From b6f0bb9b6bcf12f7021a4904df24954712ab0f78 Mon Sep 17 00:00:00 2001 From: rwrozelle Date: Tue, 3 Mar 2026 07:59:01 -0800 Subject: [PATCH 325/609] [speaker] Add off on capability to media player (#9295) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Kevin Ahrendt --- .../speaker/media_player/__init__.py | 5 ++ .../media_player/speaker_media_player.cpp | 61 +++++++++++++++++++ .../media_player/speaker_media_player.h | 3 + esphome/core/defines.h | 1 + .../speaker/common-media_player_off_on.yaml | 18 ++++++ .../media_player_off_on.esp32-idf.yaml | 9 +++ 6 files changed, 97 insertions(+) create mode 100644 tests/components/speaker/common-media_player_off_on.yaml create mode 100644 tests/components/speaker/media_player_off_on.esp32-idf.yaml diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index b302bd9b23..42ca762858 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -15,6 +15,8 @@ from esphome.const import ( CONF_FORMAT, CONF_ID, CONF_NUM_CHANNELS, + CONF_ON_TURN_OFF, + CONF_ON_TURN_ON, CONF_PATH, CONF_RAW_DATA_ID, CONF_SAMPLE_RATE, @@ -401,6 +403,9 @@ FINAL_VALIDATE_SCHEMA = cv.All( async def to_code(config): + if CONF_ON_TURN_OFF in config or CONF_ON_TURN_ON in config: + cg.add_define("USE_SPEAKER_MEDIA_PLAYER_ON_OFF", True) + var = await media_player.new_media_player(config) await cg.register_component(var, config) diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index fdf6bf66cd..3f5cb2fda6 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -51,7 +51,11 @@ static const UBaseType_t ANNOUNCEMENT_PIPELINE_TASK_PRIORITY = 1; static const char *const TAG = "speaker_media_player"; void SpeakerMediaPlayer::setup() { +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + state = media_player::MEDIA_PLAYER_STATE_OFF; +#else state = media_player::MEDIA_PLAYER_STATE_IDLE; +#endif this->media_control_command_queue_ = xQueueCreate(MEDIA_CONTROLS_QUEUE_LENGTH, sizeof(MediaCallCommand)); @@ -128,6 +132,12 @@ void SpeakerMediaPlayer::watch_media_commands_() { bool enqueue = media_command.enqueue.has_value() && media_command.enqueue.value(); if (media_command.url.has_value() || media_command.file.has_value()) { +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + if (this->state == media_player::MEDIA_PLAYER_STATE_OFF) { + this->state = media_player::MEDIA_PLAYER_STATE_ON; + publish_state(); + } +#endif PlaylistItem playlist_item; if (media_command.url.has_value()) { playlist_item.url = *media_command.url.value(); @@ -184,6 +194,12 @@ void SpeakerMediaPlayer::watch_media_commands_() { if (media_command.command.has_value()) { switch (media_command.command.value()) { case media_player::MEDIA_PLAYER_COMMAND_PLAY: +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + if (this->state == media_player::MEDIA_PLAYER_STATE_OFF) { + this->state = media_player::MEDIA_PLAYER_STATE_ON; + publish_state(); + } +#endif if ((this->media_pipeline_ != nullptr) && (this->is_paused_)) { this->media_pipeline_->set_pause_state(false); } @@ -195,10 +211,26 @@ void SpeakerMediaPlayer::watch_media_commands_() { } this->is_paused_ = true; break; +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + case media_player::MEDIA_PLAYER_COMMAND_TURN_ON: + if (this->state == media_player::MEDIA_PLAYER_STATE_OFF) { + this->state = media_player::MEDIA_PLAYER_STATE_ON; + this->publish_state(); + } + break; + case media_player::MEDIA_PLAYER_COMMAND_TURN_OFF: + this->is_turn_off_ = true; + // Intentional Fall-through +#endif case media_player::MEDIA_PLAYER_COMMAND_STOP: // Pipelines do not stop immediately after calling the stop command, so confirm its stopped before unpausing. // This avoids an audible short segment playing after receiving the stop command in a paused state. +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value()) || + (this->is_turn_off_ && this->announcement_pipeline_state_ != AudioPipelineState::STOPPED)) { +#else if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) { +#endif if (this->announcement_pipeline_ != nullptr) { this->cancel_timeout("next_ann"); this->announcement_playlist_.clear(); @@ -366,7 +398,13 @@ void SpeakerMediaPlayer::loop() { } } else { if (this->is_paused_) { +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + if (this->state != media_player::MEDIA_PLAYER_STATE_OFF) { + this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; + } +#else this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; +#endif } else if (this->media_pipeline_state_ == AudioPipelineState::PLAYING) { this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; } else if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) { @@ -399,7 +437,13 @@ void SpeakerMediaPlayer::loop() { } } } else { +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + if (this->state != media_player::MEDIA_PLAYER_STATE_OFF) { + this->state = media_player::MEDIA_PLAYER_STATE_IDLE; + } +#else this->state = media_player::MEDIA_PLAYER_STATE_IDLE; +#endif } } } @@ -409,6 +453,20 @@ void SpeakerMediaPlayer::loop() { this->publish_state(); ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state)); } +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + if (this->is_turn_off_ && (this->state == media_player::MEDIA_PLAYER_STATE_PAUSED || + this->state == media_player::MEDIA_PLAYER_STATE_IDLE)) { + this->is_turn_off_ = false; + if (this->state == media_player::MEDIA_PLAYER_STATE_PAUSED) { + this->state = media_player::MEDIA_PLAYER_STATE_IDLE; + this->publish_state(); + ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state)); + } + this->state = media_player::MEDIA_PLAYER_STATE_OFF; + this->publish_state(); + ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state)); + } +#endif } void SpeakerMediaPlayer::play_file(audio::AudioFile *media_file, bool announcement, bool enqueue) { @@ -481,6 +539,9 @@ media_player::MediaPlayerTraits SpeakerMediaPlayer::get_traits() { if (!this->single_pipeline_()) { traits.set_supports_pause(true); } +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + traits.set_supports_turn_off_on(true); +#endif if (this->announcement_format_.has_value()) { traits.get_supported_formats().push_back(this->announcement_format_.value()); diff --git a/esphome/components/speaker/media_player/speaker_media_player.h b/esphome/components/speaker/media_player/speaker_media_player.h index 6796fc9c00..3fa6f47b84 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.h +++ b/esphome/components/speaker/media_player/speaker_media_player.h @@ -144,6 +144,9 @@ class SpeakerMediaPlayer : public Component, bool is_paused_{false}; bool is_muted_{false}; +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + bool is_turn_off_{false}; +#endif uint8_t unpause_media_remaining_{0}; uint8_t unpause_announcement_remaining_{0}; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 7fbc5a0b53..8d778edf2a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -233,6 +233,7 @@ #define USE_LWIP_FAST_SELECT #define USE_WAKE_LOOP_THREADSAFE #define USE_SPEAKER +#define USE_SPEAKER_MEDIA_PLAYER_ON_OFF #define USE_SPI #define USE_VOICE_ASSISTANT #define USE_WEBSERVER diff --git a/tests/components/speaker/common-media_player_off_on.yaml b/tests/components/speaker/common-media_player_off_on.yaml new file mode 100644 index 0000000000..a5bea62c84 --- /dev/null +++ b/tests/components/speaker/common-media_player_off_on.yaml @@ -0,0 +1,18 @@ +<<: !include common.yaml + +media_player: + - platform: speaker + id: speaker_media_player_id + announcement_pipeline: + speaker: speaker_id + buffer_size: 1000000 + volume_increment: 0.02 + volume_max: 0.95 + volume_min: 0.0 + task_stack_in_psram: true + on_turn_on: + then: + - logger.log: "Turn On Media Player" + on_turn_off: + then: + - logger.log: "Turn Off Media Player" diff --git a/tests/components/speaker/media_player_off_on.esp32-idf.yaml b/tests/components/speaker/media_player_off_on.esp32-idf.yaml new file mode 100644 index 0000000000..2d5eefff19 --- /dev/null +++ b/tests/components/speaker/media_player_off_on.esp32-idf.yaml @@ -0,0 +1,9 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + i2s_bclk_pin: GPIO27 + i2s_lrclk_pin: GPIO26 + i2s_mclk_pin: GPIO25 + i2s_dout_pin: GPIO23 + +<<: !include common-media_player_off_on.yaml From d53ff7892a85d623f7c116c253075d82b2fd0a95 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Mar 2026 07:03:02 -1000 Subject: [PATCH 326/609] [socket] Cache lwip_sock pointers and inline ready() chain (#14408) --- .../components/socket/bsd_sockets_impl.cpp | 33 +++++++------- esphome/components/socket/bsd_sockets_impl.h | 7 +++ .../components/socket/lwip_sockets_impl.cpp | 33 +++++++------- esphome/components/socket/lwip_sockets_impl.h | 7 +++ esphome/components/socket/socket.cpp | 6 ++- esphome/components/socket/socket.h | 45 +++++++++++++++++-- esphome/core/application.cpp | 43 ++++++++++++------ esphome/core/application.h | 32 ++++++------- esphome/core/lwip_fast_select.c | 28 +++++------- esphome/core/lwip_fast_select.h | 45 ++++++++++++++++--- 10 files changed, 190 insertions(+), 89 deletions(-) diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index 92ecfc692b..aea7c776c6 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -11,11 +11,15 @@ namespace esphome::socket { BSDSocketImpl::BSDSocketImpl(int fd, bool monitor_loop) { this->fd_ = fd; - // Register new socket with the application for select() if monitoring requested - if (monitor_loop && this->fd_ >= 0) { - // Only set loop_monitored_ to true if registration succeeds - this->loop_monitored_ = App.register_socket_fd(this->fd_); - } + if (!monitor_loop || this->fd_ < 0) + return; +#ifdef USE_LWIP_FAST_SELECT + // Cache lwip_sock pointer and register for monitoring (hooks callback internally) + this->cached_sock_ = esphome_lwip_get_sock(this->fd_); + this->loop_monitored_ = App.register_socket(this->cached_sock_); +#else + this->loop_monitored_ = App.register_socket_fd(this->fd_); +#endif } BSDSocketImpl::~BSDSocketImpl() { @@ -26,10 +30,17 @@ BSDSocketImpl::~BSDSocketImpl() { int BSDSocketImpl::close() { if (!this->closed_) { - // Unregister from select() before closing if monitored + // Unregister before closing to avoid dangling pointer in monitored set +#ifdef USE_LWIP_FAST_SELECT + if (this->loop_monitored_) { + App.unregister_socket(this->cached_sock_); + this->cached_sock_ = nullptr; + } +#else if (this->loop_monitored_) { App.unregister_socket_fd(this->fd_); } +#endif int ret = ::close(this->fd_); this->closed_ = true; return ret; @@ -48,8 +59,6 @@ int BSDSocketImpl::setblocking(bool blocking) { return 0; } -bool BSDSocketImpl::ready() const { return socket_ready_fd(this->fd_, this->loop_monitored_); } - size_t BSDSocketImpl::getpeername_to(std::span buf) { struct sockaddr_storage storage; socklen_t len = sizeof(storage); @@ -86,14 +95,6 @@ std::unique_ptr socket_loop_monitored(int domain, int type, int protocol return create_socket(domain, type, protocol, true); } -std::unique_ptr socket_listen(int domain, int type, int protocol) { - return create_socket(domain, type, protocol, false); -} - -std::unique_ptr socket_listen_loop_monitored(int domain, int type, int protocol) { - return create_socket(domain, type, protocol, true); -} - } // namespace esphome::socket #endif // USE_SOCKET_IMPL_BSD_SOCKETS diff --git a/esphome/components/socket/bsd_sockets_impl.h b/esphome/components/socket/bsd_sockets_impl.h index d9ed9dc567..9ebbe72002 100644 --- a/esphome/components/socket/bsd_sockets_impl.h +++ b/esphome/components/socket/bsd_sockets_impl.h @@ -13,6 +13,10 @@ #include #endif +#ifdef USE_LWIP_FAST_SELECT +struct lwip_sock; +#endif + namespace esphome::socket { class BSDSocketImpl { @@ -105,6 +109,9 @@ class BSDSocketImpl { protected: int fd_{-1}; +#ifdef USE_LWIP_FAST_SELECT + struct lwip_sock *cached_sock_{nullptr}; // Cached for direct rcvevent read in ready() +#endif bool closed_{false}; bool loop_monitored_{false}; }; diff --git a/esphome/components/socket/lwip_sockets_impl.cpp b/esphome/components/socket/lwip_sockets_impl.cpp index 0322820ef4..2fad429e0f 100644 --- a/esphome/components/socket/lwip_sockets_impl.cpp +++ b/esphome/components/socket/lwip_sockets_impl.cpp @@ -11,11 +11,15 @@ namespace esphome::socket { LwIPSocketImpl::LwIPSocketImpl(int fd, bool monitor_loop) { this->fd_ = fd; - // Register new socket with the application for select() if monitoring requested - if (monitor_loop && this->fd_ >= 0) { - // Only set loop_monitored_ to true if registration succeeds - this->loop_monitored_ = App.register_socket_fd(this->fd_); - } + if (!monitor_loop || this->fd_ < 0) + return; +#ifdef USE_LWIP_FAST_SELECT + // Cache lwip_sock pointer and register for monitoring (hooks callback internally) + this->cached_sock_ = esphome_lwip_get_sock(this->fd_); + this->loop_monitored_ = App.register_socket(this->cached_sock_); +#else + this->loop_monitored_ = App.register_socket_fd(this->fd_); +#endif } LwIPSocketImpl::~LwIPSocketImpl() { @@ -26,10 +30,17 @@ LwIPSocketImpl::~LwIPSocketImpl() { int LwIPSocketImpl::close() { if (!this->closed_) { - // Unregister from select() before closing if monitored + // Unregister before closing to avoid dangling pointer in monitored set +#ifdef USE_LWIP_FAST_SELECT + if (this->loop_monitored_) { + App.unregister_socket(this->cached_sock_); + this->cached_sock_ = nullptr; + } +#else if (this->loop_monitored_) { App.unregister_socket_fd(this->fd_); } +#endif int ret = lwip_close(this->fd_); this->closed_ = true; return ret; @@ -48,8 +59,6 @@ int LwIPSocketImpl::setblocking(bool blocking) { return 0; } -bool LwIPSocketImpl::ready() const { return socket_ready_fd(this->fd_, this->loop_monitored_); } - size_t LwIPSocketImpl::getpeername_to(std::span buf) { struct sockaddr_storage storage; socklen_t len = sizeof(storage); @@ -86,14 +95,6 @@ std::unique_ptr socket_loop_monitored(int domain, int type, int protocol return create_socket(domain, type, protocol, true); } -std::unique_ptr socket_listen(int domain, int type, int protocol) { - return create_socket(domain, type, protocol, false); -} - -std::unique_ptr socket_listen_loop_monitored(int domain, int type, int protocol) { - return create_socket(domain, type, protocol, true); -} - } // namespace esphome::socket #endif // USE_SOCKET_IMPL_LWIP_SOCKETS diff --git a/esphome/components/socket/lwip_sockets_impl.h b/esphome/components/socket/lwip_sockets_impl.h index d6699aded2..c579219863 100644 --- a/esphome/components/socket/lwip_sockets_impl.h +++ b/esphome/components/socket/lwip_sockets_impl.h @@ -9,6 +9,10 @@ #include "esphome/core/helpers.h" #include "headers.h" +#ifdef USE_LWIP_FAST_SELECT +struct lwip_sock; +#endif + namespace esphome::socket { class LwIPSocketImpl { @@ -71,6 +75,9 @@ class LwIPSocketImpl { protected: int fd_{-1}; +#ifdef USE_LWIP_FAST_SELECT + struct lwip_sock *cached_sock_{nullptr}; // Cached for direct rcvevent read in ready() +#endif bool closed_{false}; bool loop_monitored_{false}; }; diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index c04671c7ee..bfb6ae8e13 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -8,7 +8,7 @@ namespace esphome::socket { -#ifdef USE_SOCKET_SELECT_SUPPORT +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) // Shared ready() implementation for fd-based socket implementations (BSD and LWIP sockets). // Checks if the Application's select() loop has marked this fd as ready. bool socket_ready_fd(int fd, bool loop_monitored) { return !loop_monitored || App.is_socket_ready_(fd); } @@ -89,6 +89,9 @@ std::unique_ptr socket_ip(int type, int protocol) { #endif /* USE_NETWORK_IPV6 */ } +#ifdef USE_SOCKET_IMPL_LWIP_TCP +// LWIP_TCP has separate Socket/ListenSocket types — needs out-of-line factory. +// BSD and LWIP_SOCKETS define this inline in socket.h. std::unique_ptr socket_ip_loop_monitored(int type, int protocol) { #if USE_NETWORK_IPV6 return socket_listen_loop_monitored(AF_INET6, type, protocol); @@ -96,6 +99,7 @@ std::unique_ptr socket_ip_loop_monitored(int type, int protocol) { return socket_listen_loop_monitored(AF_INET, type, protocol); #endif /* USE_NETWORK_IPV6 */ } +#endif socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_address, uint16_t port) { #if USE_NETWORK_IPV6 diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 546d278260..0884e4ba3e 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -6,6 +6,10 @@ #include "esphome/core/optional.h" #include "headers.h" +#ifdef USE_LWIP_FAST_SELECT +#include "esphome/core/lwip_fast_select.h" +#endif + #if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) // Include only the active implementation's header. @@ -36,12 +40,29 @@ using Socket = LWIPRawImpl; using ListenSocket = LWIPRawListenImpl; #endif -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_LWIP_FAST_SELECT +/// Shared ready() helper using cached lwip_sock pointer for direct rcvevent read. +inline bool socket_ready(struct lwip_sock *cached_sock, bool loop_monitored) { + return !loop_monitored || (cached_sock != nullptr && esphome_lwip_socket_has_data(cached_sock)); +} +#elif defined(USE_SOCKET_SELECT_SUPPORT) /// Shared ready() helper for fd-based socket implementations. /// Checks if the Application's select() loop has marked this fd as ready. bool socket_ready_fd(int fd, bool loop_monitored); #endif +// Inline ready() — defined here because it depends on socket_ready/socket_ready_fd +// declared above, while the impl headers are included before those declarations. +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) +inline bool Socket::ready() const { +#ifdef USE_LWIP_FAST_SELECT + return socket_ready(this->cached_sock_, this->loop_monitored_); +#else + return socket_ready_fd(this->fd_, this->loop_monitored_); +#endif +} +#endif + /// Create a socket of the given domain, type and protocol. std::unique_ptr socket(int domain, int type, int protocol); /// Create a socket in the newest available IP domain (IPv6 or IPv4) of the given type and protocol. @@ -56,11 +77,29 @@ std::unique_ptr socket_ip(int type, int protocol); std::unique_ptr socket_loop_monitored(int domain, int type, int protocol); /// Create a listening socket of the given domain, type and protocol. -std::unique_ptr socket_listen(int domain, int type, int protocol); /// Create a listening socket and monitor it for data in the main loop. -std::unique_ptr socket_listen_loop_monitored(int domain, int type, int protocol); /// Create a listening socket in the newest available IP domain and monitor it. +#ifdef USE_SOCKET_IMPL_LWIP_TCP +// LWIP_TCP has separate Socket/ListenSocket types — needs distinct factory functions. +std::unique_ptr socket_listen(int domain, int type, int protocol); +std::unique_ptr socket_listen_loop_monitored(int domain, int type, int protocol); std::unique_ptr socket_ip_loop_monitored(int type, int protocol); +#else +// BSD and LWIP_SOCKETS: Socket == ListenSocket, so listen variants just delegate. +inline std::unique_ptr socket_listen(int domain, int type, int protocol) { + return socket(domain, type, protocol); +} +inline std::unique_ptr socket_listen_loop_monitored(int domain, int type, int protocol) { + return socket_loop_monitored(domain, type, protocol); +} +inline std::unique_ptr socket_ip_loop_monitored(int type, int protocol) { +#if USE_NETWORK_IPV6 + return socket_loop_monitored(AF_INET6, type, protocol); +#else + return socket_loop_monitored(AF_INET, type, protocol); +#endif +} +#endif /// Set a sockaddr to the specified address and port for the IP version used by socket_ip(). /// @param addr Destination sockaddr structure diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 26cd670629..8c2ba58c86 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -552,7 +552,32 @@ void Application::after_loop_tasks_() { this->in_loop_ = false; } -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_LWIP_FAST_SELECT +bool Application::register_socket(struct lwip_sock *sock) { + // It modifies monitored_sockets_ without locking — must only be called from the main loop. + if (sock == nullptr) + return false; + esphome_lwip_hook_socket(sock); + this->monitored_sockets_.push_back(sock); + return true; +} + +void Application::unregister_socket(struct lwip_sock *sock) { + // It modifies monitored_sockets_ without locking — must only be called from the main loop. + for (size_t i = 0; i < this->monitored_sockets_.size(); i++) { + if (this->monitored_sockets_[i] != sock) + continue; + + // Swap with last element and pop - O(1) removal since order doesn't matter. + // No need to unhook the netconn callback — all LwIP sockets share the same + // static event_callback, and the socket will be closed by the caller. + if (i < this->monitored_sockets_.size() - 1) + this->monitored_sockets_[i] = this->monitored_sockets_.back(); + this->monitored_sockets_.pop_back(); + return; + } +} +#elif defined(USE_SOCKET_SELECT_SUPPORT) bool Application::register_socket_fd(int fd) { // WARNING: This function is NOT thread-safe and must only be called from the main loop // It modifies socket_fds_ and related variables without locking @@ -571,15 +596,10 @@ bool Application::register_socket_fd(int fd) { #endif this->socket_fds_.push_back(fd); -#ifdef USE_LWIP_FAST_SELECT - // Hook the socket's netconn callback for instant wake on receive events - esphome_lwip_hook_socket(fd); -#else this->socket_fds_changed_ = true; if (fd > this->max_fd_) { this->max_fd_ = fd; } -#endif return true; } @@ -595,13 +615,9 @@ void Application::unregister_socket_fd(int fd) { continue; // Swap with last element and pop - O(1) removal since order doesn't matter. - // No need to unhook the netconn callback on fast select platforms — all LwIP - // sockets share the same static event_callback, and the socket will be closed - // by the caller. if (i < this->socket_fds_.size() - 1) this->socket_fds_[i] = this->socket_fds_.back(); this->socket_fds_.pop_back(); -#ifndef USE_LWIP_FAST_SELECT this->socket_fds_changed_ = true; // Only recalculate max_fd if we removed the current max if (fd == this->max_fd_) { @@ -611,7 +627,6 @@ void Application::unregister_socket_fd(int fd) { this->max_fd_ = sock_fd; } } -#endif return; } } @@ -621,7 +636,7 @@ void Application::unregister_socket_fd(int fd) { void Application::yield_with_select_(uint32_t delay_ms) { // Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run. #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) - // Fast path (ESP32/LibreTiny): reads rcvevent directly via lwip_socket_dbg_get_socket(). + // Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers. // Safe because this runs on the main loop which owns socket lifetime (create, read, close). if (delay_ms == 0) [[unlikely]] { yield(); @@ -632,8 +647,8 @@ void Application::yield_with_select_(uint32_t delay_ms) { // If a socket still has unread data (rcvevent > 0) but the task notification was already // consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency. // This scan preserves select() semantics: return immediately when any fd is ready. - for (int fd : this->socket_fds_) { - if (esphome_lwip_socket_has_data(fd)) { + for (struct lwip_sock *sock : this->monitored_sockets_) { + if (esphome_lwip_socket_has_data(sock)) { yield(); return; } diff --git a/esphome/core/application.h b/esphome/core/application.h index 63d59c555e..40f8a00edd 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -500,14 +500,20 @@ class Application { Scheduler scheduler; - /// Register/unregister a socket file descriptor to be monitored for read events. -#ifdef USE_SOCKET_SELECT_SUPPORT - /// These functions update the fd_set used by select() in the main loop. + /// Register/unregister a socket to be monitored for read events. /// WARNING: These functions are NOT thread-safe. They must only be called from the main loop. +#ifdef USE_LWIP_FAST_SELECT + /// Fast select path: hooks netconn callback and registers for monitoring. + /// @return true if registration was successful, false if sock is null + bool register_socket(struct lwip_sock *sock); + void unregister_socket(struct lwip_sock *sock); +#elif defined(USE_SOCKET_SELECT_SUPPORT) + /// Fallback select() path: monitors file descriptors. /// NOTE: File descriptors >= FD_SETSIZE (typically 10 on ESP) will be rejected with an error. /// @return true if registration was successful, false if fd exceeds limits bool register_socket_fd(int fd); void unregister_socket_fd(int fd); +#endif #ifdef USE_WAKE_LOOP_THREADSAFE /// Wake the main event loop from another FreeRTOS task. @@ -532,7 +538,6 @@ class Application { static void IRAM_ATTR wake_loop_any_context() { esphome_lwip_wake_main_loop_any_context(); } #endif #endif -#endif #if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) /// Wake the main event loop from any context (ISR, thread, or main loop). @@ -542,23 +547,14 @@ class Application { protected: friend Component; -#ifdef USE_SOCKET_SELECT_SUPPORT +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) friend bool socket::socket_ready_fd(int fd, bool loop_monitored); #endif friend void ::setup(); friend void ::original_setup(); -#ifdef USE_SOCKET_SELECT_SUPPORT - /// Fast path for Socket::ready() via friendship - skips negative fd check. - /// Main loop only — with USE_LWIP_FAST_SELECT, reads rcvevent via - /// lwip_socket_dbg_get_socket(), which has no refcount; safe only because - /// the main loop owns socket lifetime (creates, reads, and closes sockets - /// on the same thread). -#ifdef USE_LWIP_FAST_SELECT - bool is_socket_ready_(int fd) const { return esphome_lwip_socket_has_data(fd); } -#else +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); } -#endif #endif /// Register a component, detecting loop() override at compile time. @@ -620,8 +616,12 @@ class Application { // and active_end_ is incremented // - This eliminates branch mispredictions from flag checking in the hot loop FixedVector looping_components_{}; -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_LWIP_FAST_SELECT + std::vector monitored_sockets_; // Cached lwip_sock pointers for direct rcvevent read +#elif defined(USE_SOCKET_SELECT_SUPPORT) std::vector socket_fds_; // Vector of all monitored socket file descriptors +#endif +#ifdef USE_SOCKET_SELECT_SUPPORT #if defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks #endif diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index 989f66e9be..c578a9aae9 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -140,8 +140,10 @@ _Static_assert(sizeof(TaskHandle_t) <= 4, "TaskHandle_t must be <= 4 bytes for atomic access"); _Static_assert(sizeof(netconn_callback) <= 4, "netconn_callback must be <= 4 bytes for atomic access"); -// rcvevent must fit in a single atomic read -_Static_assert(sizeof(((struct lwip_sock *) 0)->rcvevent) <= 4, "rcvevent must be <= 4 bytes for atomic access"); +// rcvevent must be exactly 2 bytes (s16_t) — the inline in lwip_fast_select.h reads it as int16_t. +// If lwIP changes this to int or similar, the offset assert would still pass but the load width would be wrong. +_Static_assert(sizeof(((struct lwip_sock *) 0)->rcvevent) == 2, + "rcvevent size changed — update int16_t cast in esphome_lwip_socket_has_data() in lwip_fast_select.h"); // Struct member alignment — natural alignment guarantees atomicity on Xtensa/RISC-V/ARM. // Misaligned access would not be atomic even if the size is <= 4 bytes. @@ -150,6 +152,10 @@ _Static_assert(offsetof(struct netconn, callback) % sizeof(netconn_callback) == _Static_assert(offsetof(struct lwip_sock, rcvevent) % sizeof(((struct lwip_sock *) 0)->rcvevent) == 0, "lwip_sock.rcvevent must be naturally aligned for atomic access"); +// Verify the hardcoded offset used in the header's inline esphome_lwip_socket_has_data(). +_Static_assert(offsetof(struct lwip_sock, rcvevent) == ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET, + "lwip_sock.rcvevent offset changed — update ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET in lwip_fast_select.h"); + // Task handle for the main loop — written once in init(), read from TCP/IP and background tasks. static TaskHandle_t s_main_loop_task = NULL; @@ -194,23 +200,11 @@ static inline struct lwip_sock *get_sock(int fd) { return sock; } -bool esphome_lwip_socket_has_data(int fd) { - struct lwip_sock *sock = get_sock(fd); - if (sock == NULL) - return false; - // volatile prevents the compiler from caching/reordering this cross-thread read. - // The write side (TCP/IP thread) commits via SYS_ARCH_UNPROTECT which releases a - // FreeRTOS mutex (ESP32) or resumes the scheduler (LibreTiny), ensuring the value - // is visible. Aligned 16-bit reads are single-instruction loads (L16SI/LH/LDRH) on - // Xtensa/RISC-V/ARM and cannot produce torn values. - return *(volatile s16_t *) &sock->rcvevent > 0; +struct lwip_sock *esphome_lwip_get_sock(int fd) { + return get_sock(fd); } -void esphome_lwip_hook_socket(int fd) { - struct lwip_sock *sock = get_sock(fd); - if (sock == NULL) - return; - +void esphome_lwip_hook_socket(struct lwip_sock *sock) { // Save original callback once — all LwIP sockets share the same static event_callback // (DEFAULT_SOCKET_EVENTCB in sockets.c, used for SOCK_RAW, SOCK_DGRAM, and SOCK_STREAM). if (s_original_callback == NULL) { diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h index 6fce34fd76..46c6b711cd 100644 --- a/esphome/core/lwip_fast_select.h +++ b/esphome/core/lwip_fast_select.h @@ -4,6 +4,17 @@ // Replaces lwip_select() with direct rcvevent reads and FreeRTOS task notifications. #include +#include + +// Forward declare lwip_sock for C++ callers that store cached pointers. +// The full definition is only available in the .c file (lwip/priv/sockets_priv.h +// conflicts with C++ compilation units). +struct lwip_sock; + +// Byte offset of rcvevent (s16_t) within struct lwip_sock. +// Verified at compile time in lwip_fast_select.c via _Static_assert. +// Anonymous enum for a compile-time constant that works in both C and C++. +enum { ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET = 8 }; #ifdef __cplusplus extern "C" { @@ -13,16 +24,38 @@ extern "C" { /// Saves the current task handle for xTaskNotifyGive() wake notifications. void esphome_lwip_fast_select_init(void); -/// Check if a LwIP socket has data ready via direct rcvevent read (~215 ns per socket). -/// Uses lwip_socket_dbg_get_socket() — a direct array lookup without the refcount that -/// get_socket()/done_socket() uses. Safe because the caller owns the socket lifetime: -/// both has_data reads and socket close/unregister happen on the main loop thread. -bool esphome_lwip_socket_has_data(int fd); +/// Look up a LwIP socket struct from a file descriptor. +/// Returns NULL if fd is invalid or the socket/netconn is not initialized. +/// Use this at registration time to cache the pointer for esphome_lwip_socket_has_data(). +struct lwip_sock *esphome_lwip_get_sock(int fd); + +/// Check if a cached LwIP socket has data ready via unlocked hint read of rcvevent. +/// This avoids lwIP core lock contention between the main loop (CPU0) and +/// streaming/networking work (CPU1). Correctness is preserved because callers +/// already handle EWOULDBLOCK on nonblocking sockets — a stale hint simply causes +/// a harmless retry on the next loop iteration. In practice, stale reads have not +/// been observed across multi-day testing, but the design does not depend on that. +/// +/// The sock pointer must have been obtained from esphome_lwip_get_sock() and must +/// remain valid (caller owns socket lifetime — no concurrent close). +/// Hot path: inlined volatile 16-bit load — no function call overhead. +/// Uses offset-based access because lwip/priv/sockets_priv.h conflicts with C++. +/// The offset and size are verified at compile time in lwip_fast_select.c. +static inline bool esphome_lwip_socket_has_data(struct lwip_sock *sock) { + // Unlocked hint read — no lwIP core lock needed. + // volatile prevents the compiler from caching/reordering this cross-thread read. + // The write side (TCP/IP thread) commits via SYS_ARCH_UNPROTECT which releases a + // FreeRTOS mutex (ESP32) or resumes the scheduler (LibreTiny), ensuring the value + // is visible. Aligned 16-bit reads are single-instruction loads (L16SI/LH/LDRH) on + // Xtensa/RISC-V/ARM and cannot produce torn values. + return *(volatile int16_t *) ((char *) sock + (int) ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET) > 0; +} /// Hook a socket's netconn callback to notify the main loop task on receive events. /// Wraps the original event_callback with one that also calls xTaskNotifyGive(). /// Must be called from the main loop after socket creation. -void esphome_lwip_hook_socket(int fd); +/// The sock pointer must have been obtained from esphome_lwip_get_sock(). +void esphome_lwip_hook_socket(struct lwip_sock *sock); /// Wake the main loop task from another FreeRTOS task — costs <1 us. /// NOT ISR-safe — must only be called from task context. From 1f1b20f4feacd769279b673be5301c1d39f41ed3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Mar 2026 07:03:24 -1000 Subject: [PATCH 327/609] [core] Pack entity string properties into PROGMEM-indexed uint8_t fields (#14171) --- .../alarm_control_panel/__init__.py | 2 +- esphome/components/api/api_connection.cpp | 4 +- esphome/components/binary_sensor/__init__.py | 12 +- .../components/binary_sensor/binary_sensor.h | 2 +- esphome/components/button/__init__.py | 12 +- esphome/components/button/button.h | 2 +- esphome/components/climate/__init__.py | 3 +- esphome/components/cover/__init__.py | 12 +- esphome/components/cover/cover.h | 2 +- esphome/components/datetime/__init__.py | 3 +- esphome/components/esp32/core.cpp | 1 + esphome/components/esp8266/core.cpp | 3 + esphome/components/event/__init__.py | 12 +- esphome/components/event/event.h | 2 +- esphome/components/fan/__init__.py | 3 +- esphome/components/host/core.cpp | 1 + esphome/components/infrared/__init__.py | 2 +- esphome/components/libretiny/core.cpp | 1 + esphome/components/light/__init__.py | 7 +- esphome/components/lock/__init__.py | 3 +- esphome/components/media_player/__init__.py | 2 +- esphome/components/mqtt/mqtt_number.cpp | 4 +- esphome/components/number/__init__.py | 16 +- esphome/components/number/number.cpp | 4 +- esphome/components/number/number_traits.h | 6 +- esphome/components/rp2040/core.cpp | 3 + esphome/components/select/__init__.py | 3 +- esphome/components/sensor/__init__.py | 16 +- esphome/components/sensor/sensor.h | 2 +- esphome/components/sprinkler/sprinkler.cpp | 4 +- esphome/components/switch/__init__.py | 12 +- esphome/components/switch/switch.h | 2 +- esphome/components/text/__init__.py | 3 +- esphome/components/text_sensor/__init__.py | 12 +- esphome/components/text_sensor/text_sensor.h | 2 +- esphome/components/update/__init__.py | 12 +- esphome/components/update/update_entity.h | 2 +- esphome/components/valve/__init__.py | 12 +- esphome/components/valve/valve.h | 2 +- esphome/components/water_heater/__init__.py | 3 +- esphome/components/web_server/web_server.cpp | 2 +- esphome/components/zephyr/core.cpp | 1 + esphome/core/defines.h | 2 + esphome/core/entity_base.cpp | 64 ++--- esphome/core/entity_base.h | 100 ++++---- esphome/core/entity_helpers.py | 227 +++++++++++++++++- esphome/core/hal.h | 1 + script/clang-tidy | 1 + tests/component_tests/sensor/test_sensor.py | 2 +- .../text_sensor/test_text_sensor.py | 4 +- tests/unit_tests/core/test_entity_helpers.py | 110 +++++++-- 51 files changed, 519 insertions(+), 206 deletions(-) diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index b1e2252ce7..b855586152 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -186,8 +186,8 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id( ) +@setup_entity("alarm_control_panel") async def setup_alarm_control_panel_core_(var, config): - await setup_entity(var, config, "alarm_control_panel") for conf in config.get(CONF_ON_STATE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 738dd1ef05..59476fac25 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -773,9 +773,9 @@ uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *number = static_cast(entity); ListEntitiesNumberResponse msg; - msg.unit_of_measurement = number->traits.get_unit_of_measurement_ref(); + msg.unit_of_measurement = number->get_unit_of_measurement_ref(); msg.mode = static_cast(number->traits.get_mode()); - msg.device_class = number->traits.get_device_class_ref(); + msg.device_class = number->get_device_class_ref(); msg.min_value = number->traits.get_min_value(); msg.max_value = number->traits.get_max_value(); msg.step = number->traits.get_step(); diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 036d78da73..1f64118560 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -60,7 +60,11 @@ from esphome.const import ( DEVICE_CLASS_WINDOW, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, +) from esphome.cpp_generator import MockObjClass from esphome.util import Registry @@ -604,11 +608,9 @@ async def _build_binary_sensor_automations(var, config): ) +@setup_entity("binary_sensor") async def setup_binary_sensor_core_(var, config): - await setup_entity(var, config, "binary_sensor") - - if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: - cg.add(var.set_device_class(device_class)) + setup_device_class(config) trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get( CONF_PUBLISH_INITIAL_STATE, False ) diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 4b655e1bd1..6ae5d04bcb 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -30,7 +30,7 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi * The sub classes should notify the front-end of new states via the publish_state() method which * handles inverted inputs for you. */ -class BinarySensor : public StatefulEntityBase, public EntityBase_DeviceClass { +class BinarySensor : public StatefulEntityBase { public: explicit BinarySensor(){}; diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index 94816a0974..12d9ebaba6 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -18,7 +18,11 @@ from esphome.const import ( DEVICE_CLASS_UPDATE, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, +) from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@esphome/core"] @@ -84,15 +88,13 @@ def button_schema( return _BUTTON_SCHEMA.extend(schema) +@setup_entity("button") async def setup_button_core_(var, config): - await setup_entity(var, config, "button") - for conf in config.get(CONF_ON_PRESS, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) - if device_class := config.get(CONF_DEVICE_CLASS): - cg.add(var.set_device_class(device_class)) + setup_device_class(config) if mqtt_id := config.get(CONF_MQTT_ID): mqtt_ = cg.new_Pvariable(mqtt_id, var) diff --git a/esphome/components/button/button.h b/esphome/components/button/button.h index be6e080917..0f7576a419 100644 --- a/esphome/components/button/button.h +++ b/esphome/components/button/button.h @@ -22,7 +22,7 @@ void log_button(const char *tag, const char *prefix, const char *type, Button *o * * A button is just a momentary switch that does not have a state, only a trigger. */ -class Button : public EntityBase, public EntityBase_DeviceClass { +class Button : public EntityBase { public: /** Press this button. This is called by the front-end. * diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 2150a30c3e..1f449ad2a4 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -268,9 +268,8 @@ def climate_schema( return _CLIMATE_SCHEMA.extend(schema) +@setup_entity("climate") async def setup_climate_core_(var, config): - await setup_entity(var, config, "climate") - visual = config[CONF_VISUAL] if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None: cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES") diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 17095f41f6..c330241f4d 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -37,7 +37,11 @@ from esphome.const import ( DEVICE_CLASS_WINDOW, ) from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, +) from esphome.cpp_generator import MockObj, MockObjClass from esphome.types import ConfigType, TemplateArgsType @@ -190,11 +194,9 @@ def cover_schema( return _COVER_SCHEMA.extend(schema) +@setup_entity("cover") async def setup_cover_core_(var, config): - await setup_entity(var, config, "cover") - - if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: - cg.add(var.set_device_class(device_class)) + setup_device_class(config) if CONF_ON_OPEN in config: _LOGGER.warning( diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 0af48f75de..8cf9aa092a 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -107,7 +107,7 @@ const LogString *cover_operation_to_str(CoverOperation op); * to control all values of the cover. Also implement get_traits() to return what operations * the cover supports. */ -class Cover : public EntityBase, public EntityBase_DeviceClass { +class Cover : public EntityBase { public: explicit Cover(); diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index 602db3827a..74c9d594f7 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -134,9 +134,8 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema: return _DATETIME_SCHEMA.extend(schema) +@setup_entity("datetime") async def setup_datetime_core_(var, config): - await setup_entity(var, config, "datetime") - if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 7ebbba609e..46c000562e 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -48,6 +48,7 @@ void arch_init() { void HOT arch_feed_wdt() { esp_task_wdt_reset(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +const char *progmem_read_ptr(const char *const *addr) { return *addr; } uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index b665124d66..159ec20e77 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -34,6 +34,9 @@ void HOT arch_feed_wdt() { system_soft_wdt_feed(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT } +const char *progmem_read_ptr(const char *const *addr) { + return reinterpret_cast(pgm_read_ptr(addr)); // NOLINT +} uint16_t progmem_read_uint16(const uint16_t *addr) { return pgm_read_word(addr); // NOLINT } diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index 8fac7a279c..14cc1505ad 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -18,7 +18,11 @@ from esphome.const import ( DEVICE_CLASS_MOTION, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, +) from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@nohat"] @@ -85,17 +89,15 @@ def event_schema( return _EVENT_SCHEMA.extend(schema) +@setup_entity("event") async def setup_event_core_(var, config, *, event_types: list[str]): - await setup_entity(var, config, "event") - for conf in config.get(CONF_ON_EVENT, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [(cg.StringRef, "event_type")], conf) cg.add(var.set_event_types(event_types)) - if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: - cg.add(var.set_device_class(device_class)) + setup_device_class(config) if mqtt_id := config.get(CONF_MQTT_ID): mqtt_ = cg.new_Pvariable(mqtt_id, var) diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index a7451407bb..5b6a94b47c 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -20,7 +20,7 @@ namespace event { LOG_ENTITY_DEVICE_CLASS(TAG, prefix, *(obj)); \ } -class Event : public EntityBase, public EntityBase_DeviceClass { +class Event : public EntityBase { public: void trigger(const std::string &event_type); diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index e839df6aee..da28c577c8 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -222,9 +222,8 @@ def validate_preset_modes(value): return value +@setup_entity("fan") async def setup_fan_core_(var, config): - await setup_entity(var, config, "fan") - cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index cb2b2e19d7..d5c61ec986 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -59,6 +59,7 @@ void HOT arch_feed_wdt() { } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +const char *progmem_read_ptr(const char *const *addr) { return *addr; } uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { struct timespec spec; diff --git a/esphome/components/infrared/__init__.py b/esphome/components/infrared/__init__.py index 5c759d6fd9..6a2a72fa5d 100644 --- a/esphome/components/infrared/__init__.py +++ b/esphome/components/infrared/__init__.py @@ -45,9 +45,9 @@ def infrared_schema(class_: type[cg.MockObjClass]) -> cv.Schema: ) +@setup_entity("infrared") async def setup_infrared_core_(var: cg.Pvariable, config: ConfigType) -> None: """Set up core infrared configuration.""" - await setup_entity(var, config, "infrared") async def register_infrared(var: cg.Pvariable, config: ConfigType) -> None: diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index 74b33a30a0..893a79440a 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -36,6 +36,7 @@ void HOT arch_feed_wdt() { lt_wdt_feed(); } uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +const char *progmem_read_ptr(const char *const *addr) { return *addr; } uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } } // namespace esphome diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 40382bbda7..4403281116 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -243,9 +243,8 @@ def validate_color_temperature_channels(value): return value -async def setup_light_core_(light_var, output_var, config): - await setup_entity(light_var, config, "light") - +@setup_entity("light") +async def setup_light_core_(light_var, config, output_var): cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE])) if (initial_state_config := config.get(CONF_INITIAL_STATE)) is not None: @@ -312,7 +311,7 @@ async def register_light(output_var, config): cg.add(cg.App.register_light(light_var)) CORE.register_platform_component("light", light_var) await cg.register_component(light_var, config) - await setup_light_core_(light_var, output_var, config) + await setup_light_core_(light_var, config, output_var) async def new_light(config, *args): diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index 9d893d3ad9..e37092756f 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -91,9 +91,8 @@ def lock_schema( return _LOCK_SCHEMA.extend(schema) +@setup_entity("lock") async def _setup_lock_core(var, config): - await setup_entity(var, config, "lock") - for conf in config.get(CONF_ON_LOCK, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index b2afbe5e58..051e386eaf 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -96,8 +96,8 @@ VolumeSetAction = media_player_ns.class_( ) +@setup_entity("media_player") async def setup_media_player_core_(var, config): - await setup_entity(var, config, "media_player") for conf_key, _ in _STATE_TRIGGERS: for conf in config.get(conf_key, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index fdc909fcc9..a2734f2beb 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -48,7 +48,7 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_MAX] = traits.get_max_value(); root[MQTT_STEP] = traits.get_step(); // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto unit_of_measurement = this->number_->traits.get_unit_of_measurement_ref(); + const auto unit_of_measurement = this->number_->get_unit_of_measurement_ref(); if (!unit_of_measurement.empty()) { root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; } @@ -57,7 +57,7 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_MODE] = NumberMqttModeStrings::get_progmem_str(static_cast(mode), static_cast(NUMBER_MODE_BOX)); } - const auto device_class = this->number_->traits.get_device_class_ref(); + const auto device_class = this->number_->get_device_class_ref(); if (!device_class.empty()) { root[MQTT_DEVICE_CLASS] = device_class; } diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 2238f2c037..0570ac0b1e 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -79,7 +79,12 @@ from esphome.const import ( DEVICE_CLASS_WIND_SPEED, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, + setup_unit_of_measurement, +) from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@esphome/core"] @@ -257,11 +262,10 @@ async def _build_number_automations(var, config): await automation.build_automation(trigger, [(float, "x")], conf) +@setup_entity("number") async def setup_number_core_( var, config, *, min_value: float, max_value: float, step: float ): - await setup_entity(var, config, "number") - cg.add(var.traits.set_min_value(min_value)) cg.add(var.traits.set_max_value(max_value)) cg.add(var.traits.set_step(step)) @@ -273,10 +277,8 @@ async def setup_number_core_( CORE.add_job(_build_number_automations, var, config) - if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None: - cg.add(var.traits.set_unit_of_measurement(unit_of_measurement)) - if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: - cg.add(var.traits.set_device_class(device_class)) + setup_device_class(config) + setup_unit_of_measurement(config) if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index 1c4126496c..c0653c3b30 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -15,8 +15,8 @@ void log_number(const char *tag, const char *prefix, const char *type, Number *o ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); LOG_ENTITY_ICON(tag, prefix, *obj); - LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, obj->traits); - LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj->traits); + LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, *obj); + LOG_ENTITY_DEVICE_CLASS(tag, prefix, *obj); } void Number::publish_state(float state) { diff --git a/esphome/components/number/number_traits.h b/esphome/components/number/number_traits.h index 5ccbb9ba48..f855813c9b 100644 --- a/esphome/components/number/number_traits.h +++ b/esphome/components/number/number_traits.h @@ -1,7 +1,7 @@ #pragma once -#include "esphome/core/entity_base.h" -#include "esphome/core/helpers.h" +#include +#include namespace esphome::number { @@ -11,7 +11,7 @@ enum NumberMode : uint8_t { NUMBER_MODE_SLIDER = 2, }; -class NumberTraits : public EntityBase_DeviceClass, public EntityBase_UnitOfMeasurement { +class NumberTraits { public: // Set/get the number value boundaries. void set_min_value(float min_value) { min_value_ = min_value; } diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index a15ee7e263..63b154d80d 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -34,6 +34,9 @@ void HOT arch_feed_wdt() { watchdog_update(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT } +const char *progmem_read_ptr(const char *const *addr) { + return reinterpret_cast(pgm_read_ptr(addr)); // NOLINT +} uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } uint32_t HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); } uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); } diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index c114b140a9..b2c17f59ac 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -92,9 +92,8 @@ def select_schema( return _SELECT_SCHEMA.extend(schema) +@setup_entity("select") async def setup_select_core_(var, config, *, options: list[str]): - await setup_entity(var, config, "select") - cg.add(var.traits.set_options(options)) for conf in config.get(CONF_ON_VALUE, []): diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 338aaae0b5..4be6ed1b84 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -106,7 +106,12 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, + setup_unit_of_measurement, +) from esphome.cpp_generator import MockObj, MockObjClass from esphome.util import Registry @@ -908,15 +913,12 @@ async def _build_sensor_automations(var, config): await automation.build_automation(trigger, [(float, "x")], conf) +@setup_entity("sensor") async def setup_sensor_core_(var, config): - await setup_entity(var, config, "sensor") - - if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: - cg.add(var.set_device_class(device_class)) + setup_device_class(config) + setup_unit_of_measurement(config) if (state_class := config.get(CONF_STATE_CLASS)) is not None: cg.add(var.set_state_class(state_class)) - if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None: - cg.add(var.set_unit_of_measurement(unit_of_measurement)) if (accuracy_decimals := config.get(CONF_ACCURACY_DECIMALS)) is not None: cg.add(var.set_accuracy_decimals(accuracy_decimals)) # Only set force_update if True (default is False) diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 54e75ee2a1..197896f6f6 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -44,7 +44,7 @@ const LogString *state_class_to_string(StateClass state_class); * * A sensor has unit of measurement and can use publish_state to send out a new value with the specified accuracy. */ -class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBase_UnitOfMeasurement { +class Sensor : public EntityBase { public: explicit Sensor(); diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index d82d7baaf6..44fb9092bc 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -567,7 +567,7 @@ void Sprinkler::set_valve_run_duration(const optional valve_number, cons return; } auto call = this->valve_[valve_number.value()].run_duration_number->make_call(); - if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) { + if (this->valve_[valve_number.value()].run_duration_number->get_unit_of_measurement_ref() == MIN_STR) { call.set_value(run_duration.value() / 60.0); } else { call.set_value(run_duration.value()); @@ -649,7 +649,7 @@ uint32_t Sprinkler::valve_run_duration(const size_t valve_number) { return 0; } if (this->valve_[valve_number].run_duration_number != nullptr) { - if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) { + if (this->valve_[valve_number].run_duration_number->get_unit_of_measurement_ref() == MIN_STR) { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state * 60)); } else { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state)); diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 6f1be7d53d..bbafc54bd1 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -22,7 +22,11 @@ from esphome.const import ( DEVICE_CLASS_SWITCH, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, +) from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@esphome/core"] @@ -154,9 +158,8 @@ async def _build_switch_automations(var, config): await automation.build_automation(trigger, [], conf) +@setup_entity("switch") async def setup_switch_core_(var, config): - await setup_entity(var, config, "switch") - if (inverted := config.get(CONF_INVERTED)) is not None: cg.add(var.set_inverted(inverted)) @@ -169,8 +172,7 @@ async def setup_switch_core_(var, config): if web_server_config := config.get(CONF_WEB_SERVER): await web_server.add_entity_config(var, web_server_config) - if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: - cg.add(var.set_device_class(device_class)) + setup_device_class(config) cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) await zigbee.setup_switch(var, config) diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index 982c640cf9..c4f8525793 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -35,7 +35,7 @@ enum SwitchRestoreMode : uint8_t { * A switch is basically just a combination of a binary sensor (for reporting switch values) * and a write_state method that writes a state to the hardware. */ -class Switch : public EntityBase, public EntityBase_DeviceClass { +class Switch : public EntityBase { public: explicit Switch(); diff --git a/esphome/components/text/__init__.py b/esphome/components/text/__init__.py index 61f7119cad..224f4580d4 100644 --- a/esphome/components/text/__init__.py +++ b/esphome/components/text/__init__.py @@ -84,6 +84,7 @@ def text_schema( return _TEXT_SCHEMA.extend(schema) +@setup_entity("text") async def setup_text_core_( var, config, @@ -92,8 +93,6 @@ async def setup_text_core_( max_length: int | None, pattern: str | None, ): - await setup_entity(var, config, "text") - cg.add(var.traits.set_min_length(min_length)) cg.add(var.traits.set_max_length(max_length)) if pattern is not None: diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 2edf202cd2..97f394ecf7 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -21,7 +21,11 @@ from esphome.const import ( DEVICE_CLASS_TIMESTAMP, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, +) from esphome.cpp_generator import MockObjClass from esphome.util import Registry @@ -208,11 +212,9 @@ async def _build_text_sensor_automations(var, config): await automation.build_automation(trigger, [(cg.std_string, "x")], conf) +@setup_entity("text_sensor") async def setup_text_sensor_core_(var, config): - await setup_entity(var, config, "text_sensor") - - if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: - cg.add(var.set_device_class(device_class)) + setup_device_class(config) if config.get(CONF_FILTERS): # must exist and not be empty cg.add_define("USE_TEXT_SENSOR_FILTER") diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index 9916aa63b2..d26cfade96 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -25,7 +25,7 @@ void log_text_sensor(const char *tag, const char *prefix, const char *type, Text public: \ void set_##name##_text_sensor(text_sensor::TextSensor *text_sensor) { this->name##_text_sensor_ = text_sensor; } -class TextSensor : public EntityBase, public EntityBase_DeviceClass { +class TextSensor : public EntityBase { public: std::string state; diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index e146f7e685..c36a4ab769 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -15,7 +15,11 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, +) from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@jesserockz"] @@ -87,11 +91,9 @@ def update_schema( return _UPDATE_SCHEMA.extend(schema) +@setup_entity("update") async def setup_update_core_(var, config): - await setup_entity(var, config, "update") - - if device_class_config := config.get(CONF_DEVICE_CLASS): - cg.add(var.set_device_class(device_class_config)) + setup_device_class(config) if on_update_available := config.get(CONF_ON_UPDATE_AVAILABLE): await automation.build_automation( diff --git a/esphome/components/update/update_entity.h b/esphome/components/update/update_entity.h index 405346bee4..82eaacaf76 100644 --- a/esphome/components/update/update_entity.h +++ b/esphome/components/update/update_entity.h @@ -29,7 +29,7 @@ enum UpdateState : uint8_t { const LogString *update_state_to_string(UpdateState state); -class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { +class UpdateEntity : public EntityBase { public: void publish_state(); diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index 73e907eb0f..22cd01988d 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -22,7 +22,11 @@ from esphome.const import ( DEVICE_CLASS_WATER, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, +) from esphome.cpp_generator import MockObjClass IS_PLATFORM_COMPONENT = True @@ -129,11 +133,9 @@ def valve_schema( return _VALVE_SCHEMA.extend(schema) +@setup_entity("valve") async def _setup_valve_core(var, config): - await setup_entity(var, config, "valve") - - if device_class_config := config.get(CONF_DEVICE_CLASS): - cg.add(var.set_device_class(device_class_config)) + setup_device_class(config) for conf in config.get(CONF_ON_OPEN, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/valve/valve.h b/esphome/components/valve/valve.h index cd46144372..aab819a778 100644 --- a/esphome/components/valve/valve.h +++ b/esphome/components/valve/valve.h @@ -101,7 +101,7 @@ const LogString *valve_operation_to_str(ValveOperation op); * to control all values of the valve. Also implement get_traits() to return what operations * the valve supports. */ -class Valve : public EntityBase, public EntityBase_DeviceClass { +class Valve : public EntityBase { public: explicit Valve(); diff --git a/esphome/components/water_heater/__init__.py b/esphome/components/water_heater/__init__.py index db32c2d919..58cf5a4054 100644 --- a/esphome/components/water_heater/__init__.py +++ b/esphome/components/water_heater/__init__.py @@ -69,10 +69,9 @@ def water_heater_schema( return _WATER_HEATER_SCHEMA.extend(schema) +@setup_entity("water_heater") async def setup_water_heater_core_(var: cg.Pvariable, config: ConfigType) -> None: """Set up the core water heater properties in C++.""" - await setup_entity(var, config, "water_heater") - visual = config[CONF_VISUAL] if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None: cg.add_define("USE_WATER_HEATER_VISUAL_OVERRIDES") diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 47e427c0d1..6b94a103cc 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1139,7 +1139,7 @@ json::SerializationBuffer<> WebServer::number_json_(number::Number *obj, float v json::JsonBuilder builder; JsonObject root = builder.root(); - const auto uom_ref = obj->traits.get_unit_of_measurement_ref(); + const auto uom_ref = obj->get_unit_of_measurement_ref(); const int8_t accuracy = step_to_accuracy_decimals(obj->traits.get_step()); // Need two buffers: one for value, one for state with UOM diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index cf3ea70245..eee7fb3f4f 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -60,6 +60,7 @@ void arch_restart() { sys_reboot(SYS_REBOOT_COLD); } uint32_t arch_get_cpu_cycle_count() { return k_cycle_get_32(); } uint32_t arch_get_cpu_freq_hz() { return sys_clock_hw_cycles_per_sec(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +const char *progmem_read_ptr(const char *const *addr) { return *addr; } uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } Mutex::Mutex() { diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 8d778edf2a..07afefd91a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -44,7 +44,9 @@ #define USE_DEEP_SLEEP #define USE_DEVICES #define USE_DISPLAY +#define USE_ENTITY_DEVICE_CLASS #define USE_ENTITY_ICON +#define USE_ENTITY_UNIT_OF_MEASUREMENT #define USE_ESP32_CAMERA_JPEG_CONVERSION #define USE_ESP32_HOSTED #define USE_ESP32_IMPROV_STATE_CALLBACK diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index f6a7ec1dfd..eafc04f92a 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -45,24 +45,42 @@ void EntityBase::set_name(const char *name, uint32_t object_id_hash) { } } -// Entity Icon -std::string EntityBase::get_icon() const { -#ifdef USE_ENTITY_ICON - if (this->icon_c_str_ == nullptr) { - return ""; - } - return this->icon_c_str_; +// Weak default lookup functions — overridden by generated code in main.cpp +__attribute__((weak)) const char *entity_device_class_lookup(uint8_t) { return ""; } +__attribute__((weak)) const char *entity_uom_lookup(uint8_t) { return ""; } +__attribute__((weak)) const char *entity_icon_lookup(uint8_t) { return ""; } + +// Entity device class (from index) +StringRef EntityBase::get_device_class_ref() const { +#ifdef USE_ENTITY_DEVICE_CLASS + return StringRef(entity_device_class_lookup(this->device_class_idx_)); #else - return ""; + return StringRef(entity_device_class_lookup(0)); #endif } -void EntityBase::set_icon(const char *icon) { -#ifdef USE_ENTITY_ICON - this->icon_c_str_ = icon; +std::string EntityBase::get_device_class() const { return std::string(this->get_device_class_ref().c_str()); } + +// Entity unit of measurement (from index) +StringRef EntityBase::get_unit_of_measurement_ref() const { +#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT + return StringRef(entity_uom_lookup(this->uom_idx_)); #else - // No-op when USE_ENTITY_ICON is not defined + return StringRef(entity_uom_lookup(0)); #endif } +std::string EntityBase::get_unit_of_measurement() const { + return std::string(this->get_unit_of_measurement_ref().c_str()); +} + +// Entity icon (from index) +StringRef EntityBase::get_icon_ref() const { +#ifdef USE_ENTITY_ICON + return StringRef(entity_icon_lookup(this->icon_idx_)); +#else + return StringRef(entity_icon_lookup(0)); +#endif +} +std::string EntityBase::get_icon() const { return std::string(this->get_icon_ref().c_str()); } // Entity Object ID - computed on-demand from name std::string EntityBase::get_object_id() const { @@ -134,24 +152,6 @@ ESPPreferenceObject EntityBase::make_entity_preference_(size_t size, uint32_t ve return global_preferences->make_preference(size, key); } -std::string EntityBase_DeviceClass::get_device_class() { - if (this->device_class_ == nullptr) { - return ""; - } - return this->device_class_; -} - -void EntityBase_DeviceClass::set_device_class(const char *device_class) { this->device_class_ = device_class; } - -std::string EntityBase_UnitOfMeasurement::get_unit_of_measurement() { - if (this->unit_of_measurement_ == nullptr) - return ""; - return this->unit_of_measurement_; -} -void EntityBase_UnitOfMeasurement::set_unit_of_measurement(const char *unit_of_measurement) { - this->unit_of_measurement_ = unit_of_measurement; -} - #ifdef USE_ENTITY_ICON void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) { if (!obj.get_icon_ref().empty()) { @@ -160,13 +160,13 @@ void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) } #endif -void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj) { +void log_entity_device_class(const char *tag, const char *prefix, const EntityBase &obj) { if (!obj.get_device_class_ref().empty()) { ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj.get_device_class_ref().c_str()); } } -void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase_UnitOfMeasurement &obj) { +void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase &obj) { if (!obj.get_unit_of_measurement_ref().empty()) { ESP_LOGCONFIG(tag, "%s Unit of Measurement: '%s'", prefix, obj.get_unit_of_measurement_ref().c_str()); } diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index cbc07cc44c..042eebb40f 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -14,6 +14,12 @@ namespace esphome { +// Extern lookup functions for entity string tables. +// Generated code provides strong definitions; weak defaults return "". +extern const char *entity_device_class_lookup(uint8_t index); +extern const char *entity_uom_lookup(uint8_t index); +extern const char *entity_icon_lookup(uint8_t index); + // Maximum device name length - keep in sync with validate_hostname() in esphome/core/config.py static constexpr size_t ESPHOME_DEVICE_NAME_MAX_LEN = 31; @@ -89,20 +95,41 @@ class EntityBase { this->flags_.entity_category = static_cast(entity_category); } + // Set entity string table indices — one call per entity from codegen. + // Packed: [23..16] icon | [15..8] UoM | [7..0] device_class (each 8 bits) + void set_entity_strings([[maybe_unused]] uint32_t packed) { +#ifdef USE_ENTITY_DEVICE_CLASS + this->device_class_idx_ = packed & 0xFF; +#endif +#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT + this->uom_idx_ = (packed >> 8) & 0xFF; +#endif +#ifdef USE_ENTITY_ICON + this->icon_idx_ = (packed >> 16) & 0xFF; +#endif + } + + // Get device class as StringRef (from packed index) + StringRef get_device_class_ref() const; + /// Get the device class as std::string (deprecated, prefer get_device_class_ref()) + ESPDEPRECATED("Use get_device_class_ref() instead for better performance (avoids string copy). Will be removed in " + "ESPHome 2026.9.0", + "2026.3.0") + std::string get_device_class() const; + // Get unit of measurement as StringRef (from packed index) + StringRef get_unit_of_measurement_ref() const; + /// Get the unit of measurement as std::string (deprecated, prefer get_unit_of_measurement_ref()) + ESPDEPRECATED("Use get_unit_of_measurement_ref() instead for better performance (avoids string copy). Will be " + "removed in ESPHome 2026.9.0", + "2026.3.0") + std::string get_unit_of_measurement() const; + // Get/set this entity's icon ESPDEPRECATED( "Use get_icon_ref() instead for better performance (avoids string copy). Will be removed in ESPHome 2026.5.0", "2025.11.0") std::string get_icon() const; - void set_icon(const char *icon); - StringRef get_icon_ref() const { - static constexpr auto EMPTY_STRING = StringRef::from_lit(""); -#ifdef USE_ENTITY_ICON - return this->icon_c_str_ == nullptr ? EMPTY_STRING : StringRef(this->icon_c_str_); -#else - return EMPTY_STRING; -#endif - } + StringRef get_icon_ref() const; #ifdef USE_DEVICES // Get/set this entity's device id @@ -173,9 +200,6 @@ class EntityBase { void calc_object_id_(); StringRef name_; -#ifdef USE_ENTITY_ICON - const char *icon_c_str_{nullptr}; -#endif uint32_t object_id_hash_{}; #ifdef USE_DEVICES Device *device_{}; @@ -190,44 +214,16 @@ class EntityBase { uint8_t entity_category : 2; // Supports up to 4 categories uint8_t reserved : 2; // Reserved for future use } flags_{}; -}; - -class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) - public: - /// Get the device class, using the manual override if set. - ESPDEPRECATED("Use get_device_class_ref() instead for better performance (avoids string copy). Will be removed in " - "ESPHome 2026.5.0", - "2025.11.0") - std::string get_device_class(); - /// Manually set the device class. - void set_device_class(const char *device_class); - /// Get the device class as StringRef - StringRef get_device_class_ref() const { - static constexpr auto EMPTY_STRING = StringRef::from_lit(""); - return this->device_class_ == nullptr ? EMPTY_STRING : StringRef(this->device_class_); - } - - protected: - const char *device_class_{nullptr}; ///< Device class override -}; - -class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming) - public: - /// Get the unit of measurement, using the manual override if set. - ESPDEPRECATED("Use get_unit_of_measurement_ref() instead for better performance (avoids string copy). Will be " - "removed in ESPHome 2026.5.0", - "2025.11.0") - std::string get_unit_of_measurement(); - /// Manually set the unit of measurement. - void set_unit_of_measurement(const char *unit_of_measurement); - /// Get the unit of measurement as StringRef - StringRef get_unit_of_measurement_ref() const { - static constexpr auto EMPTY_STRING = StringRef::from_lit(""); - return this->unit_of_measurement_ == nullptr ? EMPTY_STRING : StringRef(this->unit_of_measurement_); - } - - protected: - const char *unit_of_measurement_{nullptr}; ///< Unit of measurement override + // String table indices — packed into the 3 padding bytes after flags_ +#ifdef USE_ENTITY_DEVICE_CLASS + uint8_t device_class_idx_{}; +#endif +#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT + uint8_t uom_idx_{}; +#endif +#ifdef USE_ENTITY_ICON + uint8_t icon_idx_{}; +#endif }; /// Log entity icon if set (for use in dump_config) @@ -240,10 +236,10 @@ inline void log_entity_icon(const char *, const char *, const EntityBase &) {} #endif /// Log entity device class if set (for use in dump_config) #define LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj) log_entity_device_class(tag, prefix, obj) -void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj); +void log_entity_device_class(const char *tag, const char *prefix, const EntityBase &obj); /// Log entity unit of measurement if set (for use in dump_config) #define LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, obj) log_entity_unit_of_measurement(tag, prefix, obj) -void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase_UnitOfMeasurement &obj); +void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase &obj); /** * An entity that has a state. diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index c1801c0bda..551e35df65 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -1,9 +1,12 @@ from collections.abc import Callable +from dataclasses import dataclass, field +import functools import logging import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( + CONF_DEVICE_CLASS, CONF_DEVICE_ID, CONF_DISABLED_BY_DEFAULT, CONF_ENTITY_CATEGORY, @@ -11,15 +14,184 @@ from esphome.const import ( CONF_ID, CONF_INTERNAL, CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, ) -from esphome.core import CORE, ID -from esphome.cpp_generator import MockObj, add, get_variable +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObj, RawStatement, add, get_variable import esphome.final_validate as fv -from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case +from esphome.helpers import cpp_string_escape, fnv1_hash_object_id, sanitize, snake_case from esphome.types import ConfigType, EntityMetadata _LOGGER = logging.getLogger(__name__) +DOMAIN = "entity_string_pool" + +# Private config keys for storing registered string indices +_KEY_DC_IDX = "_entity_dc_idx" +_KEY_UOM_IDX = "_entity_uom_idx" +_KEY_ICON_IDX = "_entity_icon_idx" + +# Bit layout for set_entity_strings(packed) — must match C++ setter in entity_base.h: +# [23..16] icon (8 bits) | [15..8] UoM (8 bits) | [7..0] device_class (8 bits) +_DC_SHIFT = 0 +_UOM_SHIFT = 8 +_ICON_SHIFT = 16 + +# Maximum unique strings per category (8-bit index, 0 = not set) +_MAX_DEVICE_CLASSES = 0xFF # 255 +_MAX_UNITS = 0xFF # 255 +_MAX_ICONS = 0xFF # 255 + + +@dataclass +class EntityStringPool: + """Pool of entity string properties for PROGMEM pointer tables. + + Strings are registered during to_code() and assigned 1-based indices. + Index 0 means "not set" (empty string). At render time, the pool + generates C++ PROGMEM pointer table + lookup function per category. + """ + + device_classes: dict[str, int] = field(default_factory=dict) + units: dict[str, int] = field(default_factory=dict) + icons: dict[str, int] = field(default_factory=dict) + tables_registered: bool = False + + +def _get_pool() -> EntityStringPool: + """Get or create the entity string pool from CORE.data.""" + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = EntityStringPool() + return CORE.data[DOMAIN] + + +def _ensure_tables_registered() -> None: + """Schedule the table generation job (once).""" + pool = _get_pool() + if pool.tables_registered: + return + pool.tables_registered = True + CORE.add_job(_generate_tables_job) + + +def _generate_category_code( + table_var: str, + lookup_fn: str, + strings: dict[str, int], +) -> str: + """Generate C++ code for one string category (PROGMEM pointer table + lookup). + + Uses a PROGMEM array of string pointers. On ESP8266, pointers are stored + in flash (via PROGMEM) and read with progmem_read_ptr(). String literals + themselves remain in RAM but benefit from linker string deduplication. + Index 0 means "not set" and returns empty string. + """ + if not strings: + return "" + + sorted_strings = sorted(strings.items(), key=lambda x: x[1]) + entries = ", ".join(cpp_string_escape(s) for s, _ in sorted_strings) + count = len(sorted_strings) + + return ( + f"static const char *const {table_var}[] PROGMEM = {{{entries}}};\n" + f"const char *{lookup_fn}(uint8_t index) {{\n" + f' if (index == 0 || index > {count}) return "";\n' + f" return progmem_read_ptr(&{table_var}[index - 1]);\n" + f"}}\n" + ) + + +_CATEGORY_CONFIGS = ( + ("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes"), + ("ENTITY_UOM_TABLE", "entity_uom_lookup", "units"), + ("ENTITY_ICON_TABLE", "entity_icon_lookup", "icons"), +) + + +@coroutine_with_priority(CoroPriority.FINAL) +async def _generate_tables_job() -> None: + """Generate all entity string table C++ code as a FINAL-priority job. + + Runs after all component to_code() calls have registered their strings. + """ + pool = _get_pool() + parts = ["namespace esphome {"] + for table_var, lookup_fn, attr in _CATEGORY_CONFIGS: + code = _generate_category_code(table_var, lookup_fn, getattr(pool, attr)) + if code: + parts.append(code) + parts.append("} // namespace esphome") + cg.add_global(RawStatement("\n".join(parts))) + + +def _register_string( + value: str, category: dict[str, int], max_count: int, category_name: str +) -> int: + """Register a string in a category dict and return its 1-based index. + + Returns 0 if value is empty/None (meaning "not set"). + """ + if not value: + return 0 + if value in category: + return category[value] + idx = len(category) + 1 + if idx > max_count: + raise ValueError( + f"Too many unique {category_name} values (max {max_count}), got {idx}: '{value}'" + ) + category[value] = idx + _ensure_tables_registered() + return idx + + +def register_device_class(value: str) -> int: + """Register a device_class string and return its 1-based index.""" + return _register_string( + value, _get_pool().device_classes, _MAX_DEVICE_CLASSES, "device_class" + ) + + +def register_unit_of_measurement(value: str) -> int: + """Register a unit_of_measurement string and return its 1-based index.""" + return _register_string(value, _get_pool().units, _MAX_UNITS, "unit_of_measurement") + + +def register_icon(value: str) -> int: + """Register an icon string and return its 1-based index.""" + return _register_string(value, _get_pool().icons, _MAX_ICONS, "icon") + + +def setup_device_class(config: ConfigType) -> None: + """Register config's device_class and store its index for finalize_entity_strings.""" + idx = register_device_class(config.get(CONF_DEVICE_CLASS, "")) + if idx: + cg.add_define("USE_ENTITY_DEVICE_CLASS") + config[_KEY_DC_IDX] = idx + + +def setup_unit_of_measurement(config: ConfigType) -> None: + """Register config's unit_of_measurement and store its index for finalize_entity_strings.""" + idx = register_unit_of_measurement(config.get(CONF_UNIT_OF_MEASUREMENT, "")) + if idx: + cg.add_define("USE_ENTITY_UNIT_OF_MEASUREMENT") + config[_KEY_UOM_IDX] = idx + + +def finalize_entity_strings(var: MockObj, config: ConfigType) -> None: + """Emit a single set_entity_strings() call with all packed indices. + + Call this at the end of each component's setup function, after + setup_entity() and any register_device_class/register_unit_of_measurement calls. + """ + dc_idx = config.get(_KEY_DC_IDX, 0) + uom_idx = config.get(_KEY_UOM_IDX, 0) + icon_idx = config.get(_KEY_ICON_IDX, 0) + packed = (dc_idx << _DC_SHIFT) | (uom_idx << _UOM_SHIFT) | (icon_idx << _ICON_SHIFT) + if packed != 0: + add(var.set_entity_strings(packed)) + def get_base_entity_object_id( name: str, friendly_name: str | None, device_name: str | None = None @@ -64,8 +236,48 @@ def get_base_entity_object_id( return sanitize(snake_case(base_str)) -async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: - """Set up generic properties of an Entity. +def setup_entity(var_or_platform, config=None, platform=None): + """Set up entity properties — works as both decorator and direct call. + + Decorator mode:: + + @setup_entity("sensor") + async def setup_sensor_core_(var, config): + setup_device_class(config) + setup_unit_of_measurement(config) + ... + + Direct call mode (for entities with no extra string properties):: + + await setup_entity(var, config, "camera") + """ + if isinstance(var_or_platform, str) and config is None: + # Decorator mode: @setup_entity("sensor") + platform = var_or_platform + + def decorator(func: Callable) -> Callable: + @functools.wraps(func) + async def wrapper( + var: MockObj, config: ConfigType, *args, **kwargs + ) -> None: + await _setup_entity_impl(var, config, platform) + await func(var, config, *args, **kwargs) + finalize_entity_strings(var, config) + + return wrapper + + return decorator + + # Direct call mode: await setup_entity(var, config, "camera") + async def _do() -> None: + await _setup_entity_impl(var_or_platform, config, platform) + finalize_entity_strings(var_or_platform, config) + + return _do() + + +async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) -> None: + """Set up generic properties of an Entity (internal implementation). This function sets up the common entity properties like name, icon, entity category, etc. @@ -92,12 +304,15 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: add(var.set_disabled_by_default(True)) if CONF_INTERNAL in config: add(var.set_internal(config[CONF_INTERNAL])) + icon_idx = 0 if CONF_ICON in config: # Add USE_ENTITY_ICON define when icons are used cg.add_define("USE_ENTITY_ICON") - add(var.set_icon(config[CONF_ICON])) + icon_idx = register_icon(config[CONF_ICON]) if CONF_ENTITY_CATEGORY in config: add(var.set_entity_category(config[CONF_ENTITY_CATEGORY])) + # Store icon index for finalize_entity_strings + config[_KEY_ICON_IDX] = icon_idx def inherit_property_from(property_to_inherit, parent_id_property, transform=None): diff --git a/esphome/core/hal.h b/esphome/core/hal.h index ef45be629d..c2c9b1a325 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -42,6 +42,7 @@ void arch_feed_wdt(); uint32_t arch_get_cpu_cycle_count(); uint32_t arch_get_cpu_freq_hz(); uint8_t progmem_read_byte(const uint8_t *addr); +const char *progmem_read_ptr(const char *const *addr); uint16_t progmem_read_uint16(const uint16_t *addr); } // namespace esphome diff --git a/script/clang-tidy b/script/clang-tidy index 17bcafacc7..9c2899026d 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -79,6 +79,7 @@ def clang_options(idedata): "-Dpgm_read_byte_near(s)=(*(const uint8_t *)(s))", "-Dpgm_read_word(s)=(*(const uint16_t *)(s))", "-Dpgm_read_dword(s)=(*(const uint32_t *)(s))", + "-Dpgm_read_ptr(s)=(*(const void *const *)(s))", "-DPROGMEM=", "-DPGM_P=const char *", "-DPSTR(s)=(s)", diff --git a/tests/component_tests/sensor/test_sensor.py b/tests/component_tests/sensor/test_sensor.py index 35ce1f4e11..221e7edf2c 100644 --- a/tests/component_tests/sensor/test_sensor.py +++ b/tests/component_tests/sensor/test_sensor.py @@ -11,4 +11,4 @@ def test_sensor_device_class_set(generate_main): main_cpp = generate_main("tests/component_tests/sensor/test_sensor.yaml") # Then - assert 's_1->set_device_class("voltage");' in main_cpp + assert "s_1->set_entity_strings(" in main_cpp diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py index 1593d0b6d8..4aaebe04d1 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.py +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -54,5 +54,5 @@ def test_text_sensor_device_class_set(generate_main): main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") # Then - assert 'ts_2->set_device_class("timestamp");' in main_cpp - assert 'ts_3->set_device_class("date");' in main_cpp + assert "ts_2->set_entity_strings(" in main_cpp + assert "ts_3->set_entity_strings(" in main_cpp diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index a58d4784ce..a5cfad5ab6 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -11,6 +11,7 @@ from esphome.config_validation import Invalid from esphome.const import ( CONF_DEVICE_ID, CONF_DISABLED_BY_DEFAULT, + CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, CONF_INTERNAL, @@ -18,6 +19,8 @@ from esphome.const import ( ) from esphome.core import CORE, ID, entity_helpers from esphome.core.entity_helpers import ( + _register_string, + _setup_entity_impl, entity_duplicate_validator, get_base_entity_object_id, setup_entity, @@ -305,7 +308,7 @@ async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> CONF_NAME: "Temperature", CONF_DISABLED_BY_DEFAULT: False, } - await setup_entity(var1, config1, "sensor") + await _setup_entity_impl(var1, config1, "sensor") # Get object ID from first entity object_id1 = extract_object_id_from_expressions(added_expressions) @@ -319,7 +322,7 @@ async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> CONF_NAME: "Humidity", CONF_DISABLED_BY_DEFAULT: False, } - await setup_entity(var2, config2, "sensor") + await _setup_entity_impl(var2, config2, "sensor") # Get object ID from second entity object_id2 = extract_object_id_from_expressions(added_expressions) @@ -354,7 +357,7 @@ async def test_setup_entity_different_platforms( object_ids: list[str] = [] for var, platform in platforms: added_expressions.clear() - await setup_entity(var, config, platform) + await _setup_entity_impl(var, config, platform) object_id = extract_object_id_from_expressions(added_expressions) object_ids.append(object_id) @@ -416,7 +419,7 @@ async def test_setup_entity_with_devices( object_ids: list[str] = [] for var, config in [(sensor1, config1), (sensor2, config2)]: added_expressions.clear() - await setup_entity(var, config, "sensor") + await _setup_entity_impl(var, config, "sensor") object_id = extract_object_id_from_expressions(added_expressions) object_ids.append(object_id) @@ -438,7 +441,7 @@ async def test_setup_entity_empty_name(setup_test_environment: list[str]) -> Non CONF_DISABLED_BY_DEFAULT: False, } - await setup_entity(var, config, "sensor") + await _setup_entity_impl(var, config, "sensor") object_id = extract_object_id_from_expressions(added_expressions) # Should use friendly name @@ -460,7 +463,7 @@ async def test_setup_entity_special_characters( CONF_DISABLED_BY_DEFAULT: False, } - await setup_entity(var, config, "sensor") + await _setup_entity_impl(var, config, "sensor") object_id = extract_object_id_from_expressions(added_expressions) # Special characters should be sanitized @@ -471,7 +474,7 @@ async def test_setup_entity_special_characters( async def test_setup_entity_with_icon(setup_test_environment: list[str]) -> None: """Test setup_entity sets icon correctly.""" - added_expressions = setup_test_environment + setup_test_environment # noqa: F841 - fixture initializes CORE state var = MockObj("sensor1") @@ -481,12 +484,10 @@ async def test_setup_entity_with_icon(setup_test_environment: list[str]) -> None CONF_ICON: "mdi:thermometer", } - await setup_entity(var, config, "sensor") + await _setup_entity_impl(var, config, "sensor") - # Check icon was set - assert any( - 'sensor1.set_icon("mdi:thermometer")' in expr for expr in added_expressions - ) + # Check icon index was stored in config for finalize_entity_strings + assert config.get("_entity_icon_idx", 0) > 0 @pytest.mark.asyncio @@ -504,7 +505,7 @@ async def test_setup_entity_disabled_by_default( CONF_DISABLED_BY_DEFAULT: True, } - await setup_entity(var, config, "sensor") + await _setup_entity_impl(var, config, "sensor") # Check disabled_by_default was set assert any( @@ -790,7 +791,7 @@ async def test_setup_entity_empty_name_with_device( CONF_DEVICE_ID: device_id, } - await setup_entity(var, config, "sensor") + await _setup_entity_impl(var, config, "sensor") entity_helpers.get_variable = original_get_variable @@ -826,7 +827,7 @@ async def test_setup_entity_empty_name_with_mac_suffix( CONF_DISABLED_BY_DEFAULT: False, } - await setup_entity(var, config, "sensor") + await _setup_entity_impl(var, config, "sensor") # For empty-name entities, Python passes 0 - C++ calculates hash at runtime assert any('set_name("", 0)' in expr for expr in added_expressions), ( @@ -858,7 +859,7 @@ async def test_setup_entity_empty_name_with_mac_suffix_no_friendly_name( CONF_DISABLED_BY_DEFAULT: False, } - await setup_entity(var, config, "sensor") + await _setup_entity_impl(var, config, "sensor") # For empty-name entities, Python passes 0 - C++ calculates hash at runtime assert any('set_name("", 0)' in expr for expr in added_expressions), ( @@ -891,9 +892,84 @@ async def test_setup_entity_empty_name_no_mac_suffix_no_friendly_name( CONF_DISABLED_BY_DEFAULT: False, } - await setup_entity(var, config, "sensor") + await _setup_entity_impl(var, config, "sensor") # For empty-name entities, Python passes 0 - C++ calculates hash at runtime assert any('set_name("", 0)' in expr for expr in added_expressions), ( f"Expected set_name with hash 0, got {added_expressions}" ) + + +def test_register_string_overflow() -> None: + """Test _register_string raises ValueError when max count is exceeded.""" + category: dict[str, int] = {} + for i in range(3): + _register_string(f"val_{i}", category, 3, "test") + with pytest.raises(ValueError, match="Too many unique test values"): + _register_string("overflow", category, 3, "test") + + +@pytest.mark.asyncio +async def test_setup_entity_with_entity_category( + setup_test_environment: list[str], +) -> None: + """Test setup_entity sets entity_category correctly.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + config = { + CONF_NAME: "Temperature", + CONF_DISABLED_BY_DEFAULT: False, + CONF_ENTITY_CATEGORY: "diagnostic", + } + await _setup_entity_impl(var, config, "sensor") + assert any( + 'set_entity_category("diagnostic")' in expr for expr in added_expressions + ) + + +@pytest.mark.asyncio +async def test_setup_entity_direct_call(setup_test_environment: list[str]) -> None: + """Test setup_entity in direct call mode (legacy / backward compat).""" + added_expressions = setup_test_environment + + var = MockObj("camera1") + config = { + CONF_NAME: "My Camera", + CONF_DISABLED_BY_DEFAULT: False, + CONF_ICON: "mdi:camera", + } + + # Direct call mode: await setup_entity(var, config, "camera") + await setup_entity(var, config, "camera") + + # Should have called set_name + object_id = extract_object_id_from_expressions(added_expressions) + assert object_id == "my_camera" + + # Icon index should have been stored and finalized + assert config.get("_entity_icon_idx", 0) > 0 + + +@pytest.mark.asyncio +async def test_setup_entity_decorator_mode(setup_test_environment: list[str]) -> None: + """Test setup_entity in decorator mode.""" + added_expressions = setup_test_environment + + body_called = False + + @setup_entity("sensor") + async def my_setup(var, config): + nonlocal body_called + body_called = True + + var = MockObj("sensor1") + config = { + CONF_NAME: "Temperature", + CONF_DISABLED_BY_DEFAULT: False, + } + + await my_setup(var, config) + + assert body_called + object_id = extract_object_id_from_expressions(added_expressions) + assert object_id == "temperature" From 78602ccacb33dcf3acdad5a55be497af52cc4287 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Mar 2026 07:03:50 -1000 Subject: [PATCH 328/609] [ci] Add lint check to prevent powf in core and base entity platforms (#14126) --- esphome/core/helpers.cpp | 4 ++-- esphome/core/helpers.h | 4 ++-- script/ci-custom.py | 48 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index c75799fe57..00b447ebf2 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -706,7 +706,7 @@ float gamma_correct(float value, float gamma) { if (gamma <= 0.0f) return value; - return powf(value, gamma); + return powf(value, gamma); // NOLINT - deprecated, removal 2026.9.0 } float gamma_uncorrect(float value, float gamma) { if (value <= 0.0f) @@ -714,7 +714,7 @@ float gamma_uncorrect(float value, float gamma) { if (gamma <= 0.0f) return value; - return powf(value, 1 / gamma); + return powf(value, 1 / gamma); // NOLINT - deprecated, removal 2026.9.0 } void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation, float &value) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 187b383f65..6ce5de4975 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -512,8 +512,8 @@ template class SmallBufferWithHeapFallb ///@{ /// Compute 10^exp using iterative multiplication/division. -/// Avoids pulling in powf/__ieee754_powf (~2.3KB flash) for small integer exponents. -/// Matches powf(10, exp) for the int8_t exponent range used by sensor accuracy_decimals. +/// Avoids pulling in powf/__ieee754_powf (~2.3KB flash) for small integer exponents. // NOLINT +/// Matches powf(10, exp) for the int8_t exponent range used by sensor accuracy_decimals. // NOLINT inline float pow10_int(int8_t exp) { float result = 1.0f; if (exp >= 0) { diff --git a/script/ci-custom.py b/script/ci-custom.py index f428eb0821..b60d7d7740 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -841,6 +841,54 @@ def lint_no_scanf(fname, match): ) +# Base entity platforms - these are linked into most builds and should not +# pull in powf/__ieee754_powf (~2.3KB flash). +BASE_ENTITY_PLATFORMS = [ + "alarm_control_panel", + "binary_sensor", + "button", + "climate", + "cover", + "datetime", + "event", + "fan", + "light", + "lock", + "media_player", + "number", + "select", + "sensor", + "switch", + "text", + "text_sensor", + "update", + "valve", + "water_heater", +] + +# Directories protected from powf: core + all base entity platforms +POWF_PROTECTED_DIRS = ["esphome/core"] + [ + f"esphome/components/{p}" for p in BASE_ENTITY_PLATFORMS +] + + +@lint_re_check( + r"[^\w]powf\s*\(" + CPP_RE_EOL, + include=[ + f"{d}/*.{ext}" for d in POWF_PROTECTED_DIRS for ext in ["h", "cpp", "tcc"] + ], +) +def lint_no_powf_in_core(fname, match): + return ( + f"{highlight('powf()')} pulls in __ieee754_powf (~2.3KB flash) and is not allowed in " + f"core or base entity platform code. These files are linked into every build.\n" + f"Please use alternatives:\n" + f" - {highlight('pow10_int(exp)')} for integer powers of 10 (from helpers.h)\n" + f" - Precomputed lookup tables for gamma/non-integer exponents\n" + f"(If powf is strictly necessary, add `// NOLINT` to the line)" + ) + + LOG_MULTILINE_RE = re.compile(r"ESP_LOG\w+\s*\(.*?;", re.DOTALL) LOG_BAD_CONTINUATION_RE = re.compile(r'\\n(?:[^ \\"\r\n\t]|"\s*\n\s*"[^ \\])') LOG_PERCENT_S_CONTINUATION_RE = re.compile(r'\\n(?:%s|"\s*\n\s*"%s)') From 4f69c487daa565dbe43a87f5b489f3dd40a3ff68 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Mar 2026 07:04:12 -1000 Subject: [PATCH 329/609] [bk72xx] Fix ~100ms loop stalls by raising main task priority (#14420) --- esphome/components/libretiny/core.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index 893a79440a..6bb2d9dcc1 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -7,6 +7,9 @@ #include "esphome/core/helpers.h" #include "preferences.h" +#include +#include + void setup(); void loop(); @@ -22,6 +25,22 @@ void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } void arch_init() { libretiny::setup_preferences(); lt_wdt_enable(10000L); +#ifdef USE_BK72XX + // BK72xx SDK creates the main Arduino task at priority 3, which is lower than + // all WiFi (4-5), LwIP (4), and TCP/IP (7) tasks. This causes ~100ms loop + // stalls whenever WiFi background processing runs, because the main task + // cannot resume until every higher-priority task finishes. + // + // By contrast, RTL87xx creates the main task at osPriorityRealtime (highest). + // + // Raise to priority 6: above WiFi/LwIP tasks (4-5) so they don't preempt the + // main loop, but below the TCP/IP thread (7) so packet processing keeps priority. + // This is safe because ESPHome yields voluntarily via yield_with_select_() and + // the Arduino mainTask yield() after each loop() iteration. + static constexpr UBaseType_t MAIN_TASK_PRIORITY = 6; + static_assert(MAIN_TASK_PRIORITY < configMAX_PRIORITIES, "MAIN_TASK_PRIORITY must be less than configMAX_PRIORITIES"); + vTaskPrioritySet(nullptr, MAIN_TASK_PRIORITY); +#endif #if LT_GPIO_RECOVER lt_gpio_recover(); #endif From b209c903bb6991c1242d40e347e3625f594bdaab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Mar 2026 07:05:15 -1000 Subject: [PATCH 330/609] [core] Inline trivial Component state accessors (#14425) --- esphome/core/component.cpp | 8 -------- esphome/core/component.h | 12 ++++++------ 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 53cb50a44c..4ccc747819 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -233,7 +233,6 @@ void Component::call_dump_config_() { } } -uint8_t Component::get_component_state() const { return this->component_state_; } void Component::call() { uint8_t state = this->component_state_ & COMPONENT_STATE_MASK; switch (state) { @@ -339,9 +338,6 @@ void Component::reset_to_construction_state() { this->status_clear_error(); } } -bool Component::is_in_loop_state() const { - return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP; -} void Component::defer(std::function &&f) { // NOLINT App.scheduler.set_timeout(this, static_cast(nullptr), 0, std::move(f)); } @@ -380,16 +376,12 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std: App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); #pragma GCC diagnostic pop } -bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } bool Component::is_ready() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP || (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE || (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP; } -bool Component::is_idle() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE; } bool Component::can_proceed() { return true; } -bool Component::status_has_warning() const { return this->component_state_ & STATUS_LED_WARNING; } -bool Component::status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; } bool Component::set_status_flag_(uint8_t flag) { if ((this->component_state_ & flag) != 0) return false; diff --git a/esphome/core/component.h b/esphome/core/component.h index d8102ea670..e5127b0c9f 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -142,7 +142,7 @@ class Component { */ virtual void on_powerdown() {} - uint8_t get_component_state() const; + uint8_t get_component_state() const { return this->component_state_; } /** Reset this component back to the construction state to allow setup to run again. * @@ -154,7 +154,7 @@ class Component { * * @return True if in loop state, false otherwise. */ - bool is_in_loop_state() const; + bool is_in_loop_state() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP; } /** Check if this component is idle. * Being idle means being in LOOP_DONE state. @@ -162,7 +162,7 @@ class Component { * * @return True if the component is idle */ - bool is_idle() const; + bool is_idle() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE; } /** Mark this component as failed. Any future timeouts/intervals/setup/loop will no longer be called. * @@ -230,15 +230,15 @@ class Component { */ void enable_loop_soon_any_context(); - bool is_failed() const; + bool is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } bool is_ready() const; virtual bool can_proceed(); - bool status_has_warning() const; + bool status_has_warning() const { return this->component_state_ & STATUS_LED_WARNING; } - bool status_has_error() const; + bool status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; } void status_set_warning(const char *message = nullptr); void status_set_warning(const LogString *message); From 95544dddf8bd2171ff42da909fe9cd9786a10710 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Mar 2026 07:11:47 -1000 Subject: [PATCH 331/609] [ci] Add code-owner-approved label workflow (#14421) --- .github/scripts/auto-label-pr/detectors.js | 47 +----- .github/scripts/codeowners.js | 143 ++++++++++++++++ .../workflows/codeowner-approved-label.yml | 158 ++++++++++++++++++ .../workflows/codeowner-review-request.yml | 118 ++++--------- 4 files changed, 342 insertions(+), 124 deletions(-) create mode 100644 .github/scripts/codeowners.js create mode 100644 .github/workflows/codeowner-approved-label.yml diff --git a/.github/scripts/auto-label-pr/detectors.js b/.github/scripts/auto-label-pr/detectors.js index 80d8847bc1..832fcb41db 100644 --- a/.github/scripts/auto-label-pr/detectors.js +++ b/.github/scripts/auto-label-pr/detectors.js @@ -7,6 +7,7 @@ const { hasDashboardChanges, hasGitHubActionsChanges, } = require('../detect-tags'); +const { loadCodeowners, getEffectiveOwners } = require('../codeowners'); // Strategy: Merge branch detection async function detectMergeBranch(context) { @@ -148,51 +149,15 @@ async function detectGitHubActionsChanges(changedFiles) { // Strategy: Code owner detection async function detectCodeOwner(github, context, changedFiles) { const labels = new Set(); - const { owner, repo } = context.repo; try { - const { data: codeownersFile } = await github.rest.repos.getContent({ - owner, - repo, - path: 'CODEOWNERS', - }); - - const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8'); + const codeownersPatterns = loadCodeowners(); const prAuthor = context.payload.pull_request.user.login; - const codeownersLines = codeownersContent.split('\n') - .map(line => line.trim()) - .filter(line => line && !line.startsWith('#')); - - const codeownersRegexes = codeownersLines.map(line => { - const parts = line.split(/\s+/); - const pattern = parts[0]; - const owners = parts.slice(1); - - let regex; - if (pattern.endsWith('*')) { - const dir = pattern.slice(0, -1); - regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`); - } else if (pattern.includes('*')) { - // First escape all regex special chars except *, then replace * with .* - const regexPattern = pattern - .replace(/[.+?^${}()|[\]\\]/g, '\\$&') - .replace(/\*/g, '.*'); - regex = new RegExp(`^${regexPattern}$`); - } else { - regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`); - } - - return { regex, owners }; - }); - - for (const file of changedFiles) { - for (const { regex, owners } of codeownersRegexes) { - if (regex.test(file) && owners.some(owner => owner === `@${prAuthor}`)) { - labels.add('by-code-owner'); - return labels; - } - } + // Check if PR author is a codeowner of any changed file + const effective = getEffectiveOwners(changedFiles, codeownersPatterns); + if (effective.users.has(prAuthor)) { + labels.add('by-code-owner'); } } catch (error) { console.log('Failed to read or parse CODEOWNERS file:', error.message); diff --git a/.github/scripts/codeowners.js b/.github/scripts/codeowners.js new file mode 100644 index 0000000000..9a10391699 --- /dev/null +++ b/.github/scripts/codeowners.js @@ -0,0 +1,143 @@ +// Shared CODEOWNERS parsing and matching utilities. +// +// Used by: +// - codeowner-review-request.yml +// - codeowner-approved-label.yml +// - auto-label-pr/detectors.js (detectCodeOwner) + +/** + * Convert a CODEOWNERS glob pattern to a RegExp. + * + * Handles **, *, and ? wildcards after escaping regex-special characters. + */ +function globToRegex(pattern) { + let regexStr = pattern + .replace(/([.+^=!:${}()|[\]\\])/g, '\\$1') + .replace(/\*\*/g, '\x00GLOBSTAR\x00') // protect ** from next replace + .replace(/\*/g, '[^/]*') // single star + .replace(/\x00GLOBSTAR\x00/g, '.*') // restore globstar + .replace(/\?/g, '.'); + return new RegExp('^' + regexStr + '$'); +} + +/** + * Parse raw CODEOWNERS file content into an array of + * { pattern, regex, owners } objects. + * + * Each `owners` entry is the raw string from the file (e.g. "@user" or + * "@esphome/core"). + */ +function parseCodeowners(content) { + const lines = content + .split('\n') + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')); + + const patterns = []; + for (const line of lines) { + const parts = line.split(/\s+/); + if (parts.length < 2) continue; + + const pattern = parts[0]; + const owners = parts.slice(1); + const regex = globToRegex(pattern); + patterns.push({ pattern, regex, owners }); + } + return patterns; +} + +/** + * Fetch and parse the CODEOWNERS file via the GitHub API. + * + * @param {object} github - octokit instance from actions/github-script + * @param {string} owner - repo owner + * @param {string} repo - repo name + * @param {string} [ref] - git ref (SHA / branch) to read from + * @returns {Array<{pattern: string, regex: RegExp, owners: string[]}>} + */ +async function fetchCodeowners(github, owner, repo, ref) { + const params = { owner, repo, path: 'CODEOWNERS' }; + if (ref) params.ref = ref; + + const { data: file } = await github.rest.repos.getContent(params); + const content = Buffer.from(file.content, 'base64').toString('utf8'); + return parseCodeowners(content); +} + +/** + * Classify raw owner strings into individual users and teams. + * + * @param {string[]} rawOwners - e.g. ["@user1", "@esphome/core"] + * @returns {{ users: string[], teams: string[] }} + * users – login names without "@" + * teams – team slugs without the "org/" prefix + */ +function classifyOwners(rawOwners) { + const users = []; + const teams = []; + for (const o of rawOwners) { + const clean = o.startsWith('@') ? o.slice(1) : o; + if (clean.includes('/')) { + teams.push(clean.split('/')[1]); + } else { + users.push(clean); + } + } + return { users, teams }; +} + +/** + * For each file, find its effective codeowners using GitHub's + * "last match wins" semantics, then union across all files. + * + * @param {string[]} files - list of file paths + * @param {Array} codeownersPatterns - from parseCodeowners / fetchCodeowners + * @returns {{ users: Set, teams: Set, matchedFileCount: number }} + */ +function getEffectiveOwners(files, codeownersPatterns) { + const users = new Set(); + const teams = new Set(); + let matchedFileCount = 0; + + for (const file of files) { + // Last matching pattern wins for each file + let effectiveOwners = null; + for (const { regex, owners } of codeownersPatterns) { + if (regex.test(file)) { + effectiveOwners = owners; + } + } + if (effectiveOwners) { + matchedFileCount++; + const classified = classifyOwners(effectiveOwners); + for (const u of classified.users) users.add(u); + for (const t of classified.teams) teams.add(t); + } + } + + return { users, teams, matchedFileCount }; +} + +/** + * Read and parse the CODEOWNERS file from disk. + * + * Use this when the repo is already checked out (avoids an API call). + * + * @param {string} [repoRoot='.'] - path to the repo root + * @returns {Array<{pattern: string, regex: RegExp, owners: string[]}>} + */ +function loadCodeowners(repoRoot = '.') { + const fs = require('fs'); + const path = require('path'); + const content = fs.readFileSync(path.join(repoRoot, 'CODEOWNERS'), 'utf8'); + return parseCodeowners(content); +} + +module.exports = { + globToRegex, + parseCodeowners, + fetchCodeowners, + loadCodeowners, + classifyOwners, + getEffectiveOwners +}; diff --git a/.github/workflows/codeowner-approved-label.yml b/.github/workflows/codeowner-approved-label.yml new file mode 100644 index 0000000000..217ae06419 --- /dev/null +++ b/.github/workflows/codeowner-approved-label.yml @@ -0,0 +1,158 @@ +# This workflow adds/removes a 'code-owner-approved' label when a +# component-specific codeowner approves (or dismisses) a PR. +# This helps maintainers prioritize PRs that have codeowner sign-off. +# +# Only component-specific codeowners count — the catch-all @esphome/core +# team is excluded so the label reflects domain-expert approval. + +name: Codeowner Approved Label + +on: + pull_request_review: + types: [submitted, dismissed] + +permissions: + pull-requests: write + contents: read + +jobs: + codeowner-approved: + name: Run + if: ${{ github.repository == 'esphome/esphome' }} + runs-on: ubuntu-latest + steps: + - name: Checkout base branch + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.pull_request.base.sha }} + + - name: Check codeowner approval and update label + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js'); + + const owner = context.repo.owner; + const repo = context.repo.repo; + const pr_number = context.payload.pull_request.number; + const LABEL_NAME = 'code-owner-approved'; + + console.log(`Processing PR #${pr_number} for codeowner approval label`); + + try { + // Get the list of changed files in this PR (with pagination) + const prFiles = await github.paginate( + github.rest.pulls.listFiles, + { + owner, + repo, + pull_number: pr_number + } + ); + + const changedFiles = prFiles.map(file => file.filename); + console.log(`Found ${changedFiles.length} changed files`); + + if (changedFiles.length === 0) { + console.log('No changed files found, skipping'); + return; + } + + // Parse CODEOWNERS from the checked-out base branch + const codeownersPatterns = loadCodeowners(); + + // Get effective owners using last-match-wins semantics + const effective = getEffectiveOwners(changedFiles, codeownersPatterns); + + // Only keep individual component-specific codeowners (exclude teams) + const componentCodeowners = effective.users; + + console.log(`Component-specific codeowners for changed files: ${Array.from(componentCodeowners).join(', ') || '(none)'}`); + + if (componentCodeowners.size === 0) { + console.log('No component-specific codeowners found for changed files'); + // Remove label if present since there are no component codeowners + try { + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number: pr_number, + name: LABEL_NAME + }); + console.log(`Removed '${LABEL_NAME}' label (no component codeowners)`); + } catch (error) { + if (error.status !== 404) { + console.log(`Failed to remove label: ${error.message}`); + } + } + return; + } + + // Get all reviews on the PR + const reviews = await github.paginate( + github.rest.pulls.listReviews, + { + owner, + repo, + pull_number: pr_number + } + ); + + // Get the latest review per user (reviews are returned chronologically) + const latestReviewByUser = new Map(); + for (const review of reviews) { + // Skip bot reviews and comment-only reviews + if (!review.user || review.user.type === 'Bot' || review.state === 'COMMENTED') continue; + latestReviewByUser.set(review.user.login, review); + } + + // Check if any component-specific codeowner has an active approval + let hasCodeownerApproval = false; + for (const [login, review] of latestReviewByUser) { + if (review.state === 'APPROVED' && componentCodeowners.has(login)) { + console.log(`Codeowner '${login}' has approved`); + hasCodeownerApproval = true; + break; + } + } + + // Get current labels to check if label is already present + const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ + owner, + repo, + issue_number: pr_number + }); + const hasLabel = currentLabels.some(label => label.name === LABEL_NAME); + + if (hasCodeownerApproval && !hasLabel) { + // Add the label + await github.rest.issues.addLabels({ + owner, + repo, + issue_number: pr_number, + labels: [LABEL_NAME] + }); + console.log(`Added '${LABEL_NAME}' label`); + } else if (!hasCodeownerApproval && hasLabel) { + // Remove the label + try { + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number: pr_number, + name: LABEL_NAME + }); + console.log(`Removed '${LABEL_NAME}' label`); + } catch (error) { + if (error.status !== 404) { + console.log(`Failed to remove label: ${error.message}`); + } + } + } else { + console.log(`Label already ${hasLabel ? 'present' : 'absent'}, no change needed`); + } + + } catch (error) { + console.error(error); + core.setFailed(`Failed to process codeowner approval label: ${error.message}`); + } diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml index 6f4351b298..02bf0e4a29 100644 --- a/.github/workflows/codeowner-review-request.yml +++ b/.github/workflows/codeowner-review-request.yml @@ -24,10 +24,17 @@ jobs: if: ${{ github.repository == 'esphome/esphome' && !github.event.pull_request.draft }} runs-on: ubuntu-latest steps: + - name: Checkout base branch + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.pull_request.base.sha }} + - name: Request reviews from component codeowners uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | + const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js'); + const owner = context.repo.owner; const repo = context.repo.repo; const pr_number = context.payload.pull_request.number; @@ -38,12 +45,15 @@ jobs: const BOT_COMMENT_MARKER = ''; try { - // Get the list of changed files in this PR - const { data: files } = await github.rest.pulls.listFiles({ - owner, - repo, - pull_number: pr_number - }); + // Get the list of changed files in this PR (with pagination) + const files = await github.paginate( + github.rest.pulls.listFiles, + { + owner, + repo, + pull_number: pr_number + } + ); const changedFiles = files.map(file => file.filename); console.log(`Found ${changedFiles.length} changed files`); @@ -53,32 +63,10 @@ jobs: return; } - // Fetch CODEOWNERS file from root - const { data: codeownersFile } = await github.rest.repos.getContent({ - owner, - repo, - path: 'CODEOWNERS', - ref: context.payload.pull_request.base.sha - }); - const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8'); + // Parse CODEOWNERS from the checked-out base branch + const codeownersPatterns = loadCodeowners(); - // Parse CODEOWNERS file to extract all patterns and their owners - const codeownersLines = codeownersContent.split('\n') - .map(line => line.trim()) - .filter(line => line && !line.startsWith('#')); - - const codeownersPatterns = []; - - // Convert CODEOWNERS pattern to regex (robust glob handling) - function globToRegex(pattern) { - // Escape regex special characters except for glob wildcards - let regexStr = pattern - .replace(/([.+^=!:${}()|[\]\\])/g, '\\$1') // escape regex chars - .replace(/\*\*/g, '.*') // globstar - .replace(/\*/g, '[^/]*') // single star - .replace(/\?/g, '.'); // question mark - return new RegExp('^' + regexStr + '$'); - } + console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`); // Helper function to create comment body function createCommentBody(reviewersList, teamsList, matchedFileCount, isSuccessful = true) { @@ -93,50 +81,11 @@ jobs: } } - for (const line of codeownersLines) { - const parts = line.split(/\s+/); - if (parts.length < 2) continue; - - const pattern = parts[0]; - const owners = parts.slice(1); - - // Use robust glob-to-regex conversion - const regex = globToRegex(pattern); - codeownersPatterns.push({ pattern, regex, owners }); - } - - console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`); - - // Match changed files against CODEOWNERS patterns - const matchedOwners = new Set(); - const matchedTeams = new Set(); - const fileMatches = new Map(); // Track which files matched which patterns - - for (const file of changedFiles) { - for (const { pattern, regex, owners } of codeownersPatterns) { - if (regex.test(file)) { - console.log(`File '${file}' matches pattern '${pattern}' with owners: ${owners.join(', ')}`); - - if (!fileMatches.has(file)) { - fileMatches.set(file, []); - } - fileMatches.get(file).push({ pattern, owners }); - - // Add owners to the appropriate set (remove @ prefix) - for (const owner of owners) { - const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner; - if (cleanOwner.includes('/')) { - // Team mention (org/team-name) - const teamName = cleanOwner.split('/')[1]; - matchedTeams.add(teamName); - } else { - // Individual user - matchedOwners.add(cleanOwner); - } - } - } - } - } + // Match changed files against CODEOWNERS patterns using last-match-wins semantics + const effective = getEffectiveOwners(changedFiles, codeownersPatterns); + const matchedOwners = effective.users; + const matchedTeams = effective.teams; + const matchedFileCount = effective.matchedFileCount; if (matchedOwners.size === 0 && matchedTeams.size === 0) { console.log('No codeowners found for any changed files'); @@ -170,11 +119,14 @@ jobs: } // Check for completed reviews to avoid re-requesting users who have already reviewed - const { data: reviews } = await github.rest.pulls.listReviews({ - owner, - repo, - pull_number: pr_number - }); + const reviews = await github.paginate( + github.rest.pulls.listReviews, + { + owner, + repo, + pull_number: pr_number + } + ); const reviewedUsers = new Set(); reviews.forEach(review => { @@ -247,7 +199,7 @@ jobs: } const totalReviewers = reviewersList.length + teamsList.length; - console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${fileMatches.size} matched files`); + console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${matchedFileCount} matched files`); // Request reviews try { @@ -279,7 +231,7 @@ jobs: // Only add a comment if there are new codeowners to mention (not previously pinged) if (reviewersList.length > 0 || teamsList.length > 0) { - const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true); + const commentBody = createCommentBody(reviewersList, teamsList, matchedFileCount, true); await github.rest.issues.createComment({ owner, @@ -297,7 +249,7 @@ jobs: // Only try to add a comment if there are new codeowners to mention if (reviewersList.length > 0 || teamsList.length > 0) { - const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false); + const commentBody = createCommentBody(reviewersList, teamsList, matchedFileCount, false); try { await github.rest.issues.createComment({ From 380c0db0205db56960f3869b341fe20629c4a4f8 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 4 Mar 2026 07:49:38 +1100 Subject: [PATCH 332/609] [usb_uart] Don't claim interrupt interface for ch34x (#14431) --- esphome/components/usb_uart/ch34x.cpp | 9 +++++++++ esphome/components/usb_uart/usb_uart.cpp | 14 +++++++++----- esphome/components/usb_uart/usb_uart.h | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/esphome/components/usb_uart/ch34x.cpp b/esphome/components/usb_uart/ch34x.cpp index 7fa964c0cb..e6e52a9e2a 100644 --- a/esphome/components/usb_uart/ch34x.cpp +++ b/esphome/components/usb_uart/ch34x.cpp @@ -75,6 +75,15 @@ void USBUartTypeCH34X::enable_channels() { } this->start_channels(); } + +std::vector USBUartTypeCH34X::parse_descriptors(usb_device_handle_t dev_hdl) { + auto result = USBUartTypeCdcAcm::parse_descriptors(dev_hdl); + // ch34x doesn't use the interrupt endpoint, and we don't have endpoints to spare + for (auto &cdc_dev : result) { + cdc_dev.interrupt_interface_number = 0xFF; + } + return result; +} } // namespace esphome::usb_uart #endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index de81bfc587..5c0397b2cb 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -20,6 +20,7 @@ static optional get_cdc(const usb_config_desc_t *config_desc, uint8_t in // look for an interface with an interrupt endpoint (notify), and one with two bulk endpoints (data in/out) CdcEps eps{}; eps.bulk_interface_number = 0xFF; + eps.interrupt_interface_number = 0xFF; for (;;) { const auto *intf_desc = usb_parse_interface_descriptor(config_desc, intf_idx++, 0, &conf_offset); if (!intf_desc) { @@ -130,7 +131,7 @@ size_t RingBuffer::pop(uint8_t *data, size_t len) { } void USBUartChannel::write_array(const uint8_t *data, size_t len) { if (!this->initialised_.load()) { - ESP_LOGV(TAG, "Channel not initialised - write ignored"); + ESP_LOGD(TAG, "Channel not initialised - write ignored"); return; } #ifdef USE_UART_DEBUGGER @@ -415,14 +416,15 @@ void USBUartTypeCdcAcm::on_connected() { // Claim the communication (interrupt) interface so CDC class requests are accepted // by the device. Some CDC ACM implementations (e.g. EFR32 NCP) require this before // they enable data flow on the bulk endpoints. - if (channel->cdc_dev_.interrupt_interface_number != channel->cdc_dev_.bulk_interface_number) { + if (channel->cdc_dev_.interrupt_interface_number != 0xFF && + channel->cdc_dev_.interrupt_interface_number != channel->cdc_dev_.bulk_interface_number) { auto err_comm = usb_host_interface_claim(this->handle_, this->device_handle_, channel->cdc_dev_.interrupt_interface_number, 0); if (err_comm != ESP_OK) { ESP_LOGW(TAG, "Could not claim comm interface %d: %s", channel->cdc_dev_.interrupt_interface_number, esp_err_to_name(err_comm)); + channel->cdc_dev_.interrupt_interface_number = 0xFF; // Mark as unavailable, but continue anyway } else { - channel->cdc_dev_.comm_interface_claimed = true; ESP_LOGD(TAG, "Claimed comm interface %d", channel->cdc_dev_.interrupt_interface_number); } } @@ -436,6 +438,7 @@ void USBUartTypeCdcAcm::on_connected() { return; } } + this->status_clear_error(); this->enable_channels(); } @@ -453,9 +456,10 @@ void USBUartTypeCdcAcm::on_disconnected() { usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress); usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress); } - if (channel->cdc_dev_.comm_interface_claimed) { + if (channel->cdc_dev_.interrupt_interface_number != 0xFF && + channel->cdc_dev_.interrupt_interface_number != channel->cdc_dev_.bulk_interface_number) { usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.interrupt_interface_number); - channel->cdc_dev_.comm_interface_claimed = false; + channel->cdc_dev_.interrupt_interface_number = 0xFF; } usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number); // Reset the input and output started flags to their initial state to avoid the possibility of spurious restarts diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 9a9fe1c2ca..0d471e46f6 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -32,7 +32,6 @@ struct CdcEps { const usb_ep_desc_t *out_ep; uint8_t bulk_interface_number; uint8_t interrupt_interface_number; - bool comm_interface_claimed{false}; }; enum UARTParityOptions { @@ -192,6 +191,7 @@ class USBUartTypeCH34X : public USBUartTypeCdcAcm { protected: void enable_channels() override; + std::vector parse_descriptors(usb_device_handle_t dev_hdl) override; }; } // namespace esphome::usb_uart From 96793a99ce12fe46e349ca1a165869a9f29bb842 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Tue, 3 Mar 2026 21:55:56 +0100 Subject: [PATCH 333/609] [rtttl] add new codeowner (#14440) --- CODEOWNERS | 2 +- esphome/components/rtttl/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 21bee125c6..b22f85b71d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -412,7 +412,7 @@ esphome/components/rp2040_pio_led_strip/* @Papa-DMan esphome/components/rp2040_pwm/* @jesserockz esphome/components/rpi_dpi_rgb/* @clydebarrow esphome/components/rtl87xx/* @kuba2k2 -esphome/components/rtttl/* @glmnet +esphome/components/rtttl/* @glmnet @ximex esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt esphome/components/runtime_stats/* @bdraco esphome/components/rx8130/* @beormund diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py index ebbe5366aa..19412bb454 100644 --- a/esphome/components/rtttl/__init__.py +++ b/esphome/components/rtttl/__init__.py @@ -17,7 +17,7 @@ import esphome.final_validate as fv _LOGGER = logging.getLogger(__name__) -CODEOWNERS = ["@glmnet"] +CODEOWNERS = ["@glmnet", "@ximex"] CONF_RTTTL = "rtttl" CONF_ON_FINISHED_PLAYBACK = "on_finished_playback" From ee78d7a0c05b8f3d6878751c95cd5c9dae232690 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:42:41 -0500 Subject: [PATCH 334/609] [tests] Fix integration test race condition in PlatformIO cache init (#14435) Co-authored-by: Claude Opus 4.6 --- tests/integration/conftest.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 36df1bc83e..b7f7fc60b3 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -73,11 +73,6 @@ def shared_platformio_cache() -> Generator[Path]: test_cache_dir = Path.home() / ".esphome-integration-tests" cache_dir = test_cache_dir / "platformio" - # Create the temp directory that PlatformIO uses to avoid race conditions - # This ensures it exists and won't be deleted by parallel processes - platformio_tmp_dir = cache_dir / ".cache" / "tmp" - platformio_tmp_dir.mkdir(parents=True, exist_ok=True) - # Use a lock file in the home directory to ensure only one process initializes the cache # This is needed when running with pytest-xdist # The lock file must be in a directory that already exists to avoid race conditions @@ -87,8 +82,9 @@ def shared_platformio_cache() -> Generator[Path]: with open(lock_file, "w") as lock_fd: fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX) - # Check if cache needs initialization while holding the lock - if not cache_dir.exists() or not any(cache_dir.iterdir()): + # Check if the native platform is installed (the actual indicator of a populated cache) + native_platform = cache_dir / "platforms" / "native" + if not native_platform.exists(): # Create the test cache directory if it doesn't exist test_cache_dir.mkdir(exist_ok=True) From 989330d6bc5caa2b94950b55d3811c5ee2c51be3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:54:40 +1300 Subject: [PATCH 335/609] [globals] Fix handling of string booleans in yaml (#14447) --- esphome/components/globals/__init__.py | 2 +- tests/components/globals/common.yaml | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index fe11a93a4b..fe83b1ea7c 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -51,7 +51,7 @@ _RESTORING_SCHEMA = cv.Schema( def _globals_schema(config: ConfigType) -> ConfigType: """Select schema based on restore_value setting.""" - if config.get(CONF_RESTORE_VALUE, False): + if cv.boolean(config.get(CONF_RESTORE_VALUE, False)): return _RESTORING_SCHEMA(config) return _NON_RESTORING_SCHEMA(config) diff --git a/tests/components/globals/common.yaml b/tests/components/globals/common.yaml index efa3cba076..35dca0624f 100644 --- a/tests/components/globals/common.yaml +++ b/tests/components/globals/common.yaml @@ -27,3 +27,14 @@ globals: type: bool restore_value: false initial_value: "false" + # Test restore_value with string "false" - should be converted to bool false + - id: glob_no_restore_string_false + type: int + restore_value: "false" + initial_value: "42" + # Test restore_value with string "true" - should be converted to bool true + - id: glob_restore_string_true + type: int + restore_value: "true" + initial_value: "99" + update_interval: 5s From 43a6fe9b6cb446974db2f3cce1f1f9b67cbe1719 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 3 Mar 2026 19:06:36 -0600 Subject: [PATCH 336/609] [core] add a StaticTask helper to manage task lifecycles (#14446) --- esphome/core/config.py | 4 +++ esphome/core/static_task.cpp | 64 ++++++++++++++++++++++++++++++++++++ esphome/core/static_task.h | 50 ++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 esphome/core/static_task.cpp create mode 100644 esphome/core/static_task.h diff --git a/esphome/core/config.py b/esphome/core/config.py index 9411949bb9..4f526404fe 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -687,6 +687,10 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, + "static_task.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, "time_64.cpp": { PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, diff --git a/esphome/core/static_task.cpp b/esphome/core/static_task.cpp new file mode 100644 index 0000000000..4cfead44c2 --- /dev/null +++ b/esphome/core/static_task.cpp @@ -0,0 +1,64 @@ +#include "esphome/core/static_task.h" + +#ifdef USE_ESP32 + +#include "esphome/core/helpers.h" + +namespace esphome { + +bool StaticTask::create(TaskFunction_t fn, const char *name, uint32_t stack_size, void *param, UBaseType_t priority, + bool use_psram) { + if (this->handle_ != nullptr) { + // Task is already created; must call destroy() first + return false; + } + + if (this->stack_buffer_ != nullptr && (stack_size > this->stack_size_ || use_psram != this->use_psram_)) { + // Existing buffer is too small or wrong memory type; deallocate to reallocate below + RAMAllocator allocator(this->use_psram_ ? RAMAllocator::ALLOC_EXTERNAL + : RAMAllocator::ALLOC_INTERNAL); + allocator.deallocate(this->stack_buffer_, this->stack_size_); + this->stack_buffer_ = nullptr; + } + + if (this->stack_buffer_ == nullptr) { + this->stack_size_ = stack_size; + this->use_psram_ = use_psram; + RAMAllocator allocator(use_psram ? RAMAllocator::ALLOC_EXTERNAL + : RAMAllocator::ALLOC_INTERNAL); + this->stack_buffer_ = allocator.allocate(stack_size); + } + if (this->stack_buffer_ == nullptr) { + return false; + } + + this->handle_ = xTaskCreateStatic(fn, name, this->stack_size_, param, priority, this->stack_buffer_, &this->tcb_); + if (this->handle_ == nullptr) { + this->deallocate(); + return false; + } + return true; +} + +void StaticTask::destroy() { + if (this->handle_ != nullptr) { + TaskHandle_t handle = this->handle_; + this->handle_ = nullptr; + vTaskDelete(handle); + } +} + +void StaticTask::deallocate() { + this->destroy(); + if (this->stack_buffer_ != nullptr) { + RAMAllocator allocator(this->use_psram_ ? RAMAllocator::ALLOC_EXTERNAL + : RAMAllocator::ALLOC_INTERNAL); + allocator.deallocate(this->stack_buffer_, this->stack_size_); + this->stack_buffer_ = nullptr; + this->stack_size_ = 0; + } +} + +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/core/static_task.h b/esphome/core/static_task.h new file mode 100644 index 0000000000..5fd5b38f9e --- /dev/null +++ b/esphome/core/static_task.h @@ -0,0 +1,50 @@ +#pragma once + +#ifdef USE_ESP32 + +#include +#include + +#include + +namespace esphome { + +/** Helper for FreeRTOS static task management. + * Bundles TaskHandle_t, StaticTask_t, and the stack buffer into one object with create/destroy methods. + */ +class StaticTask { + public: + /// @brief Check if the task has been created and not yet destroyed. + bool is_created() const { return this->handle_ != nullptr; } + + /// @brief Get the FreeRTOS task handle. + TaskHandle_t get_handle() const { return this->handle_; } + + /// @brief Allocate stack and create task. + /// @param fn Task function + /// @param name Task name (for debug) + /// @param stack_size Stack size in StackType_t words + /// @param param Parameter passed to task function + /// @param priority FreeRTOS task priority + /// @param use_psram If true, allocate stack in PSRAM; otherwise internal RAM + /// @return true on success + bool create(TaskFunction_t fn, const char *name, uint32_t stack_size, void *param, UBaseType_t priority, + bool use_psram); + + /// @brief Delete the task but keep the stack buffer allocated for reuse by a subsequent create() call. + void destroy(); + + /// @brief Delete the task (if running) and free the stack buffer. + void deallocate(); + + protected: + TaskHandle_t handle_{nullptr}; + StaticTask_t tcb_; + StackType_t *stack_buffer_{nullptr}; + uint32_t stack_size_{0}; + bool use_psram_{false}; +}; + +} // namespace esphome + +#endif // USE_ESP32 From 9371159a7e56e49f3b2ab71c6ad93034d1c29e01 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Mar 2026 15:14:05 -1000 Subject: [PATCH 337/609] [core] Replace custom esphome::optional with std::optional (#14368) Co-authored-by: Claude Opus 4.6 --- esphome/components/am43/cover/am43_cover.cpp | 5 +- esphome/components/anova/anova.cpp | 10 +- esphome/components/ballu/ballu.cpp | 2 +- .../bang_bang/bang_bang_climate.cpp | 20 +- .../bedjet/climate/bedjet_climate.cpp | 20 +- esphome/components/bedjet/fan/bedjet_fan.cpp | 8 +- esphome/components/binary/fan/binary_fan.cpp | 15 +- .../ble_presence/ble_presence_device.h | 5 +- esphome/components/ble_rssi/ble_rssi_sensor.h | 5 +- esphome/components/climate_ir/climate_ir.cpp | 25 +- .../climate_ir_lg/climate_ir_lg.cpp | 2 +- .../components/climate_ir_lg/climate_ir_lg.h | 3 +- esphome/components/coolix/coolix.cpp | 2 +- esphome/components/coolix/coolix.h | 3 +- esphome/components/copy/cover/copy_cover.cpp | 15 +- esphome/components/copy/fan/copy_fan.cpp | 20 +- .../components/copy/select/copy_select.cpp | 5 +- .../current_based/current_based_cover.cpp | 5 +- esphome/components/daikin/daikin.cpp | 2 +- esphome/components/daikin_arc/daikin_arc.cpp | 7 +- esphome/components/daikin_brc/daikin_brc.cpp | 2 +- .../deep_sleep/deep_sleep_esp8266.cpp | 2 +- esphome/components/delonghi/delonghi.cpp | 2 +- .../demo/demo_alarm_control_panel.h | 9 +- esphome/components/demo/demo_climate.h | 48 ++-- esphome/components/demo/demo_cover.h | 10 +- esphome/components/demo/demo_fan.h | 20 +- esphome/components/demo/demo_lock.h | 5 +- esphome/components/demo/demo_valve.h | 11 +- esphome/components/emmeti/emmeti.cpp | 2 +- esphome/components/endstop/endstop_cover.cpp | 5 +- .../esp32_ble_tracker/esp32_ble_tracker.h | 2 +- .../esp32_rmt_led_strip/led_strip.cpp | 5 +- .../components/fastled_base/fastled_light.cpp | 5 +- .../components/feedback/feedback_cover.cpp | 7 +- .../fujitsu_general/fujitsu_general.cpp | 2 +- esphome/components/gree/gree.cpp | 6 +- esphome/components/haier/hon_climate.cpp | 15 +- .../components/haier/smartair2_climate.cpp | 6 +- .../components/hbridge/fan/hbridge_fan.cpp | 20 +- esphome/components/he60r/he60r.cpp | 7 +- .../hitachi_ac344/hitachi_ac344.cpp | 2 +- .../hitachi_ac424/hitachi_ac424.cpp | 2 +- .../media_player/i2s_audio_media_player.cpp | 25 +- esphome/components/infrared/infrared.cpp | 5 +- esphome/components/ledc/ledc_output.cpp | 3 +- esphome/components/mcp4461/mcp4461.cpp | 5 +- esphome/components/midea/air_conditioner.cpp | 25 +- esphome/components/midea_ir/midea_ir.cpp | 23 +- esphome/components/mitsubishi/mitsubishi.cpp | 7 +- .../select/modbus_select.cpp | 2 +- esphome/components/noblex/noblex.cpp | 2 +- esphome/components/noblex/noblex.h | 3 +- .../components/output/lock/output_lock.cpp | 5 +- esphome/components/pid/pid_climate.cpp | 10 +- esphome/components/pzem004t/pzem004t.cpp | 5 +- esphome/components/select/select_call.cpp | 2 +- esphome/components/sgp4x/sgp4x.h | 18 +- .../media_player/speaker_media_player.cpp | 31 +-- esphome/components/speed/fan/speed_fan.cpp | 20 +- esphome/components/sprinkler/sprinkler.cpp | 21 +- esphome/components/tcl112/tcl112.cpp | 2 +- .../template_alarm_control_panel.cpp | 19 +- .../template/cover/template_cover.cpp | 10 +- .../template/datetime/template_date.cpp | 27 ++- .../template/datetime/template_datetime.cpp | 54 +++-- .../template/datetime/template_time.cpp | 27 ++- .../components/template/fan/template_fan.cpp | 20 +- .../template/lock/template_lock.cpp | 5 +- .../template/valve/template_valve.cpp | 5 +- .../water_heater/template_water_heater.cpp | 15 +- .../thermostat/thermostat_climate.cpp | 50 ++-- .../time_based/time_based_cover.cpp | 5 +- .../components/tormatic/tormatic_cover.cpp | 5 +- esphome/components/toshiba/toshiba.cpp | 8 +- .../components/tuya/climate/tuya_climate.cpp | 184 +++++++++------ esphome/components/tuya/cover/tuya_cover.cpp | 16 +- esphome/components/tuya/fan/tuya_fan.cpp | 88 ++++--- esphome/components/tuya/light/tuya_light.cpp | 5 +- .../climate/uponor_smatrix_climate.cpp | 5 +- esphome/components/whirlpool/whirlpool.cpp | 2 +- esphome/components/whynter/whynter.cpp | 2 +- esphome/components/wifi/wifi_component.cpp | 10 +- .../wifi/wifi_component_esp8266.cpp | 5 +- .../wifi/wifi_component_esp_idf.cpp | 5 +- esphome/components/yashima/yashima.cpp | 10 +- esphome/components/zhlt01/zhlt01.cpp | 6 +- esphome/core/entity_base.h | 2 +- esphome/core/optional.h | 218 +----------------- esphome/cpp_types.py | 4 +- tests/component_tests/text/test_text.py | 17 +- tests/components/template/common-base.yaml | 7 +- 92 files changed, 723 insertions(+), 701 deletions(-) diff --git a/esphome/components/am43/cover/am43_cover.cpp b/esphome/components/am43/cover/am43_cover.cpp index 0d49439095..2fa26d266a 100644 --- a/esphome/components/am43/cover/am43_cover.cpp +++ b/esphome/components/am43/cover/am43_cover.cpp @@ -63,8 +63,9 @@ void Am43Component::control(const CoverCall &call) { ESP_LOGW(TAG, "[%s] Error writing stop command to device, error = %d", this->get_name().c_str(), status); } } - if (call.get_position().has_value()) { - auto pos = *call.get_position(); + auto opt_pos = call.get_position(); + if (opt_pos.has_value()) { + auto pos = *opt_pos; if (this->invert_position_) pos = 1 - pos; diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index 2693224a97..b625f92115 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -24,8 +24,9 @@ void Anova::loop() { } void Anova::control(const ClimateCall &call) { - if (call.get_mode().has_value()) { - ClimateMode mode = *call.get_mode(); + auto mode_val = call.get_mode(); + if (mode_val.has_value()) { + ClimateMode mode = *mode_val; AnovaPacket *pkt; switch (mode) { case climate::CLIMATE_MODE_OFF: @@ -45,8 +46,9 @@ void Anova::control(const ClimateCall &call) { ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } - if (call.get_target_temperature().has_value()) { - auto *pkt = this->codec_->get_set_target_temp_request(*call.get_target_temperature()); + auto target_temp = call.get_target_temperature(); + if (target_temp.has_value()) { + auto *pkt = this->codec_->get_set_target_temp_request(*target_temp); auto status = esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); diff --git a/esphome/components/ballu/ballu.cpp b/esphome/components/ballu/ballu.cpp index b33ad11c1f..deb742f8c6 100644 --- a/esphome/components/ballu/ballu.cpp +++ b/esphome/components/ballu/ballu.cpp @@ -47,7 +47,7 @@ void BalluClimate::transmit_state() { remote_state[11] = 0x1e; // Fan speed - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_HIGH: remote_state[4] |= BALLU_FAN_HIGH; break; diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index 6871e9df5d..1058bce6a4 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -45,17 +45,21 @@ void BangBangClimate::setup() { } void BangBangClimate::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value()) { - this->mode = *call.get_mode(); + auto mode = call.get_mode(); + if (mode.has_value()) { + this->mode = *mode; } - if (call.get_target_temperature_low().has_value()) { - this->target_temperature_low = *call.get_target_temperature_low(); + auto target_temperature_low = call.get_target_temperature_low(); + if (target_temperature_low.has_value()) { + this->target_temperature_low = *target_temperature_low; } - if (call.get_target_temperature_high().has_value()) { - this->target_temperature_high = *call.get_target_temperature_high(); + auto target_temperature_high = call.get_target_temperature_high(); + if (target_temperature_high.has_value()) { + this->target_temperature_high = *target_temperature_high; } - if (call.get_preset().has_value()) { - this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY); + auto preset = call.get_preset(); + if (preset.has_value()) { + this->change_away_(*preset == climate::CLIMATE_PRESET_AWAY); } this->compute_state_(); diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index 68a0342873..a17407f08f 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -96,8 +96,9 @@ void BedJetClimate::control(const ClimateCall &call) { return; } - if (call.get_mode().has_value()) { - ClimateMode mode = *call.get_mode(); + auto mode_opt = call.get_mode(); + if (mode_opt.has_value()) { + ClimateMode mode = *mode_opt; bool button_result; switch (mode) { case CLIMATE_MODE_OFF: @@ -125,8 +126,9 @@ void BedJetClimate::control(const ClimateCall &call) { } } - if (call.get_target_temperature().has_value()) { - auto target_temp = *call.get_target_temperature(); + auto target_temp_opt = call.get_target_temperature(); + if (target_temp_opt.has_value()) { + auto target_temp = *target_temp_opt; auto result = this->parent_->set_target_temp(target_temp); if (result) { @@ -134,8 +136,9 @@ void BedJetClimate::control(const ClimateCall &call) { } } - if (call.get_preset().has_value()) { - ClimatePreset preset = *call.get_preset(); + auto preset_opt = call.get_preset(); + if (preset_opt.has_value()) { + ClimatePreset preset = *preset_opt; bool result; if (preset == CLIMATE_PRESET_BOOST) { @@ -187,10 +190,11 @@ void BedJetClimate::control(const ClimateCall &call) { } } - if (call.get_fan_mode().has_value()) { + auto fan_mode_opt = call.get_fan_mode(); + if (fan_mode_opt.has_value()) { // Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments. // We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here. - auto fan_mode = *call.get_fan_mode(); + auto fan_mode = *fan_mode_opt; bool result; if (fan_mode == CLIMATE_FAN_LOW) { result = this->parent_->set_fan_speed(20); diff --git a/esphome/components/bedjet/fan/bedjet_fan.cpp b/esphome/components/bedjet/fan/bedjet_fan.cpp index e272241040..9539e169a4 100644 --- a/esphome/components/bedjet/fan/bedjet_fan.cpp +++ b/esphome/components/bedjet/fan/bedjet_fan.cpp @@ -19,7 +19,8 @@ void BedJetFan::control(const fan::FanCall &call) { } bool did_change = false; - if (call.get_state().has_value() && this->state != *call.get_state()) { + auto state_opt = call.get_state(); + if (state_opt.has_value() && this->state != *state_opt) { // Turning off is easy: if (this->state && this->parent_->button_off()) { this->state = false; @@ -36,8 +37,9 @@ void BedJetFan::control(const fan::FanCall &call) { } // ignore speed changes if not on or turning on - if (this->state && call.get_speed().has_value()) { - auto speed = *call.get_speed(); + auto speed_opt = call.get_speed(); + if (this->state && speed_opt.has_value()) { + auto speed = *speed_opt; if (speed >= 1) { this->speed = speed; // Fan.speed is 1-20, but Bedjet expects 0-19, so subtract 1 diff --git a/esphome/components/binary/fan/binary_fan.cpp b/esphome/components/binary/fan/binary_fan.cpp index a2f75242de..17d4df095a 100644 --- a/esphome/components/binary/fan/binary_fan.cpp +++ b/esphome/components/binary/fan/binary_fan.cpp @@ -18,12 +18,15 @@ fan::FanTraits BinaryFan::get_traits() { return fan::FanTraits(this->oscillating_ != nullptr, false, this->direction_ != nullptr, 0); } void BinaryFan::control(const fan::FanCall &call) { - if (call.get_state().has_value()) - this->state = *call.get_state(); - if (call.get_oscillating().has_value()) - this->oscillating = *call.get_oscillating(); - if (call.get_direction().has_value()) - this->direction = *call.get_direction(); + auto state = call.get_state(); + if (state.has_value()) + this->state = *state; + auto oscillating = call.get_oscillating(); + if (oscillating.has_value()) + this->oscillating = *oscillating; + auto direction = call.get_direction(); + if (direction.has_value()) + this->direction = *direction; this->write_state_(); this->publish_state(); diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index f2f0a3ed19..8ae5edab3a 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -76,11 +76,12 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, } break; case MATCH_BY_IBEACON_UUID: - if (!device.get_ibeacon().has_value()) { + auto maybe_ibeacon = device.get_ibeacon(); + if (!maybe_ibeacon.has_value()) { return false; } - auto ibeacon = device.get_ibeacon().value(); + auto ibeacon = *maybe_ibeacon; if (this->ibeacon_uuid_ != ibeacon.get_uuid()) { return false; diff --git a/esphome/components/ble_rssi/ble_rssi_sensor.h b/esphome/components/ble_rssi/ble_rssi_sensor.h index 80245a1fe1..81f21c94dd 100644 --- a/esphome/components/ble_rssi/ble_rssi_sensor.h +++ b/esphome/components/ble_rssi/ble_rssi_sensor.h @@ -74,11 +74,12 @@ class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDevi } break; case MATCH_BY_IBEACON_UUID: - if (!device.get_ibeacon().has_value()) { + auto maybe_ibeacon = device.get_ibeacon(); + if (!maybe_ibeacon.has_value()) { return false; } - auto ibeacon = device.get_ibeacon().value(); + auto ibeacon = *maybe_ibeacon; if (this->ibeacon_uuid_ != ibeacon.get_uuid()) { return false; diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index 50c8d459b0..cc291ff17c 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -71,16 +71,21 @@ void ClimateIR::setup() { } void ClimateIR::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value()) - this->mode = *call.get_mode(); - if (call.get_target_temperature().has_value()) - this->target_temperature = *call.get_target_temperature(); - if (call.get_fan_mode().has_value()) - this->fan_mode = *call.get_fan_mode(); - if (call.get_swing_mode().has_value()) - this->swing_mode = *call.get_swing_mode(); - if (call.get_preset().has_value()) - this->preset = *call.get_preset(); + auto mode = call.get_mode(); + if (mode.has_value()) + this->mode = *mode; + auto target_temperature = call.get_target_temperature(); + if (target_temperature.has_value()) + this->target_temperature = *target_temperature; + auto fan_mode = call.get_fan_mode(); + if (fan_mode.has_value()) + this->fan_mode = fan_mode; + auto swing_mode = call.get_swing_mode(); + if (swing_mode.has_value()) + this->swing_mode = *swing_mode; + auto preset = call.get_preset(); + if (preset.has_value()) + this->preset = preset; this->transmit_state(); this->publish_state(); } diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.cpp b/esphome/components/climate_ir_lg/climate_ir_lg.cpp index 7fe0646230..90e3d006a8 100644 --- a/esphome/components/climate_ir_lg/climate_ir_lg.cpp +++ b/esphome/components/climate_ir_lg/climate_ir_lg.cpp @@ -79,7 +79,7 @@ void LgIrClimate::transmit_state() { if (this->mode == climate::CLIMATE_MODE_OFF) { remote_state |= FAN_AUTO; } else { - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_HIGH: remote_state |= FAN_MAX; break; diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.h b/esphome/components/climate_ir_lg/climate_ir_lg.h index 00fc99ae73..958245279f 100644 --- a/esphome/components/climate_ir_lg/climate_ir_lg.h +++ b/esphome/components/climate_ir_lg/climate_ir_lg.h @@ -23,7 +23,8 @@ class LgIrClimate : public climate_ir::ClimateIR { void control(const climate::ClimateCall &call) override { this->send_swing_cmd_ = call.get_swing_mode().has_value(); // swing resets after unit powered off - if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF) + auto mode = call.get_mode(); + if (mode.has_value() && *mode == climate::CLIMATE_MODE_OFF) this->swing_mode = climate::CLIMATE_SWING_OFF; climate_ir::ClimateIR::control(call); } diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index 5c6bfd7740..d8ea676478 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -83,7 +83,7 @@ void CoolixClimate::transmit_state() { this->fan_mode = climate::CLIMATE_FAN_AUTO; remote_state |= COOLIX_FAN_MODE_AUTO_DRY; } else { - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_HIGH: remote_state |= COOLIX_FAN_MAX; break; diff --git a/esphome/components/coolix/coolix.h b/esphome/components/coolix/coolix.h index f4b4ff8e0e..51ddcdf8f2 100644 --- a/esphome/components/coolix/coolix.h +++ b/esphome/components/coolix/coolix.h @@ -23,7 +23,8 @@ class CoolixClimate : public climate_ir::ClimateIR { void control(const climate::ClimateCall &call) override { send_swing_cmd_ = call.get_swing_mode().has_value(); // swing resets after unit powered off - if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF) + auto mode = call.get_mode(); + if (mode.has_value() && *mode == climate::CLIMATE_MODE_OFF) this->swing_mode = climate::CLIMATE_SWING_OFF; climate_ir::ClimateIR::control(call); } diff --git a/esphome/components/copy/cover/copy_cover.cpp b/esphome/components/copy/cover/copy_cover.cpp index 28f8c9877c..c139869d8f 100644 --- a/esphome/components/copy/cover/copy_cover.cpp +++ b/esphome/components/copy/cover/copy_cover.cpp @@ -38,12 +38,15 @@ cover::CoverTraits CopyCover::get_traits() { void CopyCover::control(const cover::CoverCall &call) { auto call2 = source_->make_call(); call2.set_stop(call.get_stop()); - if (call.get_tilt().has_value()) - call2.set_tilt(*call.get_tilt()); - if (call.get_position().has_value()) - call2.set_position(*call.get_position()); - if (call.get_tilt().has_value()) - call2.set_tilt(*call.get_tilt()); + auto tilt = call.get_tilt(); + if (tilt.has_value()) + call2.set_tilt(*tilt); + auto position = call.get_position(); + if (position.has_value()) + call2.set_position(*position); + auto tilt2 = call.get_tilt(); + if (tilt2.has_value()) + call2.set_tilt(*tilt2); call2.perform(); } diff --git a/esphome/components/copy/fan/copy_fan.cpp b/esphome/components/copy/fan/copy_fan.cpp index b4a43cf2f1..14c600d71f 100644 --- a/esphome/components/copy/fan/copy_fan.cpp +++ b/esphome/components/copy/fan/copy_fan.cpp @@ -45,14 +45,18 @@ fan::FanTraits CopyFan::get_traits() { void CopyFan::control(const fan::FanCall &call) { auto call2 = source_->make_call(); - if (call.get_state().has_value()) - call2.set_state(*call.get_state()); - if (call.get_oscillating().has_value()) - call2.set_oscillating(*call.get_oscillating()); - if (call.get_speed().has_value()) - call2.set_speed(*call.get_speed()); - if (call.get_direction().has_value()) - call2.set_direction(*call.get_direction()); + auto state = call.get_state(); + if (state.has_value()) + call2.set_state(*state); + auto oscillating = call.get_oscillating(); + if (oscillating.has_value()) + call2.set_oscillating(*oscillating); + auto speed = call.get_speed(); + if (speed.has_value()) + call2.set_speed(*speed); + auto direction = call.get_direction(); + if (direction.has_value()) + call2.set_direction(*direction); if (call.has_preset_mode()) call2.set_preset_mode(call.get_preset_mode()); call2.perform(); diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp index e85e08e353..227fe33182 100644 --- a/esphome/components/copy/select/copy_select.cpp +++ b/esphome/components/copy/select/copy_select.cpp @@ -11,8 +11,9 @@ void CopySelect::setup() { traits.set_options(source_->traits.get_options()); - if (source_->has_state()) - this->publish_state(source_->active_index().value()); + auto idx = this->source_->active_index(); + if (idx.has_value()) + this->publish_state(*idx); } void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); } diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index 58ae7cbc34..13bf11b991 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -37,8 +37,9 @@ void CurrentBasedCover::control(const CoverCall &call) { } } } - if (call.get_position().has_value()) { - auto pos = *call.get_position(); + auto opt_pos = call.get_position(); + if (opt_pos.has_value()) { + auto pos = *opt_pos; if (fabsf(this->position - pos) < 0.01) { // already at target } else { diff --git a/esphome/components/daikin/daikin.cpp b/esphome/components/daikin/daikin.cpp index 359c63aeca..a285f3613d 100644 --- a/esphome/components/daikin/daikin.cpp +++ b/esphome/components/daikin/daikin.cpp @@ -94,7 +94,7 @@ uint8_t DaikinClimate::operation_mode_() const { uint16_t DaikinClimate::fan_speed_() const { uint16_t fan_speed; - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_QUIET: fan_speed = DAIKIN_FAN_SILENT << 8; break; diff --git a/esphome/components/daikin_arc/daikin_arc.cpp b/esphome/components/daikin_arc/daikin_arc.cpp index 4726310806..c45fa307a7 100644 --- a/esphome/components/daikin_arc/daikin_arc.cpp +++ b/esphome/components/daikin_arc/daikin_arc.cpp @@ -176,7 +176,7 @@ uint8_t DaikinArcClimate::operation_mode_() { uint16_t DaikinArcClimate::fan_speed_() { uint16_t fan_speed; - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: fan_speed = DAIKIN_FAN_1 << 8; break; @@ -485,8 +485,9 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { } void DaikinArcClimate::control(const climate::ClimateCall &call) { - if (call.get_target_humidity().has_value()) { - this->target_humidity = *call.get_target_humidity(); + auto target_humidity = call.get_target_humidity(); + if (target_humidity.has_value()) { + this->target_humidity = *target_humidity; } climate_ir::ClimateIR::control(call); } diff --git a/esphome/components/daikin_brc/daikin_brc.cpp b/esphome/components/daikin_brc/daikin_brc.cpp index 6683d70f80..1179cb07d7 100644 --- a/esphome/components/daikin_brc/daikin_brc.cpp +++ b/esphome/components/daikin_brc/daikin_brc.cpp @@ -111,7 +111,7 @@ uint8_t DaikinBrcClimate::operation_mode_() { uint8_t DaikinBrcClimate::fan_speed_swing_() { uint16_t fan_speed; - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: fan_speed = DAIKIN_BRC_FAN_1; break; diff --git a/esphome/components/deep_sleep/deep_sleep_esp8266.cpp b/esphome/components/deep_sleep/deep_sleep_esp8266.cpp index 54d2aa993d..efbd45c34e 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp8266.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp8266.cpp @@ -15,7 +15,7 @@ void DeepSleepComponent::dump_config_platform_() {} bool DeepSleepComponent::prepare_to_sleep_() { return true; } void DeepSleepComponent::deep_sleep_() { - ESP.deepSleep(*this->sleep_duration_); // NOLINT(readability-static-accessed-through-instance) + ESP.deepSleep(this->sleep_duration_.value_or(0)); // NOLINT(readability-static-accessed-through-instance) } } // namespace deep_sleep diff --git a/esphome/components/delonghi/delonghi.cpp b/esphome/components/delonghi/delonghi.cpp index 9bc0b5753d..19af703ab2 100644 --- a/esphome/components/delonghi/delonghi.cpp +++ b/esphome/components/delonghi/delonghi.cpp @@ -64,7 +64,7 @@ uint8_t DelonghiClimate::operation_mode_() { uint16_t DelonghiClimate::fan_speed_() { uint16_t fan_speed; - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: fan_speed = DELONGHI_FAN_LOW; break; diff --git a/esphome/components/demo/demo_alarm_control_panel.h b/esphome/components/demo/demo_alarm_control_panel.h index f59434830b..76cb24c2f4 100644 --- a/esphome/components/demo/demo_alarm_control_panel.h +++ b/esphome/components/demo/demo_alarm_control_panel.h @@ -29,10 +29,11 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component { protected: void control(const AlarmControlPanelCall &call) override { auto state = call.get_state().value_or(ACP_STATE_DISARMED); + auto code = call.get_code(); switch (state) { case ACP_STATE_ARMED_AWAY: - if (this->get_requires_code_to_arm() && call.get_code().has_value()) { - if (call.get_code().value() != "1234") { + if (this->get_requires_code_to_arm() && code.has_value()) { + if (*code != "1234") { this->status_momentary_error("invalid_code", 5000); return; } @@ -40,8 +41,8 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component { this->publish_state(ACP_STATE_ARMED_AWAY); break; case ACP_STATE_DISARMED: - if (this->get_requires_code() && call.get_code().has_value()) { - if (call.get_code().value() != "1234") { + if (this->get_requires_code() && code.has_value()) { + if (*code != "1234") { this->status_momentary_error("invalid_code", 5000); return; } diff --git a/esphome/components/demo/demo_climate.h b/esphome/components/demo/demo_climate.h index e2dfb0142b..c5f07ac114 100644 --- a/esphome/components/demo/demo_climate.h +++ b/esphome/components/demo/demo_climate.h @@ -45,33 +45,31 @@ class DemoClimate : public climate::Climate, public Component { protected: void control(const climate::ClimateCall &call) override { - if (call.get_mode().has_value()) { - this->mode = *call.get_mode(); - } - if (call.get_target_temperature().has_value()) { - this->target_temperature = *call.get_target_temperature(); - } - if (call.get_target_temperature_low().has_value()) { - this->target_temperature_low = *call.get_target_temperature_low(); - } - if (call.get_target_temperature_high().has_value()) { - this->target_temperature_high = *call.get_target_temperature_high(); - } - if (call.get_fan_mode().has_value()) { - this->set_fan_mode_(*call.get_fan_mode()); - } - if (call.get_swing_mode().has_value()) { - this->swing_mode = *call.get_swing_mode(); - } - if (call.has_custom_fan_mode()) { + auto mode = call.get_mode(); + if (mode.has_value()) + this->mode = *mode; + auto target_temperature = call.get_target_temperature(); + if (target_temperature.has_value()) + this->target_temperature = *target_temperature; + auto target_temperature_low = call.get_target_temperature_low(); + if (target_temperature_low.has_value()) + this->target_temperature_low = *target_temperature_low; + auto target_temperature_high = call.get_target_temperature_high(); + if (target_temperature_high.has_value()) + this->target_temperature_high = *target_temperature_high; + auto fan_mode = call.get_fan_mode(); + if (fan_mode.has_value()) + this->set_fan_mode_(*fan_mode); + auto swing_mode = call.get_swing_mode(); + if (swing_mode.has_value()) + this->swing_mode = *swing_mode; + if (call.has_custom_fan_mode()) this->set_custom_fan_mode_(call.get_custom_fan_mode()); - } - if (call.get_preset().has_value()) { - this->set_preset_(*call.get_preset()); - } - if (call.has_custom_preset()) { + auto preset = call.get_preset(); + if (preset.has_value()) + this->set_preset_(*preset); + if (call.has_custom_preset()) this->set_custom_preset_(call.get_custom_preset()); - } this->publish_state(); } climate::ClimateTraits traits() override { diff --git a/esphome/components/demo/demo_cover.h b/esphome/components/demo/demo_cover.h index ec266d46ab..69dd5a4d2d 100644 --- a/esphome/components/demo/demo_cover.h +++ b/esphome/components/demo/demo_cover.h @@ -38,8 +38,9 @@ class DemoCover : public cover::Cover, public Component { protected: void control(const cover::CoverCall &call) override { - if (call.get_position().has_value()) { - float target = *call.get_position(); + auto pos = call.get_position(); + if (pos.has_value()) { + float target = *pos; this->current_operation = target > this->position ? cover::COVER_OPERATION_OPENING : cover::COVER_OPERATION_CLOSING; @@ -49,8 +50,9 @@ class DemoCover : public cover::Cover, public Component { this->publish_state(); }); } - if (call.get_tilt().has_value()) { - this->tilt = *call.get_tilt(); + auto tilt = call.get_tilt(); + if (tilt.has_value()) { + this->tilt = *tilt; } if (call.get_stop()) { this->cancel_timeout("move"); diff --git a/esphome/components/demo/demo_fan.h b/esphome/components/demo/demo_fan.h index 09edc4e0b7..a8b397f19a 100644 --- a/esphome/components/demo/demo_fan.h +++ b/esphome/components/demo/demo_fan.h @@ -47,14 +47,18 @@ class DemoFan : public fan::Fan, public Component { protected: void control(const fan::FanCall &call) override { - if (call.get_state().has_value()) - this->state = *call.get_state(); - if (call.get_oscillating().has_value()) - this->oscillating = *call.get_oscillating(); - if (call.get_speed().has_value()) - this->speed = *call.get_speed(); - if (call.get_direction().has_value()) - this->direction = *call.get_direction(); + auto state = call.get_state(); + if (state.has_value()) + this->state = *state; + auto oscillating = call.get_oscillating(); + if (oscillating.has_value()) + this->oscillating = *oscillating; + auto speed = call.get_speed(); + if (speed.has_value()) + this->speed = *speed; + auto direction = call.get_direction(); + if (direction.has_value()) + this->direction = *direction; this->publish_state(); } diff --git a/esphome/components/demo/demo_lock.h b/esphome/components/demo/demo_lock.h index 94d0f70a14..1e3fd51db4 100644 --- a/esphome/components/demo/demo_lock.h +++ b/esphome/components/demo/demo_lock.h @@ -8,8 +8,9 @@ namespace demo { class DemoLock : public lock::Lock { protected: void control(const lock::LockCall &call) override { - auto state = *call.get_state(); - this->publish_state(state); + auto state = call.get_state(); + if (state.has_value()) + this->publish_state(*state); } }; diff --git a/esphome/components/demo/demo_valve.h b/esphome/components/demo/demo_valve.h index 55d457f176..9a3122aca5 100644 --- a/esphome/components/demo/demo_valve.h +++ b/esphome/components/demo/demo_valve.h @@ -26,12 +26,15 @@ class DemoValve : public valve::Valve { protected: void control(const valve::ValveCall &call) override { - if (call.get_position().has_value()) { - this->position = *call.get_position(); + auto pos = call.get_position(); + if (pos.has_value()) { + this->position = *pos; this->publish_state(); return; - } else if (call.get_toggle().has_value()) { - if (call.get_toggle().value()) { + } + auto toggle = call.get_toggle(); + if (toggle.has_value()) { + if (*toggle) { if (this->position == valve::VALVE_OPEN) { this->position = valve::VALVE_CLOSED; this->publish_state(); diff --git a/esphome/components/emmeti/emmeti.cpp b/esphome/components/emmeti/emmeti.cpp index d3e923cbef..04976d95d7 100644 --- a/esphome/components/emmeti/emmeti.cpp +++ b/esphome/components/emmeti/emmeti.cpp @@ -28,7 +28,7 @@ uint8_t EmmetiClimate::set_mode_() { } uint8_t EmmetiClimate::set_fan_speed_() { - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: return EMMETI_FAN_1; case climate::CLIMATE_FAN_MEDIUM: diff --git a/esphome/components/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp index ea8a5ec186..5e0b9c72d3 100644 --- a/esphome/components/endstop/endstop_cover.cpp +++ b/esphome/components/endstop/endstop_cover.cpp @@ -37,8 +37,9 @@ void EndstopCover::control(const CoverCall &call) { } } } - if (call.get_position().has_value()) { - auto pos = *call.get_position(); + auto opt_pos = call.get_position(); + if (opt_pos.has_value()) { + auto pos = *opt_pos; if (pos == this->position) { // already at target } else { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index fa0cdb6f45..7f1c2b0f7c 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -107,7 +107,7 @@ class ESPBTDevice { for (auto &it : this->manufacturer_datas_) { auto res = ESPBLEiBeacon::from_manufacturer_data(it); if (res.has_value()) - return *res; + return res; } return {}; } diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index 8bb5cbb62e..66b41931aa 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -162,7 +162,8 @@ void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bi void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { // protect from refreshing too often uint32_t now = micros(); - if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) { + auto rate = this->max_refresh_rate_.value_or(0); + if (rate != 0 && (now - this->last_refresh_) < rate) { // try again next loop iteration, so that this change won't get lost this->schedule_show(); return; @@ -301,7 +302,7 @@ void ESP32RMTLEDStripLightOutput::dump_config() { " RGB Order: %s\n" " Max refresh rate: %" PRIu32 "\n" " Number of LEDs: %u", - rgb_order, *this->max_refresh_rate_, this->num_leds_); + rgb_order, this->max_refresh_rate_.value_or(0), this->num_leds_); } float ESP32RMTLEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/fastled_base/fastled_light.cpp b/esphome/components/fastled_base/fastled_light.cpp index b3946a34b5..504b8d473e 100644 --- a/esphome/components/fastled_base/fastled_light.cpp +++ b/esphome/components/fastled_base/fastled_light.cpp @@ -21,12 +21,13 @@ void FastLEDLightOutput::dump_config() { "FastLED light:\n" " Num LEDs: %u\n" " Max refresh rate: %u", - this->num_leds_, *this->max_refresh_rate_); + this->num_leds_, this->max_refresh_rate_.value_or(0)); } void FastLEDLightOutput::write_state(light::LightState *state) { // protect from refreshing too often uint32_t now = micros(); - if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) { + uint32_t max_rate = this->max_refresh_rate_.value_or(0); + if (max_rate != 0 && (now - this->last_refresh_) < max_rate) { // try again next loop iteration, so that this change won't get lost this->schedule_show(); return; diff --git a/esphome/components/feedback/feedback_cover.cpp b/esphome/components/feedback/feedback_cover.cpp index ffb19fa091..d247bada33 100644 --- a/esphome/components/feedback/feedback_cover.cpp +++ b/esphome/components/feedback/feedback_cover.cpp @@ -269,9 +269,12 @@ void FeedbackCover::control(const CoverCall &call) { this->start_direction_(COVER_OPERATION_CLOSING); } } - } else if (call.get_position().has_value()) { + } else { + auto pos_opt = call.get_position(); + if (!pos_opt.has_value()) + return; // go to position action - auto pos = *call.get_position(); + auto pos = *pos_opt; if (pos == this->position) { // already at target, diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp index 6c7adebfea..8aa0f51728 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.cpp +++ b/esphome/components/fujitsu_general/fujitsu_general.cpp @@ -141,7 +141,7 @@ void FujitsuGeneralClimate::transmit_state() { } // Set fan - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_HIGH: SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_HIGH); break; diff --git a/esphome/components/gree/gree.cpp b/esphome/components/gree/gree.cpp index b8cf8a39a8..8a9f264932 100644 --- a/esphome/components/gree/gree.cpp +++ b/esphome/components/gree/gree.cpp @@ -180,7 +180,7 @@ uint8_t GreeClimate::operation_mode_() { uint8_t GreeClimate::fan_speed_() { // YX1FF has 4 fan speeds -- we treat low as quiet and turbo as high if (this->model_ == GREE_YX1FF) { - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_QUIET: return GREE_FAN_1; case climate::CLIMATE_FAN_LOW: @@ -195,7 +195,7 @@ uint8_t GreeClimate::fan_speed_() { } } - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: return GREE_FAN_1; case climate::CLIMATE_FAN_MEDIUM: @@ -235,7 +235,7 @@ uint8_t GreeClimate::temperature_() { uint8_t GreeClimate::preset_() { // YX1FF has sleep preset if (this->model_ == GREE_YX1FF) { - switch (this->preset.value()) { + switch (this->preset.value_or(climate::CLIMATE_PRESET_NONE)) { case climate::CLIMATE_PRESET_NONE: return GREE_PRESET_NONE; case climate::CLIMATE_PRESET_SLEEP: diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index d98d273957..be5035caa1 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -893,7 +893,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * } else { this->preset = CLIMATE_PRESET_NONE; } - should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value()); + should_publish = should_publish || (!old_preset.has_value()) || + (old_preset.value_or(CLIMATE_PRESET_NONE) != this->preset.value_or(CLIMATE_PRESET_NONE)); } { // Target temperature @@ -936,7 +937,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * this->fan_mode = CLIMATE_FAN_HIGH; break; } - should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value()); + should_publish = should_publish || (!old_fan_mode.has_value()) || + (old_fan_mode.value_or(CLIMATE_FAN_ON) != this->fan_mode.value_or(CLIMATE_FAN_ON)); } // Display status // should be before "Climate mode" because it is changing this->mode @@ -1301,7 +1303,8 @@ void HonClimate::clear_control_messages_queue_() { } bool HonClimate::prepare_pending_action() { - switch (this->action_request_.value().action) { + auto &action_request = this->action_request_.value(); // NOLINT(bugprone-unchecked-optional-access) + switch (action_request.action) { case ActionRequest::START_SELF_CLEAN: if (this->control_method_ == HonControlMethod::SET_GROUP_PARAMETERS) { uint8_t control_out_buffer[haier_protocol::MAX_FRAME_SIZE]; @@ -1315,12 +1318,12 @@ bool HonClimate::prepare_pending_action() { out_data->ac_power = 1; out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; out_data->light_status = 0; - this->action_request_.value().message = haier_protocol::HaierMessage( + action_request.message = haier_protocol::HaierMessage( haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, control_out_buffer, this->real_control_packet_size_); return true; } else if (this->control_method_ == HonControlMethod::SET_SINGLE_PARAMETER) { - this->action_request_.value().message = + action_request.message = haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::SELF_CLEANING, @@ -1343,7 +1346,7 @@ bool HonClimate::prepare_pending_action() { out_data->ac_power = 1; out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; out_data->light_status = 0; - this->action_request_.value().message = haier_protocol::HaierMessage( + action_request.message = haier_protocol::HaierMessage( haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, control_out_buffer, this->real_control_packet_size_); return true; diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp index 63c22821b3..d24f8ad849 100644 --- a/esphome/components/haier/smartair2_climate.cpp +++ b/esphome/components/haier/smartair2_climate.cpp @@ -402,7 +402,8 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin } else { this->preset = CLIMATE_PRESET_NONE; } - should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value()); + should_publish = should_publish || (!old_preset.has_value()) || + (old_preset.value_or(CLIMATE_PRESET_NONE) != this->preset.value_or(CLIMATE_PRESET_NONE)); } { // Target temperature @@ -446,7 +447,8 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin this->fan_mode = CLIMATE_FAN_HIGH; break; } - should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value()); + should_publish = should_publish || (!old_fan_mode.has_value()) || + (old_fan_mode.value_or(CLIMATE_FAN_ON) != this->fan_mode.value_or(CLIMATE_FAN_ON)); } // Display status // should be before "Climate mode" because it is changing this->mode diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 38e4129e66..89c162eebf 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -49,14 +49,18 @@ void HBridgeFan::dump_config() { } void HBridgeFan::control(const fan::FanCall &call) { - if (call.get_state().has_value()) - this->state = *call.get_state(); - if (call.get_speed().has_value()) - this->speed = *call.get_speed(); - if (call.get_oscillating().has_value()) - this->oscillating = *call.get_oscillating(); - if (call.get_direction().has_value()) - this->direction = *call.get_direction(); + auto call_state = call.get_state(); + if (call_state.has_value()) + this->state = *call_state; + auto call_speed = call.get_speed(); + if (call_speed.has_value()) + this->speed = *call_speed; + auto call_oscillating = call.get_oscillating(); + if (call_oscillating.has_value()) + this->oscillating = *call_oscillating; + auto call_direction = call.get_direction(); + if (call_direction.has_value()) + this->direction = *call_direction; this->apply_preset_mode_(call); this->write_state_(); diff --git a/esphome/components/he60r/he60r.cpp b/esphome/components/he60r/he60r.cpp index ca17930272..fdcd1a29c0 100644 --- a/esphome/components/he60r/he60r.cpp +++ b/esphome/components/he60r/he60r.cpp @@ -171,9 +171,12 @@ void HE60rCover::control(const CoverCall &call) { } else { this->toggles_needed_++; } - } else if (call.get_position().has_value()) { + } else { + auto pos_opt = call.get_position(); + if (!pos_opt.has_value()) + return; // go to position action - auto pos = *call.get_position(); + auto pos = *pos_opt; // are we at the target? if (pos == this->position) { this->start_direction_(COVER_OPERATION_IDLE); diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.cpp b/esphome/components/hitachi_ac344/hitachi_ac344.cpp index 2bcb205644..69469cab2e 100644 --- a/esphome/components/hitachi_ac344/hitachi_ac344.cpp +++ b/esphome/components/hitachi_ac344/hitachi_ac344.cpp @@ -175,7 +175,7 @@ void HitachiClimate::transmit_state() { set_temp_(static_cast(this->target_temperature)); - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: set_fan_(HITACHI_AC344_FAN_LOW); break; diff --git a/esphome/components/hitachi_ac424/hitachi_ac424.cpp b/esphome/components/hitachi_ac424/hitachi_ac424.cpp index 64f23dfc17..0b3cc99a82 100644 --- a/esphome/components/hitachi_ac424/hitachi_ac424.cpp +++ b/esphome/components/hitachi_ac424/hitachi_ac424.cpp @@ -176,7 +176,7 @@ void HitachiClimate::transmit_state() { set_temp_(static_cast(this->target_temperature)); - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: set_fan_(HITACHI_AC424_FAN_LOW); break; diff --git a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp index 39301220d5..369c964a85 100644 --- a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp +++ b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp @@ -11,17 +11,18 @@ static const char *const TAG = "audio"; void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) { media_player::MediaPlayerState play_state = media_player::MEDIA_PLAYER_STATE_PLAYING; - if (call.get_announcement().has_value()) { - play_state = call.get_announcement().value() ? media_player::MEDIA_PLAYER_STATE_ANNOUNCING - : media_player::MEDIA_PLAYER_STATE_PLAYING; + auto announcement = call.get_announcement(); + if (announcement.has_value()) { + play_state = *announcement ? media_player::MEDIA_PLAYER_STATE_ANNOUNCING : media_player::MEDIA_PLAYER_STATE_PLAYING; } - if (call.get_media_url().has_value()) { - this->current_url_ = call.get_media_url(); + auto media_url = call.get_media_url(); + if (media_url.has_value()) { + this->current_url_ = media_url; if (this->i2s_state_ != I2S_STATE_STOPPED && this->audio_ != nullptr) { if (this->audio_->isRunning()) { this->audio_->stopSong(); } - this->audio_->connecttohost(this->current_url_.value().c_str()); + this->audio_->connecttohost(media_url->c_str()); this->state = play_state; } else { this->start(); @@ -32,13 +33,15 @@ void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) { this->is_announcement_ = true; } - if (call.get_volume().has_value()) { - this->volume = call.get_volume().value(); + auto vol = call.get_volume(); + if (vol.has_value()) { + this->volume = *vol; this->set_volume_(volume); this->unmute_(); } - if (call.get_command().has_value()) { - switch (call.get_command().value()) { + auto cmd = call.get_command(); + if (cmd.has_value()) { + switch (*cmd) { case media_player::MEDIA_PLAYER_COMMAND_MUTE: this->mute_(); break; @@ -67,7 +70,7 @@ void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) { if (this->i2s_state_ != I2S_STATE_RUNNING) { return; } - switch (call.get_command().value()) { + switch (*cmd) { case media_player::MEDIA_PLAYER_COMMAND_PLAY: if (!this->audio_->isRunning()) this->audio_->pauseResume(); diff --git a/esphome/components/infrared/infrared.cpp b/esphome/components/infrared/infrared.cpp index 4431869951..658c9fd0df 100644 --- a/esphome/components/infrared/infrared.cpp +++ b/esphome/components/infrared/infrared.cpp @@ -90,8 +90,9 @@ void Infrared::control(const InfraredCall &call) { auto *transmit_data = transmit_call.get_data(); // Set carrier frequency - if (call.get_carrier_frequency().has_value()) { - transmit_data->set_carrier_frequency(call.get_carrier_frequency().value()); + auto freq = call.get_carrier_frequency(); + if (freq.has_value()) { + transmit_data->set_carrier_frequency(*freq); } // Set timings based on format diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index a01d42ac8b..21e0682257 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -56,7 +56,8 @@ optional ledc_bit_depth_for_frequency(float frequency) { esp_err_t configure_timer_frequency(ledc_mode_t speed_mode, ledc_timer_t timer_num, ledc_channel_t chan_num, uint8_t channel, uint8_t &bit_depth, float frequency) { - bit_depth = *ledc_bit_depth_for_frequency(frequency); + auto bit_depth_opt = ledc_bit_depth_for_frequency(frequency); + bit_depth = bit_depth_opt.value_or(0); if (bit_depth < 1) { ESP_LOGE(TAG, "Frequency %f can't be achieved with any bit depth", frequency); } diff --git a/esphome/components/mcp4461/mcp4461.cpp b/esphome/components/mcp4461/mcp4461.cpp index 2f2c75e05a..dc7e7019aa 100644 --- a/esphome/components/mcp4461/mcp4461.cpp +++ b/esphome/components/mcp4461/mcp4461.cpp @@ -19,8 +19,9 @@ void Mcp4461Component::setup() { // save WP/WL status this->update_write_protection_status_(); for (uint8_t i = 0; i < 8; i++) { - if (this->reg_[i].initial_value.has_value()) { - uint16_t initial_state = static_cast(*this->reg_[i].initial_value * 256.0f); + auto init_val = this->reg_[i].initial_value; + if (init_val.has_value()) { + uint16_t initial_state = static_cast(*init_val * 256.0f); this->write_wiper_level_(i, initial_state); } if (this->reg_[i].enabled) { diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index bc750e3713..4d59a4fbbc 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -56,20 +56,25 @@ void AirConditioner::on_status_change() { void AirConditioner::control(const ClimateCall &call) { dudanov::midea::ac::Control ctrl{}; - if (call.get_target_temperature().has_value()) - ctrl.targetTemp = call.get_target_temperature().value(); - if (call.get_swing_mode().has_value()) - ctrl.swingMode = Converters::to_midea_swing_mode(call.get_swing_mode().value()); - if (call.get_mode().has_value()) - ctrl.mode = Converters::to_midea_mode(call.get_mode().value()); - if (call.get_preset().has_value()) { - ctrl.preset = Converters::to_midea_preset(call.get_preset().value()); + auto target_temp_val = call.get_target_temperature(); + if (target_temp_val.has_value()) + ctrl.targetTemp = *target_temp_val; + auto swing_mode_val = call.get_swing_mode(); + if (swing_mode_val.has_value()) + ctrl.swingMode = Converters::to_midea_swing_mode(*swing_mode_val); + auto mode_val = call.get_mode(); + if (mode_val.has_value()) + ctrl.mode = Converters::to_midea_mode(*mode_val); + auto preset_val = call.get_preset(); + if (preset_val.has_value()) { + ctrl.preset = Converters::to_midea_preset(*preset_val); } else if (call.has_custom_preset()) { // get_custom_preset() returns StringRef pointing to null-terminated string literals from codegen ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().c_str()); } - if (call.get_fan_mode().has_value()) { - ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value()); + auto fan_mode_val = call.get_fan_mode(); + if (fan_mode_val.has_value()) { + ctrl.fanMode = Converters::to_midea_fan_mode(*fan_mode_val); } else if (call.has_custom_fan_mode()) { // get_custom_fan_mode() returns StringRef pointing to null-terminated string literals from codegen ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().c_str()); diff --git a/esphome/components/midea_ir/midea_ir.cpp b/esphome/components/midea_ir/midea_ir.cpp index eaee1c731c..220bb3f414 100644 --- a/esphome/components/midea_ir/midea_ir.cpp +++ b/esphome/components/midea_ir/midea_ir.cpp @@ -114,15 +114,20 @@ void MideaIR::control(const climate::ClimateCall &call) { if (call.get_mode() == climate::CLIMATE_MODE_OFF) { this->swing_mode = climate::CLIMATE_SWING_OFF; this->preset = climate::CLIMATE_PRESET_NONE; - } else if (call.get_swing_mode().has_value() && ((*call.get_swing_mode() == climate::CLIMATE_SWING_OFF && - this->swing_mode == climate::CLIMATE_SWING_VERTICAL) || - (*call.get_swing_mode() == climate::CLIMATE_SWING_VERTICAL && - this->swing_mode == climate::CLIMATE_SWING_OFF))) { - this->swing_ = true; - } else if (call.get_preset().has_value() && - ((*call.get_preset() == climate::CLIMATE_PRESET_NONE && this->preset == climate::CLIMATE_PRESET_BOOST) || - (*call.get_preset() == climate::CLIMATE_PRESET_BOOST && this->preset == climate::CLIMATE_PRESET_NONE))) { - this->boost_ = true; + } else { + auto swing = call.get_swing_mode(); + if (swing.has_value() && + ((*swing == climate::CLIMATE_SWING_OFF && this->swing_mode == climate::CLIMATE_SWING_VERTICAL) || + (*swing == climate::CLIMATE_SWING_VERTICAL && this->swing_mode == climate::CLIMATE_SWING_OFF))) { + this->swing_ = true; + } else { + auto preset = call.get_preset(); + if (preset.has_value() && + ((*preset == climate::CLIMATE_PRESET_NONE && this->preset == climate::CLIMATE_PRESET_BOOST) || + (*preset == climate::CLIMATE_PRESET_BOOST && this->preset == climate::CLIMATE_PRESET_NONE))) { + this->boost_ = true; + } + } } climate_ir::ClimateIR::control(call); } diff --git a/esphome/components/mitsubishi/mitsubishi.cpp b/esphome/components/mitsubishi/mitsubishi.cpp index d80b7aeff5..882163ff5d 100644 --- a/esphome/components/mitsubishi/mitsubishi.cpp +++ b/esphome/components/mitsubishi/mitsubishi.cpp @@ -180,7 +180,7 @@ void MitsubishiClimate::transmit_state() { // For 5Level: Low = 1, Middle = 2, Medium = 3, High = 4 // For 4Level + Quiet: Low = 1, Middle = 2, Medium = 3, High = 4, Quiet = 5 - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: remote_state[9] = 1; break; @@ -209,7 +209,8 @@ void MitsubishiClimate::transmit_state() { break; } - ESP_LOGD(TAG, "fan: %02x state: %02x", this->fan_mode.value(), remote_state[9]); + ESP_LOGD(TAG, "fan: %02x state: %02x", static_cast(this->fan_mode.value_or(climate::CLIMATE_FAN_ON)), + remote_state[9]); // Vertical Vane switch (this->swing_mode) { @@ -227,7 +228,7 @@ void MitsubishiClimate::transmit_state() { ESP_LOGD(TAG, "default_vertical_direction_: %02X", this->default_vertical_direction_); // Special modes - switch (this->preset.value()) { + switch (this->preset.value_or(climate::CLIMATE_PRESET_NONE)) { case climate::CLIMATE_PRESET_ECO: remote_state[6] = MITSUBISHI_MODE_COOL | MITSUBISHI_OTHERWISE; remote_state[8] = (remote_state[8] & ~7) | MITSUBISHI_MODE_A_COOL; diff --git a/esphome/components/modbus_controller/select/modbus_select.cpp b/esphome/components/modbus_controller/select/modbus_select.cpp index 853f4215c3..e2a54d3f60 100644 --- a/esphome/components/modbus_controller/select/modbus_select.cpp +++ b/esphome/components/modbus_controller/select/modbus_select.cpp @@ -52,7 +52,7 @@ void ModbusSelect::control(size_t index) { // Transform func requires string parameter for backward compatibility auto val = (*this->write_transform_func_)(this, std::string(option), *mapval, data); if (val.has_value()) { - mapval = *val; + mapval = val; ESP_LOGV(TAG, "write_lambda returned mapping value %lld", *mapval); } else { ESP_LOGD(TAG, "Communication handled by write_lambda - exiting control"); diff --git a/esphome/components/noblex/noblex.cpp b/esphome/components/noblex/noblex.cpp index 53f807809e..f1e76eabf2 100644 --- a/esphome/components/noblex/noblex.cpp +++ b/esphome/components/noblex/noblex.cpp @@ -71,7 +71,7 @@ void NoblexClimate::transmit_state() { break; } - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: remote_state[0] |= (IRNoblexFan::IR_NOBLEX_FAN_LOW << 2); break; diff --git a/esphome/components/noblex/noblex.h b/esphome/components/noblex/noblex.h index a8e5f41547..57990db005 100644 --- a/esphome/components/noblex/noblex.h +++ b/esphome/components/noblex/noblex.h @@ -26,7 +26,8 @@ class NoblexClimate : public climate_ir::ClimateIR { void control(const climate::ClimateCall &call) override { send_swing_cmd_ = call.get_swing_mode().has_value(); // swing resets after unit powered off - if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF) + auto mode = call.get_mode(); + if (mode.has_value() && *mode == climate::CLIMATE_MODE_OFF) this->swing_mode = climate::CLIMATE_SWING_OFF; climate_ir::ClimateIR::control(call); } diff --git a/esphome/components/output/lock/output_lock.cpp b/esphome/components/output/lock/output_lock.cpp index 2545f62481..c373cd7b7c 100644 --- a/esphome/components/output/lock/output_lock.cpp +++ b/esphome/components/output/lock/output_lock.cpp @@ -9,7 +9,10 @@ static const char *const TAG = "output.lock"; void OutputLock::dump_config() { LOG_LOCK("", "Output Lock", this); } void OutputLock::control(const lock::LockCall &call) { - auto state = *call.get_state(); + auto state_val = call.get_state(); + if (!state_val.has_value()) + return; + auto state = *state_val; if (state == lock::LOCK_STATE_LOCKED) { this->output_->turn_on(); } else if (state == lock::LOCK_STATE_UNLOCKED) { diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index 2094c0e942..54b7a688b4 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -41,10 +41,12 @@ void PIDClimate::setup() { } } void PIDClimate::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value()) - this->mode = *call.get_mode(); - if (call.get_target_temperature().has_value()) - this->target_temperature = *call.get_target_temperature(); + auto call_mode = call.get_mode(); + if (call_mode.has_value()) + this->mode = *call_mode; + auto call_target = call.get_target_temperature(); + if (call_target.has_value()) + this->target_temperature = *call_target; // If switching to off mode, set output immediately if (this->mode == climate::CLIMATE_MODE_OFF) diff --git a/esphome/components/pzem004t/pzem004t.cpp b/esphome/components/pzem004t/pzem004t.cpp index 356847825e..d0f96d6d1e 100644 --- a/esphome/components/pzem004t/pzem004t.cpp +++ b/esphome/components/pzem004t/pzem004t.cpp @@ -26,7 +26,10 @@ void PZEM004T::loop() { // PZEM004T packet size is 7 byte while (this->available() >= 7) { - auto resp = *this->read_array<7>(); + auto resp_opt = this->read_array<7>(); + if (!resp_opt.has_value()) + break; + auto resp = *resp_opt; // packet format: // 0: packet type // 1-5: data diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 2ff99c961d..45fb42c116 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -69,7 +69,7 @@ optional SelectCall::calculate_target_index_(const char *name) { ESP_LOGW(TAG, "'%s' - No option set", name); return {}; } - return this->index_.value(); + return this->index_; } // SELECT_OP_NEXT or SELECT_OP_PREVIOUS diff --git a/esphome/components/sgp4x/sgp4x.h b/esphome/components/sgp4x/sgp4x.h index 8b31bca28c..89fa627c61 100644 --- a/esphome/components/sgp4x/sgp4x.h +++ b/esphome/components/sgp4x/sgp4x.h @@ -81,22 +81,16 @@ class SGP4xComponent : public PollingComponent, public sensor::Sensor, public se void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, uint16_t std_initial, uint16_t gain_factor) { - voc_tuning_params_.value().index_offset = index_offset; - voc_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours; - voc_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours; - voc_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes; - voc_tuning_params_.value().std_initial = std_initial; - voc_tuning_params_.value().gain_factor = gain_factor; + this->voc_tuning_params_ = GasTuning{ + index_offset, learning_time_offset_hours, learning_time_gain_hours, gating_max_duration_minutes, std_initial, + gain_factor}; } void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, uint16_t gain_factor) { - nox_tuning_params_.value().index_offset = index_offset; - nox_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours; - nox_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours; - nox_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes; - nox_tuning_params_.value().std_initial = 50; - nox_tuning_params_.value().gain_factor = gain_factor; + this->nox_tuning_params_ = + GasTuning{index_offset, learning_time_offset_hours, learning_time_gain_hours, gating_max_duration_minutes, 50, + gain_factor}; } protected: diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index 3f5cb2fda6..9f168f854d 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -144,7 +144,7 @@ void SpeakerMediaPlayer::watch_media_commands_() { delete media_command.url.value(); } if (media_command.file.has_value()) { - playlist_item.file = media_command.file.value(); + playlist_item.file = media_command.file; } if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) { @@ -495,18 +495,21 @@ void SpeakerMediaPlayer::control(const media_player::MediaPlayerCall &call) { MediaCallCommand media_command; - if (this->single_pipeline_() || (call.get_announcement().has_value() && call.get_announcement().value())) { + auto ann = call.get_announcement(); + if (this->single_pipeline_() || (ann.has_value() && *ann)) { media_command.announce = true; } else { media_command.announce = false; } - if (call.get_media_url().has_value()) { - media_command.url = new std::string( - call.get_media_url().value()); // Must be manually deleted after receiving media_command from a queue + auto media_url = call.get_media_url(); + if (media_url.has_value()) { + media_command.url = + new std::string(*media_url); // Must be manually deleted after receiving media_command from a queue - if (call.get_command().has_value()) { - if (call.get_command().value() == media_player::MEDIA_PLAYER_COMMAND_ENQUEUE) { + auto cmd = call.get_command(); + if (cmd.has_value()) { + if (*cmd == media_player::MEDIA_PLAYER_COMMAND_ENQUEUE) { media_command.enqueue = true; } } @@ -515,18 +518,20 @@ void SpeakerMediaPlayer::control(const media_player::MediaPlayerCall &call) { return; } - if (call.get_volume().has_value()) { - media_command.volume = call.get_volume().value(); + auto vol = call.get_volume(); + if (vol.has_value()) { + media_command.volume = vol; // Wait 0 ticks for queue to be free, volume sets aren't that important! xQueueSend(this->media_control_command_queue_, &media_command, 0); return; } - if (call.get_command().has_value()) { - media_command.command = call.get_command().value(); + auto cmd = call.get_command(); + if (cmd.has_value()) { + media_command.command = cmd; TickType_t ticks_to_wait = portMAX_DELAY; - if ((call.get_command().value() == media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP) || - (call.get_command().value() == media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN)) { + if ((*cmd == media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP) || + (*cmd == media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN)) { ticks_to_wait = 0; // Wait 0 ticks for queue to be free, volume sets aren't that important! } xQueueSend(this->media_control_command_queue_, &media_command, ticks_to_wait); diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 55f7fd162c..d45237c467 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -21,14 +21,18 @@ void SpeedFan::setup() { void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); } void SpeedFan::control(const fan::FanCall &call) { - if (call.get_state().has_value()) - this->state = *call.get_state(); - if (call.get_speed().has_value()) - this->speed = *call.get_speed(); - if (call.get_oscillating().has_value()) - this->oscillating = *call.get_oscillating(); - if (call.get_direction().has_value()) - this->direction = *call.get_direction(); + auto call_state = call.get_state(); + if (call_state.has_value()) + this->state = *call_state; + auto call_speed = call.get_speed(); + if (call_speed.has_value()) + this->speed = *call_speed; + auto call_oscillating = call.get_oscillating(); + if (call_oscillating.has_value()) + this->oscillating = *call_oscillating; + auto call_direction = call.get_direction(); + if (call_direction.has_value()) + this->direction = *call_direction; this->apply_preset_mode_(call); this->write_state_(); diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 44fb9092bc..d1f7452054 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -44,7 +44,7 @@ SprinklerControllerSwitch::SprinklerControllerSwitch() = default; void SprinklerControllerSwitch::loop() { // Loop is only enabled when f_ has a value (see setup()) - auto s = (*this->f_)(); + auto s = (*this->f_)(); // NOLINT(bugprone-unchecked-optional-access) if (s.has_value()) { this->publish_state(*s); } @@ -89,20 +89,21 @@ void SprinklerValveOperator::loop() { uint32_t now = App.get_loop_component_start_time(); switch (this->state_) { case STARTING: - if ((now - *this->start_millis_) > this->start_delay_) { + if ((now - *this->start_millis_) > this->start_delay_) { // NOLINT(bugprone-unchecked-optional-access) this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state } break; case ACTIVE: - if ((now - *this->start_millis_) > (this->start_delay_ + this->run_duration_)) { + if ((now - *this->start_millis_) > // NOLINT(bugprone-unchecked-optional-access) + (this->start_delay_ + this->run_duration_)) { this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down } break; case STOPPING: - if ((now - *this->stop_millis_) > this->stop_delay_) { - this->kill_(); // stop_delay_has been exceeded, ensure all valves are off + if ((now - *this->stop_millis_) > this->stop_delay_) { // NOLINT(bugprone-unchecked-optional-access) + this->kill_(); // stop_delay_has been exceeded, ensure all valves are off } break; @@ -1067,7 +1068,8 @@ uint32_t Sprinkler::total_cycle_time_enabled_incomplete_valves() { if (this->valve_is_enabled_(valve)) { enabled_valve_count++; if (!this->valve_cycle_complete_(valve)) { - if (!this->active_valve().has_value() || (valve != this->active_valve().value())) { + auto active = this->active_valve(); + if (!active.has_value() || (valve != *active)) { total_time_remaining += this->valve_run_duration_adjusted(valve); incomplete_valve_count++; } else { @@ -1190,8 +1192,11 @@ switch_::Switch *Sprinkler::valve_switch(const size_t valve_number) { } switch_::Switch *Sprinkler::valve_pump_switch(const size_t valve_number) { - if (this->is_a_valid_valve(valve_number) && this->valve_[valve_number].pump_switch_index.has_value()) { - return this->pump_[this->valve_[valve_number].pump_switch_index.value()]; + if (this->is_a_valid_valve(valve_number)) { + auto idx = this->valve_[valve_number].pump_switch_index; + if (idx.has_value()) { + return this->pump_[*idx]; + } } return nullptr; } diff --git a/esphome/components/tcl112/tcl112.cpp b/esphome/components/tcl112/tcl112.cpp index a88e8e96a7..afeee3d739 100644 --- a/esphome/components/tcl112/tcl112.cpp +++ b/esphome/components/tcl112/tcl112.cpp @@ -89,7 +89,7 @@ void Tcl112Climate::transmit_state() { // Set fan uint8_t selected_fan; - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_HIGH: selected_fan = TCL112_FAN_HIGH; break; diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index 09efe678ce..651aa3c489 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -257,14 +257,16 @@ void TemplateAlarmControlPanel::bypass_before_arming() { } void TemplateAlarmControlPanel::control(const AlarmControlPanelCall &call) { - if (call.get_state()) { - if (call.get_state() == ACP_STATE_ARMED_AWAY) { + auto opt_state = call.get_state(); + if (opt_state) { + auto state = *opt_state; + if (state == ACP_STATE_ARMED_AWAY) { this->arm_(call.get_code(), ACP_STATE_ARMED_AWAY, this->arming_away_time_); - } else if (call.get_state() == ACP_STATE_ARMED_HOME) { + } else if (state == ACP_STATE_ARMED_HOME) { this->arm_(call.get_code(), ACP_STATE_ARMED_HOME, this->arming_home_time_); - } else if (call.get_state() == ACP_STATE_ARMED_NIGHT) { + } else if (state == ACP_STATE_ARMED_NIGHT) { this->arm_(call.get_code(), ACP_STATE_ARMED_NIGHT, this->arming_night_time_); - } else if (call.get_state() == ACP_STATE_DISARMED) { + } else if (state == ACP_STATE_DISARMED) { if (!this->is_code_valid_(call.get_code())) { ESP_LOGW(TAG, "Not disarming code doesn't match"); return; @@ -274,13 +276,12 @@ void TemplateAlarmControlPanel::control(const AlarmControlPanelCall &call) { #ifdef USE_BINARY_SENSOR this->bypassed_sensor_indicies_.clear(); #endif - } else if (call.get_state() == ACP_STATE_TRIGGERED) { + } else if (state == ACP_STATE_TRIGGERED) { this->publish_state(ACP_STATE_TRIGGERED); - } else if (call.get_state() == ACP_STATE_PENDING) { + } else if (state == ACP_STATE_PENDING) { this->publish_state(ACP_STATE_PENDING); } else { - ESP_LOGE(TAG, "State not yet implemented: %s", - LOG_STR_ARG(alarm_control_panel_state_to_string(*call.get_state()))); + ESP_LOGE(TAG, "State not yet implemented: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state))); } } } diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index 7f5d68623f..d5e0967e1e 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -74,8 +74,9 @@ void TemplateCover::control(const CoverCall &call) { this->prev_command_trigger_ = &this->toggle_trigger_; this->publish_state(); } - if (call.get_position().has_value()) { - auto pos = *call.get_position(); + auto pos_val = call.get_position(); + if (pos_val.has_value()) { + auto pos = *pos_val; this->stop_prev_trigger_(); if (pos == COVER_OPEN) { @@ -93,8 +94,9 @@ void TemplateCover::control(const CoverCall &call) { } } - if (call.get_tilt().has_value()) { - auto tilt = *call.get_tilt(); + auto tilt_val = call.get_tilt(); + if (tilt_val.has_value()) { + auto tilt = *tilt_val; this->tilt_trigger_.trigger(tilt); if (this->optimistic_) { diff --git a/esphome/components/template/datetime/template_date.cpp b/esphome/components/template/datetime/template_date.cpp index 8a5f11b876..c0f5d96c3d 100644 --- a/esphome/components/template/datetime/template_date.cpp +++ b/esphome/components/template/datetime/template_date.cpp @@ -48,46 +48,49 @@ void TemplateDate::update() { } void TemplateDate::control(const datetime::DateCall &call) { - bool has_year = call.get_year().has_value(); - bool has_month = call.get_month().has_value(); - bool has_day = call.get_day().has_value(); + auto opt_year = call.get_year(); + auto opt_month = call.get_month(); + auto opt_day = call.get_day(); + bool has_year = opt_year.has_value(); + bool has_month = opt_month.has_value(); + bool has_day = opt_day.has_value(); ESPTime value = {}; if (has_year) - value.year = *call.get_year(); + value.year = *opt_year; if (has_month) - value.month = *call.get_month(); + value.month = *opt_month; if (has_day) - value.day_of_month = *call.get_day(); + value.day_of_month = *opt_day; this->set_trigger_.trigger(value); if (this->optimistic_) { if (has_year) - this->year_ = *call.get_year(); + this->year_ = *opt_year; if (has_month) - this->month_ = *call.get_month(); + this->month_ = *opt_month; if (has_day) - this->day_ = *call.get_day(); + this->day_ = *opt_day; this->publish_state(); } if (this->restore_value_) { datetime::DateEntityRestoreState temp = {}; if (has_year) { - temp.year = *call.get_year(); + temp.year = *opt_year; } else { temp.year = this->year_; } if (has_month) { - temp.month = *call.get_month(); + temp.month = *opt_month; } else { temp.month = this->month_; } if (has_day) { - temp.day = *call.get_day(); + temp.day = *opt_day; } else { temp.day = this->day_; } diff --git a/esphome/components/template/datetime/template_datetime.cpp b/esphome/components/template/datetime/template_datetime.cpp index 269a1d06ca..5b8b308c00 100644 --- a/esphome/components/template/datetime/template_datetime.cpp +++ b/esphome/components/template/datetime/template_datetime.cpp @@ -54,79 +54,85 @@ void TemplateDateTime::update() { } void TemplateDateTime::control(const datetime::DateTimeCall &call) { - bool has_year = call.get_year().has_value(); - bool has_month = call.get_month().has_value(); - bool has_day = call.get_day().has_value(); - bool has_hour = call.get_hour().has_value(); - bool has_minute = call.get_minute().has_value(); - bool has_second = call.get_second().has_value(); + auto opt_year = call.get_year(); + auto opt_month = call.get_month(); + auto opt_day = call.get_day(); + auto opt_hour = call.get_hour(); + auto opt_minute = call.get_minute(); + auto opt_second = call.get_second(); + bool has_year = opt_year.has_value(); + bool has_month = opt_month.has_value(); + bool has_day = opt_day.has_value(); + bool has_hour = opt_hour.has_value(); + bool has_minute = opt_minute.has_value(); + bool has_second = opt_second.has_value(); ESPTime value = {}; if (has_year) - value.year = *call.get_year(); + value.year = *opt_year; if (has_month) - value.month = *call.get_month(); + value.month = *opt_month; if (has_day) - value.day_of_month = *call.get_day(); + value.day_of_month = *opt_day; if (has_hour) - value.hour = *call.get_hour(); + value.hour = *opt_hour; if (has_minute) - value.minute = *call.get_minute(); + value.minute = *opt_minute; if (has_second) - value.second = *call.get_second(); + value.second = *opt_second; this->set_trigger_.trigger(value); if (this->optimistic_) { if (has_year) - this->year_ = *call.get_year(); + this->year_ = *opt_year; if (has_month) - this->month_ = *call.get_month(); + this->month_ = *opt_month; if (has_day) - this->day_ = *call.get_day(); + this->day_ = *opt_day; if (has_hour) - this->hour_ = *call.get_hour(); + this->hour_ = *opt_hour; if (has_minute) - this->minute_ = *call.get_minute(); + this->minute_ = *opt_minute; if (has_second) - this->second_ = *call.get_second(); + this->second_ = *opt_second; this->publish_state(); } if (this->restore_value_) { datetime::DateTimeEntityRestoreState temp = {}; if (has_year) { - temp.year = *call.get_year(); + temp.year = *opt_year; } else { temp.year = this->year_; } if (has_month) { - temp.month = *call.get_month(); + temp.month = *opt_month; } else { temp.month = this->month_; } if (has_day) { - temp.day = *call.get_day(); + temp.day = *opt_day; } else { temp.day = this->day_; } if (has_hour) { - temp.hour = *call.get_hour(); + temp.hour = *opt_hour; } else { temp.hour = this->hour_; } if (has_minute) { - temp.minute = *call.get_minute(); + temp.minute = *opt_minute; } else { temp.minute = this->minute_; } if (has_second) { - temp.second = *call.get_second(); + temp.second = *opt_second; } else { temp.second = this->second_; } diff --git a/esphome/components/template/datetime/template_time.cpp b/esphome/components/template/datetime/template_time.cpp index 9c81687116..b5efa62ae7 100644 --- a/esphome/components/template/datetime/template_time.cpp +++ b/esphome/components/template/datetime/template_time.cpp @@ -48,46 +48,49 @@ void TemplateTime::update() { } void TemplateTime::control(const datetime::TimeCall &call) { - bool has_hour = call.get_hour().has_value(); - bool has_minute = call.get_minute().has_value(); - bool has_second = call.get_second().has_value(); + auto opt_hour = call.get_hour(); + auto opt_minute = call.get_minute(); + auto opt_second = call.get_second(); + bool has_hour = opt_hour.has_value(); + bool has_minute = opt_minute.has_value(); + bool has_second = opt_second.has_value(); ESPTime value = {}; if (has_hour) - value.hour = *call.get_hour(); + value.hour = *opt_hour; if (has_minute) - value.minute = *call.get_minute(); + value.minute = *opt_minute; if (has_second) - value.second = *call.get_second(); + value.second = *opt_second; this->set_trigger_.trigger(value); if (this->optimistic_) { if (has_hour) - this->hour_ = *call.get_hour(); + this->hour_ = *opt_hour; if (has_minute) - this->minute_ = *call.get_minute(); + this->minute_ = *opt_minute; if (has_second) - this->second_ = *call.get_second(); + this->second_ = *opt_second; this->publish_state(); } if (this->restore_value_) { datetime::TimeEntityRestoreState temp = {}; if (has_hour) { - temp.hour = *call.get_hour(); + temp.hour = *opt_hour; } else { temp.hour = this->hour_; } if (has_minute) { - temp.minute = *call.get_minute(); + temp.minute = *opt_minute; } else { temp.minute = this->minute_; } if (has_second) { - temp.second = *call.get_second(); + temp.second = *opt_second; } else { temp.second = this->second_; } diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp index cd267bd552..46a5cba9bb 100644 --- a/esphome/components/template/fan/template_fan.cpp +++ b/esphome/components/template/fan/template_fan.cpp @@ -20,14 +20,18 @@ void TemplateFan::setup() { void TemplateFan::dump_config() { LOG_FAN("", "Template Fan", this); } void TemplateFan::control(const fan::FanCall &call) { - if (call.get_state().has_value()) - this->state = *call.get_state(); - if (call.get_speed().has_value() && (this->speed_count_ > 0)) - this->speed = *call.get_speed(); - if (call.get_oscillating().has_value() && this->has_oscillating_) - this->oscillating = *call.get_oscillating(); - if (call.get_direction().has_value() && this->has_direction_) - this->direction = *call.get_direction(); + auto call_state = call.get_state(); + if (call_state.has_value()) + this->state = *call_state; + auto call_speed = call.get_speed(); + if (call_speed.has_value() && (this->speed_count_ > 0)) + this->speed = *call_speed; + auto call_oscillating = call.get_oscillating(); + if (call_oscillating.has_value() && this->has_oscillating_) + this->oscillating = *call_oscillating; + auto call_direction = call.get_direction(); + if (call_direction.has_value() && this->has_direction_) + this->direction = *call_direction; this->apply_preset_mode_(call); this->publish_state(); diff --git a/esphome/components/template/lock/template_lock.cpp b/esphome/components/template/lock/template_lock.cpp index dbc4501ce7..6e73623ae9 100644 --- a/esphome/components/template/lock/template_lock.cpp +++ b/esphome/components/template/lock/template_lock.cpp @@ -25,7 +25,10 @@ void TemplateLock::control(const lock::LockCall &call) { this->prev_trigger_->stop_action(); } - auto state = *call.get_state(); + auto opt_state = call.get_state(); + if (!opt_state.has_value()) + return; + auto state = *opt_state; if (state == LOCK_STATE_LOCKED) { this->prev_trigger_ = &this->lock_trigger_; this->lock_trigger_.trigger(); diff --git a/esphome/components/template/valve/template_valve.cpp b/esphome/components/template/valve/template_valve.cpp index 2817e1a132..3ebeec1285 100644 --- a/esphome/components/template/valve/template_valve.cpp +++ b/esphome/components/template/valve/template_valve.cpp @@ -77,8 +77,9 @@ void TemplateValve::control(const ValveCall &call) { this->prev_command_trigger_ = &this->toggle_trigger_; this->publish_state(); } - if (call.get_position().has_value()) { - auto pos = *call.get_position(); + auto pos_val = call.get_position(); + if (pos_val.has_value()) { + auto pos = *pos_val; this->stop_prev_trigger_(); if (pos == VALVE_OPEN) { diff --git a/esphome/components/template/water_heater/template_water_heater.cpp b/esphome/components/template/water_heater/template_water_heater.cpp index 57c76286a0..73081d204b 100644 --- a/esphome/components/template/water_heater/template_water_heater.cpp +++ b/esphome/components/template/water_heater/template_water_heater.cpp @@ -101,9 +101,10 @@ water_heater::WaterHeaterCallInternal TemplateWaterHeater::make_call() { } void TemplateWaterHeater::control(const water_heater::WaterHeaterCall &call) { - if (call.get_mode().has_value()) { + auto mode_val = call.get_mode(); + if (mode_val.has_value()) { if (this->optimistic_) { - this->mode_ = *call.get_mode(); + this->mode_ = *mode_val; } } if (!std::isnan(call.get_target_temperature())) { @@ -112,14 +113,16 @@ void TemplateWaterHeater::control(const water_heater::WaterHeaterCall &call) { } } - if (call.get_away().has_value()) { + auto away_val = call.get_away(); + if (away_val.has_value()) { if (this->optimistic_) { - this->set_state_flag_(water_heater::WATER_HEATER_STATE_AWAY, *call.get_away()); + this->set_state_flag_(water_heater::WATER_HEATER_STATE_AWAY, *away_val); } } - if (call.get_on().has_value()) { + auto on_val = call.get_on(); + if (on_val.has_value()) { if (this->optimistic_) { - this->set_state_flag_(water_heater::WATER_HEATER_STATE_ON, *call.get_on()); + this->set_state_flag_(water_heater::WATER_HEATER_STATE_ON, *on_val); } } diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index c666419701..d52a22f880 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -84,7 +84,7 @@ void ThermostatClimate::refresh() { this->switch_to_mode_(this->mode, false); this->switch_to_action_(this->compute_action_(), false); this->switch_to_supplemental_action_(this->compute_supplemental_action_()); - this->switch_to_fan_mode_(this->fan_mode.value(), false); + this->switch_to_fan_mode_(this->fan_mode.value_or(climate::CLIMATE_FAN_ON), false); this->switch_to_swing_mode_(this->swing_mode, false); this->switch_to_humidity_control_action_(this->compute_humidity_control_action_()); this->check_humidity_change_trigger_(); @@ -211,12 +211,13 @@ void ThermostatClimate::validate_target_humidity() { void ThermostatClimate::control(const climate::ClimateCall &call) { bool target_temperature_high_changed = false; - if (call.get_preset().has_value()) { + auto preset = call.get_preset(); + if (preset.has_value()) { // setup_complete_ blocks modifying/resetting the temps immediately after boot if (this->setup_complete_) { - this->change_preset_(call.get_preset().value()); + this->change_preset_(*preset); } else { - this->preset = call.get_preset().value(); + this->preset = preset; } } if (call.has_custom_preset()) { @@ -229,34 +230,41 @@ void ThermostatClimate::control(const climate::ClimateCall &call) { } } - if (call.get_mode().has_value()) { - this->mode = call.get_mode().value(); + auto mode = call.get_mode(); + if (mode.has_value()) { + this->mode = *mode; } - if (call.get_fan_mode().has_value()) { - this->fan_mode = call.get_fan_mode().value(); + auto fan_mode = call.get_fan_mode(); + if (fan_mode.has_value()) { + this->fan_mode = fan_mode; } - if (call.get_swing_mode().has_value()) { - this->swing_mode = call.get_swing_mode().value(); + auto swing_mode = call.get_swing_mode(); + if (swing_mode.has_value()) { + this->swing_mode = *swing_mode; } if (this->supports_two_points_) { - if (call.get_target_temperature_low().has_value()) { - this->target_temperature_low = call.get_target_temperature_low().value(); + auto target_temp_low = call.get_target_temperature_low(); + if (target_temp_low.has_value()) { + this->target_temperature_low = *target_temp_low; } - if (call.get_target_temperature_high().has_value()) { - target_temperature_high_changed = this->target_temperature_high != call.get_target_temperature_high().value(); - this->target_temperature_high = call.get_target_temperature_high().value(); + auto target_temp_high = call.get_target_temperature_high(); + if (target_temp_high.has_value()) { + target_temperature_high_changed = this->target_temperature_high != *target_temp_high; + this->target_temperature_high = *target_temp_high; } // ensure the two set points are valid and adjust one of them if necessary this->validate_target_temperatures(target_temperature_high_changed || (this->prev_mode_ == climate::CLIMATE_MODE_COOL)); } else { - if (call.get_target_temperature().has_value()) { - this->target_temperature = call.get_target_temperature().value(); + auto target_temp = call.get_target_temperature(); + if (target_temp.has_value()) { + this->target_temperature = *target_temp; this->validate_target_temperature(); } } - if (call.get_target_humidity().has_value()) { - this->target_humidity = call.get_target_humidity().value(); + auto target_humidity = call.get_target_humidity(); + if (target_humidity.has_value()) { + this->target_humidity = *target_humidity; this->validate_target_humidity(); } // make any changes happen @@ -1264,9 +1272,9 @@ bool ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTem something_changed = true; } - if (config.fan_mode_.has_value() && (this->fan_mode != config.fan_mode_.value())) { + if (config.fan_mode_.has_value() && (this->fan_mode != config.fan_mode_)) { ESP_LOGV(TAG, "Setting fan mode to %s", LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_))); - this->fan_mode = *config.fan_mode_; + this->fan_mode = config.fan_mode_; something_changed = true; } diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp index f6a3048bd4..c83829ff59 100644 --- a/esphome/components/time_based/time_based_cover.cpp +++ b/esphome/components/time_based/time_based_cover.cpp @@ -79,8 +79,9 @@ void TimeBasedCover::control(const CoverCall &call) { } } } - if (call.get_position().has_value()) { - auto pos = *call.get_position(); + auto pos_val = call.get_position(); + if (pos_val.has_value()) { + auto pos = *pos_val; if (pos == this->position) { // already at target if (this->manual_control_ && (pos == COVER_OPEN || pos == COVER_CLOSED)) { diff --git a/esphome/components/tormatic/tormatic_cover.cpp b/esphome/components/tormatic/tormatic_cover.cpp index be412d62a8..f567be0674 100644 --- a/esphome/components/tormatic/tormatic_cover.cpp +++ b/esphome/components/tormatic/tormatic_cover.cpp @@ -66,8 +66,9 @@ void Tormatic::control(const cover::CoverCall &call) { return; } - if (call.get_position().has_value()) { - auto pos = call.get_position().value(); + auto pos_val = call.get_position(); + if (pos_val.has_value()) { + auto pos = *pos_val; this->control_position_(pos); return; } diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index 7b5e78af52..e0c150537a 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -502,7 +502,7 @@ void ToshibaClimate::transmit_generic_() { } uint8_t fan; - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_QUIET: fan = TOSHIBA_FAN_SPEED_QUIET; break; @@ -567,7 +567,7 @@ void ToshibaClimate::transmit_rac_pt1411hwru_() { message[2] = RAC_PT1411HWRU_NO_FAN.code1; message[7] = RAC_PT1411HWRU_NO_FAN.code2; } else { - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: message[2] = RAC_PT1411HWRU_FAN_LOW.code1; message[7] = RAC_PT1411HWRU_FAN_LOW.code2; @@ -811,12 +811,12 @@ void ToshibaClimate::transmit_ras_2819t_() { uint8_t temp_code = get_ras_2819t_temp_code(temperature); // Get fan speed encoding for rc_code_1 - climate::ClimateFanMode effective_fan_mode = this->fan_mode.value(); + climate::ClimateFanMode effective_fan_mode = this->fan_mode.value_or(climate::CLIMATE_FAN_ON); // Dry mode only supports AUTO fan speed if (this->mode == climate::CLIMATE_MODE_DRY) { effective_fan_mode = climate::CLIMATE_FAN_AUTO; - if (this->fan_mode.value() != climate::CLIMATE_FAN_AUTO) { + if (this->fan_mode.value_or(climate::CLIMATE_FAN_ON) != climate::CLIMATE_FAN_AUTO) { ESP_LOGW(TAG, "Dry mode only supports AUTO fan speed, forcing AUTO"); } } diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp index 4d8fd4b310..6602ccd8c9 100644 --- a/esphome/components/tuya/climate/tuya_climate.cpp +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -7,8 +7,9 @@ namespace tuya { static const char *const TAG = "tuya.climate"; void TuyaClimate::setup() { - if (this->switch_id_.has_value()) { - this->parent_->register_listener(*this->switch_id_, [this](const TuyaDatapoint &datapoint) { + auto switch_id = this->switch_id_; + if (switch_id.has_value()) { + this->parent_->register_listener(*switch_id, [this](const TuyaDatapoint &datapoint) { ESP_LOGV(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool)); this->mode = climate::CLIMATE_MODE_OFF; if (datapoint.value_bool) { @@ -32,16 +33,18 @@ void TuyaClimate::setup() { this->cooling_state_pin_->setup(); this->cooling_state_ = this->cooling_state_pin_->digital_read(); } - if (this->active_state_id_.has_value()) { - this->parent_->register_listener(*this->active_state_id_, [this](const TuyaDatapoint &datapoint) { + auto active_state_id = this->active_state_id_; + if (active_state_id.has_value()) { + this->parent_->register_listener(*active_state_id, [this](const TuyaDatapoint &datapoint) { ESP_LOGV(TAG, "MCU reported active state is: %u", datapoint.value_enum); this->active_state_ = datapoint.value_enum; this->compute_state_(); this->publish_state(); }); } - if (this->target_temperature_id_.has_value()) { - this->parent_->register_listener(*this->target_temperature_id_, [this](const TuyaDatapoint &datapoint) { + auto target_temp_id = this->target_temperature_id_; + if (target_temp_id.has_value()) { + this->parent_->register_listener(*target_temp_id, [this](const TuyaDatapoint &datapoint) { this->manual_temperature_ = datapoint.value_int * this->target_temperature_multiplier_; if (this->reports_fahrenheit_) { this->manual_temperature_ = (this->manual_temperature_ - 32) * 5 / 9; @@ -53,8 +56,9 @@ void TuyaClimate::setup() { this->publish_state(); }); } - if (this->current_temperature_id_.has_value()) { - this->parent_->register_listener(*this->current_temperature_id_, [this](const TuyaDatapoint &datapoint) { + auto current_temp_id = this->current_temperature_id_; + if (current_temp_id.has_value()) { + this->parent_->register_listener(*current_temp_id, [this](const TuyaDatapoint &datapoint) { this->current_temperature = datapoint.value_int * this->current_temperature_multiplier_; if (this->reports_fahrenheit_) { this->current_temperature = (this->current_temperature - 32) * 5 / 9; @@ -65,8 +69,9 @@ void TuyaClimate::setup() { this->publish_state(); }); } - if (this->eco_id_.has_value()) { - this->parent_->register_listener(*this->eco_id_, [this](const TuyaDatapoint &datapoint) { + auto eco_id = this->eco_id_; + if (eco_id.has_value()) { + this->parent_->register_listener(*eco_id, [this](const TuyaDatapoint &datapoint) { // Whether data type is BOOL or ENUM, it will still be a 1 or a 0, so the functions below are valid in both cases this->eco_ = datapoint.value_bool; this->eco_type_ = datapoint.type; @@ -76,8 +81,9 @@ void TuyaClimate::setup() { this->publish_state(); }); } - if (this->sleep_id_.has_value()) { - this->parent_->register_listener(*this->sleep_id_, [this](const TuyaDatapoint &datapoint) { + auto sleep_id = this->sleep_id_; + if (sleep_id.has_value()) { + this->parent_->register_listener(*sleep_id, [this](const TuyaDatapoint &datapoint) { this->sleep_ = datapoint.value_bool; ESP_LOGV(TAG, "MCU reported sleep is: %s", ONOFF(this->sleep_)); this->compute_preset_(); @@ -85,8 +91,9 @@ void TuyaClimate::setup() { this->publish_state(); }); } - if (this->swing_vertical_id_.has_value()) { - this->parent_->register_listener(*this->swing_vertical_id_, [this](const TuyaDatapoint &datapoint) { + auto swing_vert_id = this->swing_vertical_id_; + if (swing_vert_id.has_value()) { + this->parent_->register_listener(*swing_vert_id, [this](const TuyaDatapoint &datapoint) { this->swing_vertical_ = datapoint.value_bool; ESP_LOGV(TAG, "MCU reported vertical swing is: %s", ONOFF(datapoint.value_bool)); this->compute_swingmode_(); @@ -94,8 +101,9 @@ void TuyaClimate::setup() { }); } - if (this->swing_horizontal_id_.has_value()) { - this->parent_->register_listener(*this->swing_horizontal_id_, [this](const TuyaDatapoint &datapoint) { + auto swing_horiz_id = this->swing_horizontal_id_; + if (swing_horiz_id.has_value()) { + this->parent_->register_listener(*swing_horiz_id, [this](const TuyaDatapoint &datapoint) { this->swing_horizontal_ = datapoint.value_bool; ESP_LOGV(TAG, "MCU reported horizontal swing is: %s", ONOFF(datapoint.value_bool)); this->compute_swingmode_(); @@ -103,8 +111,9 @@ void TuyaClimate::setup() { }); } - if (this->fan_speed_id_.has_value()) { - this->parent_->register_listener(*this->fan_speed_id_, [this](const TuyaDatapoint &datapoint) { + auto fan_speed_id = this->fan_speed_id_; + if (fan_speed_id.has_value()) { + this->parent_->register_listener(*fan_speed_id, [this](const TuyaDatapoint &datapoint) { ESP_LOGV(TAG, "MCU reported Fan Speed Mode is: %u", datapoint.value_enum); this->fan_state_ = datapoint.value_enum; this->compute_fanmode_(); @@ -139,21 +148,34 @@ void TuyaClimate::loop() { } void TuyaClimate::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value()) { - const bool switch_state = *call.get_mode() != climate::CLIMATE_MODE_OFF; + auto mode = call.get_mode(); + if (mode.has_value()) { + const bool switch_state = *mode != climate::CLIMATE_MODE_OFF; ESP_LOGV(TAG, "Setting switch: %s", ONOFF(switch_state)); - this->parent_->set_boolean_datapoint_value(*this->switch_id_, switch_state); - const climate::ClimateMode new_mode = *call.get_mode(); + auto switch_dp_id = this->switch_id_; + if (switch_dp_id.has_value()) { + this->parent_->set_boolean_datapoint_value(*switch_dp_id, switch_state); + } + const climate::ClimateMode new_mode = *mode; - if (this->active_state_id_.has_value()) { + auto active_state_dp_id = this->active_state_id_; + if (active_state_dp_id.has_value()) { if (new_mode == climate::CLIMATE_MODE_HEAT && this->supports_heat_) { - this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_heating_value_); + auto heating_val = this->active_state_heating_value_; + if (heating_val.has_value()) + this->parent_->set_enum_datapoint_value(*active_state_dp_id, *heating_val); } else if (new_mode == climate::CLIMATE_MODE_COOL && this->supports_cool_) { - this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_cooling_value_); - } else if (new_mode == climate::CLIMATE_MODE_DRY && this->active_state_drying_value_.has_value()) { - this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_drying_value_); - } else if (new_mode == climate::CLIMATE_MODE_FAN_ONLY && this->active_state_fanonly_value_.has_value()) { - this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_fanonly_value_); + auto cooling_val = this->active_state_cooling_value_; + if (cooling_val.has_value()) + this->parent_->set_enum_datapoint_value(*active_state_dp_id, *cooling_val); + } else if (new_mode == climate::CLIMATE_MODE_DRY) { + auto drying_val = this->active_state_drying_value_; + if (drying_val.has_value()) + this->parent_->set_enum_datapoint_value(*active_state_dp_id, *drying_val); + } else if (new_mode == climate::CLIMATE_MODE_FAN_ONLY) { + auto fanonly_val = this->active_state_fanonly_value_; + if (fanonly_val.has_value()) + this->parent_->set_enum_datapoint_value(*active_state_dp_id, *fanonly_val); } } else { ESP_LOGW(TAG, "Active state (mode) datapoint not configured"); @@ -163,31 +185,38 @@ void TuyaClimate::control(const climate::ClimateCall &call) { control_swing_mode_(call); control_fan_mode_(call); - if (call.get_target_temperature().has_value()) { - float target_temperature = *call.get_target_temperature(); + auto target_temp = call.get_target_temperature(); + if (target_temp.has_value()) { + float target_temperature = *target_temp; if (this->reports_fahrenheit_) target_temperature = (target_temperature * 9 / 5) + 32; ESP_LOGV(TAG, "Setting target temperature: %.1f", target_temperature); - this->parent_->set_integer_datapoint_value(*this->target_temperature_id_, - (int) (target_temperature / this->target_temperature_multiplier_)); + auto target_temp_dp_id = this->target_temperature_id_; + if (target_temp_dp_id.has_value()) { + this->parent_->set_integer_datapoint_value(*target_temp_dp_id, + (int) (target_temperature / this->target_temperature_multiplier_)); + } } - if (call.get_preset().has_value()) { - const climate::ClimatePreset preset = *call.get_preset(); - if (this->eco_id_.has_value()) { + auto preset_val = call.get_preset(); + if (preset_val.has_value()) { + const climate::ClimatePreset preset = *preset_val; + auto eco_dp_id = this->eco_id_; + if (eco_dp_id.has_value()) { const bool eco = preset == climate::CLIMATE_PRESET_ECO; ESP_LOGV(TAG, "Setting eco: %s", ONOFF(eco)); if (this->eco_type_ == TuyaDatapointType::ENUM) { - this->parent_->set_enum_datapoint_value(*this->eco_id_, eco); + this->parent_->set_enum_datapoint_value(*eco_dp_id, eco); } else { - this->parent_->set_boolean_datapoint_value(*this->eco_id_, eco); + this->parent_->set_boolean_datapoint_value(*eco_dp_id, eco); } } - if (this->sleep_id_.has_value()) { + auto sleep_dp_id = this->sleep_id_; + if (sleep_dp_id.has_value()) { const bool sleep = preset == climate::CLIMATE_PRESET_SLEEP; ESP_LOGV(TAG, "Setting sleep: %s", ONOFF(sleep)); - this->parent_->set_boolean_datapoint_value(*this->sleep_id_, sleep); + this->parent_->set_boolean_datapoint_value(*sleep_dp_id, sleep); } } } @@ -196,8 +225,9 @@ void TuyaClimate::control_swing_mode_(const climate::ClimateCall &call) { bool vertical_swing_changed = false; bool horizontal_swing_changed = false; - if (call.get_swing_mode().has_value()) { - const auto swing_mode = *call.get_swing_mode(); + auto swing_mode_val = call.get_swing_mode(); + if (swing_mode_val.has_value()) { + const auto swing_mode = *swing_mode_val; switch (swing_mode) { case climate::CLIMATE_SWING_OFF: @@ -241,14 +271,16 @@ void TuyaClimate::control_swing_mode_(const climate::ClimateCall &call) { } } - if (vertical_swing_changed && this->swing_vertical_id_.has_value()) { + auto vert_dp_id = this->swing_vertical_id_; + if (vertical_swing_changed && vert_dp_id.has_value()) { ESP_LOGV(TAG, "Setting vertical swing: %s", ONOFF(swing_vertical_)); - this->parent_->set_boolean_datapoint_value(*this->swing_vertical_id_, swing_vertical_); + this->parent_->set_boolean_datapoint_value(*vert_dp_id, swing_vertical_); } - if (horizontal_swing_changed && this->swing_horizontal_id_.has_value()) { + auto horiz_dp_id = this->swing_horizontal_id_; + if (horizontal_swing_changed && horiz_dp_id.has_value()) { ESP_LOGV(TAG, "Setting horizontal swing: %s", ONOFF(swing_horizontal_)); - this->parent_->set_boolean_datapoint_value(*this->swing_horizontal_id_, swing_horizontal_); + this->parent_->set_boolean_datapoint_value(*horiz_dp_id, swing_horizontal_); } // Publish the state after updating the swing mode @@ -256,33 +288,35 @@ void TuyaClimate::control_swing_mode_(const climate::ClimateCall &call) { } void TuyaClimate::control_fan_mode_(const climate::ClimateCall &call) { - if (call.get_fan_mode().has_value()) { - climate::ClimateFanMode fan_mode = *call.get_fan_mode(); + auto fan_mode_val = call.get_fan_mode(); + if (fan_mode_val.has_value()) { + climate::ClimateFanMode fan_mode = *fan_mode_val; uint8_t tuya_fan_speed; switch (fan_mode) { case climate::CLIMATE_FAN_LOW: - tuya_fan_speed = *fan_speed_low_value_; + tuya_fan_speed = this->fan_speed_low_value_.value_or(0); break; case climate::CLIMATE_FAN_MEDIUM: - tuya_fan_speed = *fan_speed_medium_value_; + tuya_fan_speed = this->fan_speed_medium_value_.value_or(0); break; case climate::CLIMATE_FAN_MIDDLE: - tuya_fan_speed = *fan_speed_middle_value_; + tuya_fan_speed = this->fan_speed_middle_value_.value_or(0); break; case climate::CLIMATE_FAN_HIGH: - tuya_fan_speed = *fan_speed_high_value_; + tuya_fan_speed = this->fan_speed_high_value_.value_or(0); break; case climate::CLIMATE_FAN_AUTO: - tuya_fan_speed = *fan_speed_auto_value_; + tuya_fan_speed = this->fan_speed_auto_value_.value_or(0); break; default: tuya_fan_speed = 0; break; } - if (this->fan_speed_id_.has_value()) { - this->parent_->set_enum_datapoint_value(*this->fan_speed_id_, tuya_fan_speed); + auto fan_speed_dp_id = this->fan_speed_id_; + if (fan_speed_dp_id.has_value()) { + this->parent_->set_enum_datapoint_value(*fan_speed_dp_id, tuya_fan_speed); } } } @@ -337,31 +371,39 @@ climate::ClimateTraits TuyaClimate::traits() { void TuyaClimate::dump_config() { LOG_CLIMATE("", "Tuya Climate", this); - if (this->switch_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); + auto switch_dp_id = this->switch_id_; + if (switch_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *switch_dp_id); } - if (this->active_state_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Active state has datapoint ID %u", *this->active_state_id_); + auto active_state_dp_id = this->active_state_id_; + if (active_state_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Active state has datapoint ID %u", *active_state_dp_id); } - if (this->target_temperature_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Target Temperature has datapoint ID %u", *this->target_temperature_id_); + auto target_temp_dp_id = this->target_temperature_id_; + if (target_temp_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Target Temperature has datapoint ID %u", *target_temp_dp_id); } - if (this->current_temperature_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Current Temperature has datapoint ID %u", *this->current_temperature_id_); + auto current_temp_dp_id = this->current_temperature_id_; + if (current_temp_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Current Temperature has datapoint ID %u", *current_temp_dp_id); } LOG_PIN(" Heating State Pin: ", this->heating_state_pin_); LOG_PIN(" Cooling State Pin: ", this->cooling_state_pin_); - if (this->eco_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Eco has datapoint ID %u", *this->eco_id_); + auto eco_dp_id = this->eco_id_; + if (eco_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Eco has datapoint ID %u", *eco_dp_id); } - if (this->sleep_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Sleep has datapoint ID %u", *this->sleep_id_); + auto sleep_dp_id = this->sleep_id_; + if (sleep_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Sleep has datapoint ID %u", *sleep_dp_id); } - if (this->swing_vertical_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Swing Vertical has datapoint ID %u", *this->swing_vertical_id_); + auto swing_vert_dp_id = this->swing_vertical_id_; + if (swing_vert_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Swing Vertical has datapoint ID %u", *swing_vert_dp_id); } - if (this->swing_horizontal_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Swing Horizontal has datapoint ID %u", *this->swing_horizontal_id_); + auto swing_horiz_dp_id = this->swing_horizontal_id_; + if (swing_horiz_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Swing Horizontal has datapoint ID %u", *swing_horiz_dp_id); } } diff --git a/esphome/components/tuya/cover/tuya_cover.cpp b/esphome/components/tuya/cover/tuya_cover.cpp index 14bf937cf7..125afec048 100644 --- a/esphome/components/tuya/cover/tuya_cover.cpp +++ b/esphome/components/tuya/cover/tuya_cover.cpp @@ -39,6 +39,9 @@ void TuyaCover::setup() { } }); + if (!this->position_id_.has_value()) { + return; + } uint8_t report_id = *this->position_id_; if (this->position_report_id_.has_value()) { // A position report datapoint is configured; listen to that instead. @@ -60,29 +63,30 @@ void TuyaCover::control(const cover::CoverCall &call) { if (call.get_stop()) { if (this->control_id_.has_value()) { this->parent_->force_set_enum_datapoint_value(*this->control_id_, COMMAND_STOP); - } else { + } else if (this->position_id_.has_value()) { auto pos = this->position; pos = this->invert_position_report_ ? pos : 1.0f - pos; auto position_int = static_cast(pos * this->value_range_); position_int = position_int + this->min_value_; - parent_->force_set_integer_datapoint_value(*this->position_id_, position_int); + this->parent_->force_set_integer_datapoint_value(*this->position_id_, position_int); } } - if (call.get_position().has_value()) { - auto pos = *call.get_position(); + auto pos_opt = call.get_position(); + if (pos_opt.has_value()) { + auto pos = *pos_opt; if (this->control_id_.has_value() && (pos == COVER_OPEN || pos == COVER_CLOSED)) { if (pos == COVER_OPEN) { this->parent_->force_set_enum_datapoint_value(*this->control_id_, COMMAND_OPEN); } else { this->parent_->force_set_enum_datapoint_value(*this->control_id_, COMMAND_CLOSE); } - } else { + } else if (this->position_id_.has_value()) { pos = this->invert_position_report_ ? pos : 1.0f - pos; auto position_int = static_cast(pos * this->value_range_); position_int = position_int + this->min_value_; - parent_->force_set_integer_datapoint_value(*this->position_id_, position_int); + this->parent_->force_set_integer_datapoint_value(*this->position_id_, position_int); } } diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index 9b132e0de6..a387606b77 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -7,8 +7,9 @@ namespace tuya { static const char *const TAG = "tuya.fan"; void TuyaFan::setup() { - if (this->speed_id_.has_value()) { - this->parent_->register_listener(*this->speed_id_, [this](const TuyaDatapoint &datapoint) { + auto speed_id = this->speed_id_; + if (speed_id.has_value()) { + this->parent_->register_listener(*speed_id, [this](const TuyaDatapoint &datapoint) { if (datapoint.type == TuyaDatapointType::ENUM) { ESP_LOGV(TAG, "MCU reported speed of: %d", datapoint.value_enum); if (datapoint.value_enum >= this->speed_count_) { @@ -25,15 +26,17 @@ void TuyaFan::setup() { this->speed_type_ = datapoint.type; }); } - if (this->switch_id_.has_value()) { - this->parent_->register_listener(*this->switch_id_, [this](const TuyaDatapoint &datapoint) { + auto switch_id = this->switch_id_; + if (switch_id.has_value()) { + this->parent_->register_listener(*switch_id, [this](const TuyaDatapoint &datapoint) { ESP_LOGV(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool)); this->state = datapoint.value_bool; this->publish_state(); }); } - if (this->oscillation_id_.has_value()) { - this->parent_->register_listener(*this->oscillation_id_, [this](const TuyaDatapoint &datapoint) { + auto oscillation_id = this->oscillation_id_; + if (oscillation_id.has_value()) { + this->parent_->register_listener(*oscillation_id, [this](const TuyaDatapoint &datapoint) { // Whether data type is BOOL or ENUM, it will still be a 1 or a 0, so the functions below are valid in both // scenarios ESP_LOGV(TAG, "MCU reported oscillation is: %s", ONOFF(datapoint.value_bool)); @@ -43,8 +46,9 @@ void TuyaFan::setup() { this->oscillation_type_ = datapoint.type; }); } - if (this->direction_id_.has_value()) { - this->parent_->register_listener(*this->direction_id_, [this](const TuyaDatapoint &datapoint) { + auto direction_id = this->direction_id_; + if (direction_id.has_value()) { + this->parent_->register_listener(*direction_id, [this](const TuyaDatapoint &datapoint) { ESP_LOGD(TAG, "MCU reported reverse direction is: %s", ONOFF(datapoint.value_bool)); this->direction = datapoint.value_bool ? fan::FanDirection::REVERSE : fan::FanDirection::FORWARD; this->publish_state(); @@ -60,17 +64,21 @@ void TuyaFan::setup() { void TuyaFan::dump_config() { LOG_FAN("", "Tuya Fan", this); - if (this->speed_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Speed has datapoint ID %u", *this->speed_id_); + auto speed_dp_id = this->speed_id_; + if (speed_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Speed has datapoint ID %u", *speed_dp_id); } - if (this->switch_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); + auto switch_dp_id = this->switch_id_; + if (switch_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *switch_dp_id); } - if (this->oscillation_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Oscillation has datapoint ID %u", *this->oscillation_id_); + auto oscillation_dp_id = this->oscillation_id_; + if (oscillation_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Oscillation has datapoint ID %u", *oscillation_dp_id); } - if (this->direction_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Direction has datapoint ID %u", *this->direction_id_); + auto direction_dp_id = this->direction_id_; + if (direction_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Direction has datapoint ID %u", *direction_dp_id); } } @@ -80,25 +88,41 @@ fan::FanTraits TuyaFan::get_traits() { } void TuyaFan::control(const fan::FanCall &call) { - if (this->switch_id_.has_value() && call.get_state().has_value()) { - this->parent_->set_boolean_datapoint_value(*this->switch_id_, *call.get_state()); - } - if (this->oscillation_id_.has_value() && call.get_oscillating().has_value()) { - if (this->oscillation_type_ == TuyaDatapointType::ENUM) { - this->parent_->set_enum_datapoint_value(*this->oscillation_id_, *call.get_oscillating()); - } else if (this->oscillation_type_ == TuyaDatapointType::BOOLEAN) { - this->parent_->set_boolean_datapoint_value(*this->oscillation_id_, *call.get_oscillating()); + auto switch_id = this->switch_id_; + if (switch_id.has_value()) { + auto state = call.get_state(); + if (state.has_value()) { + this->parent_->set_boolean_datapoint_value(*switch_id, *state); } } - if (this->direction_id_.has_value() && call.get_direction().has_value()) { - bool enable = *call.get_direction() == fan::FanDirection::REVERSE; - this->parent_->set_enum_datapoint_value(*this->direction_id_, enable); + auto osc_id = this->oscillation_id_; + if (osc_id.has_value()) { + auto oscillating = call.get_oscillating(); + if (oscillating.has_value()) { + if (this->oscillation_type_ == TuyaDatapointType::ENUM) { + this->parent_->set_enum_datapoint_value(*osc_id, *oscillating); + } else if (this->oscillation_type_ == TuyaDatapointType::BOOLEAN) { + this->parent_->set_boolean_datapoint_value(*osc_id, *oscillating); + } + } } - if (this->speed_id_.has_value() && call.get_speed().has_value()) { - if (this->speed_type_ == TuyaDatapointType::ENUM) { - this->parent_->set_enum_datapoint_value(*this->speed_id_, *call.get_speed() - 1); - } else if (this->speed_type_ == TuyaDatapointType::INTEGER) { - this->parent_->set_integer_datapoint_value(*this->speed_id_, *call.get_speed()); + auto dir_id = this->direction_id_; + if (dir_id.has_value()) { + auto direction = call.get_direction(); + if (direction.has_value()) { + bool enable = *direction == fan::FanDirection::REVERSE; + this->parent_->set_enum_datapoint_value(*dir_id, enable); + } + } + auto spd_id = this->speed_id_; + if (spd_id.has_value()) { + auto speed = call.get_speed(); + if (speed.has_value()) { + if (this->speed_type_ == TuyaDatapointType::ENUM) { + this->parent_->set_enum_datapoint_value(*spd_id, *speed - 1); + } else if (this->speed_type_ == TuyaDatapointType::INTEGER) { + this->parent_->set_integer_datapoint_value(*spd_id, *speed); + } } } } diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp index 097b3c1af8..620bb88d0b 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -57,6 +57,9 @@ void TuyaLight::setup() { return; } + if (!this->color_type_.has_value()) + return; + float red, green, blue; switch (*this->color_type_) { case TuyaColorType::RGBHSV: @@ -185,7 +188,7 @@ void TuyaLight::write_state(light::LightState *state) { } } - if (this->color_id_.has_value() && (brightness == 0.0f || !color_interlock_)) { + if (this->color_id_.has_value() && this->color_type_.has_value() && (brightness == 0.0f || !color_interlock_)) { std::string color_value; switch (*this->color_type_) { case TuyaColorType::RGB: { diff --git a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp index 4256b01c4e..3eae4d2d96 100644 --- a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp +++ b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp @@ -42,8 +42,9 @@ climate::ClimateTraits UponorSmatrixClimate::traits() { } void UponorSmatrixClimate::control(const climate::ClimateCall &call) { - if (call.get_target_temperature().has_value()) { - uint16_t temp = celsius_to_raw(*call.get_target_temperature()); + auto val = call.get_target_temperature(); + if (val.has_value()) { + uint16_t temp = celsius_to_raw(*val); if (this->preset == climate::CLIMATE_PRESET_ECO) { // During ECO mode, the thermostat automatically substracts the setback value from the setpoint, // so we need to add it here first diff --git a/esphome/components/whirlpool/whirlpool.cpp b/esphome/components/whirlpool/whirlpool.cpp index 6fe735362d..e9f602e97f 100644 --- a/esphome/components/whirlpool/whirlpool.cpp +++ b/esphome/components/whirlpool/whirlpool.cpp @@ -82,7 +82,7 @@ void WhirlpoolClimate::transmit_state() { remote_state[3] |= (uint8_t) (temp - this->temperature_min_()) << 4; // Fan speed - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_HIGH: remote_state[2] |= WHIRLPOOL_FAN_HIGH; break; diff --git a/esphome/components/whynter/whynter.cpp b/esphome/components/whynter/whynter.cpp index 9f57fdb843..003d2e0ba6 100644 --- a/esphome/components/whynter/whynter.cpp +++ b/esphome/components/whynter/whynter.cpp @@ -69,7 +69,7 @@ void Whynter::transmit_state() { } mode_before_ = this->mode; - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: remote_state |= FAN_LOW; break; diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 7d5d0133c1..852ff922f1 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1094,8 +1094,9 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { } #ifdef USE_WIFI_WPA2_EAP - if (ap.get_eap().has_value()) { - EAPAuth eap_config = ap.get_eap().value(); + auto eap_opt = ap.get_eap(); + if (eap_opt.has_value()) { + EAPAuth eap_config = *eap_opt; // clang-format off ESP_LOGV( TAG, @@ -1129,8 +1130,9 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { ESP_LOGV(TAG, " Channel not set"); } #ifdef USE_WIFI_MANUAL_IP - if (ap.get_manual_ip().has_value()) { - ManualIP m = *ap.get_manual_ip(); + auto manual_ip = ap.get_manual_ip(); + if (manual_ip.has_value()) { + ManualIP m = *manual_ip; char static_ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE]; char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE]; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index bd6a18a99b..02ce59502b 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -298,9 +298,10 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // setup enterprise authentication if required #ifdef USE_WIFI_WPA2_EAP - if (ap.get_eap().has_value()) { + auto eap_opt = ap.get_eap(); + if (eap_opt.has_value()) { // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0. - EAPAuth eap = ap.get_eap().value(); + EAPAuth eap = *eap_opt; ret = wifi_station_set_enterprise_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); if (ret) { ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed: %d", ret); diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 734d186205..bf432cea6e 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -403,9 +403,10 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // setup enterprise authentication if required #ifdef USE_WIFI_WPA2_EAP - if (ap.get_eap().has_value()) { + auto eap_opt = ap.get_eap(); + if (eap_opt.has_value()) { // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0. - EAPAuth eap = ap.get_eap().value(); + EAPAuth eap = *eap_opt; #if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) err = esp_eap_client_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); #else diff --git a/esphome/components/yashima/yashima.cpp b/esphome/components/yashima/yashima.cpp index bf91420620..4a64e6c41c 100644 --- a/esphome/components/yashima/yashima.cpp +++ b/esphome/components/yashima/yashima.cpp @@ -120,10 +120,12 @@ void YashimaClimate::setup() { } void YashimaClimate::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value()) - this->mode = *call.get_mode(); - if (call.get_target_temperature().has_value()) - this->target_temperature = *call.get_target_temperature(); + auto call_mode = call.get_mode(); + if (call_mode.has_value()) + this->mode = *call_mode; + auto call_target = call.get_target_temperature(); + if (call_target.has_value()) + this->target_temperature = *call_target; this->transmit_state_(); this->publish_state(); diff --git a/esphome/components/zhlt01/zhlt01.cpp b/esphome/components/zhlt01/zhlt01.cpp index 36d1737c14..e5ab5915e4 100644 --- a/esphome/components/zhlt01/zhlt01.cpp +++ b/esphome/components/zhlt01/zhlt01.cpp @@ -13,7 +13,7 @@ void ZHLT01Climate::transmit_state() { ir_message[1] = 0x00; // Timer off // Byte 3 : Turbo mode - if (this->preset.value() == climate::CLIMATE_PRESET_BOOST) { + if (this->preset.value_or(climate::CLIMATE_PRESET_NONE) == climate::CLIMATE_PRESET_BOOST) { ir_message[3] = AC1_FAN_TURBO; } @@ -47,7 +47,7 @@ void ZHLT01Climate::transmit_state() { } // -- Fan - switch (this->preset.value()) { + switch (this->preset.value_or(climate::CLIMATE_PRESET_NONE)) { case climate::CLIMATE_PRESET_BOOST: ir_message[7] |= AC1_FAN3; break; @@ -55,7 +55,7 @@ void ZHLT01Climate::transmit_state() { ir_message[7] |= AC1_FAN_SILENT; break; default: - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: ir_message[7] |= AC1_FAN1; break; diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 042eebb40f..54d4ae311f 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -248,7 +248,7 @@ void log_entity_unit_of_measurement(const char *tag, const char *prefix, const E template class StatefulEntityBase : public EntityBase { public: virtual bool has_state() const { return this->state_.has_value(); } - virtual const T &get_state() const { return this->state_.value(); } + virtual const T &get_state() const { return this->state_.value(); } // NOLINT(bugprone-unchecked-optional-access) virtual T get_state_default(T default_value) const { return this->state_.value_or(default_value); } void invalidate_state() { this->set_new_state({}); } diff --git a/esphome/core/optional.h b/esphome/core/optional.h index 7f9db7817d..88a02aa8b2 100644 --- a/esphome/core/optional.h +++ b/esphome/core/optional.h @@ -1,220 +1,12 @@ #pragma once -// -// Copyright (c) 2017 Martin Moene -// -// https://github.com/martinmoene/optional-bare -// -// This code is licensed under the MIT License (MIT). -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// -// Modified by Otto Winter on 18.05.18 -#include +#include namespace esphome { -// type for nullopt - -struct nullopt_t { // NOLINT - struct init {}; // NOLINT - nullopt_t(init /*unused*/) {} -}; - -// extra parenthesis to prevent the most vexing parse: - -const nullopt_t nullopt((nullopt_t::init())); // NOLINT - -// Simplistic optional: requires T to be default constructible, copyable. - -template class optional { // NOLINT - private: - using safe_bool = void (optional::*)() const; - - public: - using value_type = T; - - optional() {} - - optional(nullopt_t /*unused*/) {} - - optional(T const &arg) : has_value_(true), value_(arg) {} // NOLINT - - template optional(optional const &other) : has_value_(other.has_value()), value_(other.value()) {} - - optional &operator=(nullopt_t /*unused*/) { - reset(); - return *this; - } - bool operator==(optional const &rhs) const { - if (has_value() && rhs.has_value()) - return value() == rhs.value(); - return !has_value() && !rhs.has_value(); - } - - template optional &operator=(optional const &other) { - has_value_ = other.has_value(); - value_ = other.value(); - return *this; - } - - void swap(optional &rhs) noexcept { - using std::swap; - if (has_value() && rhs.has_value()) { - swap(**this, *rhs); - } else if (!has_value() && rhs.has_value()) { - initialize(*rhs); - rhs.reset(); - } else if (has_value() && !rhs.has_value()) { - rhs.initialize(**this); - reset(); - } - } - - // observers - - value_type const *operator->() const { return &value_; } - - value_type *operator->() { return &value_; } - - value_type const &operator*() const { return value_; } - - value_type &operator*() { return value_; } - - operator safe_bool() const { return has_value() ? &optional::this_type_does_not_support_comparisons : nullptr; } - - bool has_value() const { return has_value_; } - - value_type const &value() const { return value_; } - - value_type &value() { return value_; } - - template value_type value_or(U const &v) const { return has_value() ? value() : static_cast(v); } - - // modifiers - - void reset() { has_value_ = false; } - - private: - void this_type_does_not_support_comparisons() const {} // NOLINT - - template void initialize(V const &value) { // NOLINT - value_ = value; - has_value_ = true; - } - - bool has_value_{false}; // NOLINT - value_type value_; // NOLINT -}; - -// Relational operators - -template inline bool operator==(optional const &x, optional const &y) { - return bool(x) != bool(y) ? false : !bool(x) ? true : *x == *y; -} - -template inline bool operator!=(optional const &x, optional const &y) { - return !(x == y); -} - -template inline bool operator<(optional const &x, optional const &y) { - return (!y) ? false : (!x) ? true : *x < *y; -} - -template inline bool operator>(optional const &x, optional const &y) { return (y < x); } - -template inline bool operator<=(optional const &x, optional const &y) { return !(y < x); } - -template inline bool operator>=(optional const &x, optional const &y) { return !(x < y); } - -// Comparison with nullopt - -template inline bool operator==(optional const &x, nullopt_t /*unused*/) { return (!x); } - -template inline bool operator==(nullopt_t /*unused*/, optional const &x) { return (!x); } - -template inline bool operator!=(optional const &x, nullopt_t /*unused*/) { return bool(x); } - -template inline bool operator!=(nullopt_t /*unused*/, optional const &x) { return bool(x); } - -template inline bool operator<(optional const & /*unused*/, nullopt_t /*unused*/) { return false; } - -template inline bool operator<(nullopt_t /*unused*/, optional const &x) { return bool(x); } - -template inline bool operator<=(optional const &x, nullopt_t /*unused*/) { return (!x); } - -template inline bool operator<=(nullopt_t /*unused*/, optional const & /*unused*/) { return true; } - -template inline bool operator>(optional const &x, nullopt_t /*unused*/) { return bool(x); } - -template inline bool operator>(nullopt_t /*unused*/, optional const & /*unused*/) { return false; } - -template inline bool operator>=(optional const & /*unused*/, nullopt_t /*unused*/) { return true; } - -template inline bool operator>=(nullopt_t /*unused*/, optional const &x) { return (!x); } - -// Comparison with T - -template inline bool operator==(optional const &x, U const &v) { - return bool(x) ? *x == v : false; -} - -template inline bool operator==(U const &v, optional const &x) { - return bool(x) ? v == *x : false; -} - -template inline bool operator!=(optional const &x, U const &v) { - return bool(x) ? *x != v : true; -} - -template inline bool operator!=(U const &v, optional const &x) { - return bool(x) ? v != *x : true; -} - -template inline bool operator<(optional const &x, U const &v) { - return bool(x) ? *x < v : true; -} - -template inline bool operator<(U const &v, optional const &x) { - return bool(x) ? v < *x : false; -} - -template inline bool operator<=(optional const &x, U const &v) { - return bool(x) ? *x <= v : true; -} - -template inline bool operator<=(U const &v, optional const &x) { - return bool(x) ? v <= *x : false; -} - -template inline bool operator>(optional const &x, U const &v) { - return bool(x) ? *x > v : false; -} - -template inline bool operator>(U const &v, optional const &x) { - return bool(x) ? v > *x : true; -} - -template inline bool operator>=(optional const &x, U const &v) { - return bool(x) ? *x >= v : false; -} - -template inline bool operator>=(U const &v, optional const &x) { - return bool(x) ? v >= *x : true; -} - -// Specialized algorithms - -template void swap(optional &x, optional &y) noexcept { x.swap(y); } - -// Convenience function to create an optional. - -template inline optional make_optional(T const &v) { return optional(v); } +using std::make_optional; +using std::nullopt; +using std::nullopt_t; +using std::optional; } // namespace esphome diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 6d255bc0be..8dd77de843 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -31,9 +31,7 @@ Component = esphome_ns.class_("Component") ComponentPtr = Component.operator("ptr") PollingComponent = esphome_ns.class_("PollingComponent", Component) Application = esphome_ns.class_("Application") -# Create optional with explicit namespace to avoid ambiguity with std::optional -# The generated code will use esphome::optional instead of just optional -optional = global_ns.namespace("esphome").class_("optional") +optional = global_ns.namespace("std").class_("optional") arduino_json_ns = global_ns.namespace("ArduinoJson") JsonObject = arduino_json_ns.class_("JsonObject") JsonObjectConst = arduino_json_ns.class_("JsonObjectConst") diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 6b047bc62f..16f5f980a5 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -66,5 +66,20 @@ def test_text_config_lamda_is_set(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert "it_4->set_template([]() -> esphome::optional {" in main_cpp + assert "it_4->set_template([]() -> std::optional {" in main_cpp assert 'return std::string{"Hello"};' in main_cpp + + +def test_esphome_optional_alias_works(generate_main): + """ + Test that esphome::optional alias compiles (backward compatibility) + """ + # Given + + # When + main_cpp = generate_main("tests/component_tests/text/test_text.yaml") + + # Then + # Codegen emits std::optional, but esphome::optional must also work + # via the using alias in esphome/core/optional.h + assert "std::optional" in main_cpp diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index e9ddfcf43e..ed398b0abd 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -28,9 +28,14 @@ esphome: # Test C++ API: set_template() with stateless lambda (no captures) # NOTE: set_template() is not intended to be a public API, but we test it to ensure it doesn't break. - lambda: |- - id(template_sens).set_template([]() -> esphome::optional { + id(template_sens).set_template([]() -> std::optional { return 123.0f; }); + # Test that esphome::optional alias still works for backward compatibility + - lambda: |- + id(template_sens).set_template([]() -> esphome::optional { + return 42.0f; + }); - datetime.date.set: id: test_date From 8911d9d28f1adb2414f67a00c0035295e554f388 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 3 Mar 2026 19:42:36 -0600 Subject: [PATCH 338/609] [media_source] Clarify threading contract (#14433) --- esphome/components/media_source/media_source.h | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/esphome/components/media_source/media_source.h b/esphome/components/media_source/media_source.h index 688c27134f..f21ba486b8 100644 --- a/esphome/components/media_source/media_source.h +++ b/esphome/components/media_source/media_source.h @@ -67,11 +67,13 @@ class MediaSource { /// @brief Start playing the given URI /// Sources should validate the URI and state, returning false if the source is busy. /// The orchestrator is responsible for stopping active sources before starting a new one. + /// @note Must only be called from the main loop. /// @param uri URI to play; e.g., "http://stream_url" /// @return true if playback started successfully, false otherwise virtual bool play_uri(const std::string &uri) = 0; - /// @brief Handle playback commands (pause, stop, next, etc.) + /// @brief Handle playback commands; e.g., pause, stop, next, etc. + /// @note Must only be called from the main loop. /// @param command Command to execute virtual void handle_command(MediaSourceCommand command) = 0; @@ -81,7 +83,8 @@ class MediaSource { // === State Access === - /// @brief Get current playback state (must only be called from the main loop) + /// @brief Get current playback state + /// @note Must only be called from the main loop. /// @return Current state of this source MediaSourceState get_state() const { return this->state_; } @@ -136,9 +139,10 @@ class MediaSource { virtual void notify_audio_played(uint32_t frames, int64_t timestamp) {} protected: - /// @brief Update state and notify listener (must only be called from the main loop) + /// @brief Update state and notify listener /// This is the only way to change state_, ensuring listener notifications always fire. /// Sources running FreeRTOS tasks should signal via event groups and call this from loop(). + /// @note Must only be called from the main loop. /// @param state New state to set void set_state_(MediaSourceState state) { if (this->state_ != state) { From cba34e770e8077f95305bdbbf5352def17d58dcf Mon Sep 17 00:00:00 2001 From: Tilman Vogel Date: Wed, 4 Mar 2026 03:18:36 +0100 Subject: [PATCH 339/609] [core] improve help text for --device option, mention `OTA` (#14445) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/__main__.py | 7 ++++--- esphome/const.py | 3 +++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index ffedb90bde..0164e2eeb3 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -23,6 +23,7 @@ import esphome.codegen as cg from esphome.config import iter_component_configs, read_config, strip_default_ids from esphome.const import ( ALLOWED_NAME_CHARS, + ARGUMENT_HELP_DEVICE, CONF_API, CONF_BAUD_RATE, CONF_BROKER, @@ -1367,7 +1368,7 @@ def parse_args(argv): parser_upload.add_argument( "--device", action="append", - help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.", + help=ARGUMENT_HELP_DEVICE, ) parser_upload.add_argument( "--upload_speed", @@ -1390,7 +1391,7 @@ def parse_args(argv): parser_logs.add_argument( "--device", action="append", - help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.", + help=ARGUMENT_HELP_DEVICE, ) parser_logs.add_argument( "--reset", @@ -1420,7 +1421,7 @@ def parse_args(argv): parser_run.add_argument( "--device", action="append", - help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.", + help=ARGUMENT_HELP_DEVICE, ) parser_run.add_argument( "--upload_speed", diff --git a/esphome/const.py b/esphome/const.py index d5625f6a54..060e962573 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -11,6 +11,9 @@ VALID_SUBSTITUTIONS_CHARACTERS = ( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" ) +# CLI Help Text Constants +ARGUMENT_HELP_DEVICE = "Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses. Use 'OTA' for resolving from MQTT, DNS or mDNS and avoiding the interactive prompt." + class Platform(StrEnum): """Platform identifiers for ESPHome.""" From 37146ff565aa239338948401b8493210af6ba5da Mon Sep 17 00:00:00 2001 From: JiriPrchal <163323169+JiriPrchal@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:00:09 +0100 Subject: [PATCH 340/609] [integration] Add set method to publish and save sensor value (#13316) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .../integration/integration_sensor.h | 13 ++++++---- esphome/components/integration/sensor.py | 25 +++++++++++++++++-- .../components/integration/common-esp32.yaml | 10 ++++++++ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/esphome/components/integration/integration_sensor.h b/esphome/components/integration/integration_sensor.h index f075d163fe..6c4ef7049b 100644 --- a/esphome/components/integration/integration_sensor.h +++ b/esphome/components/integration/integration_sensor.h @@ -32,6 +32,7 @@ class IntegrationSensor : public sensor::Sensor, public Component { void set_method(IntegrationMethod method) { method_ = method; } void set_restore(bool restore) { restore_ = restore; } void reset() { this->publish_and_save_(0.0f); } + void set_value(float value) { this->publish_and_save_(value); } protected: void process_sensor_value_(float value); @@ -71,14 +72,16 @@ class IntegrationSensor : public sensor::Sensor, public Component { float last_value_{0.0f}; }; -template class ResetAction : public Action { +template class ResetAction : public Action, public Parented { public: - explicit ResetAction(IntegrationSensor *parent) : parent_(parent) {} - void play(const Ts &...x) override { this->parent_->reset(); } +}; - protected: - IntegrationSensor *parent_; +template class SetValueAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, value) + + void play(const Ts &...x) override { this->parent_->set_value(this->value_.value(x...)); } }; } // namespace integration diff --git a/esphome/components/integration/sensor.py b/esphome/components/integration/sensor.py index 3c04a338dd..2676638556 100644 --- a/esphome/components/integration/sensor.py +++ b/esphome/components/integration/sensor.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_RESTORE, CONF_SENSOR, CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE, ) from esphome.core.entity_helpers import inherit_property_from @@ -17,6 +18,7 @@ IntegrationSensor = integration_ns.class_( "IntegrationSensor", sensor.Sensor, cg.Component ) ResetAction = integration_ns.class_("ResetAction", automation.Action) +SetValueAction = integration_ns.class_("SetValueAction", automation.Action) IntegrationSensorTime = integration_ns.enum("IntegrationSensorTime") INTEGRATION_TIMES = { @@ -111,5 +113,24 @@ async def to_code(config): ), ) async def sensor_integration_reset_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "sensor.integration.set_value", + SetValueAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(IntegrationSensor), + cv.Required(CONF_VALUE): cv.templatable(cv.float_), + } + ), +) +async def sensor_integration_set_value_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_VALUE], args, float) + cg.add(var.set_value(template_)) + return var diff --git a/tests/components/integration/common-esp32.yaml b/tests/components/integration/common-esp32.yaml index 248106fd60..26550d3c5c 100644 --- a/tests/components/integration/common-esp32.yaml +++ b/tests/components/integration/common-esp32.yaml @@ -1,9 +1,19 @@ +esphome: + on_boot: + then: + - sensor.integration.reset: + id: integration_sensor + - sensor.integration.set_value: + id: integration_sensor + value: 100.0 + sensor: - platform: adc id: my_sensor pin: ${pin} attenuation: 12db - platform: integration + id: integration_sensor sensor: my_sensor name: Integration Sensor time_unit: s From 065773ed4c3e84243e483409aefba0403c92f9a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Mar 2026 07:17:28 -1000 Subject: [PATCH 341/609] [runtime_stats] Use micros() for accurate per-component timing (#14452) --- .../runtime_stats/runtime_stats.cpp | 22 ++++---- .../components/runtime_stats/runtime_stats.h | 56 +++++++++---------- esphome/core/component.cpp | 7 ++- esphome/core/component.h | 16 +++++- 4 files changed, 60 insertions(+), 41 deletions(-) diff --git a/esphome/components/runtime_stats/runtime_stats.cpp b/esphome/components/runtime_stats/runtime_stats.cpp index 410695da04..d9fa22d949 100644 --- a/esphome/components/runtime_stats/runtime_stats.cpp +++ b/esphome/components/runtime_stats/runtime_stats.cpp @@ -13,12 +13,12 @@ RuntimeStatsCollector::RuntimeStatsCollector() : log_interval_(60000), next_log_ global_runtime_stats = this; } -void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time) { +void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_us, uint32_t current_time) { if (component == nullptr) return; // Record stats using component pointer as key - this->component_stats_[component].record_time(duration_ms); + this->component_stats_[component].record_time(duration_us); if (this->next_log_time_ == 0) { this->next_log_time_ = current_time + this->log_interval_; @@ -58,15 +58,16 @@ void RuntimeStatsCollector::log_stats_() { // Sort by period runtime (descending) std::sort(sorted, sorted + count, [this](Component *a, Component *b) { - return this->component_stats_[a].get_period_time_ms() > this->component_stats_[b].get_period_time_ms(); + return this->component_stats_[a].get_period_time_us() > this->component_stats_[b].get_period_time_us(); }); // Log top components by period runtime for (size_t i = 0; i < count; i++) { const auto &stats = this->component_stats_[sorted[i]]; - ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", - LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_period_count(), stats.get_period_avg_time_ms(), - stats.get_period_max_time_ms(), stats.get_period_time_ms()); + ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms", + LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_period_count(), + stats.get_period_avg_time_us() / 1000.0f, stats.get_period_max_time_us() / 1000.0f, + stats.get_period_time_us() / 1000.0f); } // Log total stats since boot (only for active components - idle ones haven't changed) @@ -74,14 +75,15 @@ void RuntimeStatsCollector::log_stats_() { // Re-sort by total runtime for all-time stats std::sort(sorted, sorted + count, [this](Component *a, Component *b) { - return this->component_stats_[a].get_total_time_ms() > this->component_stats_[b].get_total_time_ms(); + return this->component_stats_[a].get_total_time_us() > this->component_stats_[b].get_total_time_us(); }); for (size_t i = 0; i < count; i++) { const auto &stats = this->component_stats_[sorted[i]]; - ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", - LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_total_count(), stats.get_total_avg_time_ms(), - stats.get_total_max_time_ms(), stats.get_total_time_ms()); + ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms", + LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_total_count(), + stats.get_total_avg_time_us() / 1000.0f, stats.get_total_max_time_us() / 1000.0f, + stats.get_total_time_us() / 1000.0); } } diff --git a/esphome/components/runtime_stats/runtime_stats.h b/esphome/components/runtime_stats/runtime_stats.h index c7fea7474b..0847529720 100644 --- a/esphome/components/runtime_stats/runtime_stats.h +++ b/esphome/components/runtime_stats/runtime_stats.h @@ -22,58 +22,58 @@ class ComponentRuntimeStats { public: ComponentRuntimeStats() : period_count_(0), - period_time_ms_(0), - period_max_time_ms_(0), + period_time_us_(0), + period_max_time_us_(0), total_count_(0), - total_time_ms_(0), - total_max_time_ms_(0) {} + total_time_us_(0), + total_max_time_us_(0) {} - void record_time(uint32_t duration_ms) { + void record_time(uint32_t duration_us) { // Update period counters this->period_count_++; - this->period_time_ms_ += duration_ms; - if (duration_ms > this->period_max_time_ms_) - this->period_max_time_ms_ = duration_ms; + this->period_time_us_ += duration_us; + if (duration_us > this->period_max_time_us_) + this->period_max_time_us_ = duration_us; - // Update total counters + // Update total counters (uint64_t to avoid overflow — uint32_t would overflow after ~10 hours) this->total_count_++; - this->total_time_ms_ += duration_ms; - if (duration_ms > this->total_max_time_ms_) - this->total_max_time_ms_ = duration_ms; + this->total_time_us_ += duration_us; + if (duration_us > this->total_max_time_us_) + this->total_max_time_us_ = duration_us; } void reset_period_stats() { this->period_count_ = 0; - this->period_time_ms_ = 0; - this->period_max_time_ms_ = 0; + this->period_time_us_ = 0; + this->period_max_time_us_ = 0; } // Period stats (reset each logging interval) uint32_t get_period_count() const { return this->period_count_; } - uint32_t get_period_time_ms() const { return this->period_time_ms_; } - uint32_t get_period_max_time_ms() const { return this->period_max_time_ms_; } - float get_period_avg_time_ms() const { - return this->period_count_ > 0 ? this->period_time_ms_ / static_cast(this->period_count_) : 0.0f; + uint32_t get_period_time_us() const { return this->period_time_us_; } + uint32_t get_period_max_time_us() const { return this->period_max_time_us_; } + float get_period_avg_time_us() const { + return this->period_count_ > 0 ? this->period_time_us_ / static_cast(this->period_count_) : 0.0f; } - // Total stats (persistent until reboot) + // Total stats (persistent until reboot, uint64_t to avoid overflow) uint32_t get_total_count() const { return this->total_count_; } - uint32_t get_total_time_ms() const { return this->total_time_ms_; } - uint32_t get_total_max_time_ms() const { return this->total_max_time_ms_; } - float get_total_avg_time_ms() const { - return this->total_count_ > 0 ? this->total_time_ms_ / static_cast(this->total_count_) : 0.0f; + uint64_t get_total_time_us() const { return this->total_time_us_; } + uint32_t get_total_max_time_us() const { return this->total_max_time_us_; } + float get_total_avg_time_us() const { + return this->total_count_ > 0 ? this->total_time_us_ / static_cast(this->total_count_) : 0.0f; } protected: // Period stats (reset each logging interval) uint32_t period_count_; - uint32_t period_time_ms_; - uint32_t period_max_time_ms_; + uint32_t period_time_us_; + uint32_t period_max_time_us_; // Total stats (persistent until reboot) uint32_t total_count_; - uint32_t total_time_ms_; - uint32_t total_max_time_ms_; + uint64_t total_time_us_; + uint32_t total_max_time_us_; }; class RuntimeStatsCollector { @@ -83,7 +83,7 @@ class RuntimeStatsCollector { void set_log_interval(uint32_t log_interval) { this->log_interval_ = log_interval; } uint32_t get_log_interval() const { return this->log_interval_; } - void record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time); + void record_component_time(Component *component, uint32_t duration_us, uint32_t current_time); // Process any pending stats printing (should be called after component loop) void process_pending_stats(uint32_t current_time); diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 4ccc747819..8c2c8d38e8 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -529,9 +529,12 @@ uint32_t WarnIfComponentBlockingGuard::finish() { uint32_t curr_time = millis(); uint32_t blocking_time = curr_time - this->started_; #ifdef USE_RUNTIME_STATS - // Record component runtime stats + // Use micros() for accurate sub-millisecond timing. millis() has insufficient + // resolution — most components complete in microseconds but millis() only has + // 1ms granularity, so results were essentially random noise. if (global_runtime_stats != nullptr) { - global_runtime_stats->record_component_time(this->component_, blocking_time, curr_time); + uint32_t duration_us = micros() - this->started_us_; + global_runtime_stats->record_component_time(this->component_, duration_us, curr_time); } #endif if (blocking_time > WARN_IF_BLOCKING_OVER_MS) { diff --git a/esphome/core/component.h b/esphome/core/component.h index e5127b0c9f..59222dc4f4 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -563,10 +563,21 @@ class PollingComponent : public Component { uint32_t update_interval_; }; +#ifdef USE_RUNTIME_STATS +uint32_t micros(); // Forward declare for inline constructor +#endif + class WarnIfComponentBlockingGuard { public: WarnIfComponentBlockingGuard(Component *component, uint32_t start_time) - : started_(start_time), component_(component) {} + : started_(start_time), + component_(component) +#ifdef USE_RUNTIME_STATS + , + started_us_(micros()) +#endif + { + } // Finish the timing operation and return the current time uint32_t finish(); @@ -576,6 +587,9 @@ class WarnIfComponentBlockingGuard { protected: uint32_t started_; Component *component_; +#ifdef USE_RUNTIME_STATS + uint32_t started_us_; +#endif }; // Function to clear setup priority overrides after all components are set up From ac19d05db26a891b6dd0d4301b8e8321f9c48fe0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Mar 2026 07:17:41 -1000 Subject: [PATCH 342/609] [core] Call loop() directly in main loop, bypass call() indirection (#14451) --- esphome/core/application.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 8c2ba58c86..db1c8a0c0a 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -153,6 +153,14 @@ void Application::setup() { this->setup_wake_loop_threadsafe_(); #endif + // Ensure all active looping components are in LOOP state. + // Components after the last blocking component only got one call() during setup + // (CONSTRUCTION→SETUP) and never received the second call() (SETUP→LOOP). + // The main loop calls loop() directly, bypassing call()'s state machine. + for (uint16_t i = 0; i < this->looping_components_active_end_; i++) { + this->looping_components_[i]->set_component_state_(COMPONENT_STATE_LOOP); + } + this->schedule_dump_config(); } void Application::loop() { @@ -173,7 +181,7 @@ void Application::loop() { { this->set_current_component(component); WarnIfComponentBlockingGuard guard{component, last_op_end_time}; - component->call(); + component->loop(); // Use the finish method to get the current time as the end time last_op_end_time = guard.finish(); } From b2e8544c584c29a923a88622a7b7ba10373a761f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Mar 2026 07:18:31 -1000 Subject: [PATCH 343/609] [ld2412] Add integration tests with mock UART (#14448) --- .../fixtures/uart_mock_ld2412.yaml | 171 ++++++++ .../uart_mock_ld2412_engineering.yaml | 213 +++++++++ tests/integration/test_uart_mock_ld2412.py | 407 ++++++++++++++++++ 3 files changed, 791 insertions(+) create mode 100644 tests/integration/fixtures/uart_mock_ld2412.yaml create mode 100644 tests/integration/fixtures/uart_mock_ld2412_engineering.yaml create mode 100644 tests/integration/test_uart_mock_ld2412.py diff --git a/tests/integration/fixtures/uart_mock_ld2412.yaml b/tests/integration/fixtures/uart_mock_ld2412.yaml new file mode 100644 index 0000000000..a502f36a25 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_ld2412.yaml @@ -0,0 +1,171 @@ +esphome: + name: uart-mock-ld2412-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy ld2412's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + id: mock_uart + baud_rate: 256000 + injections: + # Phase 1 (t=100ms): Valid LD2412 normal mode data frame - happy path + # The buffer is clean at this point, so this frame should parse correctly. + # Moving target: 100cm, energy 50 + # Still target: 120cm, energy 25 + # Target state: 0x03 (moving + still) + # detection_distance = 100 (LD2412 computes from moving target when MOVE_BITMASK set) + # + # Frame layout (24 bytes): + # [0-3] F4 F3 F2 F1 = data frame header + # [4-5] 0D 00 = length 13 + # [6] 02 = data type (normal) + # [7] AA = data header marker + # [8] 03 = target states (moving+still) + # [9-10] 64 00 = moving distance 100 (0x0064) + # [11] 32 = moving energy 50 + # [12-13] 78 00 = still distance 120 (0x0078) + # [14] 19 = still energy 25 + # [15-16] 64 00 = detect distance bytes (ignored by LD2412 code) + # [17] 00 = padding + # [18] 55 = data footer marker + # [19] 00 = CRC/check + # [20-23] F8 F7 F6 F5 = data frame footer + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x0D, 0x00, + 0x02, 0xAA, + 0x03, + 0x64, 0x00, + 0x32, + 0x78, 0x00, + 0x19, + 0x64, 0x00, + 0x00, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 2 (t=300ms): Garbage bytes + # LD2412's parser rejects bytes that don't match the frame header at + # position 0 (must start with F4 or FD), so buffer stays empty. + - delay: 200ms + inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22] + + # Phase 3 (t=400ms): Truncated frame (header + partial data, no footer) + # Starts with valid data frame header so parser accepts it. + # After this, buffer_pos_ = 8. + - delay: 100ms + inject_rx: [0xF4, 0xF3, 0xF2, 0xF1, 0x0D, 0x00, 0x02, 0xAA] + + # Phase 4 (t=600ms): Overflow - inject 60 bytes of 0xFF (MAX_LINE_LENGTH=54) + # Buffer has 8 bytes from phase 3 (garbage in phase 2 was rejected). + # Overflow math: buffer_pos_ starts at 8, overflow triggers when + # buffer_pos_ reaches 53 (MAX_LINE_LENGTH - 1). Need 45 more bytes to + # fill positions 8-52, then byte 46 triggers overflow. After overflow, + # buffer_pos_ = 0 and remaining 0xFF bytes are rejected (don't match header). + - delay: 200ms + inject_rx: + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ] + + # Phase 5 (t=700ms): Valid frame after overflow - recovery test + # Buffer was reset by overflow. This valid frame should parse correctly. + # Moving target: 50cm, energy 100 + # Still target: 75cm, energy 80 + # detection_distance = 50 (moving target distance, since MOVE_BITMASK set) + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x0D, 0x00, + 0x02, 0xAA, + 0x03, + 0x32, 0x00, + 0x64, + 0x4B, 0x00, + 0x50, + 0x32, 0x00, + 0x00, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + +ld2412: + id: ld2412_dev + uart_id: mock_uart + +sensor: + - platform: ld2412 + ld2412_id: ld2412_dev + moving_distance: + name: "Moving Distance" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + still_distance: + name: "Still Distance" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + moving_energy: + name: "Moving Energy" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + still_energy: + name: "Still Energy" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + detection_distance: + name: "Detection Distance" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + +binary_sensor: + - platform: ld2412 + ld2412_id: ld2412_dev + has_target: + name: "Has Target" + filters: + - settle: 50ms + has_moving_target: + name: "Has Moving Target" + filters: + - settle: 50ms + has_still_target: + name: "Has Still Target" + filters: + - settle: 50ms diff --git a/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml b/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml new file mode 100644 index 0000000000..3c669fc9a9 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml @@ -0,0 +1,213 @@ +esphome: + name: uart-mock-ld2412-eng-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy ld2412's DEPENDENCIES = ["uart"] +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + id: mock_uart + baud_rate: 256000 + injections: + # Phase 1 (t=100ms): Valid LD2412 engineering mode data frame + # + # Engineering mode frame layout (52 bytes): + # [0-3] F4 F3 F2 F1 = data frame header + # [4-5] 2A 00 = length 42 + # [6] 01 = data type (engineering mode) + # [7] AA = data header marker + # [8] 03 = target states (moving+still) + # [9-10] 1E 00 = moving distance 30 (0x001E) + # [11] 64 = moving energy 100 + # [12-13] 1E 00 = still distance 30 (0x001E) + # [14] 64 = still energy 100 + # [15-16] 00 00 = detection distance bytes (ignored) + # [17-30] gate moving energies (14 gates) + # [31-44] gate still energies (14 gates) + # [45] 57 = light sensor value 87 + # [46] 55 = data footer marker + # [47] 00 = check + # [48-51] F8 F7 F6 F5 = data frame footer + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x2A, 0x00, + 0x01, 0xAA, + 0x03, + 0x1E, 0x00, + 0x64, + 0x1E, 0x00, + 0x64, + 0x00, 0x00, + 0x64, 0x41, 0x06, 0x0E, 0x2B, 0x16, 0x03, 0x03, 0x07, 0x05, 0x09, 0x08, 0x07, 0x06, + 0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x50, 0x40, 0x30, 0x20, 0x10, + 0x57, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 2 (t=200ms): Second engineering mode frame with different values + # Moving at 73cm, still at 30cm + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x2A, 0x00, + 0x01, 0xAA, + 0x03, + 0x49, 0x00, + 0x64, + 0x1E, 0x00, + 0x64, + 0x21, 0x00, + 0x11, 0x64, 0x05, 0x29, 0x39, 0x10, 0x03, 0x11, 0x0E, 0x08, 0x06, 0x04, 0x03, 0x02, + 0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x50, 0x40, 0x30, 0x20, 0x10, + 0x57, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 3 (t=300ms): Frame with still target at 291cm (multi-byte distance) + # This tests encode_uint16 with high byte > 0 + # Target state: 0x02 (still only) -> detection_distance = still distance = 291 + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x2A, 0x00, + 0x01, 0xAA, + 0x02, + 0x2F, 0x00, + 0x36, + 0x23, 0x01, + 0x64, + 0x21, 0x00, + 0x2F, 0x36, 0x09, 0x0D, 0x15, 0x0B, 0x06, 0x06, 0x08, 0x09, 0x08, 0x07, 0x06, 0x05, + 0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x5A, 0x3D, 0x30, 0x20, 0x10, 0x08, 0x04, + 0x57, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + +ld2412: + id: ld2412_dev + uart_id: mock_uart + +sensor: + - platform: ld2412 + ld2412_id: ld2412_dev + moving_distance: + name: "Moving Distance" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + still_distance: + name: "Still Distance" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + moving_energy: + name: "Moving Energy" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + still_energy: + name: "Still Energy" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + detection_distance: + name: "Detection Distance" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + light: + name: "Light" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + gate_0: + move_energy: + name: "Gate 0 Move Energy" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + still_energy: + name: "Gate 0 Still Energy" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + gate_1: + move_energy: + name: "Gate 1 Move Energy" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + still_energy: + name: "Gate 1 Still Energy" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + gate_2: + move_energy: + name: "Gate 2 Move Energy" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + still_energy: + name: "Gate 2 Still Energy" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + +binary_sensor: + - platform: ld2412 + ld2412_id: ld2412_dev + has_target: + name: "Has Target" + filters: + - settle: 50ms + has_moving_target: + name: "Has Moving Target" + filters: + - settle: 50ms + has_still_target: + name: "Has Still Target" + filters: + - settle: 50ms diff --git a/tests/integration/test_uart_mock_ld2412.py b/tests/integration/test_uart_mock_ld2412.py new file mode 100644 index 0000000000..cf7324ceed --- /dev/null +++ b/tests/integration/test_uart_mock_ld2412.py @@ -0,0 +1,407 @@ +"""Integration test for LD2412 component with mock UART. + +Tests: +test_uart_mock_ld2412 (normal mode): + 1. Happy path - valid data frame publishes correct sensor values + 2. Garbage resilience - random bytes don't crash the component + 3. Truncated frame handling - partial frame doesn't corrupt state + 4. Buffer overflow recovery - overflow resets the parser + 5. Post-overflow parsing - next valid frame after overflow is parsed correctly + 6. TX logging - verifies LD2412 sends expected setup commands + +test_uart_mock_ld2412_engineering (engineering mode): + 1. Engineering mode frames with per-gate energy data and light sensor + 2. Multi-byte still distance (291cm) using high byte > 0 + 3. Gate energy sensor values + 4. Detection distance computed from target state +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path + +from aioesphomeapi import ( + BinarySensorInfo, + BinarySensorState, + EntityState, + SensorInfo, + SensorState, +) +import pytest + +from .state_utils import InitialStateHelper, build_key_to_entity_mapping, find_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_uart_mock_ld2412( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test LD2412 data parsing with happy path, garbage, overflow, and recovery.""" + # Replace external component path placeholder + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track overflow warning in logs + overflow_seen = loop.create_future() + + # Track TX data logged by the mock for assertions + tx_log_lines: list[str] = [] + + def line_callback(line: str) -> None: + if "Max command length exceeded" in line and not overflow_seen.done(): + overflow_seen.set_result(True) + # Capture all TX log lines from uart_mock + if "uart_mock" in line and "TX " in line: + tx_log_lines.append(line) + + # Track sensor state updates (after initial state is swallowed) + sensor_states: dict[str, list[float]] = { + "moving_distance": [], + "still_distance": [], + "moving_energy": [], + "still_energy": [], + "detection_distance": [], + } + binary_states: dict[str, list[bool]] = { + "has_target": [], + "has_moving_target": [], + "has_still_target": [], + } + + # Signal when we see recovery frame values + recovery_received = loop.create_future() + + def on_state(state: EntityState) -> None: + if isinstance(state, SensorState) and not state.missing_state: + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in sensor_states: + sensor_states[sensor_name].append(state.state) + # Check if this is the recovery frame (moving_distance = 50) + if ( + sensor_name == "moving_distance" + and state.state == pytest.approx(50.0) + and not recovery_received.done() + ): + recovery_received.set_result(True) + elif isinstance(state, BinarySensorState): + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in binary_states: + binary_states[sensor_name].append(state.state) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + + # Build key mappings for all sensor types + all_names = list(sensor_states.keys()) + list(binary_states.keys()) + # Sort by descending length to avoid substring collisions + # (e.g., "still_energy" matching "gate_0_still_energy") + all_names.sort(key=len, reverse=True) + key_to_sensor = build_key_to_entity_mapping(entities, all_names) + + # Set up initial state helper + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Phase 1 values are in the initial states (swallowed by InitialStateHelper). + # Verify them via initial_states dict. + moving_dist_entity = find_entity(entities, "moving_distance", SensorInfo) + assert moving_dist_entity is not None + initial_moving = initial_state_helper.initial_states.get(moving_dist_entity.key) + assert initial_moving is not None and isinstance(initial_moving, SensorState) + assert initial_moving.state == pytest.approx(100.0), ( + f"Initial moving distance should be 100, got {initial_moving.state}" + ) + + still_dist_entity = find_entity(entities, "still_distance", SensorInfo) + assert still_dist_entity is not None + initial_still = initial_state_helper.initial_states.get(still_dist_entity.key) + assert initial_still is not None and isinstance(initial_still, SensorState) + assert initial_still.state == pytest.approx(120.0), ( + f"Initial still distance should be 120, got {initial_still.state}" + ) + + moving_energy_entity = find_entity(entities, "moving_energy", SensorInfo) + assert moving_energy_entity is not None + initial_me = initial_state_helper.initial_states.get(moving_energy_entity.key) + assert initial_me is not None and isinstance(initial_me, SensorState) + assert initial_me.state == pytest.approx(50.0), ( + f"Initial moving energy should be 50, got {initial_me.state}" + ) + + still_energy_entity = find_entity(entities, "still_energy", SensorInfo) + assert still_energy_entity is not None + initial_se = initial_state_helper.initial_states.get(still_energy_entity.key) + assert initial_se is not None and isinstance(initial_se, SensorState) + assert initial_se.state == pytest.approx(25.0), ( + f"Initial still energy should be 25, got {initial_se.state}" + ) + + # LD2412 detection_distance = moving_distance when MOVE_BITMASK is set + detect_dist_entity = find_entity(entities, "detection_distance", SensorInfo) + assert detect_dist_entity is not None + initial_dd = initial_state_helper.initial_states.get(detect_dist_entity.key) + assert initial_dd is not None and isinstance(initial_dd, SensorState) + assert initial_dd.state == pytest.approx(100.0), ( + f"Initial detection distance should be 100, got {initial_dd.state}" + ) + + # Wait for the recovery frame (Phase 5) to be parsed + # This proves the component survived garbage + truncated + overflow + try: + await asyncio.wait_for(recovery_received, timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for recovery frame. Received sensor states:\n" + f" moving_distance: {sensor_states['moving_distance']}\n" + f" still_distance: {sensor_states['still_distance']}\n" + f" moving_energy: {sensor_states['moving_energy']}\n" + f" still_energy: {sensor_states['still_energy']}\n" + f" detection_distance: {sensor_states['detection_distance']}" + ) + + # Verify overflow warning was logged + assert overflow_seen.done(), ( + "Expected 'Max command length exceeded' warning in logs" + ) + + # Verify LD2412 sent setup commands (TX logging) + assert len(tx_log_lines) > 0, "Expected TX log lines from uart_mock" + tx_data = " ".join(tx_log_lines) + # Verify command frame header appears (FD:FC:FB:FA) + assert "FD:FC:FB:FA" in tx_data, ( + "Expected LD2412 command frame header FD:FC:FB:FA in TX log" + ) + # Verify command frame footer appears (04:03:02:01) + assert "04:03:02:01" in tx_data, ( + "Expected LD2412 command frame footer 04:03:02:01 in TX log" + ) + + # Recovery frame values (Phase 5, after overflow) + assert len(sensor_states["moving_distance"]) >= 1, ( + f"Expected recovery moving_distance, got: {sensor_states['moving_distance']}" + ) + # Find the recovery value (moving_distance = 50) + recovery_values = [ + v for v in sensor_states["moving_distance"] if v == pytest.approx(50.0) + ] + assert len(recovery_values) >= 1, ( + f"Expected moving_distance=50 in recovery, got: {sensor_states['moving_distance']}" + ) + + # Recovery frame: moving=50, still=75, energy=100/80, detect=50 + recovery_idx = next( + i + for i, v in enumerate(sensor_states["moving_distance"]) + if v == pytest.approx(50.0) + ) + assert sensor_states["still_distance"][recovery_idx] == pytest.approx(75.0), ( + f"Recovery still distance should be 75, got {sensor_states['still_distance'][recovery_idx]}" + ) + assert sensor_states["moving_energy"][recovery_idx] == pytest.approx(100.0), ( + f"Recovery moving energy should be 100, got {sensor_states['moving_energy'][recovery_idx]}" + ) + assert sensor_states["still_energy"][recovery_idx] == pytest.approx(80.0), ( + f"Recovery still energy should be 80, got {sensor_states['still_energy'][recovery_idx]}" + ) + # LD2412 detection_distance = moving_distance when MOVE_BITMASK set + assert sensor_states["detection_distance"][recovery_idx] == pytest.approx( + 50.0 + ), ( + f"Recovery detection distance should be 50, got {sensor_states['detection_distance'][recovery_idx]}" + ) + + # Verify binary sensors detected targets + has_target_entity = find_entity(entities, "has_target", BinarySensorInfo) + assert has_target_entity is not None + initial_ht = initial_state_helper.initial_states.get(has_target_entity.key) + assert initial_ht is not None and isinstance(initial_ht, BinarySensorState) + assert initial_ht.state is True, "Has target should be True" + + has_moving_entity = find_entity(entities, "has_moving_target", BinarySensorInfo) + assert has_moving_entity is not None + initial_hm = initial_state_helper.initial_states.get(has_moving_entity.key) + assert initial_hm is not None and isinstance(initial_hm, BinarySensorState) + assert initial_hm.state is True, "Has moving target should be True" + + has_still_entity = find_entity(entities, "has_still_target", BinarySensorInfo) + assert has_still_entity is not None + initial_hs = initial_state_helper.initial_states.get(has_still_entity.key) + assert initial_hs is not None and isinstance(initial_hs, BinarySensorState) + assert initial_hs.state is True, "Has still target should be True" + + +@pytest.mark.asyncio +async def test_uart_mock_ld2412_engineering( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test LD2412 engineering mode with per-gate energy, light, and multi-byte distance.""" + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track sensor state updates (after initial state is swallowed) + sensor_states: dict[str, list[float]] = { + "moving_distance": [], + "still_distance": [], + "moving_energy": [], + "still_energy": [], + "detection_distance": [], + "light": [], + "gate_0_move_energy": [], + "gate_1_move_energy": [], + "gate_2_move_energy": [], + "gate_0_still_energy": [], + "gate_1_still_energy": [], + "gate_2_still_energy": [], + } + binary_states: dict[str, list[bool]] = { + "has_target": [], + "has_moving_target": [], + "has_still_target": [], + } + + # Signal when we see Phase 3 frame values + phase3_still_received = loop.create_future() + phase3_detect_received = loop.create_future() + + def on_state(state: EntityState) -> None: + if isinstance(state, SensorState) and not state.missing_state: + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in sensor_states: + sensor_states[sensor_name].append(state.state) + if ( + sensor_name == "still_distance" + and state.state == pytest.approx(291.0) + and not phase3_still_received.done() + ): + phase3_still_received.set_result(True) + if ( + sensor_name == "detection_distance" + and state.state == pytest.approx(291.0) + and not phase3_detect_received.done() + ): + phase3_detect_received.set_result(True) + elif isinstance(state, BinarySensorState): + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in binary_states: + binary_states[sensor_name].append(state.state) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + + all_names = list(sensor_states.keys()) + list(binary_states.keys()) + # Sort by descending length to avoid substring collisions + # (e.g., "still_energy" matching "gate_0_still_energy") + all_names.sort(key=len, reverse=True) + key_to_sensor = build_key_to_entity_mapping(entities, all_names) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Phase 1 initial values (engineering mode frame): + # moving=30, energy=100, still=30, energy=100, detect=30 + moving_dist_entity = find_entity(entities, "moving_distance", SensorInfo) + assert moving_dist_entity is not None + initial_moving = initial_state_helper.initial_states.get(moving_dist_entity.key) + assert initial_moving is not None and isinstance(initial_moving, SensorState) + assert initial_moving.state == pytest.approx(30.0), ( + f"Initial moving distance should be 30, got {initial_moving.state}" + ) + + still_dist_entity = find_entity(entities, "still_distance", SensorInfo) + assert still_dist_entity is not None + initial_still = initial_state_helper.initial_states.get(still_dist_entity.key) + assert initial_still is not None and isinstance(initial_still, SensorState) + assert initial_still.state == pytest.approx(30.0), ( + f"Initial still distance should be 30, got {initial_still.state}" + ) + + # Verify engineering mode sensors from initial state + # Gate 0 moving energy = 0x64 = 100 + gate0_move_entity = find_entity(entities, "gate_0_move_energy", SensorInfo) + assert gate0_move_entity is not None + initial_g0m = initial_state_helper.initial_states.get(gate0_move_entity.key) + assert initial_g0m is not None and isinstance(initial_g0m, SensorState) + assert initial_g0m.state == pytest.approx(100.0), ( + f"Gate 0 move energy should be 100, got {initial_g0m.state}" + ) + + # Gate 1 moving energy = 0x41 = 65 + gate1_move_entity = find_entity(entities, "gate_1_move_energy", SensorInfo) + assert gate1_move_entity is not None + initial_g1m = initial_state_helper.initial_states.get(gate1_move_entity.key) + assert initial_g1m is not None and isinstance(initial_g1m, SensorState) + assert initial_g1m.state == pytest.approx(65.0), ( + f"Gate 1 move energy should be 65, got {initial_g1m.state}" + ) + + # Light sensor = 0x57 = 87 + light_entity = find_entity(entities, "light", SensorInfo) + assert light_entity is not None + initial_light = initial_state_helper.initial_states.get(light_entity.key) + assert initial_light is not None and isinstance(initial_light, SensorState) + assert initial_light.state == pytest.approx(87.0), ( + f"Light sensor should be 87, got {initial_light.state}" + ) + + # Wait for Phase 3 frame: still_distance = 291cm (multi-byte) + try: + await asyncio.wait_for(phase3_still_received, timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 3 still_distance. Received:\n" + f" still_distance: {sensor_states['still_distance']}\n" + f" moving_distance: {sensor_states['moving_distance']}" + ) + + assert pytest.approx(291.0) in sensor_states["still_distance"], ( + f"Expected still_distance=291, got: {sensor_states['still_distance']}" + ) + + # Wait for Phase 3: detection_distance = 291 (still-only target) + # target_state=0x02 so LD2412 uses still_distance for detection_distance. + # The throttle_with_priority filter may delay this value. + try: + await asyncio.wait_for(phase3_detect_received, timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for detection_distance=291 (still-only target). " + f"Received: {sensor_states['detection_distance']}" + ) + + assert pytest.approx(291.0) in sensor_states["detection_distance"], ( + f"Expected detection_distance=291, got: {sensor_states['detection_distance']}" + ) From 5ba880f19b967b153340befa1acf271066f8671c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:57:45 -0500 Subject: [PATCH 344/609] [sx127x] Fix preamble MSB register always written as zero (#14457) Co-authored-by: Claude Opus 4.6 --- esphome/components/sx127x/sx127x.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index caf68b6d51..f6aa11b634 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -186,7 +186,7 @@ void SX127x::configure_fsk_ook_() { } else { this->write_register_(REG_PREAMBLE_DETECT, PREAMBLE_DETECTOR_OFF); } - this->write_register_(REG_PREAMBLE_SIZE_MSB, this->preamble_size_ >> 16); + this->write_register_(REG_PREAMBLE_SIZE_MSB, this->preamble_size_ >> 8); this->write_register_(REG_PREAMBLE_SIZE_LSB, this->preamble_size_ & 0xFF); // config sync generation and setup ook threshold @@ -214,7 +214,7 @@ void SX127x::configure_lora_() { // config preamble if (this->preamble_size_ >= 6) { - this->write_register_(REG_PREAMBLE_LEN_MSB, this->preamble_size_ >> 16); + this->write_register_(REG_PREAMBLE_LEN_MSB, this->preamble_size_ >> 8); this->write_register_(REG_PREAMBLE_LEN_LSB, this->preamble_size_ & 0xFF); } From 9abba79c5429ee9c9c4e9a4f77ca4c67b4a756b2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:58:24 -0500 Subject: [PATCH 345/609] [remote_base][remote_receiver] Fix OOB access in pronto comparison and RMT buffer allocation (#14459) Co-authored-by: Claude Opus 4.6 --- esphome/components/remote_base/pronto_protocol.cpp | 6 +++++- esphome/components/remote_receiver/remote_receiver_rmt.cpp | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index 43029cbc2f..6903cd4605 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -44,9 +44,13 @@ bool ProntoData::operator==(const ProntoData &rhs) const { std::vector data1 = encode_pronto(data); std::vector data2 = encode_pronto(rhs.data); + if (data1.size() != data2.size() || data1.empty()) { + return false; + } + uint32_t total_diff = 0; // Don't need to check the last one, it's the large gap at the end. - for (std::vector::size_type i = 0; i < data1.size() - 1; ++i) { + for (size_t i = 0; i < data1.size() - 1; ++i) { int diff = data2[i] - data1[i]; diff *= diff; if (rhs.delta == -1 && diff > 9) diff --git a/esphome/components/remote_receiver/remote_receiver_rmt.cpp b/esphome/components/remote_receiver/remote_receiver_rmt.cpp index 357a36d052..96b23bd0f5 100644 --- a/esphome/components/remote_receiver/remote_receiver_rmt.cpp +++ b/esphome/components/remote_receiver/remote_receiver_rmt.cpp @@ -106,7 +106,7 @@ void RemoteReceiverComponent::setup() { this->store_.filter_symbols = this->filter_symbols_; this->store_.receive_size = this->receive_symbols_ * sizeof(rmt_symbol_word_t); this->store_.buffer_size = std::max((event_size + this->store_.receive_size) * 2, this->buffer_size_); - this->store_.buffer = new uint8_t[this->buffer_size_]; + this->store_.buffer = new uint8_t[this->store_.buffer_size]; error = rmt_receive(this->channel_, (uint8_t *) this->store_.buffer + event_size, this->store_.receive_size, &this->store_.config); if (error != ESP_OK) { From 246a8bff0cb9c2de5a740fe8529f072c609dce51 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:58:42 -0500 Subject: [PATCH 346/609] [pn7160][pn7150][pn532] Fix tag purge skipping, NDEF bounds check, and NDEF length byte order (#14460) Co-authored-by: Claude Opus 4.6 --- esphome/components/pn532/pn532_mifare_ultralight.cpp | 4 ++-- esphome/components/pn7150/pn7150.cpp | 6 +++--- esphome/components/pn7150/pn7150_mifare_ultralight.cpp | 4 ++-- esphome/components/pn7160/pn7160.cpp | 6 +++--- esphome/components/pn7160/pn7160_mifare_ultralight.cpp | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/esphome/components/pn532/pn532_mifare_ultralight.cpp b/esphome/components/pn532/pn532_mifare_ultralight.cpp index a8a8e2d573..0e0dc1542f 100644 --- a/esphome/components/pn532/pn532_mifare_ultralight.cpp +++ b/esphome/components/pn532/pn532_mifare_ultralight.cpp @@ -99,7 +99,7 @@ bool PN532::find_mifare_ultralight_ndef_(const std::vector &page_3_to_6 uint8_t &message_start_index) { const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector - if (!(page_3_to_6.size() > p4_offset + 5)) { + if (!(page_3_to_6.size() > p4_offset + 6)) { return false; } @@ -134,7 +134,7 @@ bool PN532::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage * } else { encoded.insert(encoded.begin() + 1, 0xFF); encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); - encoded.insert(encoded.begin() + 2, message_length & 0xFF); + encoded.insert(encoded.begin() + 3, message_length & 0xFF); } encoded.push_back(0xFE); diff --git a/esphome/components/pn7150/pn7150.cpp b/esphome/components/pn7150/pn7150.cpp index 8c76c8b88c..d68bea41b3 100644 --- a/esphome/components/pn7150/pn7150.cpp +++ b/esphome/components/pn7150/pn7150.cpp @@ -562,9 +562,9 @@ optional PN7150::find_tag_uid_(const nfc::NfcTagUid &uid) { } void PN7150::purge_old_tags_() { - for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { - if (millis() - this->discovered_endpoint_[i].last_seen > this->tag_ttl_) { - this->erase_tag_(i); + for (size_t i = this->discovered_endpoint_.size(); i > 0; i--) { + if (millis() - this->discovered_endpoint_[i - 1].last_seen > this->tag_ttl_) { + this->erase_tag_(i - 1); } } } diff --git a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp index 46f5dba2b7..854ddd1be1 100644 --- a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp +++ b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp @@ -100,7 +100,7 @@ uint8_t PN7150::find_mifare_ultralight_ndef_(const std::vector &page_3_ uint8_t &message_start_index) { const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector - if (!(page_3_to_6.size() > p4_offset + 5)) { + if (!(page_3_to_6.size() > p4_offset + 6)) { return nfc::STATUS_FAILED; } @@ -135,7 +135,7 @@ uint8_t PN7150::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::sha } else { encoded.insert(encoded.begin() + 1, 0xFF); encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); - encoded.insert(encoded.begin() + 2, message_length & 0xFF); + encoded.insert(encoded.begin() + 3, message_length & 0xFF); } encoded.push_back(0xFE); diff --git a/esphome/components/pn7160/pn7160.cpp b/esphome/components/pn7160/pn7160.cpp index 3fcd1221a7..5f0f8d0629 100644 --- a/esphome/components/pn7160/pn7160.cpp +++ b/esphome/components/pn7160/pn7160.cpp @@ -589,9 +589,9 @@ optional PN7160::find_tag_uid_(const nfc::NfcTagUid &uid) { } void PN7160::purge_old_tags_() { - for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { - if (millis() - this->discovered_endpoint_[i].last_seen > this->tag_ttl_) { - this->erase_tag_(i); + for (size_t i = this->discovered_endpoint_.size(); i > 0; i--) { + if (millis() - this->discovered_endpoint_[i - 1].last_seen > this->tag_ttl_) { + this->erase_tag_(i - 1); } } } diff --git a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp index 9dc8d3dd2d..8ca0fa2c11 100644 --- a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp +++ b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp @@ -100,7 +100,7 @@ uint8_t PN7160::find_mifare_ultralight_ndef_(const std::vector &page_3_ uint8_t &message_start_index) { const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector - if (!(page_3_to_6.size() > p4_offset + 5)) { + if (!(page_3_to_6.size() > p4_offset + 6)) { return nfc::STATUS_FAILED; } @@ -135,7 +135,7 @@ uint8_t PN7160::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::sha } else { encoded.insert(encoded.begin() + 1, 0xFF); encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); - encoded.insert(encoded.begin() + 2, message_length & 0xFF); + encoded.insert(encoded.begin() + 3, message_length & 0xFF); } encoded.push_back(0xFE); From c37ab1de841631dd1802a2abbfde655f49513e12 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:58:52 -0500 Subject: [PATCH 347/609] [fingerprint_grow] Fix OOB write and uint16 overflow (#14462) Co-authored-by: Claude Opus 4.6 --- .../fingerprint_grow/fingerprint_grow.cpp | 23 ++++++++++--------- .../fingerprint_grow/fingerprint_grow.h | 4 ++-- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index da4535fc82..a633fbca28 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -361,7 +361,7 @@ void FingerprintGrowComponent::aura_led_control(uint8_t state, uint8_t speed, ui } } -uint8_t FingerprintGrowComponent::transfer_(std::vector *p_data_buffer) { +uint8_t FingerprintGrowComponent::transfer_(std::vector &data_buffer) { while (this->available()) this->read(); this->write((uint8_t) (START_CODE >> 8)); @@ -372,12 +372,12 @@ uint8_t FingerprintGrowComponent::transfer_(std::vector *p_data_buffer) this->write(this->address_[3]); this->write(COMMAND); - uint16_t wire_length = p_data_buffer->size() + 2; + uint16_t wire_length = data_buffer.size() + 2; this->write((uint8_t) (wire_length >> 8)); this->write((uint8_t) (wire_length & 0xFF)); uint16_t sum = (wire_length >> 8) + (wire_length & 0xFF) + COMMAND; - for (auto data : *p_data_buffer) { + for (auto data : data_buffer) { this->write(data); sum += data; } @@ -385,7 +385,7 @@ uint8_t FingerprintGrowComponent::transfer_(std::vector *p_data_buffer) this->write((uint8_t) (sum >> 8)); this->write((uint8_t) (sum & 0xFF)); - p_data_buffer->clear(); + data_buffer.clear(); uint8_t byte; uint16_t idx = 0, length = 0; @@ -431,9 +431,9 @@ uint8_t FingerprintGrowComponent::transfer_(std::vector *p_data_buffer) length |= byte; break; default: - p_data_buffer->push_back(byte); + data_buffer.push_back(byte); if ((idx - 8) == length) { - switch ((*p_data_buffer)[0]) { + switch (data_buffer[0]) { case OK: case NO_FINGER: case IMAGE_FAIL: @@ -453,25 +453,26 @@ uint8_t FingerprintGrowComponent::transfer_(std::vector *p_data_buffer) ESP_LOGE(TAG, "Reader failed to process request"); break; default: - ESP_LOGE(TAG, "Unknown response received from reader: 0x%.2X", (*p_data_buffer)[0]); + ESP_LOGE(TAG, "Unknown response received from reader: 0x%.2X", data_buffer[0]); break; } this->last_transfer_ms_ = millis(); - return (*p_data_buffer)[0]; + return data_buffer[0]; } break; } idx++; } ESP_LOGE(TAG, "No response received from reader"); - (*p_data_buffer)[0] = TIMEOUT; + data_buffer.clear(); + data_buffer.push_back(TIMEOUT); this->last_transfer_ms_ = millis(); return TIMEOUT; } uint8_t FingerprintGrowComponent::send_command_() { this->sensor_wakeup_(); - return this->transfer_(&this->data_); + return this->transfer_(this->data_); } void FingerprintGrowComponent::sensor_wakeup_() { @@ -517,7 +518,7 @@ void FingerprintGrowComponent::sensor_wakeup_() { std::vector buffer = {VERIFY_PASSWORD, (uint8_t) (this->password_ >> 24), (uint8_t) (this->password_ >> 16), (uint8_t) (this->password_ >> 8), (uint8_t) (this->password_ & 0xFF)}; - if (this->transfer_(&buffer) != OK) { + if (this->transfer_(buffer) != OK) { ESP_LOGE(TAG, "Wrong password"); } } diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index 370b26f56a..db9d5ce564 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -169,7 +169,7 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic bool set_password_(); bool get_parameters_(); void get_fingerprint_count_(); - uint8_t transfer_(std::vector *p_data_buffer); + uint8_t transfer_(std::vector &data_buffer); uint8_t send_command_(); void sensor_wakeup_(); void sensor_sleep_(); @@ -190,7 +190,7 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic bool is_sensor_awake_ = false; uint32_t last_transfer_ms_ = 0; uint32_t last_aura_led_control_ = 0; - uint16_t last_aura_led_duration_ = 0; + uint32_t last_aura_led_duration_ = 0; uint16_t system_identifier_code_ = 0; uint32_t idle_period_to_sleep_ms_ = UINT32_MAX; sensor::Sensor *fingerprint_count_sensor_{nullptr}; From 22fc3aab392c8cad0c2d8a74e6074b83e7c17139 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:19:46 -0500 Subject: [PATCH 348/609] [ld2420] Fix buffer overflows in simple mode, energy mode, and calibration (#14458) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2420/ld2420.cpp | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index f14400d15a..1e671363c9 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -460,6 +460,10 @@ void LD2420Component::handle_energy_mode_(uint8_t *buffer, int len) { uint8_t index = 6; // Start at presence byte position uint16_t range; const uint8_t elements = sizeof(this->gate_energy_) / sizeof(this->gate_energy_[0]); + if (len < static_cast(index + 1 + sizeof(range) + elements * sizeof(this->gate_energy_[0]))) { + ESP_LOGW(TAG, "Energy frame too short: %d bytes", len); + return; + } this->set_presence_(buffer[index]); index++; memcpy(&range, &buffer[index], sizeof(range)); @@ -471,8 +475,11 @@ void LD2420Component::handle_energy_mode_(uint8_t *buffer, int len) { } if (this->current_operating_mode == OP_CALIBRATE_MODE) { - this->update_radar_data(gate_energy_, sample_number_counter); - this->sample_number_counter > CALIBRATE_SAMPLES ? this->sample_number_counter = 0 : this->sample_number_counter++; + this->update_radar_data(gate_energy_, this->sample_number_counter); + this->sample_number_counter++; + if (this->sample_number_counter >= CALIBRATE_SAMPLES) { + this->sample_number_counter = 0; + } } // Resonable refresh rate for home assistant database size health @@ -503,22 +510,20 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) { char *endptr{nullptr}; char outbuf[bufsize]{0}; while (true) { - if (inbuf[pos - 2] == 'O' && inbuf[pos - 1] == 'F' && inbuf[pos] == 'F') { + if (pos >= 2 && inbuf[pos - 2] == 'O' && inbuf[pos - 1] == 'F' && inbuf[pos] == 'F') { this->set_presence_(false); - } else if (inbuf[pos - 1] == 'O' && inbuf[pos] == 'N') { + } else if (pos >= 1 && inbuf[pos - 1] == 'O' && inbuf[pos] == 'N') { this->set_presence_(true); } if (inbuf[pos] >= '0' && inbuf[pos] <= '9') { if (index < bufsize - 1) { outbuf[index++] = inbuf[pos]; - pos++; } + } + if (pos < len - 1) { + pos++; } else { - if (pos < len - 1) { - pos++; - } else { - break; - } + break; } } outbuf[index] = '\0'; From 4928e678d1f54ee261789ab283280f317b425357 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 4 Mar 2026 13:37:22 -0600 Subject: [PATCH 349/609] [mixer][resampler][speaker] Use core static task manager (#14454) --- .../mixer/speaker/mixer_speaker.cpp | 91 +++------------ .../components/mixer/speaker/mixer_speaker.h | 18 +-- .../resampler/speaker/resampler_speaker.cpp | 60 +--------- .../resampler/speaker/resampler_speaker.h | 19 +-- .../speaker/media_player/audio_pipeline.cpp | 110 ++++-------------- .../speaker/media_player/audio_pipeline.h | 13 +-- 6 files changed, 49 insertions(+), 262 deletions(-) diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index 100acbebc3..8e1278206f 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -438,24 +438,14 @@ void MixerSpeaker::loop() { // Handle pending start request if (event_group_bits & MIXER_TASK_COMMAND_START) { // Only start the task if it's fully stopped and cleaned up - if (!this->status_has_error() && (this->task_handle_ == nullptr) && (this->task_stack_buffer_ == nullptr)) { - esp_err_t err = this->start_task_(); - switch (err) { - case ESP_OK: - xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_START); - break; - case ESP_ERR_NO_MEM: - ESP_LOGE(TAG, "Failed to start; retrying in 1 second"); - this->status_momentary_error("memory-failure", 1000); - return; - case ESP_ERR_INVALID_STATE: - ESP_LOGE(TAG, "Failed to start; retrying in 1 second"); - this->status_momentary_error("task-failure", 1000); - return; - default: - ESP_LOGE(TAG, "Failed to start; retrying in 1 second"); - this->status_momentary_error("failure", 1000); - return; + if (!this->status_has_error() && !this->task_.is_created()) { + if (this->task_.create(audio_mixer_task, "mixer", TASK_STACK_SIZE, (void *) this, MIXER_TASK_PRIORITY, + this->task_stack_in_psram_)) { + xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_START); + } else { + ESP_LOGE(TAG, "Failed to start; retrying in 1 second"); + this->status_momentary_error("failure", 1000); + return; } } } @@ -478,13 +468,12 @@ void MixerSpeaker::loop() { xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STOPPING); } if (event_group_bits & MIXER_TASK_STATE_STOPPED) { - if (this->delete_task_() == ESP_OK) { - ESP_LOGD(TAG, "Stopped"); - xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS); - } + this->task_.deallocate(); + ESP_LOGD(TAG, "Stopped"); + xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS); } - if (this->task_handle_ != nullptr) { + if (this->task_.is_created()) { // If the mixer task is running, check if all source speakers are stopped bool all_stopped = true; @@ -497,7 +486,7 @@ void MixerSpeaker::loop() { // Send stop command signal to the mixer task since no source speakers are active xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_STOP); } - } else if (this->task_stack_buffer_ == nullptr) { + } else { // Task is fully stopped and cleaned up, check if we can disable loop event_group_bits = xEventGroupGetBits(this->event_group_); if (event_group_bits == 0) { @@ -538,60 +527,6 @@ esp_err_t MixerSpeaker::start(audio::AudioStreamInfo &stream_info) { return ESP_OK; } -esp_err_t MixerSpeaker::start_task_() { - if (this->task_stack_buffer_ == nullptr) { - if (this->task_stack_in_psram_) { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); - this->task_stack_buffer_ = stack_allocator.allocate(TASK_STACK_SIZE); - } else { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); - this->task_stack_buffer_ = stack_allocator.allocate(TASK_STACK_SIZE); - } - } - - if (this->task_stack_buffer_ == nullptr) { - return ESP_ERR_NO_MEM; - } - - if (this->task_handle_ == nullptr) { - this->task_handle_ = xTaskCreateStatic(audio_mixer_task, "mixer", TASK_STACK_SIZE, (void *) this, - MIXER_TASK_PRIORITY, this->task_stack_buffer_, &this->task_stack_); - } - - if (this->task_handle_ == nullptr) { - return ESP_ERR_INVALID_STATE; - } - - return ESP_OK; -} - -esp_err_t MixerSpeaker::delete_task_() { - if (this->task_handle_ != nullptr) { - // Delete the task - vTaskDelete(this->task_handle_); - this->task_handle_ = nullptr; - } - - if ((this->task_handle_ == nullptr) && (this->task_stack_buffer_ != nullptr)) { - // Deallocate the task stack buffer - if (this->task_stack_in_psram_) { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); - stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); - } else { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); - stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); - } - - this->task_stack_buffer_ = nullptr; - } - - if ((this->task_handle_ != nullptr) || (this->task_stack_buffer_ != nullptr)) { - return ESP_ERR_INVALID_STATE; - } - - return ESP_OK; -} - void MixerSpeaker::copy_frames(const int16_t *input_buffer, audio::AudioStreamInfo input_stream_info, int16_t *output_buffer, audio::AudioStreamInfo output_stream_info, uint32_t frames_to_transfer) { diff --git a/esphome/components/mixer/speaker/mixer_speaker.h b/esphome/components/mixer/speaker/mixer_speaker.h index e920f9895a..0e0b33c39b 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.h +++ b/esphome/components/mixer/speaker/mixer_speaker.h @@ -8,8 +8,8 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/static_task.h" -#include #include #include @@ -143,8 +143,6 @@ class MixerSpeaker : public Component { /// @param stream_info The calling source speaker's audio stream information /// @return ESP_ERR_NOT_SUPPORTED if the incoming stream is incompatible due to unsupported bits per sample /// ESP_ERR_INVALID_ARG if the incoming stream is incompatible to be mixed with the other input audio stream - /// ESP_ERR_NO_MEM if there isn't enough memory for the task's stack - /// ESP_ERR_INVALID_STATE if the task fails to start /// ESP_OK if the incoming stream is compatible and the mixer task starts esp_err_t start(audio::AudioStreamInfo &stream_info); @@ -188,16 +186,6 @@ class MixerSpeaker : public Component { static void audio_mixer_task(void *params); - /// @brief Starts the mixer task after allocating memory for the task stack. - /// @return ESP_ERR_NO_MEM if there isn't enough memory for the task's stack - /// ESP_ERR_INVALID_STATE if the task didn't start - /// ESP_OK if successful - esp_err_t start_task_(); - - /// @brief If the task is stopped, it sets the task handle to the nullptr and deallocates its stack - /// @return ESP_OK if the task was stopped, ESP_ERR_INVALID_STATE otherwise. - esp_err_t delete_task_(); - EventGroupHandle_t event_group_{nullptr}; FixedVector source_speakers_; @@ -207,9 +195,7 @@ class MixerSpeaker : public Component { bool queue_mode_; bool task_stack_in_psram_{false}; - TaskHandle_t task_handle_{nullptr}; - StaticTask_t task_stack_; - StackType_t *task_stack_buffer_{nullptr}; + StaticTask task_; optional audio_stream_info_; diff --git a/esphome/components/resampler/speaker/resampler_speaker.cpp b/esphome/components/resampler/speaker/resampler_speaker.cpp index 74420f906a..1303bc459e 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.cpp +++ b/esphome/components/resampler/speaker/resampler_speaker.cpp @@ -147,7 +147,7 @@ void ResamplerSpeaker::loop() { xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STOPPING); } if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPED) { - this->delete_task_(); + this->task_.deallocate(); ESP_LOGD(TAG, "Stopped"); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ALL_BITS); } @@ -190,7 +190,7 @@ void ResamplerSpeaker::loop() { this->output_speaker_->stop(); } - if (this->output_speaker_->is_stopped() && (this->task_handle_ == nullptr)) { + if (this->output_speaker_->is_stopped() && !this->task_.is_created()) { // Only transition to stopped state once the output speaker and resampler task are fully stopped this->waiting_for_output_ = false; this->state_ = speaker::STATE_STOPPED; @@ -209,9 +209,6 @@ void ResamplerSpeaker::loop() { void ResamplerSpeaker::set_start_error_(esp_err_t err) { switch (err) { - case ESP_ERR_INVALID_STATE: - this->status_set_error(LOG_STR("Task failed to start")); - break; case ESP_ERR_NO_MEM: this->status_set_error(LOG_STR("Not enough memory")); break; @@ -267,36 +264,12 @@ esp_err_t ResamplerSpeaker::start_() { if (this->requires_resampling_()) { // Start the resampler task to handle converting sample rates - return this->start_task_(); - } - - return ESP_OK; -} - -esp_err_t ResamplerSpeaker::start_task_() { - if (this->task_stack_buffer_ == nullptr) { - if (this->task_stack_in_psram_) { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); - this->task_stack_buffer_ = stack_allocator.allocate(TASK_STACK_SIZE); - } else { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); - this->task_stack_buffer_ = stack_allocator.allocate(TASK_STACK_SIZE); + if (!this->task_.create(resample_task, "resampler", TASK_STACK_SIZE, (void *) this, RESAMPLER_TASK_PRIORITY, + this->task_stack_in_psram_)) { + return ESP_ERR_NO_MEM; } } - if (this->task_stack_buffer_ == nullptr) { - return ESP_ERR_NO_MEM; - } - - if (this->task_handle_ == nullptr) { - this->task_handle_ = xTaskCreateStatic(resample_task, "resampler", TASK_STACK_SIZE, (void *) this, - RESAMPLER_TASK_PRIORITY, this->task_stack_buffer_, &this->task_stack_); - } - - if (this->task_handle_ == nullptr) { - return ESP_ERR_INVALID_STATE; - } - return ESP_OK; } @@ -305,33 +278,12 @@ void ResamplerSpeaker::stop() { this->send_command_(ResamplingEventGroupBits::CO void ResamplerSpeaker::enter_stopping_state_() { this->state_ = speaker::STATE_STOPPING; this->state_start_ms_ = App.get_loop_component_start_time(); - if (this->task_handle_ != nullptr) { + if (this->task_.is_created()) { xEventGroupSetBits(this->event_group_, ResamplingEventGroupBits::TASK_COMMAND_STOP); } this->output_speaker_->stop(); } -void ResamplerSpeaker::delete_task_() { - if (this->task_handle_ != nullptr) { - // Delete the suspended task - vTaskDelete(this->task_handle_); - this->task_handle_ = nullptr; - } - - if (this->task_stack_buffer_ != nullptr) { - // Deallocate the task stack buffer - if (this->task_stack_in_psram_) { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); - stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); - } else { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); - stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); - } - - this->task_stack_buffer_ = nullptr; - } -} - void ResamplerSpeaker::finish() { this->send_command_(ResamplingEventGroupBits::COMMAND_FINISH); } bool ResamplerSpeaker::has_buffered_data() const { diff --git a/esphome/components/resampler/speaker/resampler_speaker.h b/esphome/components/resampler/speaker/resampler_speaker.h index c1ebd7e7b5..cdbc1c22db 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.h +++ b/esphome/components/resampler/speaker/resampler_speaker.h @@ -7,8 +7,8 @@ #include "esphome/components/speaker/speaker.h" #include "esphome/core/component.h" +#include "esphome/core/static_task.h" -#include #include namespace esphome { @@ -57,15 +57,9 @@ class ResamplerSpeaker : public Component, public speaker::Speaker { protected: /// @brief Starts the output speaker after setting the resampled stream info. If resampling is required, it starts the /// task. - /// @return ESP_OK if resampling is required - /// return value of start_task_() if resampling is required - esp_err_t start_(); - - /// @brief Starts the resampler task after allocating the task stack /// @return ESP_OK if successful, - /// ESP_ERR_NO_MEM if the task stack couldn't be allocated - /// ESP_ERR_INVALID_STATE if the task wasn't created - esp_err_t start_task_(); + /// ESP_ERR_NO_MEM if the resampler task couldn't be created + esp_err_t start_(); /// @brief Transitions to STATE_STOPPING, records the stopping timestamp, sends the task stop command if the task is /// running, and stops the output speaker. @@ -74,9 +68,6 @@ class ResamplerSpeaker : public Component, public speaker::Speaker { /// @brief Sets the appropriate status error based on the start failure reason. void set_start_error_(esp_err_t err); - /// @brief Deletes the resampler task if suspended, deallocates the task stack, and resets the related pointers. - void delete_task_(); - /// @brief Sends a command via event group bits, enables the loop, and optionally wakes the main loop. void send_command_(uint32_t command_bit, bool wake_loop = false); @@ -92,9 +83,7 @@ class ResamplerSpeaker : public Component, public speaker::Speaker { bool task_stack_in_psram_{false}; bool waiting_for_output_{false}; - TaskHandle_t task_handle_{nullptr}; - StaticTask_t task_stack_; - StackType_t *task_stack_buffer_{nullptr}; + StaticTask task_; audio::AudioStreamInfo target_stream_info_; diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index 177743feb1..8cea3abcfc 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -87,20 +87,20 @@ void AudioPipeline::set_pause_state(bool pause_state) { } void AudioPipeline::suspend_tasks() { - if (this->read_task_handle_ != nullptr) { - vTaskSuspend(this->read_task_handle_); + if (this->read_task_.is_created()) { + vTaskSuspend(this->read_task_.get_handle()); } - if (this->decode_task_handle_ != nullptr) { - vTaskSuspend(this->decode_task_handle_); + if (this->decode_task_.is_created()) { + vTaskSuspend(this->decode_task_.get_handle()); } } void AudioPipeline::resume_tasks() { - if (this->read_task_handle_ != nullptr) { - vTaskResume(this->read_task_handle_); + if (this->read_task_.is_created()) { + vTaskResume(this->read_task_.get_handle()); } - if (this->decode_task_handle_ != nullptr) { - vTaskResume(this->decode_task_handle_); + if (this->decode_task_.is_created()) { + vTaskResume(this->decode_task_.get_handle()); } } @@ -159,7 +159,7 @@ AudioPipelineState AudioPipeline::process_state() { // Init command pending if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) { // Only start if there is no pending stop command - if ((this->read_task_handle_ == nullptr) || (this->decode_task_handle_ == nullptr)) { + if (!this->read_task_.is_created() || !this->decode_task_.is_created()) { // At least one task isn't running this->start_tasks_(); } @@ -202,8 +202,9 @@ AudioPipelineState AudioPipeline::process_state() { if (!this->is_playing_) { // The tasks have been stopped for two ``process_state`` calls in a row, so delete the tasks - if ((this->read_task_handle_ != nullptr) || (this->decode_task_handle_ != nullptr)) { - this->delete_tasks_(); + if (this->read_task_.is_created() || this->decode_task_.is_created()) { + this->read_task_.deallocate(); + this->decode_task_.deallocate(); if (this->hard_stop_) { // Stop command was sent, so immediately end the playback this->speaker_->stop(); @@ -234,7 +235,7 @@ AudioPipelineState AudioPipeline::process_state() { } } - if ((this->read_task_handle_ == nullptr) && (this->decode_task_handle_ == nullptr)) { + if (!this->read_task_.is_created() && !this->decode_task_.is_created()) { // No tasks are running, so the pipeline is stopped. xEventGroupClearBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP); return AudioPipelineState::STOPPED; @@ -262,94 +263,25 @@ esp_err_t AudioPipeline::allocate_communications_() { } esp_err_t AudioPipeline::start_tasks_() { - if (this->read_task_handle_ == nullptr) { - if (this->read_task_stack_buffer_ == nullptr) { - // Reader task uses the AudioReader class which uses esp_http_client. This crashes on IDF 5.4 if the task stack is - // in PSRAM. As a workaround, always allocate the read task in internal memory. - RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); - this->read_task_stack_buffer_ = stack_allocator.allocate(READ_TASK_STACK_SIZE); - } - - if (this->read_task_stack_buffer_ == nullptr) { + if (!this->read_task_.is_created()) { + // Reader task uses the AudioReader class which uses esp_http_client. This crashes on IDF 5.4 if the task stack is + // in PSRAM. As a workaround, always allocate the read task in internal memory. + if (!this->read_task_.create(read_task, (this->base_name_ + "_read").c_str(), READ_TASK_STACK_SIZE, (void *) this, + this->priority_, false)) { return ESP_ERR_NO_MEM; } - - if (this->read_task_handle_ == nullptr) { - this->read_task_handle_ = - xTaskCreateStatic(read_task, (this->base_name_ + "_read").c_str(), READ_TASK_STACK_SIZE, (void *) this, - this->priority_, this->read_task_stack_buffer_, &this->read_task_stack_); - } - - if (this->read_task_handle_ == nullptr) { - return ESP_ERR_INVALID_STATE; - } } - if (this->decode_task_handle_ == nullptr) { - if (this->decode_task_stack_buffer_ == nullptr) { - if (this->task_stack_in_psram_) { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); - this->decode_task_stack_buffer_ = stack_allocator.allocate(DECODE_TASK_STACK_SIZE); - } else { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); - this->decode_task_stack_buffer_ = stack_allocator.allocate(DECODE_TASK_STACK_SIZE); - } - } - - if (this->decode_task_stack_buffer_ == nullptr) { + if (!this->decode_task_.is_created()) { + if (!this->decode_task_.create(decode_task, (this->base_name_ + "_decode").c_str(), DECODE_TASK_STACK_SIZE, + (void *) this, this->priority_, this->task_stack_in_psram_)) { return ESP_ERR_NO_MEM; } - - if (this->decode_task_handle_ == nullptr) { - this->decode_task_handle_ = - xTaskCreateStatic(decode_task, (this->base_name_ + "_decode").c_str(), DECODE_TASK_STACK_SIZE, (void *) this, - this->priority_, this->decode_task_stack_buffer_, &this->decode_task_stack_); - } - - if (this->decode_task_handle_ == nullptr) { - return ESP_ERR_INVALID_STATE; - } } return ESP_OK; } -void AudioPipeline::delete_tasks_() { - if (this->read_task_handle_ != nullptr) { - vTaskDelete(this->read_task_handle_); - - if (this->read_task_stack_buffer_ != nullptr) { - if (this->task_stack_in_psram_) { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); - stack_allocator.deallocate(this->read_task_stack_buffer_, READ_TASK_STACK_SIZE); - } else { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); - stack_allocator.deallocate(this->read_task_stack_buffer_, READ_TASK_STACK_SIZE); - } - - this->read_task_stack_buffer_ = nullptr; - this->read_task_handle_ = nullptr; - } - } - - if (this->decode_task_handle_ != nullptr) { - vTaskDelete(this->decode_task_handle_); - - if (this->decode_task_stack_buffer_ != nullptr) { - if (this->task_stack_in_psram_) { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); - stack_allocator.deallocate(this->decode_task_stack_buffer_, DECODE_TASK_STACK_SIZE); - } else { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); - stack_allocator.deallocate(this->decode_task_stack_buffer_, DECODE_TASK_STACK_SIZE); - } - - this->decode_task_stack_buffer_ = nullptr; - this->decode_task_handle_ = nullptr; - } - } -} - void AudioPipeline::read_task(void *params) { AudioPipeline *this_pipeline = (AudioPipeline *) params; diff --git a/esphome/components/speaker/media_player/audio_pipeline.h b/esphome/components/speaker/media_player/audio_pipeline.h index 6fffde6c20..2c78572835 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.h +++ b/esphome/components/speaker/media_player/audio_pipeline.h @@ -8,10 +8,10 @@ #include "esphome/components/speaker/speaker.h" #include "esphome/core/ring_buffer.h" +#include "esphome/core/static_task.h" #include "esp_err.h" -#include #include #include @@ -104,9 +104,6 @@ class AudioPipeline { /// @return ESP_OK if successful or an appropriate error if not esp_err_t start_tasks_(); - /// @brief Resets the task related pointers and deallocates their stacks. - void delete_tasks_(); - std::string base_name_; UBaseType_t priority_; @@ -143,15 +140,11 @@ class AudioPipeline { // Handles reading the media file from flash or a url static void read_task(void *params); - TaskHandle_t read_task_handle_{nullptr}; - StaticTask_t read_task_stack_; - StackType_t *read_task_stack_buffer_{nullptr}; + StaticTask read_task_; // Decodes the media file into PCM audio static void decode_task(void *params); - TaskHandle_t decode_task_handle_{nullptr}; - StaticTask_t decode_task_stack_; - StackType_t *decode_task_stack_buffer_{nullptr}; + StaticTask decode_task_; }; } // namespace speaker From 0c883b80c4376906eb5dc67f4b77d1249ee67b1d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:05:49 -0500 Subject: [PATCH 350/609] [inkplate][ezo_pmp][ezo][packet_transport] Fix use-after-free bugs (#14467) Co-authored-by: Claude Opus 4.6 --- esphome/components/ezo/ezo.cpp | 3 +- esphome/components/ezo_pmp/ezo_pmp.cpp | 36 ++++++++++--------- esphome/components/ezo_pmp/ezo_pmp.h | 2 +- esphome/components/inkplate/inkplate.cpp | 20 ++++++++--- .../packet_transport/packet_transport.cpp | 2 +- .../packet_transport/packet_transport.h | 2 +- 6 files changed, 39 insertions(+), 26 deletions(-) diff --git a/esphome/components/ezo/ezo.cpp b/esphome/components/ezo/ezo.cpp index e4036021df..2dc65b7d14 100644 --- a/esphome/components/ezo/ezo.cpp +++ b/esphome/components/ezo/ezo.cpp @@ -66,8 +66,9 @@ void EZOSensor::loop() { if (to_run->command_type == EzoCommandType::EZO_SLEEP || to_run->command_type == EzoCommandType::EZO_I2C) { // Commands with no return data + bool update_address = to_run->command_type == EzoCommandType::EZO_I2C; this->commands_.pop_front(); - if (to_run->command_type == EzoCommandType::EZO_I2C) + if (update_address) this->address_ = this->new_address_; return; } diff --git a/esphome/components/ezo_pmp/ezo_pmp.cpp b/esphome/components/ezo_pmp/ezo_pmp.cpp index bf6e3926b8..4ce4da57ff 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.cpp +++ b/esphome/components/ezo_pmp/ezo_pmp.cpp @@ -165,22 +165,23 @@ void EzoPMP::read_command_result_() { continue; } - switch (current_parameter) { - case 1: - first_parameter_buffer[position_in_parameter_buffer] = current_char; - first_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; - break; - case 2: - second_parameter_buffer[position_in_parameter_buffer] = current_char; - second_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; - break; - case 3: - third_parameter_buffer[position_in_parameter_buffer] = current_char; - third_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; - break; + if (position_in_parameter_buffer < sizeof(first_parameter_buffer) - 1) { + switch (current_parameter) { + case 1: + first_parameter_buffer[position_in_parameter_buffer] = current_char; + first_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; + break; + case 2: + second_parameter_buffer[position_in_parameter_buffer] = current_char; + second_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; + break; + case 3: + third_parameter_buffer[position_in_parameter_buffer] = current_char; + third_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; + break; + } + position_in_parameter_buffer++; } - - position_in_parameter_buffer++; } auto parsed_first_parameter = parse_number(first_parameter_buffer); @@ -404,7 +405,8 @@ void EzoPMP::send_next_command_() { break; case EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS: // Run an arbitrary command - command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "%s", this->arbitrary_command_); + command_buffer_length = + snprintf((char *) command_buffer, sizeof(command_buffer), "%s", this->arbitrary_command_.c_str()); ESP_LOGI(TAG, "Sending arbitrary command: %s", (char *) command_buffer); break; @@ -541,7 +543,7 @@ void EzoPMP::change_i2c_address(int address) { } void EzoPMP::exec_arbitrary_command(const std::basic_string &command) { - this->arbitrary_command_ = command.c_str(); + this->arbitrary_command_ = command; this->queue_command_(EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS, 0, 0, true); } diff --git a/esphome/components/ezo_pmp/ezo_pmp.h b/esphome/components/ezo_pmp/ezo_pmp.h index d4917e7f4b..bbfd899170 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.h +++ b/esphome/components/ezo_pmp/ezo_pmp.h @@ -85,7 +85,7 @@ class EzoPMP : public PollingComponent, public i2c::I2CDevice { bool is_paused_flag_ = false; bool is_dosing_flag_ = false; - const char *arbitrary_command_{nullptr}; + std::string arbitrary_command_{}; void send_next_command_(); void read_command_result_(); diff --git a/esphome/components/inkplate/inkplate.cpp b/esphome/components/inkplate/inkplate.cpp index c921c643fa..df9c2b29c7 100644 --- a/esphome/components/inkplate/inkplate.cpp +++ b/esphome/components/inkplate/inkplate.cpp @@ -63,16 +63,26 @@ void Inkplate::initialize_() { if (buffer_size == 0) return; - if (this->partial_buffer_ != nullptr) + if (this->partial_buffer_ != nullptr) { allocator.deallocate(this->partial_buffer_, buffer_size); - if (this->partial_buffer_2_ != nullptr) + this->partial_buffer_ = nullptr; + } + if (this->partial_buffer_2_ != nullptr) { allocator.deallocate(this->partial_buffer_2_, buffer_size * 2); - if (this->buffer_ != nullptr) + this->partial_buffer_2_ = nullptr; + } + if (this->buffer_ != nullptr) { allocator.deallocate(this->buffer_, buffer_size); - if (this->glut_ != nullptr) + this->buffer_ = nullptr; + } + if (this->glut_ != nullptr) { allocator32.deallocate(this->glut_, 256 * 9); - if (this->glut2_ != nullptr) + this->glut_ = nullptr; + } + if (this->glut2_ != nullptr) { allocator32.deallocate(this->glut2_, 256 * 9); + this->glut2_ = nullptr; + } this->buffer_ = allocator.allocate(buffer_size); if (this->buffer_ == nullptr) { diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 7b7a852398..d2c5920001 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -249,7 +249,7 @@ void PacketTransport::init_data_() { } else { add(this->data_, DATA_KEY); } - for (auto pkey : this->ping_keys_) { + for (const auto &pkey : this->ping_keys_) { add(this->data_, PING_KEY); add(this->data_, pkey.second); } diff --git a/esphome/components/packet_transport/packet_transport.h b/esphome/components/packet_transport/packet_transport.h index 57f40874b5..a236744231 100644 --- a/esphome/components/packet_transport/packet_transport.h +++ b/esphome/components/packet_transport/packet_transport.h @@ -150,7 +150,7 @@ class PacketTransport : public PollingComponent { std::vector ping_header_{}; std::vector header_{}; std::vector data_{}; - std::map ping_keys_{}; + std::map ping_keys_{}; const char *platform_name_{""}; void add_key_(const char *name, uint32_t key); void send_ping_pong_request_(); From e11a91411b800497b812a9baf374fca36af5e085 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:36:52 -0500 Subject: [PATCH 351/609] [esp32_improv][rf_bridge][esp32_ble_server][display][lvgl][pipsolar] Fix unsigned integer underflows (#14466) Co-authored-by: Claude Opus 4.6 Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/display/display.cpp | 3 +++ esphome/components/esp32_ble_server/ble_characteristic.cpp | 6 +++++- esphome/components/esp32_improv/esp32_improv_component.cpp | 2 ++ esphome/components/lvgl/lvgl_esphome.cpp | 2 +- esphome/components/pipsolar/pipsolar.cpp | 4 +++- esphome/components/rf_bridge/rf_bridge.cpp | 2 +- 6 files changed, 15 insertions(+), 4 deletions(-) diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 2bd7d03600..f8569b6e7c 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -661,6 +661,9 @@ void Display::printf(int x, int y, BaseFont *font, const char *format, ...) { void Display::set_writer(display_writer_t &&writer) { this->writer_ = writer; } void Display::set_pages(std::vector pages) { + if (pages.empty()) + return; + for (auto *page : pages) page->set_parent(this); diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index a1b1ff94bb..d4ccefd9b2 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -209,7 +209,11 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt esp_gatt_rsp_t response; if (param->read.is_long) { - if (this->value_.size() - this->value_read_offset_ < max_offset) { + if (this->value_read_offset_ >= this->value_.size()) { + response.attr_value.len = 0; + response.attr_value.offset = this->value_read_offset_; + this->value_read_offset_ = 0; + } else if (this->value_.size() - this->value_read_offset_ < max_offset) { // Last message in the chain response.attr_value.len = this->value_.size() - this->value_read_offset_; response.attr_value.offset = this->value_read_offset_; diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 83bc842a3d..e4ae49f235 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -314,6 +314,8 @@ void ESP32ImprovComponent::dump_config() { } void ESP32ImprovComponent::process_incoming_data_() { + if (this->incoming_data_.size() < 3) + return; uint8_t length = this->incoming_data_[1]; #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index bb373abb88..3e447e9169 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -421,7 +421,7 @@ void LvglComponent::write_random_() { col = col / this->draw_rounding * this->draw_rounding; auto row = random_uint32() % this->disp_drv_.ver_res; row = row / this->draw_rounding * this->draw_rounding; - auto size = (random_uint32() % 32) / this->draw_rounding * this->draw_rounding - 1; + auto size = ((random_uint32() % 32) / this->draw_rounding + 2) * this->draw_rounding - 1; lv_area_t area; area.x1 = col; area.y1 = row; diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index f95bf4aedb..9c5caec775 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -162,13 +162,15 @@ void Pipsolar::loop() { } uint8_t Pipsolar::check_incoming_length_(uint8_t length) { - if (this->read_pos_ - 3 == length) { + if (this->read_pos_ >= 3 && this->read_pos_ - 3 == length) { return 1; } return 0; } uint8_t Pipsolar::check_incoming_crc_() { + if (this->read_pos_ < 3) + return 0; uint16_t crc16; crc16 = this->pipsolar_crc_(read_buffer_, read_pos_ - 3); if (((uint8_t) ((crc16) >> 8)) == read_buffer_[read_pos_ - 3] && diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index d8c148145c..700e2ba162 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -74,7 +74,7 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { data.length = raw[2]; data.protocol = raw[3]; char next_byte[3]; // 2 hex chars + null - for (uint8_t i = 0; i < data.length - 1; i++) { + for (uint8_t i = 0; i + 1 < data.length; i++) { buf_append_printf(next_byte, sizeof(next_byte), 0, "%02X", raw[4 + i]); data.code += next_byte; } From 61ea6c3b2f759c325c1428b371e4e9f8a00425fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Mar 2026 12:46:26 -1000 Subject: [PATCH 352/609] [ci] Add missing issues: write permission to codeowner approval workflow (#14477) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/codeowner-approved-label.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/codeowner-approved-label.yml b/.github/workflows/codeowner-approved-label.yml index 217ae06419..200f18f544 100644 --- a/.github/workflows/codeowner-approved-label.yml +++ b/.github/workflows/codeowner-approved-label.yml @@ -12,7 +12,8 @@ on: types: [submitted, dismissed] permissions: - pull-requests: write + issues: write + pull-requests: read contents: read jobs: From 55103c0652bdece270ac09e9f3224c2693d7e2d2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:18:14 -0500 Subject: [PATCH 353/609] [ds2484] Fix read64() using uint8_t accumulator instead of uint64_t (#14479) Co-authored-by: Claude Opus 4.6 --- esphome/components/ds2484/ds2484.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/ds2484/ds2484.cpp b/esphome/components/ds2484/ds2484.cpp index 7c890ff433..0b36f86874 100644 --- a/esphome/components/ds2484/ds2484.cpp +++ b/esphome/components/ds2484/ds2484.cpp @@ -110,9 +110,9 @@ uint8_t DS2484OneWireBus::read8() { } uint64_t DS2484OneWireBus::read64() { - uint8_t response = 0; + uint64_t response = 0; for (uint8_t i = 0; i < 8; i++) { - response |= (this->read8() << (i * 8)); + response |= (static_cast(this->read8()) << (i * 8)); } return response; } From b6d7e8e14de939b4d6714a42138edb914fd346c4 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:18:28 -0500 Subject: [PATCH 354/609] [sgp30] Fix serial number truncation from 48-bit to 24-bit (#14478) Co-authored-by: Claude Opus 4.6 --- esphome/components/sgp30/sgp30.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index 18814405d4..35e5b3dd42 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -41,7 +41,9 @@ void SGP30Component::setup() { this->mark_failed(); return; } - this->serial_number_ = encode_uint24(raw_serial_number[0], raw_serial_number[1], raw_serial_number[2]); + this->serial_number_ = (static_cast(raw_serial_number[0]) << 32) | + (static_cast(raw_serial_number[1]) << 16) | + static_cast(raw_serial_number[2]); ESP_LOGD(TAG, "Serial number: %" PRIu64, this->serial_number_); // Featureset identification for future use From c8e7f78a2567e51d4fa178799ad3351ce046272d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:32:50 -0500 Subject: [PATCH 355/609] [zwave_proxy] Fix uint8_t overflow for buffer index and frame end (#14480) Co-authored-by: Claude Opus 4.6 --- esphome/components/zwave_proxy/zwave_proxy.cpp | 4 ++++ esphome/components/zwave_proxy/zwave_proxy.h | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index 8506b19e7f..b0836ac072 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -281,6 +281,10 @@ bool ZWaveProxy::parse_byte_(uint8_t byte) { break; } case ZWAVE_PARSING_STATE_READ_BL_MENU: + if (this->buffer_index_ >= this->buffer_.size()) { + this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START; + break; + } this->buffer_[this->buffer_index_++] = byte; if (!byte) { this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START; diff --git a/esphome/components/zwave_proxy/zwave_proxy.h b/esphome/components/zwave_proxy/zwave_proxy.h index eb26316f49..12cb9a90a1 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.h +++ b/esphome/components/zwave_proxy/zwave_proxy.h @@ -81,10 +81,10 @@ class ZWaveProxy : public uart::UARTDevice, public Component { api::APIConnection *api_connection_{nullptr}; // Current subscribed client uint32_t setup_time_{0}; // Time when setup() was called - // 8-bit values (grouped together to minimize padding) - uint8_t buffer_index_{0}; // Index for populating the data buffer - uint8_t end_frame_after_{0}; // Payload reception ends after this index - uint8_t last_response_{0}; // Last response type sent + // Small values (grouped by size to minimize padding) + uint16_t buffer_index_{0}; // Index for populating the data buffer + uint16_t end_frame_after_{0}; // Payload reception ends after this index + uint8_t last_response_{0}; // Last response type sent ZWaveParsingState parsing_state_{ZWAVE_PARSING_STATE_WAIT_START}; bool in_bootloader_{false}; // True if the device is detected to be in bootloader mode bool home_id_ready_{false}; // True when home ID has been received from Z-Wave module From c0143ac6d662967a8a3fbb008242ee4bc5f92a1f Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:46:40 -0600 Subject: [PATCH 356/609] [ai] Add docs note about keeping component index pages in sync (#14465) Co-authored-by: Brandon Harvey Co-authored-by: J. Nick Koston --- .ai/instructions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.ai/instructions.md b/.ai/instructions.md index 3c24177827..240a47a52f 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -286,6 +286,7 @@ This document provides essential context for AI models interacting with this pro * **Documentation Contributions:** * Documentation is hosted in the separate `esphome/esphome-docs` repository. * The contribution workflow is the same as for the codebase. + * When editing a component's documentation page, also update the corresponding component index page to ensure both pages remain in sync. * **Best Practices:** * **Component Development:** Keep dependencies minimal, provide clear error messages, and write comprehensive docstrings and tests. From 5df4fd0a271251d0dc29e0b76148b50550cfb582 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Mar 2026 15:51:51 -1000 Subject: [PATCH 357/609] [tests] Fix flaky uart_mock integration tests (#14476) --- .../external_components/uart_mock/__init__.py | 5 + .../uart_mock/uart_mock.cpp | 37 +- .../external_components/uart_mock/uart_mock.h | 4 + .../fixtures/uart_mock_ld2410.yaml | 8 + .../uart_mock_ld2410_engineering.yaml | 8 + .../fixtures/uart_mock_ld2412.yaml | 8 + .../uart_mock_ld2412_engineering.yaml | 8 + .../fixtures/uart_mock_modbus.yaml | 8 + .../fixtures/uart_mock_modbus_timing.yaml | 8 + tests/integration/state_utils.py | 96 ++++- tests/integration/test_uart_mock_ld2410.py | 353 ++++++----------- tests/integration/test_uart_mock_ld2412.py | 362 ++++++------------ tests/integration/test_uart_mock_modbus.py | 14 +- 13 files changed, 420 insertions(+), 499 deletions(-) diff --git a/tests/integration/fixtures/external_components/uart_mock/__init__.py b/tests/integration/fixtures/external_components/uart_mock/__init__.py index 8deab4c21e..abb3abcc41 100644 --- a/tests/integration/fixtures/external_components/uart_mock/__init__.py +++ b/tests/integration/fixtures/external_components/uart_mock/__init__.py @@ -43,6 +43,7 @@ CONF_INJECT_RX = "inject_rx" CONF_EXPECT_TX = "expect_tx" CONF_PERIODIC_RX = "periodic_rx" CONF_ON_TX = "on_tx" +CONF_AUTO_START = "auto_start" UART_PARITY_OPTIONS = { "NONE": uart.UARTParityOptions.UART_CONFIG_PARITY_NONE, @@ -95,6 +96,7 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_INJECTIONS, default=[]): cv.ensure_list(INJECTION_SCHEMA), cv.Optional(CONF_RESPONSES, default=[]): cv.ensure_list(RESPONSE_SCHEMA), cv.Optional(CONF_PERIODIC_RX, default=[]): cv.ensure_list(PERIODIC_RX_SCHEMA), + cv.Optional(CONF_AUTO_START, default=True): cv.boolean, cv.Optional(CONF_ON_TX): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MockUartTXTrigger), @@ -138,6 +140,9 @@ async def to_code(config): cg.add(var.set_data_bits(config[CONF_DATA_BITS])) cg.add(var.set_parity(config[CONF_PARITY])) + if not config[CONF_AUTO_START]: + cg.add(var.set_auto_start(False)) + for injection in config[CONF_INJECTIONS]: rx_data = injection[CONF_INJECT_RX] delay_ms = injection[CONF_DELAY] diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp index a4a1c41234..83a13793be 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -16,17 +16,21 @@ void MockUartComponent::setup() { } void MockUartComponent::loop() { - uint32_t now = App.get_loop_component_start_time(); - - // Initialize scenario start time on first loop() call, after all components have - // finished setup(). This prevents injection delays from being consumed during setup. if (!this->loop_started_) { this->loop_started_ = true; - this->scenario_start_ms_ = now; - this->cumulative_delay_ms_ = 0; - ESP_LOGD(TAG, "Scenario started at %u ms", now); + if (this->auto_start_) { + this->start_scenario(); + } else { + ESP_LOGD(TAG, "Scenario waiting for manual start"); + } } + if (!this->scenario_active_) { + return; + } + + uint32_t now = App.get_loop_component_start_time(); + // Process at most ONE timed injection per loop iteration. // This ensures each injection is in a separate loop cycle, giving the consuming // component (e.g., LD2410) a chance to process each batch independently. @@ -50,6 +54,19 @@ void MockUartComponent::loop() { } } +void MockUartComponent::start_scenario() { + uint32_t now = App.get_loop_component_start_time(); + this->scenario_active_ = true; + this->scenario_start_ms_ = now; + this->cumulative_delay_ms_ = 0; + this->injection_index_ = 0; + this->tx_buffer_.clear(); + for (auto &periodic : this->periodic_rx_) { + periodic.last_inject_ms = now; + } + ESP_LOGD(TAG, "Scenario started at %u ms", now); +} + void MockUartComponent::dump_config() { ESP_LOGCONFIG(TAG, "Mock UART Component:\n" @@ -78,10 +95,12 @@ void MockUartComponent::write_array(const uint8_t *data, size_t len) { } #endif - this->try_match_response_(); + if (this->scenario_active_) { + this->try_match_response_(); + } // This directly calls a tx_hook (lambda) as an alternative to the simpler match_response mechanism. - if (this->tx_hook_) { + if (this->tx_hook_ && this->scenario_active_) { std::vector buf(data, data + len); this->tx_hook_(buf); } diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h index 5bbc3c1bf6..b721512f96 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h @@ -37,6 +37,8 @@ class MockUartComponent : public uart::UARTComponent, public Component { void add_response(const std::vector &expect_tx, const std::vector &inject_rx); void add_periodic_rx(const std::vector &data, uint32_t interval_ms); + void start_scenario(); + void set_auto_start(bool auto_start) { this->auto_start_ = auto_start; } void set_tx_hook(std::function &)> &&cb) { this->tx_hook_ = std::move(cb); } void inject_to_rx_buffer(const std::vector &data); void inject_to_rx_buffer(const uint8_t *data, size_t len); @@ -55,6 +57,8 @@ class MockUartComponent : public uart::UARTComponent, public Component { uint32_t scenario_start_ms_{0}; uint32_t cumulative_delay_ms_{0}; bool loop_started_{false}; + bool auto_start_{true}; + bool scenario_active_{false}; // TX-triggered responses struct Response { diff --git a/tests/integration/fixtures/uart_mock_ld2410.yaml b/tests/integration/fixtures/uart_mock_ld2410.yaml index 9a81468263..59838b0599 100644 --- a/tests/integration/fixtures/uart_mock_ld2410.yaml +++ b/tests/integration/fixtures/uart_mock_ld2410.yaml @@ -20,6 +20,7 @@ uart: uart_mock: id: mock_uart baud_rate: 256000 + auto_start: false injections: # Phase 1 (t=100ms): Valid LD2410 normal mode data frame - happy path # The buffer is clean at this point, so this frame should parse correctly. @@ -143,3 +144,10 @@ binary_sensor: name: "Has Moving Target" has_still_target: name: "Has Still Target" + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: 'id(mock_uart).start_scenario();' diff --git a/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml b/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml index 3b730fc1f8..4625ae8511 100644 --- a/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml +++ b/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml @@ -19,6 +19,7 @@ uart: uart_mock: id: mock_uart baud_rate: 256000 + auto_start: false injections: # Phase 1 (t=100ms): Valid LD2410 engineering mode data frame # Captured from a real Screek Human Presence Sensor 1U with LD2410 firmware 2.4.x @@ -154,3 +155,10 @@ binary_sensor: name: "Has Still Target" out_pin_presence_status: name: "Out Pin Presence" + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: 'id(mock_uart).start_scenario();' diff --git a/tests/integration/fixtures/uart_mock_ld2412.yaml b/tests/integration/fixtures/uart_mock_ld2412.yaml index a502f36a25..9cf9d6bb87 100644 --- a/tests/integration/fixtures/uart_mock_ld2412.yaml +++ b/tests/integration/fixtures/uart_mock_ld2412.yaml @@ -20,6 +20,7 @@ uart: uart_mock: id: mock_uart baud_rate: 256000 + auto_start: false injections: # Phase 1 (t=100ms): Valid LD2412 normal mode data frame - happy path # The buffer is clean at this point, so this frame should parse correctly. @@ -169,3 +170,10 @@ binary_sensor: name: "Has Still Target" filters: - settle: 50ms + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: 'id(mock_uart).start_scenario();' diff --git a/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml b/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml index 3c669fc9a9..103dbed132 100644 --- a/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml +++ b/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml @@ -19,6 +19,7 @@ uart: uart_mock: id: mock_uart baud_rate: 256000 + auto_start: false injections: # Phase 1 (t=100ms): Valid LD2412 engineering mode data frame # @@ -211,3 +212,10 @@ binary_sensor: name: "Has Still Target" filters: - settle: 50ms + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: 'id(mock_uart).start_scenario();' diff --git a/tests/integration/fixtures/uart_mock_modbus.yaml b/tests/integration/fixtures/uart_mock_modbus.yaml index 89b9b91861..0a3492a0d2 100644 --- a/tests/integration/fixtures/uart_mock_modbus.yaml +++ b/tests/integration/fixtures/uart_mock_modbus.yaml @@ -22,6 +22,7 @@ uart_mock: baud_rate: 9600 rx_full_threshold: 120 rx_timeout: 2 + auto_start: false debug: responses: - expect_tx: [0x01, 0x03, 0x00, 0x03, 0x00, 0x01, 0x74, 0x0A] # Read holding register 1 on device 1 @@ -38,3 +39,10 @@ sensor: name: "basic_register" address: 0x03 register_type: holding + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: 'id(virtual_uart_dev).start_scenario();' diff --git a/tests/integration/fixtures/uart_mock_modbus_timing.yaml b/tests/integration/fixtures/uart_mock_modbus_timing.yaml index cd485ca394..c4e29e5fe8 100644 --- a/tests/integration/fixtures/uart_mock_modbus_timing.yaml +++ b/tests/integration/fixtures/uart_mock_modbus_timing.yaml @@ -22,6 +22,7 @@ uart_mock: baud_rate: 9600 rx_full_threshold: 120 rx_timeout: 2 + auto_start: false debug: on_tx: - then: @@ -52,3 +53,10 @@ sensor: phase_a: voltage: name: sdm_voltage + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: 'id(virtual_uart_dev).start_scenario();' diff --git a/tests/integration/state_utils.py b/tests/integration/state_utils.py index b649056f2b..e8c2cc5e66 100644 --- a/tests/integration/state_utils.py +++ b/tests/integration/state_utils.py @@ -3,10 +3,17 @@ from __future__ import annotations import asyncio +from collections.abc import Callable import logging from typing import TypeVar -from aioesphomeapi import ButtonInfo, EntityInfo, EntityState +from aioesphomeapi import ( + BinarySensorState, + ButtonInfo, + EntityInfo, + EntityState, + SensorState, +) _LOGGER = logging.getLogger(__name__) @@ -234,3 +241,90 @@ class InitialStateHelper: asyncio.TimeoutError: If initial states aren't received within timeout """ await asyncio.wait_for(self._initial_states_received, timeout=timeout) + + +class SensorStateCollector: + """Collects sensor and binary sensor state updates and provides wait helpers. + + Usage: + collector = SensorStateCollector( + sensor_names=["moving_distance", "still_distance"], + binary_sensor_names=["has_target"], + ) + # Use collector.on_state as the callback (or wrap it) + client.subscribe_states(helper.on_state_wrapper(collector.on_state)) + + # Wait for all sensors to have at least one value + await collector.wait_for_all(timeout=3.0) + + # Access collected states + assert collector.sensor_states["moving_distance"][0] == approx(100.0) + """ + + def __init__( + self, + sensor_names: list[str], + binary_sensor_names: list[str] | None = None, + entities: list[EntityInfo] | None = None, + ) -> None: + self.sensor_states: dict[str, list[float]] = {name: [] for name in sensor_names} + self.binary_states: dict[str, list[bool]] = { + name: [] for name in (binary_sensor_names or []) + } + self._key_to_sensor: dict[int, str] = {} + self._waiters: list[tuple[Callable[[], bool], asyncio.Future[bool]]] = [] + + if entities is not None: + self.build_key_mapping(entities) + + def build_key_mapping(self, entities: list[EntityInfo]) -> None: + """Build key-to-name mapping from entities. Sorted by descending length.""" + all_names = list(self.sensor_states.keys()) + list(self.binary_states.keys()) + all_names.sort(key=len, reverse=True) + self._key_to_sensor = build_key_to_entity_mapping(entities, all_names) + + def on_state(self, state: EntityState) -> None: + """Process a state update.""" + if isinstance(state, SensorState) and not state.missing_state: + sensor_name = self._key_to_sensor.get(state.key) + if sensor_name and sensor_name in self.sensor_states: + self.sensor_states[sensor_name].append(state.state) + self._check_waiters() + elif isinstance(state, BinarySensorState): + sensor_name = self._key_to_sensor.get(state.key) + if sensor_name and sensor_name in self.binary_states: + self.binary_states[sensor_name].append(state.state) + self._check_waiters() + + def _check_waiters(self) -> None: + """Check all pending waiters and resolve any whose condition is met.""" + for condition, future in self._waiters: + if not future.done() and condition(): + future.set_result(True) + + def _all_have_values(self) -> bool: + """Check if all sensor and binary sensor lists have at least one value.""" + return all(len(v) >= 1 for v in self.sensor_states.values()) and all( + len(v) >= 1 for v in self.binary_states.values() + ) + + async def wait_for_all(self, timeout: float = 3.0) -> None: + """Wait until all sensors and binary sensors have at least one value.""" + if self._all_have_values(): + return + future: asyncio.Future[bool] = asyncio.get_running_loop().create_future() + self._waiters.append((self._all_have_values, future)) + await asyncio.wait_for(future, timeout=timeout) + + def add_waiter(self, condition: Callable[[], bool]) -> asyncio.Future[bool]: + """Add a custom waiter that resolves when condition returns True. + + Returns: + A future that resolves when the condition is met. + """ + future: asyncio.Future[bool] = asyncio.get_running_loop().create_future() + if condition(): + future.set_result(True) + else: + self._waiters.append((condition, future)) + return future diff --git a/tests/integration/test_uart_mock_ld2410.py b/tests/integration/test_uart_mock_ld2410.py index e01d6ff8e8..ce0e1bb7ec 100644 --- a/tests/integration/test_uart_mock_ld2410.py +++ b/tests/integration/test_uart_mock_ld2410.py @@ -21,16 +21,10 @@ from __future__ import annotations import asyncio from pathlib import Path -from aioesphomeapi import ( - BinarySensorInfo, - BinarySensorState, - EntityState, - SensorInfo, - SensorState, -) +from aioesphomeapi import ButtonInfo import pytest -from .state_utils import InitialStateHelper, build_key_to_entity_mapping, find_entity +from .state_utils import InitialStateHelper, SensorStateCollector, find_entity from .types import APIClientConnectedFactory, RunCompiledFunction @@ -64,100 +58,65 @@ async def test_uart_mock_ld2410( if "uart_mock" in line and "TX " in line: tx_log_lines.append(line) - # Track sensor state updates (after initial state is swallowed) - sensor_states: dict[str, list[float]] = { - "moving_distance": [], - "still_distance": [], - "moving_energy": [], - "still_energy": [], - "detection_distance": [], - } - binary_states: dict[str, list[bool]] = { - "has_target": [], - "has_moving_target": [], - "has_still_target": [], - } + collector = SensorStateCollector( + sensor_names=[ + "moving_distance", + "still_distance", + "moving_energy", + "still_energy", + "detection_distance", + ], + binary_sensor_names=[ + "has_target", + "has_moving_target", + "has_still_target", + ], + ) # Signal when we see recovery frame values - recovery_received = loop.create_future() - - def on_state(state: EntityState) -> None: - if isinstance(state, SensorState) and not state.missing_state: - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in sensor_states: - sensor_states[sensor_name].append(state.state) - # Check if this is the recovery frame (moving_distance = 50) - if ( - sensor_name == "moving_distance" - and state.state == pytest.approx(50.0) - and not recovery_received.done() - ): - recovery_received.set_result(True) - elif isinstance(state, BinarySensorState): - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in binary_states: - binary_states[sensor_name].append(state.state) + recovery_received = collector.add_waiter( + lambda: pytest.approx(50.0) in collector.sensor_states["moving_distance"] + ) async with ( run_compiled(yaml_config, line_callback=line_callback), api_client_connected() as client, ): entities, _ = await client.list_entities_services() - - # Build key mappings for all sensor types - all_names = list(sensor_states.keys()) + list(binary_states.keys()) - key_to_sensor = build_key_to_entity_mapping(entities, all_names) + collector.build_key_mapping(entities) # Set up initial state helper initial_state_helper = InitialStateHelper(entities) - client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + client.subscribe_states( + initial_state_helper.on_state_wrapper(collector.on_state) + ) try: await initial_state_helper.wait_for_initial_states() except TimeoutError: pytest.fail("Timeout waiting for initial states") - # Phase 1 values are in the initial states (swallowed by InitialStateHelper). - # Verify them via initial_states dict. - moving_dist_entity = find_entity(entities, "moving_distance", SensorInfo) - assert moving_dist_entity is not None - initial_moving = initial_state_helper.initial_states.get(moving_dist_entity.key) - assert initial_moving is not None and isinstance(initial_moving, SensorState) - assert initial_moving.state == pytest.approx(100.0), ( - f"Initial moving distance should be 100, got {initial_moving.state}" - ) + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) - still_dist_entity = find_entity(entities, "still_distance", SensorInfo) - assert still_dist_entity is not None - initial_still = initial_state_helper.initial_states.get(still_dist_entity.key) - assert initial_still is not None and isinstance(initial_still, SensorState) - assert initial_still.state == pytest.approx(120.0), ( - f"Initial still distance should be 120, got {initial_still.state}" - ) + # Wait for Phase 1 - all sensors and binary sensors have at least one value + try: + await collector.wait_for_all(timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 1 frame. Received:\n" + f" sensor_states: {collector.sensor_states}\n" + f" binary_states: {collector.binary_states}" + ) - moving_energy_entity = find_entity(entities, "moving_energy", SensorInfo) - assert moving_energy_entity is not None - initial_me = initial_state_helper.initial_states.get(moving_energy_entity.key) - assert initial_me is not None and isinstance(initial_me, SensorState) - assert initial_me.state == pytest.approx(50.0), ( - f"Initial moving energy should be 50, got {initial_me.state}" - ) - - still_energy_entity = find_entity(entities, "still_energy", SensorInfo) - assert still_energy_entity is not None - initial_se = initial_state_helper.initial_states.get(still_energy_entity.key) - assert initial_se is not None and isinstance(initial_se, SensorState) - assert initial_se.state == pytest.approx(25.0), ( - f"Initial still energy should be 25, got {initial_se.state}" - ) - - detect_dist_entity = find_entity(entities, "detection_distance", SensorInfo) - assert detect_dist_entity is not None - initial_dd = initial_state_helper.initial_states.get(detect_dist_entity.key) - assert initial_dd is not None and isinstance(initial_dd, SensorState) - assert initial_dd.state == pytest.approx(300.0), ( - f"Initial detection distance should be 300, got {initial_dd.state}" - ) + # Phase 1 values: moving=100, still=120, energy=50/25, detect=300 + assert collector.sensor_states["moving_distance"][0] == pytest.approx(100.0) + assert collector.sensor_states["still_distance"][0] == pytest.approx(120.0) + assert collector.sensor_states["moving_energy"][0] == pytest.approx(50.0) + assert collector.sensor_states["still_energy"][0] == pytest.approx(25.0) + assert collector.sensor_states["detection_distance"][0] == pytest.approx(300.0) # Wait for the recovery frame (Phase 5) to be parsed # This proves the component survived garbage + truncated + overflow @@ -165,12 +124,8 @@ async def test_uart_mock_ld2410( await asyncio.wait_for(recovery_received, timeout=15.0) except TimeoutError: pytest.fail( - f"Timeout waiting for recovery frame. Received sensor states:\n" - f" moving_distance: {sensor_states['moving_distance']}\n" - f" still_distance: {sensor_states['still_distance']}\n" - f" moving_energy: {sensor_states['moving_energy']}\n" - f" still_energy: {sensor_states['still_energy']}\n" - f" detection_distance: {sensor_states['detection_distance']}" + f"Timeout waiting for recovery frame. Received:\n" + f" sensor_states: {collector.sensor_states}" ) # Verify overflow warning was logged @@ -183,67 +138,36 @@ async def test_uart_mock_ld2410( # A5 (MAC), AB (distance res), AE (light), 61 (params), FE (config off) assert len(tx_log_lines) > 0, "Expected TX log lines from uart_mock" tx_data = " ".join(tx_log_lines) - # Verify command frame header appears (FD:FC:FB:FA) assert "FD:FC:FB:FA" in tx_data, ( "Expected LD2410 command frame header FD:FC:FB:FA in TX log" ) - # Verify command frame footer appears (04:03:02:01) assert "04:03:02:01" in tx_data, ( "Expected LD2410 command frame footer 04:03:02:01 in TX log" ) - # Recovery frame values (Phase 5, after overflow) - assert len(sensor_states["moving_distance"]) >= 1, ( - f"Expected recovery moving_distance, got: {sensor_states['moving_distance']}" - ) - # Find the recovery value (moving_distance = 50) - recovery_values = [ - v for v in sensor_states["moving_distance"] if v == pytest.approx(50.0) - ] - assert len(recovery_values) >= 1, ( - f"Expected moving_distance=50 in recovery, got: {sensor_states['moving_distance']}" - ) - # Recovery frame: moving=50, still=75, energy=100/80, detect=127 recovery_idx = next( i - for i, v in enumerate(sensor_states["moving_distance"]) + for i, v in enumerate(collector.sensor_states["moving_distance"]) if v == pytest.approx(50.0) ) - assert sensor_states["still_distance"][recovery_idx] == pytest.approx(75.0), ( - f"Recovery still distance should be 75, got {sensor_states['still_distance'][recovery_idx]}" + assert collector.sensor_states["still_distance"][recovery_idx] == pytest.approx( + 75.0 ) - assert sensor_states["moving_energy"][recovery_idx] == pytest.approx(100.0), ( - f"Recovery moving energy should be 100, got {sensor_states['moving_energy'][recovery_idx]}" + assert collector.sensor_states["moving_energy"][recovery_idx] == pytest.approx( + 100.0 ) - assert sensor_states["still_energy"][recovery_idx] == pytest.approx(80.0), ( - f"Recovery still energy should be 80, got {sensor_states['still_energy'][recovery_idx]}" - ) - assert sensor_states["detection_distance"][recovery_idx] == pytest.approx( - 127.0 - ), ( - f"Recovery detection distance should be 127, got {sensor_states['detection_distance'][recovery_idx]}" + assert collector.sensor_states["still_energy"][recovery_idx] == pytest.approx( + 80.0 ) + assert collector.sensor_states["detection_distance"][ + recovery_idx + ] == pytest.approx(127.0) - # Verify binary sensors detected targets - # Binary sensors could be in initial states or forwarded states - has_target_entity = find_entity(entities, "has_target", BinarySensorInfo) - assert has_target_entity is not None - initial_ht = initial_state_helper.initial_states.get(has_target_entity.key) - assert initial_ht is not None and isinstance(initial_ht, BinarySensorState) - assert initial_ht.state is True, "Has target should be True" - - has_moving_entity = find_entity(entities, "has_moving_target", BinarySensorInfo) - assert has_moving_entity is not None - initial_hm = initial_state_helper.initial_states.get(has_moving_entity.key) - assert initial_hm is not None and isinstance(initial_hm, BinarySensorState) - assert initial_hm.state is True, "Has moving target should be True" - - has_still_entity = find_entity(entities, "has_still_target", BinarySensorInfo) - assert has_still_entity is not None - initial_hs = initial_state_helper.initial_states.get(has_still_entity.key) - assert initial_hs is not None and isinstance(initial_hs, BinarySensorState) - assert initial_hs.state is True, "Has still target should be True" + # Verify binary sensors detected targets (from Phase 1 frame) + assert collector.binary_states["has_target"][0] is True + assert collector.binary_states["has_moving_target"][0] is True + assert collector.binary_states["has_still_target"][0] is True @pytest.mark.asyncio @@ -260,133 +184,82 @@ async def test_uart_mock_ld2410_engineering( "EXTERNAL_COMPONENT_PATH", external_components_path ) - loop = asyncio.get_running_loop() + collector = SensorStateCollector( + sensor_names=[ + "moving_distance", + "still_distance", + "moving_energy", + "still_energy", + "detection_distance", + "light", + "gate_0_move_energy", + "gate_1_move_energy", + "gate_2_move_energy", + "gate_0_still_energy", + "gate_1_still_energy", + "gate_2_still_energy", + ], + binary_sensor_names=[ + "has_target", + "has_moving_target", + "has_still_target", + "out_pin_presence", + ], + ) - # Track sensor state updates (after initial state is swallowed) - sensor_states: dict[str, list[float]] = { - "moving_distance": [], - "still_distance": [], - "moving_energy": [], - "still_energy": [], - "detection_distance": [], - "light": [], - "gate_0_move_energy": [], - "gate_1_move_energy": [], - "gate_2_move_energy": [], - "gate_0_still_energy": [], - "gate_1_still_energy": [], - "gate_2_still_energy": [], - } - binary_states: dict[str, list[bool]] = { - "has_target": [], - "has_moving_target": [], - "has_still_target": [], - "out_pin_presence": [], - } - - # Signal when we see Phase 3 frame (still_distance = 291) - phase3_received = loop.create_future() - - def on_state(state: EntityState) -> None: - if isinstance(state, SensorState) and not state.missing_state: - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in sensor_states: - sensor_states[sensor_name].append(state.state) - if ( - sensor_name == "still_distance" - and state.state == pytest.approx(291.0) - and not phase3_received.done() - ): - phase3_received.set_result(True) - elif isinstance(state, BinarySensorState): - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in binary_states: - binary_states[sensor_name].append(state.state) + # Signal when we see Phase 3 frame values + phase3_received = collector.add_waiter( + lambda: pytest.approx(291.0) in collector.sensor_states["still_distance"] + ) async with ( run_compiled(yaml_config), api_client_connected() as client, ): entities, _ = await client.list_entities_services() - - all_names = list(sensor_states.keys()) + list(binary_states.keys()) - key_to_sensor = build_key_to_entity_mapping(entities, all_names) + collector.build_key_mapping(entities) initial_state_helper = InitialStateHelper(entities) - client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + client.subscribe_states( + initial_state_helper.on_state_wrapper(collector.on_state) + ) try: await initial_state_helper.wait_for_initial_states() except TimeoutError: pytest.fail("Timeout waiting for initial states") - # Phase 1 initial values (engineering mode frame): + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + + # Wait for Phase 1 - all sensors and binary sensors have at least one value + try: + await collector.wait_for_all(timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 1 frame. Received:\n" + f" sensor_states: {collector.sensor_states}\n" + f" binary_states: {collector.binary_states}" + ) + + # Phase 1 values (engineering mode frame): # moving=30, energy=100, still=30, energy=100, detect=0 - moving_dist_entity = find_entity(entities, "moving_distance", SensorInfo) - assert moving_dist_entity is not None - initial_moving = initial_state_helper.initial_states.get(moving_dist_entity.key) - assert initial_moving is not None and isinstance(initial_moving, SensorState) - assert initial_moving.state == pytest.approx(30.0), ( - f"Initial moving distance should be 30, got {initial_moving.state}" - ) - - still_dist_entity = find_entity(entities, "still_distance", SensorInfo) - assert still_dist_entity is not None - initial_still = initial_state_helper.initial_states.get(still_dist_entity.key) - assert initial_still is not None and isinstance(initial_still, SensorState) - assert initial_still.state == pytest.approx(30.0), ( - f"Initial still distance should be 30, got {initial_still.state}" - ) - - # Verify engineering mode sensors from initial state - # Gate 0 moving energy = 0x64 = 100 - gate0_move_entity = find_entity(entities, "gate_0_move_energy", SensorInfo) - assert gate0_move_entity is not None - initial_g0m = initial_state_helper.initial_states.get(gate0_move_entity.key) - assert initial_g0m is not None and isinstance(initial_g0m, SensorState) - assert initial_g0m.state == pytest.approx(100.0), ( - f"Gate 0 move energy should be 100, got {initial_g0m.state}" - ) - - # Gate 1 moving energy = 0x41 = 65 - gate1_move_entity = find_entity(entities, "gate_1_move_energy", SensorInfo) - assert gate1_move_entity is not None - initial_g1m = initial_state_helper.initial_states.get(gate1_move_entity.key) - assert initial_g1m is not None and isinstance(initial_g1m, SensorState) - assert initial_g1m.state == pytest.approx(65.0), ( - f"Gate 1 move energy should be 65, got {initial_g1m.state}" - ) - - # Light sensor = 0x57 = 87 - light_entity = find_entity(entities, "light", SensorInfo) - assert light_entity is not None - initial_light = initial_state_helper.initial_states.get(light_entity.key) - assert initial_light is not None and isinstance(initial_light, SensorState) - assert initial_light.state == pytest.approx(87.0), ( - f"Light sensor should be 87, got {initial_light.state}" - ) - - # Out pin presence = 0x01 = True - out_pin_entity = find_entity(entities, "out_pin_presence", BinarySensorInfo) - assert out_pin_entity is not None - initial_out = initial_state_helper.initial_states.get(out_pin_entity.key) - assert initial_out is not None and isinstance(initial_out, BinarySensorState) - assert initial_out.state is True, "Out pin presence should be True" + assert collector.sensor_states["moving_distance"][0] == pytest.approx(30.0) + assert collector.sensor_states["still_distance"][0] == pytest.approx(30.0) + assert collector.sensor_states["gate_0_move_energy"][0] == pytest.approx(100.0) + assert collector.sensor_states["gate_1_move_energy"][0] == pytest.approx(65.0) + assert collector.sensor_states["light"][0] == pytest.approx(87.0) + assert collector.binary_states["out_pin_presence"][0] is True # Wait for Phase 3 frame (still_distance = 291cm, multi-byte) try: await asyncio.wait_for(phase3_received, timeout=15.0) except TimeoutError: pytest.fail( - f"Timeout waiting for Phase 3 frame. Received sensor states:\n" - f" still_distance: {sensor_states['still_distance']}\n" - f" moving_distance: {sensor_states['moving_distance']}" + f"Timeout waiting for Phase 3 frame. Received:\n" + f" still_distance: {collector.sensor_states['still_distance']}" ) - # Phase 3: still distance = 0x0123 = 291cm (multi-byte distance test) - phase3_still = [ - v for v in sensor_states["still_distance"] if v == pytest.approx(291.0) - ] - assert len(phase3_still) >= 1, ( - f"Expected still_distance=291, got: {sensor_states['still_distance']}" - ) + assert pytest.approx(291.0) in collector.sensor_states["still_distance"] diff --git a/tests/integration/test_uart_mock_ld2412.py b/tests/integration/test_uart_mock_ld2412.py index cf7324ceed..a964ba0073 100644 --- a/tests/integration/test_uart_mock_ld2412.py +++ b/tests/integration/test_uart_mock_ld2412.py @@ -21,16 +21,10 @@ from __future__ import annotations import asyncio from pathlib import Path -from aioesphomeapi import ( - BinarySensorInfo, - BinarySensorState, - EntityState, - SensorInfo, - SensorState, -) +from aioesphomeapi import ButtonInfo import pytest -from .state_utils import InitialStateHelper, build_key_to_entity_mapping, find_entity +from .state_utils import InitialStateHelper, SensorStateCollector, find_entity from .types import APIClientConnectedFactory, RunCompiledFunction @@ -64,104 +58,65 @@ async def test_uart_mock_ld2412( if "uart_mock" in line and "TX " in line: tx_log_lines.append(line) - # Track sensor state updates (after initial state is swallowed) - sensor_states: dict[str, list[float]] = { - "moving_distance": [], - "still_distance": [], - "moving_energy": [], - "still_energy": [], - "detection_distance": [], - } - binary_states: dict[str, list[bool]] = { - "has_target": [], - "has_moving_target": [], - "has_still_target": [], - } + collector = SensorStateCollector( + sensor_names=[ + "moving_distance", + "still_distance", + "moving_energy", + "still_energy", + "detection_distance", + ], + binary_sensor_names=[ + "has_target", + "has_moving_target", + "has_still_target", + ], + ) # Signal when we see recovery frame values - recovery_received = loop.create_future() - - def on_state(state: EntityState) -> None: - if isinstance(state, SensorState) and not state.missing_state: - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in sensor_states: - sensor_states[sensor_name].append(state.state) - # Check if this is the recovery frame (moving_distance = 50) - if ( - sensor_name == "moving_distance" - and state.state == pytest.approx(50.0) - and not recovery_received.done() - ): - recovery_received.set_result(True) - elif isinstance(state, BinarySensorState): - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in binary_states: - binary_states[sensor_name].append(state.state) + recovery_received = collector.add_waiter( + lambda: pytest.approx(50.0) in collector.sensor_states["moving_distance"] + ) async with ( run_compiled(yaml_config, line_callback=line_callback), api_client_connected() as client, ): entities, _ = await client.list_entities_services() - - # Build key mappings for all sensor types - all_names = list(sensor_states.keys()) + list(binary_states.keys()) - # Sort by descending length to avoid substring collisions - # (e.g., "still_energy" matching "gate_0_still_energy") - all_names.sort(key=len, reverse=True) - key_to_sensor = build_key_to_entity_mapping(entities, all_names) + collector.build_key_mapping(entities) # Set up initial state helper initial_state_helper = InitialStateHelper(entities) - client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + client.subscribe_states( + initial_state_helper.on_state_wrapper(collector.on_state) + ) try: await initial_state_helper.wait_for_initial_states() except TimeoutError: pytest.fail("Timeout waiting for initial states") - # Phase 1 values are in the initial states (swallowed by InitialStateHelper). - # Verify them via initial_states dict. - moving_dist_entity = find_entity(entities, "moving_distance", SensorInfo) - assert moving_dist_entity is not None - initial_moving = initial_state_helper.initial_states.get(moving_dist_entity.key) - assert initial_moving is not None and isinstance(initial_moving, SensorState) - assert initial_moving.state == pytest.approx(100.0), ( - f"Initial moving distance should be 100, got {initial_moving.state}" - ) + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) - still_dist_entity = find_entity(entities, "still_distance", SensorInfo) - assert still_dist_entity is not None - initial_still = initial_state_helper.initial_states.get(still_dist_entity.key) - assert initial_still is not None and isinstance(initial_still, SensorState) - assert initial_still.state == pytest.approx(120.0), ( - f"Initial still distance should be 120, got {initial_still.state}" - ) + # Wait for Phase 1 - all sensors and binary sensors have at least one value + try: + await collector.wait_for_all(timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 1 frame. Received:\n" + f" sensor_states: {collector.sensor_states}\n" + f" binary_states: {collector.binary_states}" + ) - moving_energy_entity = find_entity(entities, "moving_energy", SensorInfo) - assert moving_energy_entity is not None - initial_me = initial_state_helper.initial_states.get(moving_energy_entity.key) - assert initial_me is not None and isinstance(initial_me, SensorState) - assert initial_me.state == pytest.approx(50.0), ( - f"Initial moving energy should be 50, got {initial_me.state}" - ) - - still_energy_entity = find_entity(entities, "still_energy", SensorInfo) - assert still_energy_entity is not None - initial_se = initial_state_helper.initial_states.get(still_energy_entity.key) - assert initial_se is not None and isinstance(initial_se, SensorState) - assert initial_se.state == pytest.approx(25.0), ( - f"Initial still energy should be 25, got {initial_se.state}" - ) - - # LD2412 detection_distance = moving_distance when MOVE_BITMASK is set - detect_dist_entity = find_entity(entities, "detection_distance", SensorInfo) - assert detect_dist_entity is not None - initial_dd = initial_state_helper.initial_states.get(detect_dist_entity.key) - assert initial_dd is not None and isinstance(initial_dd, SensorState) - assert initial_dd.state == pytest.approx(100.0), ( - f"Initial detection distance should be 100, got {initial_dd.state}" - ) + # Phase 1 values: moving=100, still=120, energy=50/25, detect=100 + assert collector.sensor_states["moving_distance"][0] == pytest.approx(100.0) + assert collector.sensor_states["still_distance"][0] == pytest.approx(120.0) + assert collector.sensor_states["moving_energy"][0] == pytest.approx(50.0) + assert collector.sensor_states["still_energy"][0] == pytest.approx(25.0) + assert collector.sensor_states["detection_distance"][0] == pytest.approx(100.0) # Wait for the recovery frame (Phase 5) to be parsed # This proves the component survived garbage + truncated + overflow @@ -169,12 +124,8 @@ async def test_uart_mock_ld2412( await asyncio.wait_for(recovery_received, timeout=3.0) except TimeoutError: pytest.fail( - f"Timeout waiting for recovery frame. Received sensor states:\n" - f" moving_distance: {sensor_states['moving_distance']}\n" - f" still_distance: {sensor_states['still_distance']}\n" - f" moving_energy: {sensor_states['moving_energy']}\n" - f" still_energy: {sensor_states['still_energy']}\n" - f" detection_distance: {sensor_states['detection_distance']}" + f"Timeout waiting for recovery frame. Received:\n" + f" sensor_states: {collector.sensor_states}" ) # Verify overflow warning was logged @@ -185,67 +136,36 @@ async def test_uart_mock_ld2412( # Verify LD2412 sent setup commands (TX logging) assert len(tx_log_lines) > 0, "Expected TX log lines from uart_mock" tx_data = " ".join(tx_log_lines) - # Verify command frame header appears (FD:FC:FB:FA) assert "FD:FC:FB:FA" in tx_data, ( "Expected LD2412 command frame header FD:FC:FB:FA in TX log" ) - # Verify command frame footer appears (04:03:02:01) assert "04:03:02:01" in tx_data, ( "Expected LD2412 command frame footer 04:03:02:01 in TX log" ) - # Recovery frame values (Phase 5, after overflow) - assert len(sensor_states["moving_distance"]) >= 1, ( - f"Expected recovery moving_distance, got: {sensor_states['moving_distance']}" - ) - # Find the recovery value (moving_distance = 50) - recovery_values = [ - v for v in sensor_states["moving_distance"] if v == pytest.approx(50.0) - ] - assert len(recovery_values) >= 1, ( - f"Expected moving_distance=50 in recovery, got: {sensor_states['moving_distance']}" - ) - # Recovery frame: moving=50, still=75, energy=100/80, detect=50 recovery_idx = next( i - for i, v in enumerate(sensor_states["moving_distance"]) + for i, v in enumerate(collector.sensor_states["moving_distance"]) if v == pytest.approx(50.0) ) - assert sensor_states["still_distance"][recovery_idx] == pytest.approx(75.0), ( - f"Recovery still distance should be 75, got {sensor_states['still_distance'][recovery_idx]}" + assert collector.sensor_states["still_distance"][recovery_idx] == pytest.approx( + 75.0 ) - assert sensor_states["moving_energy"][recovery_idx] == pytest.approx(100.0), ( - f"Recovery moving energy should be 100, got {sensor_states['moving_energy'][recovery_idx]}" + assert collector.sensor_states["moving_energy"][recovery_idx] == pytest.approx( + 100.0 ) - assert sensor_states["still_energy"][recovery_idx] == pytest.approx(80.0), ( - f"Recovery still energy should be 80, got {sensor_states['still_energy'][recovery_idx]}" - ) - # LD2412 detection_distance = moving_distance when MOVE_BITMASK set - assert sensor_states["detection_distance"][recovery_idx] == pytest.approx( - 50.0 - ), ( - f"Recovery detection distance should be 50, got {sensor_states['detection_distance'][recovery_idx]}" + assert collector.sensor_states["still_energy"][recovery_idx] == pytest.approx( + 80.0 ) + assert collector.sensor_states["detection_distance"][ + recovery_idx + ] == pytest.approx(50.0) - # Verify binary sensors detected targets - has_target_entity = find_entity(entities, "has_target", BinarySensorInfo) - assert has_target_entity is not None - initial_ht = initial_state_helper.initial_states.get(has_target_entity.key) - assert initial_ht is not None and isinstance(initial_ht, BinarySensorState) - assert initial_ht.state is True, "Has target should be True" - - has_moving_entity = find_entity(entities, "has_moving_target", BinarySensorInfo) - assert has_moving_entity is not None - initial_hm = initial_state_helper.initial_states.get(has_moving_entity.key) - assert initial_hm is not None and isinstance(initial_hm, BinarySensorState) - assert initial_hm.state is True, "Has moving target should be True" - - has_still_entity = find_entity(entities, "has_still_target", BinarySensorInfo) - assert has_still_entity is not None - initial_hs = initial_state_helper.initial_states.get(has_still_entity.key) - assert initial_hs is not None and isinstance(initial_hs, BinarySensorState) - assert initial_hs.state is True, "Has still target should be True" + # Verify binary sensors detected targets (from Phase 1 frame) + assert collector.binary_states["has_target"][0] is True + assert collector.binary_states["has_moving_target"][0] is True + assert collector.binary_states["has_still_target"][0] is True @pytest.mark.asyncio @@ -262,120 +182,75 @@ async def test_uart_mock_ld2412_engineering( "EXTERNAL_COMPONENT_PATH", external_components_path ) - loop = asyncio.get_running_loop() - - # Track sensor state updates (after initial state is swallowed) - sensor_states: dict[str, list[float]] = { - "moving_distance": [], - "still_distance": [], - "moving_energy": [], - "still_energy": [], - "detection_distance": [], - "light": [], - "gate_0_move_energy": [], - "gate_1_move_energy": [], - "gate_2_move_energy": [], - "gate_0_still_energy": [], - "gate_1_still_energy": [], - "gate_2_still_energy": [], - } - binary_states: dict[str, list[bool]] = { - "has_target": [], - "has_moving_target": [], - "has_still_target": [], - } + collector = SensorStateCollector( + sensor_names=[ + "moving_distance", + "still_distance", + "moving_energy", + "still_energy", + "detection_distance", + "light", + "gate_0_move_energy", + "gate_1_move_energy", + "gate_2_move_energy", + "gate_0_still_energy", + "gate_1_still_energy", + "gate_2_still_energy", + ], + binary_sensor_names=[ + "has_target", + "has_moving_target", + "has_still_target", + ], + ) # Signal when we see Phase 3 frame values - phase3_still_received = loop.create_future() - phase3_detect_received = loop.create_future() - - def on_state(state: EntityState) -> None: - if isinstance(state, SensorState) and not state.missing_state: - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in sensor_states: - sensor_states[sensor_name].append(state.state) - if ( - sensor_name == "still_distance" - and state.state == pytest.approx(291.0) - and not phase3_still_received.done() - ): - phase3_still_received.set_result(True) - if ( - sensor_name == "detection_distance" - and state.state == pytest.approx(291.0) - and not phase3_detect_received.done() - ): - phase3_detect_received.set_result(True) - elif isinstance(state, BinarySensorState): - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in binary_states: - binary_states[sensor_name].append(state.state) + phase3_still_received = collector.add_waiter( + lambda: pytest.approx(291.0) in collector.sensor_states["still_distance"] + ) + phase3_detect_received = collector.add_waiter( + lambda: pytest.approx(291.0) in collector.sensor_states["detection_distance"] + ) async with ( run_compiled(yaml_config), api_client_connected() as client, ): entities, _ = await client.list_entities_services() - - all_names = list(sensor_states.keys()) + list(binary_states.keys()) - # Sort by descending length to avoid substring collisions - # (e.g., "still_energy" matching "gate_0_still_energy") - all_names.sort(key=len, reverse=True) - key_to_sensor = build_key_to_entity_mapping(entities, all_names) + collector.build_key_mapping(entities) initial_state_helper = InitialStateHelper(entities) - client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + client.subscribe_states( + initial_state_helper.on_state_wrapper(collector.on_state) + ) try: await initial_state_helper.wait_for_initial_states() except TimeoutError: pytest.fail("Timeout waiting for initial states") - # Phase 1 initial values (engineering mode frame): + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + + # Wait for Phase 1 - all sensors and binary sensors have at least one value + try: + await collector.wait_for_all(timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 1 frame. Received:\n" + f" sensor_states: {collector.sensor_states}\n" + f" binary_states: {collector.binary_states}" + ) + + # Phase 1 values (engineering mode frame): # moving=30, energy=100, still=30, energy=100, detect=30 - moving_dist_entity = find_entity(entities, "moving_distance", SensorInfo) - assert moving_dist_entity is not None - initial_moving = initial_state_helper.initial_states.get(moving_dist_entity.key) - assert initial_moving is not None and isinstance(initial_moving, SensorState) - assert initial_moving.state == pytest.approx(30.0), ( - f"Initial moving distance should be 30, got {initial_moving.state}" - ) - - still_dist_entity = find_entity(entities, "still_distance", SensorInfo) - assert still_dist_entity is not None - initial_still = initial_state_helper.initial_states.get(still_dist_entity.key) - assert initial_still is not None and isinstance(initial_still, SensorState) - assert initial_still.state == pytest.approx(30.0), ( - f"Initial still distance should be 30, got {initial_still.state}" - ) - - # Verify engineering mode sensors from initial state - # Gate 0 moving energy = 0x64 = 100 - gate0_move_entity = find_entity(entities, "gate_0_move_energy", SensorInfo) - assert gate0_move_entity is not None - initial_g0m = initial_state_helper.initial_states.get(gate0_move_entity.key) - assert initial_g0m is not None and isinstance(initial_g0m, SensorState) - assert initial_g0m.state == pytest.approx(100.0), ( - f"Gate 0 move energy should be 100, got {initial_g0m.state}" - ) - - # Gate 1 moving energy = 0x41 = 65 - gate1_move_entity = find_entity(entities, "gate_1_move_energy", SensorInfo) - assert gate1_move_entity is not None - initial_g1m = initial_state_helper.initial_states.get(gate1_move_entity.key) - assert initial_g1m is not None and isinstance(initial_g1m, SensorState) - assert initial_g1m.state == pytest.approx(65.0), ( - f"Gate 1 move energy should be 65, got {initial_g1m.state}" - ) - - # Light sensor = 0x57 = 87 - light_entity = find_entity(entities, "light", SensorInfo) - assert light_entity is not None - initial_light = initial_state_helper.initial_states.get(light_entity.key) - assert initial_light is not None and isinstance(initial_light, SensorState) - assert initial_light.state == pytest.approx(87.0), ( - f"Light sensor should be 87, got {initial_light.state}" - ) + assert collector.sensor_states["moving_distance"][0] == pytest.approx(30.0) + assert collector.sensor_states["still_distance"][0] == pytest.approx(30.0) + assert collector.sensor_states["gate_0_move_energy"][0] == pytest.approx(100.0) + assert collector.sensor_states["gate_1_move_energy"][0] == pytest.approx(65.0) + assert collector.sensor_states["light"][0] == pytest.approx(87.0) # Wait for Phase 3 frame: still_distance = 291cm (multi-byte) try: @@ -383,25 +258,18 @@ async def test_uart_mock_ld2412_engineering( except TimeoutError: pytest.fail( f"Timeout waiting for Phase 3 still_distance. Received:\n" - f" still_distance: {sensor_states['still_distance']}\n" - f" moving_distance: {sensor_states['moving_distance']}" + f" still_distance: {collector.sensor_states['still_distance']}" ) - assert pytest.approx(291.0) in sensor_states["still_distance"], ( - f"Expected still_distance=291, got: {sensor_states['still_distance']}" - ) + assert pytest.approx(291.0) in collector.sensor_states["still_distance"] # Wait for Phase 3: detection_distance = 291 (still-only target) - # target_state=0x02 so LD2412 uses still_distance for detection_distance. - # The throttle_with_priority filter may delay this value. try: await asyncio.wait_for(phase3_detect_received, timeout=3.0) except TimeoutError: pytest.fail( - f"Timeout waiting for detection_distance=291 (still-only target). " - f"Received: {sensor_states['detection_distance']}" + f"Timeout waiting for detection_distance=291. " + f"Received: {collector.sensor_states['detection_distance']}" ) - assert pytest.approx(291.0) in sensor_states["detection_distance"], ( - f"Expected detection_distance=291, got: {sensor_states['detection_distance']}" - ) + assert pytest.approx(291.0) in collector.sensor_states["detection_distance"] diff --git a/tests/integration/test_uart_mock_modbus.py b/tests/integration/test_uart_mock_modbus.py index 309cb56dc9..bf3c069750 100644 --- a/tests/integration/test_uart_mock_modbus.py +++ b/tests/integration/test_uart_mock_modbus.py @@ -12,10 +12,10 @@ from __future__ import annotations import asyncio from pathlib import Path -from aioesphomeapi import EntityState, SensorState +from aioesphomeapi import ButtonInfo, EntityState, SensorState import pytest -from .state_utils import InitialStateHelper, build_key_to_entity_mapping +from .state_utils import InitialStateHelper, build_key_to_entity_mapping, find_entity from .types import APIClientConnectedFactory, RunCompiledFunction @@ -74,6 +74,11 @@ async def test_uart_mock_modbus( except TimeoutError: pytest.fail("Timeout waiting for initial states") + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + # Wait for basic register to be updated with successful parse try: await asyncio.wait_for(basic_register_changed, timeout=15.0) @@ -143,6 +148,11 @@ async def test_uart_mock_modbus_timing( except TimeoutError: pytest.fail("Timeout waiting for initial states") + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + # Wait for voltage to be updated with successful parse try: await asyncio.wait_for(voltage_changed, timeout=15.0) From 0ff5270632f59a8fd4d7ed25cd71c2ce92ae5b07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Mar 2026 16:57:19 -1000 Subject: [PATCH 358/609] [ci] Fix codeowner approval label workflow for fork PRs (#14490) Co-authored-by: Claude Opus 4.6 --- .github/scripts/codeowners.js | 88 +++++++++- .../codeowner-approved-label-update.yml | 95 +++++++++++ .../workflows/codeowner-approved-label.yml | 153 +++++------------- 3 files changed, 217 insertions(+), 119 deletions(-) create mode 100644 .github/workflows/codeowner-approved-label-update.yml diff --git a/.github/scripts/codeowners.js b/.github/scripts/codeowners.js index 9a10391699..5d69c11b1a 100644 --- a/.github/scripts/codeowners.js +++ b/.github/scripts/codeowners.js @@ -2,7 +2,7 @@ // // Used by: // - codeowner-review-request.yml -// - codeowner-approved-label.yml +// - codeowner-approved-label.yml + codeowner-approved-label-update.yml // - auto-label-pr/detectors.js (detectCodeOwner) /** @@ -133,11 +133,95 @@ function loadCodeowners(repoRoot = '.') { return parseCodeowners(content); } +/** Possible label actions returned by determineLabelAction. */ +const LabelAction = Object.freeze({ + ADD: 'add', + REMOVE: 'remove', + NONE: 'none', +}); + +/** + * Determine what label action is needed for a PR based on codeowner approvals. + * + * Checks changed files against CODEOWNERS patterns, reviews, and current labels + * to decide if the label should be added, removed, or left unchanged. + * + * @param {object} github - octokit instance from actions/github-script + * @param {string} owner - repo owner + * @param {string} repo - repo name + * @param {number} pr_number - pull request number + * @param {Array} codeownersPatterns - from loadCodeowners / fetchCodeowners + * @param {string} labelName - label to manage + * @returns {Promise} + */ +async function determineLabelAction(github, owner, repo, pr_number, codeownersPatterns, labelName) { + // Get the list of changed files in this PR + const prFiles = await github.paginate( + github.rest.pulls.listFiles, + { owner, repo, pull_number: pr_number } + ); + + const changedFiles = prFiles.map(file => file.filename); + console.log(`Found ${changedFiles.length} changed files`); + + if (changedFiles.length === 0) { + console.log('No changed files found'); + return LabelAction.NONE; + } + + // Get effective owners using last-match-wins semantics + const effective = getEffectiveOwners(changedFiles, codeownersPatterns); + const componentCodeowners = effective.users; + + console.log(`Component-specific codeowners: ${Array.from(componentCodeowners).join(', ') || '(none)'}`); + + // Get current labels + const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ + owner, repo, issue_number: pr_number + }); + const hasLabel = currentLabels.some(label => label.name === labelName); + + if (componentCodeowners.size === 0) { + console.log('No component-specific codeowners found'); + return hasLabel ? LabelAction.REMOVE : LabelAction.NONE; + } + + // Get all reviews and find latest per user + const reviews = await github.paginate( + github.rest.pulls.listReviews, + { owner, repo, pull_number: pr_number } + ); + + const latestReviewByUser = new Map(); + for (const review of reviews) { + if (!review.user || review.user.type === 'Bot' || review.state === 'COMMENTED') continue; + latestReviewByUser.set(review.user.login, review); + } + + // Check if any component-specific codeowner has an active approval + let hasCodeownerApproval = false; + for (const [login, review] of latestReviewByUser) { + if (review.state === 'APPROVED' && componentCodeowners.has(login)) { + console.log(`Codeowner '${login}' has approved`); + hasCodeownerApproval = true; + break; + } + } + + if (hasCodeownerApproval && !hasLabel) return LabelAction.ADD; + if (!hasCodeownerApproval && hasLabel) return LabelAction.REMOVE; + + console.log(`Label already ${hasLabel ? 'present' : 'absent'}, no change needed`); + return LabelAction.NONE; +} + module.exports = { globToRegex, parseCodeowners, fetchCodeowners, loadCodeowners, classifyOwners, - getEffectiveOwners + getEffectiveOwners, + LabelAction, + determineLabelAction }; diff --git a/.github/workflows/codeowner-approved-label-update.yml b/.github/workflows/codeowner-approved-label-update.yml new file mode 100644 index 0000000000..9168cce1d6 --- /dev/null +++ b/.github/workflows/codeowner-approved-label-update.yml @@ -0,0 +1,95 @@ +# Fallback for fork PRs: phase 1 (codeowner-approved-label.yml) handles +# non-fork PRs directly but can't write labels on fork PRs (read-only token). +# This workflow re-determines the action and applies it if needed. + +name: Codeowner Approved Label Update + +on: + workflow_run: + workflows: ["Codeowner Approved Label"] + types: [completed] + +permissions: + issues: write + pull-requests: read + contents: read + +jobs: + update-label: + name: Run + if: > + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'pull_request_review' + runs-on: ubuntu-latest + steps: + - name: Get PR details + id: pr + env: + GH_TOKEN: ${{ github.token }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + REPO: ${{ github.repository }} + run: | + pr_data=$(gh pr list --repo "$REPO" --state open --search "$HEAD_SHA" \ + --json number,baseRefName --jq '.[0] // empty') + + if [ -z "$pr_data" ]; then + echo "No open PR found for SHA $HEAD_SHA, skipping" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + pr_number=$(echo "$pr_data" | jq -r '.number') + base_ref=$(echo "$pr_data" | jq -r '.baseRefName') + + echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT" + echo "base_ref=$base_ref" >> "$GITHUB_OUTPUT" + echo "Found PR #$pr_number targeting $base_ref" + + - name: Checkout base repository + if: steps.pr.outputs.skip != 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: ${{ github.repository }} + ref: ${{ steps.pr.outputs.base_ref }} + sparse-checkout: | + .github/scripts/codeowners.js + CODEOWNERS + + - name: Update label + if: steps.pr.outputs.skip != 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + PR_NUMBER: ${{ steps.pr.outputs.pr_number }} + with: + script: | + const { loadCodeowners, determineLabelAction, LabelAction } = require('./.github/scripts/codeowners.js'); + + const owner = context.repo.owner; + const repo = context.repo.repo; + const pr_number = parseInt(process.env.PR_NUMBER, 10); + const LABEL_NAME = 'code-owner-approved'; + + console.log(`Processing PR #${pr_number} for codeowner approval label`); + + const codeownersPatterns = loadCodeowners(); + const action = await determineLabelAction( + github, owner, repo, pr_number, codeownersPatterns, LABEL_NAME + ); + + if (action === LabelAction.ADD) { + await github.rest.issues.addLabels({ + owner, repo, issue_number: pr_number, labels: [LABEL_NAME] + }); + console.log(`Added '${LABEL_NAME}' label`); + } else if (action === LabelAction.REMOVE) { + try { + await github.rest.issues.removeLabel({ + owner, repo, issue_number: pr_number, name: LABEL_NAME + }); + console.log(`Removed '${LABEL_NAME}' label`); + } catch (error) { + if (error.status !== 404) throw error; + } + } else { + console.log('No label change needed'); + } diff --git a/.github/workflows/codeowner-approved-label.yml b/.github/workflows/codeowner-approved-label.yml index 200f18f544..12199bd0b0 100644 --- a/.github/workflows/codeowner-approved-label.yml +++ b/.github/workflows/codeowner-approved-label.yml @@ -1,9 +1,9 @@ -# This workflow adds/removes a 'code-owner-approved' label when a -# component-specific codeowner approves (or dismisses) a PR. -# This helps maintainers prioritize PRs that have codeowner sign-off. +# Adds/removes a 'code-owner-approved' label when a component-specific +# codeowner approves (or dismisses) a PR. # -# Only component-specific codeowners count — the catch-all @esphome/core -# team is excluded so the label reflects domain-expert approval. +# Handles non-fork PRs directly. For fork PRs the GITHUB_TOKEN is read-only, +# so label writes are deferred to codeowner-approved-label-update.yml which +# triggers via workflow_run with write permissions. name: Codeowner Approved Label @@ -26,134 +26,53 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.base.sha }} + sparse-checkout: | + .github/scripts/codeowners.js + CODEOWNERS - name: Check codeowner approval and update label uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + PR_NUMBER: ${{ github.event.pull_request.number }} with: script: | - const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js'); + const { loadCodeowners, determineLabelAction, LabelAction } = require('./.github/scripts/codeowners.js'); const owner = context.repo.owner; const repo = context.repo.repo; - const pr_number = context.payload.pull_request.number; + const pr_number = parseInt(process.env.PR_NUMBER, 10); const LABEL_NAME = 'code-owner-approved'; console.log(`Processing PR #${pr_number} for codeowner approval label`); + const codeownersPatterns = loadCodeowners(); + const action = await determineLabelAction( + github, owner, repo, pr_number, codeownersPatterns, LABEL_NAME + ); + + if (action === LabelAction.NONE) { + console.log('No label change needed'); + return; + } + try { - // Get the list of changed files in this PR (with pagination) - const prFiles = await github.paginate( - github.rest.pulls.listFiles, - { - owner, - repo, - pull_number: pr_number - } - ); - - const changedFiles = prFiles.map(file => file.filename); - console.log(`Found ${changedFiles.length} changed files`); - - if (changedFiles.length === 0) { - console.log('No changed files found, skipping'); - return; - } - - // Parse CODEOWNERS from the checked-out base branch - const codeownersPatterns = loadCodeowners(); - - // Get effective owners using last-match-wins semantics - const effective = getEffectiveOwners(changedFiles, codeownersPatterns); - - // Only keep individual component-specific codeowners (exclude teams) - const componentCodeowners = effective.users; - - console.log(`Component-specific codeowners for changed files: ${Array.from(componentCodeowners).join(', ') || '(none)'}`); - - if (componentCodeowners.size === 0) { - console.log('No component-specific codeowners found for changed files'); - // Remove label if present since there are no component codeowners - try { - await github.rest.issues.removeLabel({ - owner, - repo, - issue_number: pr_number, - name: LABEL_NAME - }); - console.log(`Removed '${LABEL_NAME}' label (no component codeowners)`); - } catch (error) { - if (error.status !== 404) { - console.log(`Failed to remove label: ${error.message}`); - } - } - return; - } - - // Get all reviews on the PR - const reviews = await github.paginate( - github.rest.pulls.listReviews, - { - owner, - repo, - pull_number: pr_number - } - ); - - // Get the latest review per user (reviews are returned chronologically) - const latestReviewByUser = new Map(); - for (const review of reviews) { - // Skip bot reviews and comment-only reviews - if (!review.user || review.user.type === 'Bot' || review.state === 'COMMENTED') continue; - latestReviewByUser.set(review.user.login, review); - } - - // Check if any component-specific codeowner has an active approval - let hasCodeownerApproval = false; - for (const [login, review] of latestReviewByUser) { - if (review.state === 'APPROVED' && componentCodeowners.has(login)) { - console.log(`Codeowner '${login}' has approved`); - hasCodeownerApproval = true; - break; - } - } - - // Get current labels to check if label is already present - const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ - owner, - repo, - issue_number: pr_number - }); - const hasLabel = currentLabels.some(label => label.name === LABEL_NAME); - - if (hasCodeownerApproval && !hasLabel) { - // Add the label + if (action === LabelAction.ADD) { await github.rest.issues.addLabels({ - owner, - repo, - issue_number: pr_number, - labels: [LABEL_NAME] + owner, repo, issue_number: pr_number, labels: [LABEL_NAME] }); console.log(`Added '${LABEL_NAME}' label`); - } else if (!hasCodeownerApproval && hasLabel) { - // Remove the label - try { - await github.rest.issues.removeLabel({ - owner, - repo, - issue_number: pr_number, - name: LABEL_NAME - }); - console.log(`Removed '${LABEL_NAME}' label`); - } catch (error) { - if (error.status !== 404) { - console.log(`Failed to remove label: ${error.message}`); - } - } - } else { - console.log(`Label already ${hasLabel ? 'present' : 'absent'}, no change needed`); + } else if (action === LabelAction.REMOVE) { + await github.rest.issues.removeLabel({ + owner, repo, issue_number: pr_number, name: LABEL_NAME + }); + console.log(`Removed '${LABEL_NAME}' label`); } - } catch (error) { - console.error(error); - core.setFailed(`Failed to process codeowner approval label: ${error.message}`); + if (error.status === 403) { + console.log('Fork PR: deferring label write to phase 2 workflow'); + } else if (error.status === 404) { + console.log('Label already removed'); + } else { + throw error; + } } From f5c37bf486d1a8905b7518478fec23a2e6e3b9ae Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:24:01 +1100 Subject: [PATCH 359/609] [packet_transport] Minimise heap allocations (#14482) --- .../packet_transport/packet_transport.cpp | 79 ++-- .../packet_transport/packet_transport.h | 19 +- script/cpp_unit_test.py | 44 +- tests/components/packet_transport/common.h | 98 ++++ .../components/packet_transport/cpp_test.yaml | 11 + .../packet_transport_test.cpp | 445 ++++++++++++++++++ 6 files changed, 659 insertions(+), 37 deletions(-) create mode 100644 tests/components/packet_transport/common.h create mode 100644 tests/components/packet_transport/cpp_test.yaml create mode 100644 tests/components/packet_transport/packet_transport_test.cpp diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index d2c5920001..f241cc6114 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -3,6 +3,8 @@ #include "esphome/core/helpers.h" #include "packet_transport.h" +#include + #include "esphome/components/xxtea/xxtea.h" namespace esphome { @@ -77,7 +79,7 @@ enum DecodeResult { DECODE_EMPTY, }; -static const size_t MAX_PING_KEYS = 4; +static constexpr size_t MAX_PING_KEYS = 4; static inline void add(std::vector &vec, uint32_t data) { vec.push_back(data & 0xFF); @@ -168,7 +170,7 @@ class PacketDecoder { return true; } - bool decrypt(const uint32_t *key) { + bool decrypt(const uint32_t *key) const { if (this->get_remaining_size() % 4 != 0) { return false; } @@ -249,9 +251,9 @@ void PacketTransport::init_data_() { } else { add(this->data_, DATA_KEY); } - for (const auto &pkey : this->ping_keys_) { + for (auto &value : this->ping_keys_ | std::views::values) { add(this->data_, PING_KEY); - add(this->data_, pkey.second); + add(this->data_, value); } } @@ -331,7 +333,7 @@ void PacketTransport::update() { auto now = millis() / 1000; if (this->last_key_time_ + this->ping_pong_recyle_time_ < now) { this->resend_ping_key_ = this->ping_pong_enable_; - ESP_LOGV(TAG, "Ping request, age %u", now - this->last_key_time_); + ESP_LOGV(TAG, "Ping request, age %" PRIu32, now - this->last_key_time_); this->last_key_time_ = now; } for (const auto &provider : this->providers_) { @@ -339,24 +341,32 @@ void PacketTransport::update() { if (key_response_age > (this->ping_pong_recyle_time_ * 2u)) { #ifdef USE_STATUS_SENSOR if (provider.second.status_sensor != nullptr && provider.second.status_sensor->state) { - ESP_LOGI(TAG, "Ping status for %s timeout at %u with age %u", provider.first.c_str(), now, key_response_age); + ESP_LOGI(TAG, "Ping status for %s timeout at %" PRIu32 " with age %" PRIu32, provider.first.c_str(), now, + key_response_age); provider.second.status_sensor->publish_state(false); } #endif #ifdef USE_SENSOR - for (auto &sensor : this->remote_sensors_[provider.first]) { - sensor.second->publish_state(NAN); + auto it = this->remote_sensors_.find(provider.first); + if (it != this->remote_sensors_.end()) { + for (auto &val : it->second | std::views::values) { + val->publish_state(NAN); + } } #endif #ifdef USE_BINARY_SENSOR - for (auto &sensor : this->remote_binary_sensors_[provider.first]) { - sensor.second->invalidate_state(); + auto bs_it = this->remote_binary_sensors_.find(provider.first); + if (bs_it != this->remote_binary_sensors_.end()) { + for (auto &val : bs_it->second | std::views::values) { + val->invalidate_state(); + } } #endif } else { #ifdef USE_STATUS_SENSOR if (provider.second.status_sensor != nullptr && !provider.second.status_sensor->state) { - ESP_LOGI(TAG, "Ping status for %s restored at %u with age %u", provider.first.c_str(), now, key_response_age); + ESP_LOGI(TAG, "Ping status for %s restored at %" PRIu32 " with age %" PRIu32, provider.first.c_str(), now, + key_response_age); provider.second.status_sensor->publish_state(true); } #endif @@ -367,11 +377,16 @@ void PacketTransport::update() { void PacketTransport::add_key_(const char *name, uint32_t key) { if (!this->is_encrypted_()) return; - if (this->ping_keys_.count(name) == 0 && this->ping_keys_.size() == MAX_PING_KEYS) { - ESP_LOGW(TAG, "Ping key from %s discarded", name); - return; + auto it = this->ping_keys_.find(name); + if (it == this->ping_keys_.end()) { + if (this->ping_keys_.size() == MAX_PING_KEYS) { + ESP_LOGW(TAG, "Ping key from %s discarded", name); + return; + } + this->ping_keys_.emplace(name, key); // allocates string key once only + } else { + it->second = key; // key string already exists in map, no allocation } - this->ping_keys_[name] = key; this->updated_ = true; ESP_LOGV(TAG, "Ping key from %s now %X", name, (unsigned) key); } @@ -431,17 +446,19 @@ void PacketTransport::process_(std::span data) { return; } - if (this->providers_.count(namebuf) == 0) { + auto it = this->providers_.find(namebuf); + if (it == this->providers_.end()) { ESP_LOGVV(TAG, "Unknown hostname %s", namebuf); return; } + auto &provider = it->second; ESP_LOGV(TAG, "Found hostname %s", namebuf); #ifdef USE_SENSOR - auto &sensors = this->remote_sensors_[namebuf]; + auto &sensors = this->remote_sensors_.try_emplace(namebuf).first->second; #endif #ifdef USE_BINARY_SENSOR - auto &binary_sensors = this->remote_binary_sensors_[namebuf]; + auto &binary_sensors = this->remote_binary_sensors_.try_emplace(namebuf).first->second; #endif if (!decoder.bump_to(4)) { @@ -453,7 +470,6 @@ void PacketTransport::process_(std::span data) { return; } - auto &provider = this->providers_[namebuf]; // if encryption not used with this host, ping check is pointless since it would be easily spoofed. if (provider.encryption_key.empty()) ping_key_seen = true; @@ -495,16 +511,19 @@ void PacketTransport::process_(std::span data) { if (decoder.decode(BINARY_SENSOR_KEY, namebuf, sizeof(namebuf), byte) == DECODE_OK) { ESP_LOGV(TAG, "Got binary sensor %s %d", namebuf, byte); #ifdef USE_BINARY_SENSOR - if (binary_sensors.count(namebuf) != 0) - binary_sensors[namebuf]->publish_state(byte != 0); + auto bs = binary_sensors.find(namebuf); + if (bs != binary_sensors.end()) { + bs->second->publish_state(byte != 0); + } #endif continue; } if (decoder.decode(SENSOR_KEY, namebuf, sizeof(namebuf), rdata.u32) == DECODE_OK) { ESP_LOGV(TAG, "Got sensor %s %f", namebuf, rdata.f32); #ifdef USE_SENSOR - if (sensors.count(namebuf) != 0) - sensors[namebuf]->publish_state(rdata.f32); + auto sensor_it = sensors.find(namebuf); + if (sensor_it != sensors.end()) + sensor_it->second->publish_state(rdata.f32); #endif continue; } @@ -537,12 +556,18 @@ void PacketTransport::dump_config() { ESP_LOGCONFIG(TAG, " Remote host: %s", host.first.c_str()); ESP_LOGCONFIG(TAG, " Encrypted: %s", YESNO(!host.second.encryption_key.empty())); #ifdef USE_SENSOR - for (const auto &sensor : this->remote_sensors_[host.first.c_str()]) - ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.first.c_str()); + auto rs = this->remote_sensors_.find(host.first.c_str()); + if (rs != this->remote_sensors_.end()) { + for (const auto &key : rs->second | std::views::keys) + ESP_LOGCONFIG(TAG, " Sensor: %s", key.c_str()); + } #endif #ifdef USE_BINARY_SENSOR - for (const auto &sensor : this->remote_binary_sensors_[host.first.c_str()]) - ESP_LOGCONFIG(TAG, " Binary Sensor: %s", sensor.first.c_str()); + auto rbs = this->remote_binary_sensors_.find(host.first.c_str()); + if (rbs != this->remote_binary_sensors_.end()) { + for (const auto &key : rbs->second | std::views::keys) + ESP_LOGCONFIG(TAG, " Binary Sensor: %s", key.c_str()); + } #endif } } diff --git a/esphome/components/packet_transport/packet_transport.h b/esphome/components/packet_transport/packet_transport.h index a236744231..b3798738e2 100644 --- a/esphome/components/packet_transport/packet_transport.h +++ b/esphome/components/packet_transport/packet_transport.h @@ -24,6 +24,9 @@ namespace esphome { namespace packet_transport { +// std::less provides allocation-free comparison with const char * +template using string_map_t = std::map>; + struct Provider { std::vector encryption_key; const char *name; @@ -79,15 +82,15 @@ class PacketTransport : public PollingComponent { #endif void add_provider(const char *hostname) { - if (this->providers_.count(hostname) == 0) { + if (!this->providers_.contains(hostname)) { Provider provider{}; provider.name = hostname; this->providers_[hostname] = provider; #ifdef USE_SENSOR - this->remote_sensors_[hostname] = std::map(); + this->remote_sensors_[hostname] = string_map_t(); #endif #ifdef USE_BINARY_SENSOR - this->remote_binary_sensors_[hostname] = std::map(); + this->remote_binary_sensors_[hostname] = string_map_t(); #endif } } @@ -139,23 +142,23 @@ class PacketTransport : public PollingComponent { #ifdef USE_SENSOR std::vector sensors_{}; - std::map> remote_sensors_{}; + string_map_t> remote_sensors_{}; #endif #ifdef USE_BINARY_SENSOR std::vector binary_sensors_{}; - std::map> remote_binary_sensors_{}; + string_map_t> remote_binary_sensors_{}; #endif - std::map providers_{}; + string_map_t providers_{}; std::vector ping_header_{}; std::vector header_{}; std::vector data_{}; - std::map ping_keys_{}; + string_map_t ping_keys_{}; const char *platform_name_{""}; void add_key_(const char *name, uint32_t key); void send_ping_pong_request_(); - inline bool is_encrypted_() { return !this->encryption_key_.empty(); } + bool is_encrypted_() const { return !this->encryption_key_.empty(); } }; } // namespace packet_transport diff --git a/script/cpp_unit_test.py b/script/cpp_unit_test.py index 02b133060a..b87261ab33 100755 --- a/script/cpp_unit_test.py +++ b/script/cpp_unit_test.py @@ -12,6 +12,7 @@ from esphome.__main__ import command_compile, parse_args from esphome.config import validate_config from esphome.core import CORE from esphome.platformio_api import get_idedata +from esphome.yaml_util import load_yaml # This must coincide with the version in /platformio.ini PLATFORMIO_GOOGLE_TEST_LIB = "google/googletest@^1.15.2" @@ -44,6 +45,38 @@ def filter_components_without_tests(components: list[str]) -> list[str]: return filtered_components +# Name of optional per-component YAML config merged into the test build +# before validation so that platform defines (USE_SENSOR, etc.) are generated. +CPP_TEST_CONFIG_FILE = "cpp_test.yaml" + + +def load_component_test_configs(components: list[str]) -> dict: + """Load cpp_test.yaml files from test component directories. + + These configs are merged into the base test config *before* validation + so that entity registration runs during code generation, which causes + the corresponding USE_* defines to be emitted. + """ + merged: dict = {} + for component in components: + config_file = COMPONENTS_TESTS_DIR / component / CPP_TEST_CONFIG_FILE + if not config_file.exists(): + continue + component_config = load_yaml(config_file) + if not component_config: + continue + for key, value in component_config.items(): + if ( + key in merged + and isinstance(merged[key], list) + and isinstance(value, list) + ): + merged[key].extend(value) + else: + merged[key] = value + return merged + + def create_test_config(config_name: str, includes: list[str]) -> dict: """Create ESPHome test configuration for C++ unit tests. @@ -115,6 +148,11 @@ def run_tests(selected_components: list[str]) -> int: config = create_test_config(config_name, includes) + # Merge component-specific test configs (e.g. sensor instances) before + # validation so that entity registration and USE_* defines work. + extra_config = load_component_test_configs(components) + config.update(extra_config) + CORE.config_path = COMPONENTS_TESTS_DIR / "dummy.yaml" CORE.dashboard = None @@ -122,8 +160,10 @@ def run_tests(selected_components: list[str]) -> int: config = validate_config(config, {}) # Add all components and dependencies to the base configuration after validation, so their files - # are added to the build. - config.update({key: {} for key in components_with_dependencies}) + # are added to the build. Use setdefault to avoid overwriting entries that were + # already validated (e.g. sensor instances from cpp_test.yaml). + for key in components_with_dependencies: + config.setdefault(key, {}) print(f"Testing components: {', '.join(components)}") CORE.config = config diff --git a/tests/components/packet_transport/common.h b/tests/components/packet_transport/common.h new file mode 100644 index 0000000000..f8caa7bb68 --- /dev/null +++ b/tests/components/packet_transport/common.h @@ -0,0 +1,98 @@ +#pragma once +#include +#include +#include +#include +#include +#include "esphome/components/packet_transport/packet_transport.h" + +namespace esphome::packet_transport::testing { + +// Protocol constants mirrored from packet_transport.cpp for test packet construction. +static constexpr uint16_t MAGIC_NUMBER = 0x4553; +static constexpr uint16_t MAGIC_PING = 0x5048; + +// Concrete testable implementation of PacketTransport. +// Captures sent packets and exposes protected members for verification. +// +// Sensor round-trip tests require USE_SENSOR / USE_BINARY_SENSOR to be defined, +// which happens when 'sensor' and 'binary_sensor' components are in the build. +// Run with --all or include those components to enable the full test suite. +class TestablePacketTransport : public PacketTransport { + public: + using PacketTransport::add_key_; + using PacketTransport::data_; + using PacketTransport::encryption_key_; + using PacketTransport::flush_; + using PacketTransport::header_; + using PacketTransport::increment_code_; + using PacketTransport::init_data_; + using PacketTransport::is_encrypted_; + using PacketTransport::is_provider_; + using PacketTransport::name_; + using PacketTransport::ping_key_; + using PacketTransport::ping_keys_; + using PacketTransport::ping_pong_enable_; + using PacketTransport::ping_pong_recyle_time_; + using PacketTransport::process_; + using PacketTransport::providers_; + using PacketTransport::rolling_code_; + using PacketTransport::rolling_code_enable_; + using PacketTransport::send_data_; + using PacketTransport::updated_; +#ifdef USE_SENSOR + using PacketTransport::add_data_; + using PacketTransport::remote_sensors_; + using PacketTransport::sensors_; +#endif +#ifdef USE_BINARY_SENSOR + using PacketTransport::add_binary_data_; + using PacketTransport::binary_sensors_; + using PacketTransport::remote_binary_sensors_; +#endif + + // NOTE: std::vector is used here for test convenience. For production code, + // consider using StaticVector or FixedVector from esphome/core/helpers.h instead. + mutable std::vector> sent_packets; + size_t max_packet_size{512}; + bool send_enabled{true}; + + void send_packet(const std::vector &buf) const override { this->sent_packets.push_back(buf); } + size_t get_max_packet_size() override { return this->max_packet_size; } + bool should_send() override { return this->send_enabled; } + + /// Build the packet header for testing without requiring App or global_preferences. + void init_for_test(const char *name) { + this->name_ = name; + this->header_.clear(); + // MAGIC_NUMBER as uint16_t little-endian + this->header_.push_back(MAGIC_NUMBER & 0xFF); + this->header_.push_back((MAGIC_NUMBER >> 8) & 0xFF); + // Length-prefixed hostname + auto len = strlen(name); + this->header_.push_back(static_cast(len)); + for (size_t i = 0; i < len; i++) + this->header_.push_back(name[i]); + // Pad to 4-byte boundary + while (this->header_.size() & 0x3) + this->header_.push_back(0); + } +}; + +/// Build a MAGIC_PING packet for testing add_key_ / ping-pong flows. +inline std::vector build_ping_packet(const char *hostname, uint32_t key) { + std::vector packet; + packet.push_back(MAGIC_PING & 0xFF); + packet.push_back((MAGIC_PING >> 8) & 0xFF); + auto len = strlen(hostname); + packet.push_back(static_cast(len)); + for (size_t i = 0; i < len; i++) + packet.push_back(hostname[i]); + packet.push_back(key & 0xFF); + packet.push_back((key >> 8) & 0xFF); + packet.push_back((key >> 16) & 0xFF); + packet.push_back((key >> 24) & 0xFF); + return packet; +} + +} // namespace esphome::packet_transport::testing diff --git a/tests/components/packet_transport/cpp_test.yaml b/tests/components/packet_transport/cpp_test.yaml new file mode 100644 index 0000000000..fa39df3c0a --- /dev/null +++ b/tests/components/packet_transport/cpp_test.yaml @@ -0,0 +1,11 @@ +# Extra component configuration required by C++ unit tests. +# Loaded by cpp_unit_test.py and merged into the test build config +# before validation, so that platform defines (USE_SENSOR, etc.) are generated. + +sensor: + - platform: template + id: test_cpp_sensor + +binary_sensor: + - platform: template + id: test_cpp_binary_sensor diff --git a/tests/components/packet_transport/packet_transport_test.cpp b/tests/components/packet_transport/packet_transport_test.cpp new file mode 100644 index 0000000000..d8f11ca607 --- /dev/null +++ b/tests/components/packet_transport/packet_transport_test.cpp @@ -0,0 +1,445 @@ +#include "common.h" + +namespace esphome::packet_transport::testing { + +// --- Configuration setter tests --- + +TEST(PacketTransportTest, SetIsProvider) { + TestablePacketTransport transport; + transport.set_is_provider(true); + EXPECT_TRUE(transport.is_provider_); +} + +TEST(PacketTransportTest, SetEncryptionKey) { + TestablePacketTransport transport; + std::vector key(32, 0xAB); + transport.set_encryption_key(key); + EXPECT_EQ(transport.encryption_key_, key); + EXPECT_TRUE(transport.is_encrypted_()); +} + +TEST(PacketTransportTest, NoEncryptionByDefault) { + TestablePacketTransport transport; + EXPECT_FALSE(transport.is_encrypted_()); +} + +TEST(PacketTransportTest, SetRollingCodeEnable) { + TestablePacketTransport transport; + transport.set_rolling_code_enable(true); + EXPECT_TRUE(transport.rolling_code_enable_); +} + +TEST(PacketTransportTest, SetPingPongEnable) { + TestablePacketTransport transport; + transport.set_ping_pong_enable(true); + EXPECT_TRUE(transport.ping_pong_enable_); +} + +TEST(PacketTransportTest, SetPingPongRecycleTime) { + TestablePacketTransport transport; + transport.set_ping_pong_recycle_time(600); + EXPECT_EQ(transport.ping_pong_recyle_time_, 600u); +} + +// --- Provider management --- + +TEST(PacketTransportTest, AddProvider) { + TestablePacketTransport transport; + transport.add_provider("host1"); + EXPECT_TRUE(transport.providers_.contains("host1")); + EXPECT_EQ(transport.providers_.size(), 1u); +} + +TEST(PacketTransportTest, AddProviderDuplicate) { + TestablePacketTransport transport; + transport.add_provider("host1"); + transport.add_provider("host1"); + EXPECT_EQ(transport.providers_.size(), 1u); +} + +TEST(PacketTransportTest, SetProviderEncryption) { + TestablePacketTransport transport; + transport.add_provider("host1"); + std::vector key(32, 0xCD); + transport.set_provider_encryption("host1", key); + EXPECT_EQ(transport.providers_["host1"].encryption_key, key); +} + +// --- Sensor management (requires USE_SENSOR / USE_BINARY_SENSOR) --- + +#ifdef USE_SENSOR +TEST(PacketTransportTest, AddSensor) { + TestablePacketTransport transport; + sensor::Sensor s; + transport.add_sensor("temp", &s); + ASSERT_EQ(transport.sensors_.size(), 1u); + EXPECT_STREQ(transport.sensors_[0].id, "temp"); + EXPECT_EQ(transport.sensors_[0].sensor, &s); + EXPECT_TRUE(transport.sensors_[0].updated); +} + +TEST(PacketTransportTest, AddRemoteSensor) { + TestablePacketTransport transport; + sensor::Sensor s; + transport.add_remote_sensor("host1", "remote_temp", &s); + EXPECT_TRUE(transport.providers_.contains("host1")); + EXPECT_EQ(transport.remote_sensors_["host1"]["remote_temp"], &s); +} +#endif + +#ifdef USE_BINARY_SENSOR +TEST(PacketTransportTest, AddBinarySensor) { + TestablePacketTransport transport; + binary_sensor::BinarySensor bs; + transport.add_binary_sensor("motion", &bs); + ASSERT_EQ(transport.binary_sensors_.size(), 1u); + EXPECT_STREQ(transport.binary_sensors_[0].id, "motion"); + EXPECT_EQ(transport.binary_sensors_[0].sensor, &bs); +} + +TEST(PacketTransportTest, AddRemoteBinarySensor) { + TestablePacketTransport transport; + binary_sensor::BinarySensor bs; + transport.add_remote_binary_sensor("host1", "remote_motion", &bs); + EXPECT_TRUE(transport.providers_.contains("host1")); + EXPECT_EQ(transport.remote_binary_sensors_["host1"]["remote_motion"], &bs); +} +#endif + +// --- Unencrypted round-trip tests (require USE_SENSOR / USE_BINARY_SENSOR) --- + +#ifdef USE_SENSOR +TEST(PacketTransportTest, UnencryptedSensorRoundTrip) { + // Encoder + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + sensor::Sensor local_sensor; + local_sensor.state = 42.5f; + encoder.add_sensor("temp", &local_sensor); + + encoder.send_data_(true); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + // Decoder + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + sensor::Sensor remote_sensor; + remote_sensor.state = -999.0f; // sentinel + decoder.add_remote_sensor("sender", "temp", &remote_sensor); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + EXPECT_FLOAT_EQ(remote_sensor.state, 42.5f); +} +#endif + +#ifdef USE_BINARY_SENSOR +TEST(PacketTransportTest, UnencryptedBinarySensorRoundTrip) { + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + binary_sensor::BinarySensor local_bs; + local_bs.state = true; + encoder.add_binary_sensor("motion", &local_bs); + + encoder.send_data_(true); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + binary_sensor::BinarySensor remote_bs; + decoder.add_remote_binary_sensor("sender", "motion", &remote_bs); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + EXPECT_TRUE(remote_bs.state); +} +#endif + +#if defined(USE_SENSOR) && defined(USE_BINARY_SENSOR) +TEST(PacketTransportTest, MultipleSensorsRoundTrip) { + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + + sensor::Sensor s1, s2; + s1.state = 10.0f; + s2.state = 20.0f; + encoder.add_sensor("s1", &s1); + encoder.add_sensor("s2", &s2); + + binary_sensor::BinarySensor bs1; + bs1.state = true; + encoder.add_binary_sensor("bs1", &bs1); + + encoder.send_data_(true); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + sensor::Sensor rs1, rs2; + binary_sensor::BinarySensor rbs1; + rs1.state = -999.0f; + rs2.state = -999.0f; + decoder.add_remote_sensor("sender", "s1", &rs1); + decoder.add_remote_sensor("sender", "s2", &rs2); + decoder.add_remote_binary_sensor("sender", "bs1", &rbs1); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + + EXPECT_FLOAT_EQ(rs1.state, 10.0f); + EXPECT_FLOAT_EQ(rs2.state, 20.0f); + EXPECT_TRUE(rbs1.state); +} +#endif + +// --- Encrypted round-trip --- + +#ifdef USE_SENSOR +TEST(PacketTransportTest, EncryptedSensorRoundTrip) { + std::vector key(32); + for (int i = 0; i < 32; i++) + key[i] = i; + + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + encoder.set_encryption_key(key); + sensor::Sensor local_sensor; + local_sensor.state = 99.9f; + encoder.add_sensor("temp", &local_sensor); + + encoder.send_data_(true); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + sensor::Sensor remote_sensor; + remote_sensor.state = -999.0f; + decoder.add_remote_sensor("sender", "temp", &remote_sensor); + decoder.set_provider_encryption("sender", key); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + EXPECT_FLOAT_EQ(remote_sensor.state, 99.9f); +} + +// --- Selective send --- + +TEST(PacketTransportTest, SendDataOnlyUpdated) { + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + + sensor::Sensor s1, s2; + s1.state = 1.0f; + s2.state = 2.0f; + encoder.add_sensor("s1", &s1); + encoder.add_sensor("s2", &s2); + + // Mark s1 as not updated, only s2 as updated + encoder.sensors_[0].updated = false; + encoder.sensors_[1].updated = true; + + encoder.send_data_(false); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + sensor::Sensor rs1, rs2; + rs1.state = -999.0f; + rs2.state = -999.0f; + decoder.add_remote_sensor("sender", "s1", &rs1); + decoder.add_remote_sensor("sender", "s2", &rs2); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + + EXPECT_FLOAT_EQ(rs1.state, -999.0f); // not updated, not sent + EXPECT_FLOAT_EQ(rs2.state, 2.0f); // updated, sent +} +#endif + +// --- Ping key tests --- + +TEST(PacketTransportTest, PingKeyStoredWhenEncrypted) { + TestablePacketTransport transport; + transport.init_for_test("receiver"); + transport.set_encryption_key(std::vector(32, 0xAA)); + + auto ping = build_ping_packet("requester", 0xDEADBEEF); + transport.process_({ping.data(), ping.size()}); + + ASSERT_EQ(transport.ping_keys_.size(), 1u); + EXPECT_EQ(transport.ping_keys_["requester"], 0xDEADBEEFu); +} + +TEST(PacketTransportTest, PingKeyIgnoredWhenNotEncrypted) { + TestablePacketTransport transport; + transport.init_for_test("receiver"); + // No encryption key — add_key_ should be a no-op + + auto ping = build_ping_packet("requester", 0xDEADBEEF); + transport.process_({ping.data(), ping.size()}); + + EXPECT_TRUE(transport.ping_keys_.empty()); +} + +TEST(PacketTransportTest, PingKeyUpdatedOnRepeat) { + TestablePacketTransport transport; + transport.init_for_test("receiver"); + transport.set_encryption_key(std::vector(32, 0xAA)); + + auto ping1 = build_ping_packet("host1", 0x1111); + transport.process_({ping1.data(), ping1.size()}); + EXPECT_EQ(transport.ping_keys_["host1"], 0x1111u); + + // Same host, new key value — should update in place + auto ping2 = build_ping_packet("host1", 0x2222); + transport.process_({ping2.data(), ping2.size()}); + EXPECT_EQ(transport.ping_keys_.size(), 1u); + EXPECT_EQ(transport.ping_keys_["host1"], 0x2222u); +} + +TEST(PacketTransportTest, PingKeyMaxLimit) { + TestablePacketTransport transport; + transport.init_for_test("receiver"); + transport.set_encryption_key(std::vector(32, 0xAA)); + + // Fill to MAX_PING_KEYS (4) + for (int i = 0; i < 4; i++) { + char name[16]; + snprintf(name, sizeof(name), "host%d", i); + auto ping = build_ping_packet(name, 0x1000 + i); + transport.process_({ping.data(), ping.size()}); + } + EXPECT_EQ(transport.ping_keys_.size(), 4u); + + // 5th key should be discarded + auto ping = build_ping_packet("host4", 0x9999); + transport.process_({ping.data(), ping.size()}); + EXPECT_EQ(transport.ping_keys_.size(), 4u); + EXPECT_FALSE(transport.ping_keys_.contains("host4")); +} + +#ifdef USE_SENSOR +TEST(PacketTransportTest, PingKeyIncludedInTransmittedPacket) { + std::vector key(32, 0xBB); + + // Responder: encrypted, owns a sensor + TestablePacketTransport responder; + responder.init_for_test("responder"); + responder.set_encryption_key(key); + sensor::Sensor local_sensor; + local_sensor.state = 77.7f; + responder.add_sensor("temp", &local_sensor); + + // Requester sends a MAGIC_PING that the responder processes + auto ping = build_ping_packet("requester", 0xDEADBEEF); + responder.process_({ping.data(), ping.size()}); + ASSERT_EQ(responder.ping_keys_.size(), 1u); + + // Responder sends sensor data — ping key should be embedded + responder.send_data_(true); + ASSERT_EQ(responder.sent_packets.size(), 1u); + + // Requester: encrypted provider, ping-pong enabled, expects key 0xDEADBEEF + TestablePacketTransport requester; + requester.init_for_test("requester"); + requester.set_ping_pong_enable(true); + requester.ping_key_ = 0xDEADBEEF; + sensor::Sensor remote_sensor; + remote_sensor.state = -999.0f; + requester.add_remote_sensor("responder", "temp", &remote_sensor); + requester.set_provider_encryption("responder", key); + + // The requester decrypts the packet and finds its ping key echoed back, + // which gates the sensor data — if the key is missing, data is blocked. + auto &packet = responder.sent_packets[0]; + requester.process_({packet.data(), packet.size()}); + EXPECT_FLOAT_EQ(remote_sensor.state, 77.7f); +} + +TEST(PacketTransportTest, MissingPingKeyBlocksSensorData) { + std::vector key(32, 0xBB); + + // Responder sends data WITHOUT receiving any MAGIC_PING first — no ping keys + TestablePacketTransport responder; + responder.init_for_test("responder"); + responder.set_encryption_key(key); + sensor::Sensor local_sensor; + local_sensor.state = 77.7f; + responder.add_sensor("temp", &local_sensor); + responder.send_data_(true); + ASSERT_EQ(responder.sent_packets.size(), 1u); + + // Requester with ping-pong enabled expects a key that isn't in the packet + TestablePacketTransport requester; + requester.init_for_test("requester"); + requester.set_ping_pong_enable(true); + requester.ping_key_ = 0xDEADBEEF; + sensor::Sensor remote_sensor; + remote_sensor.state = -999.0f; + requester.add_remote_sensor("responder", "temp", &remote_sensor); + requester.set_provider_encryption("responder", key); + + auto &packet = responder.sent_packets[0]; + requester.process_({packet.data(), packet.size()}); + EXPECT_FLOAT_EQ(remote_sensor.state, -999.0f); // blocked — ping key not found +} +#endif + +// --- Process error handling --- + +TEST(PacketTransportTest, ProcessShortBuffer) { + TestablePacketTransport transport; + transport.init_for_test("receiver"); + uint8_t buf[] = {0x53}; + // Too short for a magic number - should return safely + transport.process_({buf, 1}); +} + +TEST(PacketTransportTest, ProcessBadMagic) { + TestablePacketTransport transport; + transport.init_for_test("receiver"); + uint8_t buf[] = {0xFF, 0xFF, 0x00, 0x00}; + // Wrong magic - should return safely + transport.process_({buf, sizeof(buf)}); +} + +TEST(PacketTransportTest, ProcessOwnHostname) { + TestablePacketTransport transport; + transport.init_for_test("myself"); + // Build a packet from "myself" using a separate encoder + TestablePacketTransport fake_sender; + fake_sender.init_for_test("myself"); + fake_sender.send_data_(true); + ASSERT_EQ(fake_sender.sent_packets.size(), 1u); + + auto &packet = fake_sender.sent_packets[0]; + // Should be silently ignored because hostname matches our own + transport.process_({packet.data(), packet.size()}); +} + +TEST(PacketTransportTest, ProcessUnknownHostname) { + TestablePacketTransport transport; + transport.init_for_test("receiver"); + // No providers registered - "unknown" will not be found + TestablePacketTransport sender; + sender.init_for_test("unknown"); + sender.send_data_(true); + ASSERT_EQ(sender.sent_packets.size(), 1u); + + auto &packet = sender.sent_packets[0]; + // Should return safely without crash + transport.process_({packet.data(), packet.size()}); +} + +// --- Send disabled --- + +TEST(PacketTransportTest, NoSendWhenDisabled) { + TestablePacketTransport transport; + transport.init_for_test("sender"); + transport.send_enabled = false; + transport.send_data_(true); + EXPECT_TRUE(transport.sent_packets.empty()); +} + +} // namespace esphome::packet_transport::testing From 0e2a10c5f02cd4ba37ed53dde370bdd0a54c43ac Mon Sep 17 00:00:00 2001 From: rwrozelle Date: Wed, 4 Mar 2026 19:34:13 -0800 Subject: [PATCH 360/609] [openthread] Cache is_connected() for cheap inline access (#14484) Co-authored-by: J. Nick Koston --- esphome/components/openthread/openthread.cpp | 25 ++++++------------- esphome/components/openthread/openthread.h | 5 +++- .../components/openthread/openthread_esp.cpp | 3 +++ 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 92897a7e96..9452f5a41e 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -1,9 +1,7 @@ #include "esphome/core/defines.h" #ifdef USE_OPENTHREAD #include "openthread.h" -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) #include "esp_openthread.h" -#endif #include @@ -48,22 +46,15 @@ void OpenThreadComponent::dump_config() { } } -bool OpenThreadComponent::is_connected() { - auto lock = InstanceLock::try_acquire(100); - if (!lock) { - ESP_LOGW(TAG, "Failed to acquire OpenThread lock in is_connected"); - return false; +void OpenThreadComponent::on_state_changed_(otChangedFlags flags, void *context) { + if (flags & OT_CHANGED_THREAD_ROLE) { + auto *self = static_cast(context); + // This runs on the OpenThread task thread with the OT lock held, + // so we can safely call otThreadGetDeviceRole directly. + otInstance *instance = esp_openthread_get_instance(); + otDeviceRole role = otThreadGetDeviceRole(instance); + self->connected_ = role >= OT_DEVICE_ROLE_CHILD; } - - otInstance *instance = lock->get_instance(); - if (instance == nullptr) { - return false; - } - - otDeviceRole role = otThreadGetDeviceRole(instance); - - // TODO: If we're a leader, check that there is at least 1 known peer - return role >= OT_DEVICE_ROLE_CHILD; } // Gets the off-mesh routable address diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 728847afa5..d853c58f95 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -26,7 +27,7 @@ class OpenThreadComponent : public Component { bool teardown() override; float get_setup_priority() const override { return setup_priority::WIFI; } - bool is_connected(); + bool is_connected() const { return this->connected_; } network::IPAddresses get_ip_addresses(); std::optional get_omr_address(); void ot_main(); @@ -42,6 +43,7 @@ class OpenThreadComponent : public Component { protected: std::optional get_omr_address_(InstanceLock &lock); + static void on_state_changed_(otChangedFlags flags, void *context); std::function factory_reset_external_callback_; #if CONFIG_OPENTHREAD_MTD uint32_t poll_period_{0}; @@ -49,6 +51,7 @@ class OpenThreadComponent : public Component { std::optional output_power_{}; bool teardown_started_{false}; bool teardown_complete_{false}; + bool connected_{false}; private: // Stores a pointer to a string literal (static storage duration). diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 2af78b729f..2296e32b7f 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -175,6 +175,9 @@ void OpenThreadComponent::ot_main() { // Pass the existing dataset, or NULL which will use the preprocessor definitions ESP_ERROR_CHECK(esp_openthread_auto_start(dataset.mLength > 0 ? &dataset : nullptr)); + // Register state change callback to update connected_ reactively instead of polling + otSetStateChangedCallback(instance, OpenThreadComponent::on_state_changed_, this); + esp_openthread_launch_mainloop(); // Clean up From c49c23d5d90ef0d4463479ffe8a2038252405d4f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Mar 2026 07:21:04 -1000 Subject: [PATCH 361/609] [network] Inline network::is_connected() and ethernet is_connected() (#14464) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../ethernet/ethernet_component.cpp | 2 - .../components/ethernet/ethernet_component.h | 2 +- esphome/components/network/util.cpp | 44 +----------------- esphome/components/network/util.h | 45 ++++++++++++++++++- 4 files changed, 46 insertions(+), 47 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index f855bc89cc..098f7be972 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -687,8 +687,6 @@ void EthernetComponent::start_connect_() { this->status_set_warning(); } -bool EthernetComponent::is_connected() { return this->state_ == EthernetComponentState::CONNECTED; } - void EthernetComponent::dump_connect_params_() { esp_netif_ip_info_t ip; esp_netif_get_ip_info(this->eth_netif_, &ip); diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 1cd44d2b2c..f5a31d78eb 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -76,7 +76,7 @@ class EthernetComponent : public Component { void dump_config() override; float get_setup_priority() const override; void on_powerdown() override { powerdown(); } - bool is_connected(); + bool is_connected() { return this->state_ == EthernetComponentState::CONNECTED; } #ifdef USE_ETHERNET_SPI void set_clk_pin(uint8_t clk_pin); diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index e397d77077..226b11b8cd 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -1,53 +1,11 @@ #include "util.h" #include "esphome/core/defines.h" #ifdef USE_NETWORK -#ifdef USE_WIFI -#include "esphome/components/wifi/wifi_component.h" -#endif - -#ifdef USE_ETHERNET -#include "esphome/components/ethernet/ethernet_component.h" -#endif - -#ifdef USE_OPENTHREAD -#include "esphome/components/openthread/openthread.h" -#endif - -#ifdef USE_MODEM -#include "esphome/components/modem/modem_component.h" -#endif namespace esphome::network { // The order of the components is important: WiFi should come after any possible main interfaces (it may be used as -// an AP that use a previous interface for NAT). - -bool is_connected() { -#ifdef USE_ETHERNET - if (ethernet::global_eth_component != nullptr && ethernet::global_eth_component->is_connected()) - return true; -#endif - -#ifdef USE_MODEM - if (modem::global_modem_component != nullptr) - return modem::global_modem_component->is_connected(); -#endif - -#ifdef USE_WIFI - if (wifi::global_wifi_component != nullptr) - return wifi::global_wifi_component->is_connected(); -#endif - -#ifdef USE_OPENTHREAD - if (openthread::global_openthread_component != nullptr) - return openthread::global_openthread_component->is_connected(); -#endif - -#ifdef USE_HOST - return true; // Assume its connected -#endif - return false; -} +// an AP that uses a previous interface for NAT). bool is_disabled() { #ifdef USE_MODEM diff --git a/esphome/components/network/util.h b/esphome/components/network/util.h index ae949ab0a8..4b700fe74c 100644 --- a/esphome/components/network/util.h +++ b/esphome/components/network/util.h @@ -2,12 +2,55 @@ #include "esphome/core/defines.h" #ifdef USE_NETWORK #include +#include "esphome/core/helpers.h" #include "ip_address.h" +#ifdef USE_ETHERNET +#include "esphome/components/ethernet/ethernet_component.h" +#endif +#ifdef USE_MODEM +#include "esphome/components/modem/modem_component.h" +#endif +#ifdef USE_WIFI +#include "esphome/components/wifi/wifi_component.h" +#endif +#ifdef USE_OPENTHREAD +#include "esphome/components/openthread/openthread.h" +#endif + namespace esphome::network { +// The order of the components is important: WiFi should come after any possible main interfaces (it may be used as +// an AP that uses a previous interface for NAT). + /// Return whether the node is connected to the network (through wifi, eth, ...) -bool is_connected(); +ESPHOME_ALWAYS_INLINE inline bool is_connected() { +#ifdef USE_ETHERNET + if (ethernet::global_eth_component != nullptr && ethernet::global_eth_component->is_connected()) + return true; +#endif + +#ifdef USE_MODEM + if (modem::global_modem_component != nullptr) + return modem::global_modem_component->is_connected(); +#endif + +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) + return wifi::global_wifi_component->is_connected(); +#endif + +#ifdef USE_OPENTHREAD + if (openthread::global_openthread_component != nullptr) + return openthread::global_openthread_component->is_connected(); +#endif + +#ifdef USE_HOST + return true; // Assume it's connected +#endif + return false; +} + /// Return whether the network is disabled (only wifi for now) bool is_disabled(); /// Get the active network hostname From 2777d359904e117190e10f353c3601e67012478f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Mar 2026 07:21:44 -1000 Subject: [PATCH 362/609] [api] Devirtualize frame helper calls when protocol is fixed at compile time (#14468) --- esphome/components/api/api_connection.cpp | 5 +++-- esphome/components/api/api_connection.h | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 59476fac25..98ba1abe0b 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -114,9 +114,10 @@ APIConnection::APIConnection(std::unique_ptr sock, APIServer *pa this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; } #elif defined(USE_API_PLAINTEXT) - this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; + this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; #elif defined(USE_API_NOISE) - this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; + this->helper_ = + std::unique_ptr{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; #else #error "No frame helper defined" #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 37855b2482..aae8db3c68 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -3,6 +3,12 @@ #include "esphome/core/defines.h" #ifdef USE_API #include "api_frame_helper.h" +#ifdef USE_API_NOISE +#include "api_frame_helper_noise.h" +#endif +#ifdef USE_API_PLAINTEXT +#include "api_frame_helper_plaintext.h" +#endif #include "api_pb2.h" #include "api_pb2_service.h" #include "api_server.h" @@ -489,7 +495,13 @@ class APIConnection final : public APIServerConnectionBase { // === Optimal member ordering for 32-bit systems === // Group 1: Pointers (4 bytes each on 32-bit) +#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT) std::unique_ptr helper_; +#elif defined(USE_API_NOISE) + std::unique_ptr helper_; +#elif defined(USE_API_PLAINTEXT) + std::unique_ptr helper_; +#endif APIServer *parent_; // Group 2: Iterator union (saves ~16 bytes vs separate iterators) From a061397469da387ea8086fb494fcfe61861c222b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:16:06 -0500 Subject: [PATCH 363/609] [dfrobot_sen0395][sx1509] Fix structural bugs (#14494) Co-authored-by: Claude Opus 4.6 --- esphome/components/dfrobot_sen0395/commands.h | 14 ++++++-------- esphome/components/sx1509/sx1509.cpp | 6 +++--- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/esphome/components/dfrobot_sen0395/commands.h b/esphome/components/dfrobot_sen0395/commands.h index 3b0551b184..95167efb4d 100644 --- a/esphome/components/dfrobot_sen0395/commands.h +++ b/esphome/components/dfrobot_sen0395/commands.h @@ -30,11 +30,9 @@ class Command { class ReadStateCommand : public Command { public: + ReadStateCommand() { timeout_ms_ = 500; } uint8_t execute(DfrobotSen0395Component *parent) override; uint8_t on_message(std::string &message) override; - - protected: - uint32_t timeout_ms_{500}; }; class PowerCommand : public Command { @@ -99,12 +97,12 @@ class ResetSystemCommand : public Command { class SaveCfgCommand : public Command { public: - SaveCfgCommand() { cmd_ = "saveCfg 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89"; } + SaveCfgCommand() { + cmd_ = "saveCfg 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89"; + cmd_duration_ms_ = 3000; + timeout_ms_ = 3500; + } uint8_t on_message(std::string &message) override; - - protected: - uint32_t cmd_duration_ms_{3000}; - uint32_t timeout_ms_{3500}; }; class LedModeCommand : public Command { diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp index 746ec9cda3..dfe1277297 100644 --- a/esphome/components/sx1509/sx1509.cpp +++ b/esphome/components/sx1509/sx1509.cpp @@ -56,11 +56,11 @@ void SX1509Component::loop() { return; } int row, col; - for (row = 0; row < 7; row++) { + for (row = 0; row < 8; row++) { if (key_data & (1 << row)) break; } - for (col = 8; col < 15; col++) { + for (col = 8; col < 16; col++) { if (key_data & (1 << col)) break; } @@ -229,7 +229,7 @@ void SX1509Component::setup_keypad_() { this->read_byte_16(REG_DIR_B, &this->ddr_mask_); for (int i = 0; i < this->rows_; i++) this->ddr_mask_ &= ~(1 << i); - for (int i = 8; i < (this->cols_ * 2); i++) + for (int i = 8; i < (8 + this->cols_); i++) this->ddr_mask_ |= (1 << i); this->write_byte_16(REG_DIR_B, this->ddr_mask_); From 01f4275202a8e9e82b2b7486ceefe18f5ffa1238 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:16:33 -0500 Subject: [PATCH 364/609] [veml7700] Fix initial settling timeout using raw enum instead of milliseconds (#14487) Co-authored-by: Claude Opus 4.6 --- esphome/components/veml7700/veml7700.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/veml7700/veml7700.cpp b/esphome/components/veml7700/veml7700.cpp index eb286ba21b..1ed484119b 100644 --- a/esphome/components/veml7700/veml7700.cpp +++ b/esphome/components/veml7700/veml7700.cpp @@ -141,7 +141,7 @@ void VEML7700Component::loop() { // Datasheet: 2.5 ms before the first measurement is needed, allowing for the correct start of the signal processor // and oscillator. // Reality: wait for couple integration times to have first samples captured - this->set_timeout(2 * this->integration_time_, [this]() { this->state_ = State::IDLE; }); + this->set_timeout(2 * get_itime_ms(this->integration_time_), [this]() { this->state_ = State::IDLE; }); } if (this->is_ready()) { From 3df4ef9362cb7843f91c28cb2cfef1691dac8c52 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:31:26 -0500 Subject: [PATCH 365/609] [ssd1322][ssd1325][ssd1327] Fix nibble mask bug in grayscale draw_pixel (#14496) Co-authored-by: Claude Opus 4.6 --- esphome/components/ssd1322_base/ssd1322_base.cpp | 2 +- esphome/components/ssd1325_base/ssd1325_base.cpp | 2 +- esphome/components/ssd1327_base/ssd1327_base.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/ssd1322_base/ssd1322_base.cpp b/esphome/components/ssd1322_base/ssd1322_base.cpp index 23576e7b2c..1fce826ad9 100644 --- a/esphome/components/ssd1322_base/ssd1322_base.cpp +++ b/esphome/components/ssd1322_base/ssd1322_base.cpp @@ -169,7 +169,7 @@ void HOT SSD1322::draw_absolute_pixel_internal(int x, int y, Color color) { // ensure 'color4' is valid (only 4 bits aka 1 nibble) and shift the bits left when necessary color4 = (color4 & SSD1322_COLORMASK) << shift; // first mask off the nibble we must change... - this->buffer_[pos] &= (~SSD1322_COLORMASK >> shift); + this->buffer_[pos] &= (static_cast(~SSD1322_COLORMASK) >> shift); // ...then lay the new nibble back on top. done! this->buffer_[pos] |= color4; } diff --git a/esphome/components/ssd1325_base/ssd1325_base.cpp b/esphome/components/ssd1325_base/ssd1325_base.cpp index e7d2386ac7..fe7df9674b 100644 --- a/esphome/components/ssd1325_base/ssd1325_base.cpp +++ b/esphome/components/ssd1325_base/ssd1325_base.cpp @@ -202,7 +202,7 @@ void HOT SSD1325::draw_absolute_pixel_internal(int x, int y, Color color) { // ensure 'color4' is valid (only 4 bits aka 1 nibble) and shift the bits left when necessary color4 = (color4 & SSD1325_COLORMASK) << shift; // first mask off the nibble we must change... - this->buffer_[pos] &= (~SSD1325_COLORMASK >> shift); + this->buffer_[pos] &= (static_cast(~SSD1325_COLORMASK) >> shift); // ...then lay the new nibble back on top. done! this->buffer_[pos] |= color4; } diff --git a/esphome/components/ssd1327_base/ssd1327_base.cpp b/esphome/components/ssd1327_base/ssd1327_base.cpp index 2498bfcd67..87e52206f2 100644 --- a/esphome/components/ssd1327_base/ssd1327_base.cpp +++ b/esphome/components/ssd1327_base/ssd1327_base.cpp @@ -145,7 +145,7 @@ void HOT SSD1327::draw_absolute_pixel_internal(int x, int y, Color color) { // ensure 'color4' is valid (only 4 bits aka 1 nibble) and shift the bits left when necessary color4 = (color4 & SSD1327_COLORMASK) << shift; // first mask off the nibble we must change... - this->buffer_[pos] &= (~SSD1327_COLORMASK >> shift); + this->buffer_[pos] &= (static_cast(~SSD1327_COLORMASK) >> shift); // ...then lay the new nibble back on top. done! this->buffer_[pos] |= color4; } From 4a5d8449fd28aa80b801dc270a3f2e9e20344794 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:33:12 -0500 Subject: [PATCH 366/609] [sht4x][grove_tb6612fng] Fix logic bugs (#14497) Co-authored-by: Claude Opus 4.6 --- esphome/components/grove_tb6612fng/grove_tb6612fng.cpp | 2 +- esphome/components/sht4x/sht4x.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp index a249984647..428c8ec4a8 100644 --- a/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp @@ -131,7 +131,7 @@ void GroveMotorDriveTB6612FNG::stepper_run(StepperModeTypeT mode, int16_t steps, buffer_[4] = ms_per_step; buffer_[5] = (ms_per_step >> 8); - if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN, buffer_, 1) != i2c::ERROR_OK) { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN, buffer_, 6) != i2c::ERROR_OK) { ESP_LOGW(TAG, "Run stepper failed!"); this->status_set_warning(); return; diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 9d29746f0b..42be326202 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -10,7 +10,7 @@ static const uint8_t MEASURECOMMANDS[] = {0xFD, 0xF6, 0xE0}; static const uint8_t SERIAL_NUMBER_COMMAND = 0x89; void SHT4XComponent::start_heater_() { - uint8_t cmd[] = {MEASURECOMMANDS[this->heater_command_]}; + uint8_t cmd[] = {this->heater_command_}; ESP_LOGD(TAG, "Heater turning on"); if (this->write(cmd, 1) != i2c::ERROR_OK) { From 9518d88a2aa7a5f7d5b263ffabeddeebd8f9d17d Mon Sep 17 00:00:00 2001 From: rwrozelle Date: Thu, 5 Mar 2026 10:35:20 -0800 Subject: [PATCH 367/609] [openthread] static log level code quality improvement (#14456) Co-authored-by: J. Nick Koston --- esphome/components/openthread/__init__.py | 21 +++++++++++++++++++ .../openthread/test.esp32-c6-idf.yaml | 6 ++++++ 2 files changed, 27 insertions(+) diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 5c64cf31dc..21373b77df 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -14,9 +14,12 @@ import esphome.config_validation as cv from esphome.const import ( CONF_CHANNEL, CONF_ENABLE_IPV6, + CONF_FRAMEWORK, CONF_ID, + CONF_LOG_LEVEL, CONF_OUTPUT_POWER, CONF_USE_ADDRESS, + PLATFORM_ESP32, ) from esphome.core import CORE, TimePeriodMilliseconds import esphome.final_validate as fv @@ -46,6 +49,15 @@ AUTO_LOAD = ["network"] CONFLICTS_WITH = ["wifi"] DEPENDENCIES = ["esp32"] +IDF_TO_OT_LOG_LEVEL = { + "NONE": "NONE", + "ERROR": "CRIT", + "WARN": "WARN", + "INFO": "NOTE", + "DEBUG": "INFO", + "VERBOSE": "DEBG", +} + CONF_DEVICE_TYPES = [ "FTD", "MTD", @@ -198,6 +210,15 @@ def _final_validate(_): "Please set `enable_ipv6: true` in the `network` configuration." ) + if ( + (esp32_config := full_config.get(PLATFORM_ESP32)) is not None + and (fw_config := esp32_config.get(CONF_FRAMEWORK)) is not None + and (log_level := fw_config.get(CONF_LOG_LEVEL)) is not None + ): + add_idf_sdkconfig_option("CONFIG_OPENTHREAD_LOG_LEVEL_DYNAMIC", False) + ot_log_level = IDF_TO_OT_LOG_LEVEL.get(log_level, log_level) + add_idf_sdkconfig_option(f"CONFIG_OPENTHREAD_LOG_LEVEL_{ot_log_level}", True) + FINAL_VALIDATE_SCHEMA = _final_validate diff --git a/tests/components/openthread/test.esp32-c6-idf.yaml b/tests/components/openthread/test.esp32-c6-idf.yaml index 77abc433c1..008edd5397 100644 --- a/tests/components/openthread/test.esp32-c6-idf.yaml +++ b/tests/components/openthread/test.esp32-c6-idf.yaml @@ -1,3 +1,9 @@ +esp32: + board: esp32-c6-devkitc-1 + framework: + type: esp-idf + log_level: DEBUG + network: enable_ipv6: true From e1d0c6da09ecad433c77ff8efdad60c730f020d8 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:09:23 -0500 Subject: [PATCH 368/609] [dfplayer][ufire_ise][ufire_ec][qmp6988][atm90e26] Fix wrong operators and masks (#14491) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/atm90e26/atm90e26.cpp | 2 +- esphome/components/dfplayer/dfplayer.cpp | 1 + esphome/components/qmp6988/qmp6988.cpp | 2 +- esphome/components/ufire_ec/ufire_ec.cpp | 2 +- esphome/components/ufire_ise/ufire_ise.cpp | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/esphome/components/atm90e26/atm90e26.cpp b/esphome/components/atm90e26/atm90e26.cpp index 2203dd0d71..e6602411bb 100644 --- a/esphome/components/atm90e26/atm90e26.cpp +++ b/esphome/components/atm90e26/atm90e26.cpp @@ -197,7 +197,7 @@ float ATM90E26Component::get_reactive_power_() { float ATM90E26Component::get_power_factor_() { const uint16_t val = this->read16_(ATM90E26_REGISTER_POWERF); // signed if (val & 0x8000) { - return -(val & 0x7FF) / 1000.0f; + return -(val & 0x7FFF) / 1000.0f; } else { return val / 1000.0f; } diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index 79f8fd03c3..1e1c33adaf 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -260,6 +260,7 @@ void DFPlayer::loop() { ESP_LOGV(TAG, "Playback finished (USB drive)"); this->is_playing_ = false; this->on_finished_playback_callback_.call(); + break; case 0x3D: ESP_LOGV(TAG, "Playback finished (SD card)"); this->is_playing_ = false; diff --git a/esphome/components/qmp6988/qmp6988.cpp b/esphome/components/qmp6988/qmp6988.cpp index 24fe34e785..17d91c3633 100644 --- a/esphome/components/qmp6988/qmp6988.cpp +++ b/esphome/components/qmp6988/qmp6988.cpp @@ -251,7 +251,7 @@ void QMP6988Component::set_power_mode_(uint8_t power_mode) { void QMP6988Component::write_filter_(QMP6988IIRFilter filter) { uint8_t data; - data = (filter & 0x03); + data = (filter & QMP6988_CONFIG_REG_FILTER_MSK); this->write_byte(QMP6988_CONFIG_REG, data); delay(10); } diff --git a/esphome/components/ufire_ec/ufire_ec.cpp b/esphome/components/ufire_ec/ufire_ec.cpp index 3868dc92b7..a1c3568a1a 100644 --- a/esphome/components/ufire_ec/ufire_ec.cpp +++ b/esphome/components/ufire_ec/ufire_ec.cpp @@ -8,7 +8,7 @@ static const char *const TAG = "ufire_ec"; void UFireECComponent::setup() { uint8_t version; - if (!this->read_byte(REGISTER_VERSION, &version) && version != 0xFF) { + if (!this->read_byte(REGISTER_VERSION, &version) || version == 0xFF) { this->mark_failed(); return; } diff --git a/esphome/components/ufire_ise/ufire_ise.cpp b/esphome/components/ufire_ise/ufire_ise.cpp index 486a506391..e967fc53c3 100644 --- a/esphome/components/ufire_ise/ufire_ise.cpp +++ b/esphome/components/ufire_ise/ufire_ise.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "ufire_ise"; void UFireISEComponent::setup() { uint8_t version; - if (!this->read_byte(REGISTER_VERSION, &version) && version != 0xFF) { + if (!this->read_byte(REGISTER_VERSION, &version) || version == 0xFF) { this->mark_failed(); return; } From cce7a09fa995692dae38a4fdecc34bd297c167a0 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:09:34 -0500 Subject: [PATCH 369/609] [pn532_spi] Fix preamble check logic and OOB access when full_len is zero (#14486) Co-authored-by: Claude Opus 4.6 --- esphome/components/pn532_spi/pn532_spi.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/esphome/components/pn532_spi/pn532_spi.cpp b/esphome/components/pn532_spi/pn532_spi.cpp index 118421c47f..553c6d26a6 100644 --- a/esphome/components/pn532_spi/pn532_spi.cpp +++ b/esphome/components/pn532_spi/pn532_spi.cpp @@ -88,9 +88,10 @@ bool PN532Spi::read_response(uint8_t command, std::vector &data) { #endif ESP_LOGV(TAG, "Header data: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), header.data(), header.size())); - if (header[0] != 0x00 && header[1] != 0x00 && header[2] != 0xFF) { + if (header[0] != 0x00 || header[1] != 0x00 || header[2] != 0xFF) { // invalid packet ESP_LOGV(TAG, "read data invalid preamble!"); + this->disable(); return false; } @@ -100,15 +101,20 @@ bool PN532Spi::read_response(uint8_t command, std::vector &data) { if (!valid_header) { ESP_LOGV(TAG, "read data invalid header!"); + this->disable(); return false; } - // full length of message, including command response + // full length of message, including command response (minimum 2: TFI + command response) uint8_t full_len = header[3]; + if (full_len < 2) { + ESP_LOGV(TAG, "read data has no payload"); + this->disable(); + return false; + } + // length of data, excluding command response uint8_t len = full_len - 1; - if (full_len == 0) - len = 0; ESP_LOGV(TAG, "Reading response of length %d", len); From e210e414bd0ed93c678ecdc3e8895918a517716d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Mar 2026 09:15:02 -1000 Subject: [PATCH 370/609] [ota] Devirtualize OTA backend calls (#14473) --- esphome/components/esphome/ota/ota_esphome.h | 4 +-- .../http_request/ota/ota_http_request.cpp | 7 +---- .../http_request/ota/ota_http_request.h | 4 +-- esphome/components/ota/ota_backend.h | 13 --------- .../ota/ota_backend_arduino_libretiny.cpp | 2 +- .../ota/ota_backend_arduino_libretiny.h | 16 ++++++----- .../ota/ota_backend_arduino_rp2040.cpp | 2 +- .../ota/ota_backend_arduino_rp2040.h | 16 ++++++----- .../components/ota/ota_backend_esp8266.cpp | 2 +- esphome/components/ota/ota_backend_esp8266.h | 16 ++++++----- .../components/ota/ota_backend_esp_idf.cpp | 2 +- esphome/components/ota/ota_backend_esp_idf.h | 16 ++++++----- esphome/components/ota/ota_backend_factory.h | 27 +++++++++++++++++++ esphome/components/ota/ota_backend_host.cpp | 2 +- esphome/components/ota/ota_backend_host.h | 16 ++++++----- .../web_server/ota/ota_web_server.cpp | 4 +-- 16 files changed, 84 insertions(+), 65 deletions(-) create mode 100644 esphome/components/ota/ota_backend_factory.h diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 08edacad92..f3a5952398 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -2,7 +2,7 @@ #include "esphome/core/defines.h" #ifdef USE_OTA -#include "esphome/components/ota/ota_backend.h" +#include "esphome/components/ota/ota_backend_factory.h" #include "esphome/components/socket/socket.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -86,7 +86,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { socket::ListenSocket *server_{nullptr}; std::unique_ptr client_; - std::unique_ptr backend_; + ota::OTABackendPtr backend_; uint32_t client_connect_time_{0}; uint16_t port_; diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 0db3a50b47..5dd21c314c 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -8,10 +8,6 @@ #include "esphome/components/md5/md5.h" #include "esphome/components/watchdog/watchdog.h" -#include "esphome/components/ota/ota_backend.h" -#include "esphome/components/ota/ota_backend_esp8266.h" -#include "esphome/components/ota/ota_backend_arduino_rp2040.h" -#include "esphome/components/ota/ota_backend_esp_idf.h" namespace esphome { namespace http_request { @@ -69,8 +65,7 @@ void OtaHttpRequestComponent::flash() { } } -void OtaHttpRequestComponent::cleanup_(std::unique_ptr backend, - const std::shared_ptr &container) { +void OtaHttpRequestComponent::cleanup_(ota::OTABackendPtr backend, const std::shared_ptr &container) { if (this->update_started_) { ESP_LOGV(TAG, "Aborting OTA backend"); backend->abort(); diff --git a/esphome/components/http_request/ota/ota_http_request.h b/esphome/components/http_request/ota/ota_http_request.h index 6d39b0d466..70e4559fa7 100644 --- a/esphome/components/http_request/ota/ota_http_request.h +++ b/esphome/components/http_request/ota/ota_http_request.h @@ -1,6 +1,6 @@ #pragma once -#include "esphome/components/ota/ota_backend.h" +#include "esphome/components/ota/ota_backend_factory.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" @@ -39,7 +39,7 @@ class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented< void flash(); protected: - void cleanup_(std::unique_ptr backend, const std::shared_ptr &container); + void cleanup_(ota::OTABackendPtr backend, const std::shared_ptr &container); uint8_t do_ota_(); std::string get_url_with_auth_(const std::string &url); bool http_get_md5_(); diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index e03afd4fc6..bc603a6e9e 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -49,17 +49,6 @@ enum OTAState { OTA_ERROR, }; -class OTABackend { - public: - virtual ~OTABackend() = default; - virtual OTAResponseTypes begin(size_t image_size) = 0; - virtual void set_update_md5(const char *md5) = 0; - virtual OTAResponseTypes write(uint8_t *data, size_t len) = 0; - virtual OTAResponseTypes end() = 0; - virtual void abort() = 0; - virtual bool supports_compression() = 0; -}; - /** Listener interface for OTA state changes. * * Components can implement this interface to receive OTA state updates @@ -130,7 +119,5 @@ OTAGlobalCallback *get_global_ota_callback(); // - notify_state_deferred_() when in separate task (e.g., web_server OTA) // This ensures proper listener execution in all contexts. #endif -std::unique_ptr make_ota_backend(); - } // namespace ota } // namespace esphome diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.cpp b/esphome/components/ota/ota_backend_arduino_libretiny.cpp index b4ecad1227..d364f75007 100644 --- a/esphome/components/ota/ota_backend_arduino_libretiny.cpp +++ b/esphome/components/ota/ota_backend_arduino_libretiny.cpp @@ -12,7 +12,7 @@ namespace ota { static const char *const TAG = "ota.arduino_libretiny"; -std::unique_ptr make_ota_backend() { return make_unique(); } +std::unique_ptr make_ota_backend() { return make_unique(); } OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size) { // Handle UPDATE_SIZE_UNKNOWN (0) which is used by web server OTA diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.h b/esphome/components/ota/ota_backend_arduino_libretiny.h index 8f9d268eec..4514bf84bd 100644 --- a/esphome/components/ota/ota_backend_arduino_libretiny.h +++ b/esphome/components/ota/ota_backend_arduino_libretiny.h @@ -7,19 +7,21 @@ namespace esphome { namespace ota { -class ArduinoLibreTinyOTABackend final : public OTABackend { +class ArduinoLibreTinyOTABackend final { public: - OTAResponseTypes begin(size_t image_size) override; - void set_update_md5(const char *md5) override; - OTAResponseTypes write(uint8_t *data, size_t len) override; - OTAResponseTypes end() override; - void abort() override; - bool supports_compression() override { return false; } + OTAResponseTypes begin(size_t image_size); + void set_update_md5(const char *md5); + OTAResponseTypes write(uint8_t *data, size_t len); + OTAResponseTypes end(); + void abort(); + bool supports_compression() { return false; } private: bool md5_set_{false}; }; +std::unique_ptr make_ota_backend(); + } // namespace ota } // namespace esphome diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.cpp b/esphome/components/ota/ota_backend_arduino_rp2040.cpp index ee1ba48d50..e2a57ec665 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.cpp +++ b/esphome/components/ota/ota_backend_arduino_rp2040.cpp @@ -14,7 +14,7 @@ namespace ota { static const char *const TAG = "ota.arduino_rp2040"; -std::unique_ptr make_ota_backend() { return make_unique(); } +std::unique_ptr make_ota_backend() { return make_unique(); } OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) { // OTA size of 0 is not currently handled, but diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.h b/esphome/components/ota/ota_backend_arduino_rp2040.h index 6a708f9c57..0956cb4b4b 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.h +++ b/esphome/components/ota/ota_backend_arduino_rp2040.h @@ -9,19 +9,21 @@ namespace esphome { namespace ota { -class ArduinoRP2040OTABackend final : public OTABackend { +class ArduinoRP2040OTABackend final { public: - OTAResponseTypes begin(size_t image_size) override; - void set_update_md5(const char *md5) override; - OTAResponseTypes write(uint8_t *data, size_t len) override; - OTAResponseTypes end() override; - void abort() override; - bool supports_compression() override { return false; } + OTAResponseTypes begin(size_t image_size); + void set_update_md5(const char *md5); + OTAResponseTypes write(uint8_t *data, size_t len); + OTAResponseTypes end(); + void abort(); + bool supports_compression() { return false; } private: bool md5_set_{false}; }; +std::unique_ptr make_ota_backend(); + } // namespace ota } // namespace esphome diff --git a/esphome/components/ota/ota_backend_esp8266.cpp b/esphome/components/ota/ota_backend_esp8266.cpp index 4b84708cd9..1f9a77e426 100644 --- a/esphome/components/ota/ota_backend_esp8266.cpp +++ b/esphome/components/ota/ota_backend_esp8266.cpp @@ -48,7 +48,7 @@ namespace esphome::ota { static const char *const TAG = "ota.esp8266"; -std::unique_ptr make_ota_backend() { return make_unique(); } +std::unique_ptr make_ota_backend() { return make_unique(); } OTAResponseTypes ESP8266OTABackend::begin(size_t image_size) { // Handle UPDATE_SIZE_UNKNOWN (0) by calculating available space diff --git a/esphome/components/ota/ota_backend_esp8266.h b/esphome/components/ota/ota_backend_esp8266.h index 52f657f006..6213289acc 100644 --- a/esphome/components/ota/ota_backend_esp8266.h +++ b/esphome/components/ota/ota_backend_esp8266.h @@ -12,15 +12,15 @@ namespace esphome::ota { /// OTA backend for ESP8266 using native SDK functions. /// This implementation bypasses the Arduino Updater library to save ~228 bytes of RAM /// by not having a global Update object in .bss. -class ESP8266OTABackend final : public OTABackend { +class ESP8266OTABackend final { public: - OTAResponseTypes begin(size_t image_size) override; - void set_update_md5(const char *md5) override; - OTAResponseTypes write(uint8_t *data, size_t len) override; - OTAResponseTypes end() override; - void abort() override; + OTAResponseTypes begin(size_t image_size); + void set_update_md5(const char *md5); + OTAResponseTypes write(uint8_t *data, size_t len); + OTAResponseTypes end(); + void abort(); // Compression supported in all ESP8266 Arduino versions ESPHome supports (>= 2.7.0) - bool supports_compression() override { return true; } + bool supports_compression() { return true; } protected: /// Erase flash sector if current address is at sector boundary @@ -54,5 +54,7 @@ class ESP8266OTABackend final : public OTABackend { bool md5_set_{false}; }; +std::unique_ptr make_ota_backend(); + } // namespace esphome::ota #endif // USE_ESP8266 diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index 93c65a9624..925bb39645 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -11,7 +11,7 @@ namespace esphome { namespace ota { -std::unique_ptr make_ota_backend() { return make_unique(); } +std::unique_ptr make_ota_backend() { return make_unique(); } OTAResponseTypes IDFOTABackend::begin(size_t image_size) { #ifdef USE_OTA_ROLLBACK diff --git a/esphome/components/ota/ota_backend_esp_idf.h b/esphome/components/ota/ota_backend_esp_idf.h index 7f7f6115c5..a0f538afc0 100644 --- a/esphome/components/ota/ota_backend_esp_idf.h +++ b/esphome/components/ota/ota_backend_esp_idf.h @@ -10,14 +10,14 @@ namespace esphome { namespace ota { -class IDFOTABackend final : public OTABackend { +class IDFOTABackend final { public: - OTAResponseTypes begin(size_t image_size) override; - void set_update_md5(const char *md5) override; - OTAResponseTypes write(uint8_t *data, size_t len) override; - OTAResponseTypes end() override; - void abort() override; - bool supports_compression() override { return false; } + OTAResponseTypes begin(size_t image_size); + void set_update_md5(const char *md5); + OTAResponseTypes write(uint8_t *data, size_t len); + OTAResponseTypes end(); + void abort(); + bool supports_compression() { return false; } private: esp_ota_handle_t update_handle_{0}; @@ -27,6 +27,8 @@ class IDFOTABackend final : public OTABackend { bool md5_set_{false}; }; +std::unique_ptr make_ota_backend(); + } // namespace ota } // namespace esphome #endif // USE_ESP32 diff --git a/esphome/components/ota/ota_backend_factory.h b/esphome/components/ota/ota_backend_factory.h new file mode 100644 index 0000000000..7c79f02702 --- /dev/null +++ b/esphome/components/ota/ota_backend_factory.h @@ -0,0 +1,27 @@ +#pragma once + +#include "ota_backend.h" + +#include + +#ifdef USE_ESP8266 +#include "ota_backend_esp8266.h" +#elif defined(USE_ESP32) +#include "ota_backend_esp_idf.h" +#elif defined(USE_RP2040) +#include "ota_backend_arduino_rp2040.h" +#elif defined(USE_LIBRETINY) +#include "ota_backend_arduino_libretiny.h" +#elif defined(USE_HOST) +#include "ota_backend_host.h" +#else +// Stub for static analysis when no platform is defined +namespace esphome::ota { +struct StubOTABackend {}; +std::unique_ptr make_ota_backend(); +} // namespace esphome::ota +#endif + +namespace esphome::ota { +using OTABackendPtr = decltype(make_ota_backend()); +} // namespace esphome::ota diff --git a/esphome/components/ota/ota_backend_host.cpp b/esphome/components/ota/ota_backend_host.cpp index ddab174bed..2e2132418d 100644 --- a/esphome/components/ota/ota_backend_host.cpp +++ b/esphome/components/ota/ota_backend_host.cpp @@ -8,7 +8,7 @@ namespace esphome::ota { // Stub implementation - OTA is not supported on host platform. // All methods return error codes to allow compilation of configs with OTA triggers. -std::unique_ptr make_ota_backend() { return make_unique(); } +std::unique_ptr make_ota_backend() { return make_unique(); } OTAResponseTypes HostOTABackend::begin(size_t image_size) { return OTA_RESPONSE_ERROR_UPDATE_PREPARE; } diff --git a/esphome/components/ota/ota_backend_host.h b/esphome/components/ota/ota_backend_host.h index 5a2dcfcf39..300facf72f 100644 --- a/esphome/components/ota/ota_backend_host.h +++ b/esphome/components/ota/ota_backend_host.h @@ -7,15 +7,17 @@ namespace esphome::ota { /// Stub OTA backend for host platform - allows compilation but does not implement OTA. /// All operations return error codes immediately. This enables configurations with /// OTA triggers to compile for host platform during development. -class HostOTABackend final : public OTABackend { +class HostOTABackend final { public: - OTAResponseTypes begin(size_t image_size) override; - void set_update_md5(const char *md5) override; - OTAResponseTypes write(uint8_t *data, size_t len) override; - OTAResponseTypes end() override; - void abort() override; - bool supports_compression() override { return false; } + OTAResponseTypes begin(size_t image_size); + void set_update_md5(const char *md5); + OTAResponseTypes write(uint8_t *data, size_t len); + OTAResponseTypes end(); + void abort(); + bool supports_compression() { return false; } }; +std::unique_ptr make_ota_backend(); + } // namespace esphome::ota #endif diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 4be162ccd3..95b166901a 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -1,7 +1,7 @@ #include "ota_web_server.h" #ifdef USE_WEBSERVER_OTA -#include "esphome/components/ota/ota_backend.h" +#include "esphome/components/ota/ota_backend_factory.h" #include "esphome/core/application.h" #include "esphome/core/log.h" @@ -71,7 +71,7 @@ class OTARequestHandler : public AsyncWebHandler { bool ota_success_{false}; private: - std::unique_ptr ota_backend_{nullptr}; + ota::OTABackendPtr ota_backend_{nullptr}; }; void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) { From 44d314d069503831849c5d0de8e7839898f7eddd Mon Sep 17 00:00:00 2001 From: Olivier ARCHER Date: Thu, 5 Mar 2026 20:22:37 +0100 Subject: [PATCH 371/609] [GPS] fix component Python declaration to match C++ implementation (#14519) --- esphome/components/gps/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index 045a5a6c84..ab48417a4e 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -34,7 +34,7 @@ AUTO_LOAD = ["sensor"] CODEOWNERS = ["@coogle", "@ximex"] gps_ns = cg.esphome_ns.namespace("gps") -GPS = gps_ns.class_("GPS", cg.Component, uart.UARTDevice) +GPS = gps_ns.class_("GPS", cg.PollingComponent, uart.UARTDevice) GPSListener = gps_ns.class_("GPSListener") MULTI_CONF = True From 6f0460b0ee3b34469888ac249b18c5a269648d51 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:46:47 -0500 Subject: [PATCH 372/609] [sim800l][tormatic][tx20] Fix OOB access, div-by-zero, and off-by-one (#14512) Co-authored-by: J. Nick Koston --- esphome/components/sim800l/sim800l.cpp | 5 +++-- esphome/components/tormatic/tormatic_cover.cpp | 3 +++ esphome/components/tx20/tx20.cpp | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index 2115c72cef..913d920c94 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -196,7 +196,8 @@ void Sim800LComponent::parse_cmd_(std::string message) { case STATE_CREG_WAIT: { // Response: "+CREG: 0,1" -- the one there means registered ok // "+CREG: -,-" means not registered ok - bool registered = message.compare(0, 6, "+CREG:") == 0 && (message[9] == '1' || message[9] == '5'); + bool registered = + message.size() > 9 && message.compare(0, 6, "+CREG:") == 0 && (message[9] == '1' || message[9] == '5'); if (registered) { if (!this->registered_) { ESP_LOGD(TAG, "Registered OK"); @@ -205,7 +206,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { this->expect_ack_ = true; } else { ESP_LOGW(TAG, "Registration Fail"); - if (message[7] == '0') { // Network registration is disable, enable it + if (message.size() > 7 && message[7] == '0') { // Network registration is disabled, enable it send_cmd_("AT+CREG=1"); this->expect_ack_ = true; this->state_ = STATE_SETUP_CMGF; diff --git a/esphome/components/tormatic/tormatic_cover.cpp b/esphome/components/tormatic/tormatic_cover.cpp index f567be0674..37a269088e 100644 --- a/esphome/components/tormatic/tormatic_cover.cpp +++ b/esphome/components/tormatic/tormatic_cover.cpp @@ -183,6 +183,9 @@ void Tormatic::recompute_position_() { duration = this->close_duration_; } + if (duration == 0) + return; + auto delta = direction * diff / duration; this->position = clamp(this->position + delta, COVER_CLOSED, COVER_OPEN); diff --git a/esphome/components/tx20/tx20.cpp b/esphome/components/tx20/tx20.cpp index 6bc5f0bb51..3e0234fac0 100644 --- a/esphome/components/tx20/tx20.cpp +++ b/esphome/components/tx20/tx20.cpp @@ -191,7 +191,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { arg->tx20_available = true; return; } - if (index <= MAX_BUFFER_SIZE) { + if (index < MAX_BUFFER_SIZE) { arg->buffer[index] = delay; } arg->spent_time += delay; From 05ddc85412c8baf985e76015ed750407faa3d4e0 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:46:56 -0500 Subject: [PATCH 373/609] [rc522][sml][kamstrup_kmp] Fix buffer bounds checks (#14515) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/kamstrup_kmp/kamstrup_kmp.cpp | 4 ++-- esphome/components/rc522/rc522.cpp | 1 + esphome/components/sml/sml_parser.cpp | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp index 00c65a1937..29de651255 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp @@ -136,7 +136,7 @@ void KamstrupKMPComponent::read_command_(uint16_t command) { int timeout = 250; // ms // Read the data from the UART - while (timeout > 0) { + while (timeout > 0 && buffer_len < static_cast(sizeof(buffer))) { if (this->available()) { data = this->read(); if (data > -1) { @@ -246,7 +246,7 @@ void KamstrupKMPComponent::parse_command_message_(uint16_t command, const uint8_ } void KamstrupKMPComponent::set_sensor_value_(uint16_t command, float value, uint8_t unit_idx) { - const char *unit = UNITS[unit_idx]; + const char *unit = unit_idx < sizeof(UNITS) / sizeof(UNITS[0]) ? UNITS[unit_idx] : ""; // Standard sensors if (command == CMD_HEAT_ENERGY && this->heat_energy_sensor_ != nullptr) { diff --git a/esphome/components/rc522/rc522.cpp b/esphome/components/rc522/rc522.cpp index 91fae7fa34..c5f7ec2cd4 100644 --- a/esphome/components/rc522/rc522.cpp +++ b/esphome/components/rc522/rc522.cpp @@ -169,6 +169,7 @@ void RC522::loop() { default: ESP_LOGE(TAG, "uid_idx_ invalid, uid_idx_ = %d", uid_idx_); state_ = STATE_DONE; + return; } buffer_[1] = 32; pcd_transceive_data_(2); diff --git a/esphome/components/sml/sml_parser.cpp b/esphome/components/sml/sml_parser.cpp index 16e37949dc..ed086e385d 100644 --- a/esphome/components/sml/sml_parser.cpp +++ b/esphome/components/sml/sml_parser.cpp @@ -35,6 +35,8 @@ bool SmlFile::setup_node(SmlNode *node) { // Check if we need additional length bytes if (overlength) { + if (this->pos_ + 1 >= this->buffer_.size()) + return false; // Shift the current length to the higher nibble // and add the lower nibble of the next byte to the length length = (length << 4) + (this->buffer_[this->pos_ + 1] & 0x0f); From d6f3186b3d11d6d6aa330751ff634c0d1889e911 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:47:10 -0500 Subject: [PATCH 374/609] [haier][bedjet][vbus][lightwaverf] Fix buffer overflow bugs (#14493) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/bedjet/bedjet_codec.cpp | 28 +++++++++++++------ .../components/haier/smartair2_climate.cpp | 2 +- esphome/components/lightwaverf/LwTx.cpp | 4 +++ esphome/components/vbus/vbus.cpp | 4 ++- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/esphome/components/bedjet/bedjet_codec.cpp b/esphome/components/bedjet/bedjet_codec.cpp index 9a312e226c..7a959390f3 100644 --- a/esphome/components/bedjet/bedjet_codec.cpp +++ b/esphome/components/bedjet/bedjet_codec.cpp @@ -1,4 +1,5 @@ #include "bedjet_codec.h" +#include #include #include @@ -68,6 +69,10 @@ BedjetPacket *BedjetCodec::get_set_runtime_remaining_request(const uint8_t hour, /** Decodes the extra bytes that were received after being notified with a partial packet. */ void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) { + if (length < 5) { + ESP_LOGVV(TAG, "Received extra: %d bytes (too short)", length); + return; + } ESP_LOGVV(TAG, "Received extra: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); uint8_t offset = this->last_buffer_size_; if (offset > 0 && length + offset <= sizeof(BedjetStatusPacket)) { @@ -90,14 +95,19 @@ void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) { * @return `true` if the packet was decoded and represents a "partial" packet; `false` otherwise. */ bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) { + if (length < 5) { + ESP_LOGW(TAG, "Received short packet: %d bytes", length); + return false; + } ESP_LOGV(TAG, "Received: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); if (data[1] == PACKET_FORMAT_V3_HOME && data[3] == PACKET_TYPE_STATUS) { // Clear old buffer memset(&this->buf_, 0, sizeof(BedjetStatusPacket)); // Copy new data into buffer - memcpy(&this->buf_, data, length); - this->last_buffer_size_ = length; + size_t copy_len = std::min(static_cast(length), sizeof(BedjetStatusPacket)); + memcpy(&this->buf_, data, copy_len); + this->last_buffer_size_ = copy_len; // TODO: validate the packet checksum? if (this->buf_.mode < 7 && this->buf_.target_temp_step >= 38 && this->buf_.target_temp_step <= 86 && @@ -113,13 +123,15 @@ bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) { } } else if (data[1] == PACKET_FORMAT_DEBUG || data[3] == PACKET_TYPE_DEBUG) { // We don't actually know the packet format for this. Dump packets to log, in case a pattern presents itself. - ESP_LOGVV(TAG, - "received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF; [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, " - "[12]=%d, [-1]=%d", - bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8], - data[9], data[10], data[11], data[12], data[length - 1]); + if (length >= 13) { + ESP_LOGVV(TAG, + "received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF; [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, " + "[12]=%d, [-1]=%d", + bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8], + data[9], data[10], data[11], data[12], data[length - 1]); + } - if (this->has_status()) { + if (this->has_status() && length >= 7) { this->status_packet_->ambient_temp_step = data[6]; } } else { diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp index d24f8ad849..e91224e2d8 100644 --- a/esphome/components/haier/smartair2_climate.cpp +++ b/esphome/components/haier/smartair2_climate.cpp @@ -385,7 +385,7 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() { } haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { - if (size < sizeof(smartair2_protocol::HaierStatus)) + if (size != sizeof(smartair2_protocol::HaierStatus)) return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; smartair2_protocol::HaierStatus packet; memcpy(&packet, packet_buffer, size); diff --git a/esphome/components/lightwaverf/LwTx.cpp b/esphome/components/lightwaverf/LwTx.cpp index f5ef6ddb2c..b69b93b978 100644 --- a/esphome/components/lightwaverf/LwTx.cpp +++ b/esphome/components/lightwaverf/LwTx.cpp @@ -108,6 +108,10 @@ bool LwTx::lwtx_free() { return !this->tx_msg_active; } Send a LightwaveRF message (10 nibbles in bytes) **/ void LwTx::lwtx_send(const std::vector &msg) { + if (msg.size() < TX_MSGLEN) { + ESP_LOGW("lightwaverf.sensor", "Message too short: %zu < %u", msg.size(), static_cast(TX_MSGLEN)); + return; + } if (this->tx_translate) { for (uint8_t i = 0; i < TX_MSGLEN; i++) { this->tx_buf[i] = TX_NIBBLE[msg[i] & 0xF]; diff --git a/esphome/components/vbus/vbus.cpp b/esphome/components/vbus/vbus.cpp index b9496a08de..8616da010d 100644 --- a/esphome/components/vbus/vbus.cpp +++ b/esphome/components/vbus/vbus.cpp @@ -1,6 +1,7 @@ #include "vbus.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include #include namespace esphome { @@ -106,9 +107,10 @@ void VBus::loop() { continue; #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_size(VBUS_MAX_LOG_BYTES)]; + size_t log_bytes = std::min(this->buffer_.size(), static_cast(VBUS_MAX_LOG_BYTES)); #endif ESP_LOGV(TAG, "P2 C%04x %04x->%04x: %s", this->command_, this->source_, this->dest_, - format_hex_to(hex_buf, this->buffer_.data(), this->buffer_.size())); + format_hex_to(hex_buf, this->buffer_.data(), log_bytes)); for (auto &listener : this->listeners_) listener->on_message(this->command_, this->source_, this->dest_, this->buffer_); this->state_ = 0; From 9961c8180aec582a856b455d35a2e0ae19e2d3e4 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:47:32 -0500 Subject: [PATCH 375/609] [alpha3][mpu6886][emc2101] Fix copy-paste bugs (#14492) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/alpha3/alpha3.cpp | 2 +- esphome/components/emc2101/emc2101.cpp | 2 +- esphome/components/mpu6886/mpu6886.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/alpha3/alpha3.cpp b/esphome/components/alpha3/alpha3.cpp index f22a8e2444..6e82ec047d 100644 --- a/esphome/components/alpha3/alpha3.cpp +++ b/esphome/components/alpha3/alpha3.cpp @@ -125,7 +125,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc this->current_sensor_->publish_state(NAN); if (this->speed_sensor_ != nullptr) this->speed_sensor_->publish_state(NAN); - if (this->speed_sensor_ != nullptr) + if (this->voltage_sensor_ != nullptr) this->voltage_sensor_->publish_state(NAN); break; } diff --git a/esphome/components/emc2101/emc2101.cpp b/esphome/components/emc2101/emc2101.cpp index 7d85cd31cf..068e25568f 100644 --- a/esphome/components/emc2101/emc2101.cpp +++ b/esphome/components/emc2101/emc2101.cpp @@ -72,7 +72,7 @@ void Emc2101Component::setup() { config |= EMC2101_DAC_BIT; } if (this->inverted_) { - config |= EMC2101_POLARITY_BIT; + reg(EMC2101_REGISTER_FAN_CONFIG) |= EMC2101_POLARITY_BIT; } if (this->dac_mode_) { // DAC mode configurations diff --git a/esphome/components/mpu6886/mpu6886.cpp b/esphome/components/mpu6886/mpu6886.cpp index 68b77b59c9..02747da306 100644 --- a/esphome/components/mpu6886/mpu6886.cpp +++ b/esphome/components/mpu6886/mpu6886.cpp @@ -80,7 +80,7 @@ void MPU6886Component::setup() { accel_config &= 0b11100111; accel_config |= (MPU6886_RANGE_2G << 3); ESP_LOGV(TAG, " Output accel_config: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(accel_config)); - if (!this->write_byte(MPU6886_REGISTER_GYRO_CONFIG, gyro_config)) { + if (!this->write_byte(MPU6886_REGISTER_ACCEL_CONFIG, accel_config)) { this->mark_failed(); return; } From 22d90d702d36fb7f91a6fbfc3d69dd554aa2cbe9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:47:55 -0500 Subject: [PATCH 376/609] [usb_cdc_acm][scd4x][pulse_counter][mopeka_std_check][ruuvi_ble] Fix assorted one-liner bugs (#14495) Co-authored-by: Claude Opus 4.6 --- .../components/mopeka_std_check/mopeka_std_check.cpp | 6 +++--- .../components/mopeka_std_check/mopeka_std_check.h | 2 +- .../components/pulse_counter/pulse_counter_sensor.cpp | 3 ++- esphome/components/ruuvi_ble/ruuvi_ble.cpp | 11 +++++++---- esphome/components/scd4x/scd4x.cpp | 3 ++- esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp | 2 +- 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.cpp b/esphome/components/mopeka_std_check/mopeka_std_check.cpp index 6322b550c9..88bd7b02fd 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.cpp +++ b/esphome/components/mopeka_std_check/mopeka_std_check.cpp @@ -108,7 +108,7 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) } // Get temperature of sensor - uint8_t temp_in_c = this->parse_temperature_(mopeka_data); + int8_t temp_in_c = this->parse_temperature_(mopeka_data); if (this->temperature_ != nullptr) { this->temperature_->publish_state(temp_in_c); } @@ -223,12 +223,12 @@ uint8_t MopekaStdCheck::parse_battery_level_(const mopeka_std_package *message) return (uint8_t) percent; } -uint8_t MopekaStdCheck::parse_temperature_(const mopeka_std_package *message) { +int8_t MopekaStdCheck::parse_temperature_(const mopeka_std_package *message) { uint8_t tmp = message->raw_temp; if (tmp == 0x0) { return -40; } else { - return (uint8_t) ((tmp - 25.0f) * 1.776964f); + return static_cast((tmp - 25.0f) * 1.776964f); } } diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.h b/esphome/components/mopeka_std_check/mopeka_std_check.h index 897b5414ed..45588988c5 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.h +++ b/esphome/components/mopeka_std_check/mopeka_std_check.h @@ -71,7 +71,7 @@ class MopekaStdCheck : public Component, public esp32_ble_tracker::ESPBTDeviceLi float get_lpg_speed_of_sound_(float temperature); uint8_t parse_battery_level_(const mopeka_std_package *message); - uint8_t parse_temperature_(const mopeka_std_package *message); + int8_t parse_temperature_(const mopeka_std_package *message); }; } // namespace mopeka_std_check diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index 5e62c0a410..ec00bd024e 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -175,7 +175,8 @@ void PulseCounterSensor::setup() { void PulseCounterSensor::set_total_pulses(uint32_t pulses) { this->current_total_ = pulses; - this->total_sensor_->publish_state(pulses); + if (this->total_sensor_ != nullptr) + this->total_sensor_->publish_state(pulses); } void PulseCounterSensor::dump_config() { diff --git a/esphome/components/ruuvi_ble/ruuvi_ble.cpp b/esphome/components/ruuvi_ble/ruuvi_ble.cpp index 1b126bdef0..bf088873ce 100644 --- a/esphome/components/ruuvi_ble/ruuvi_ble.cpp +++ b/esphome/components/ruuvi_ble/ruuvi_ble.cpp @@ -63,10 +63,13 @@ bool parse_ruuvi_data_byte(const esp32_ble_tracker::adv_data_t &adv_data, RuuviP result.acceleration_x = data[6] == 0xFF && data[7] == 0xFF ? NAN : acceleration_x; result.acceleration_y = data[8] == 0xFF && data[9] == 0xFF ? NAN : acceleration_y; result.acceleration_z = data[10] == 0xFF && data[11] == 0xFF ? NAN : acceleration_z; - result.acceleration = result.acceleration_x == NAN || result.acceleration_y == NAN || result.acceleration_z == NAN - ? NAN - : sqrtf(acceleration_x * acceleration_x + acceleration_y * acceleration_y + - acceleration_z * acceleration_z); + if ((data[6] != 0xFF || data[7] != 0xFF) && (data[8] != 0xFF || data[9] != 0xFF) && + (data[10] != 0xFF || data[11] != 0xFF)) { + result.acceleration = + sqrtf(acceleration_x * acceleration_x + acceleration_y * acceleration_y + acceleration_z * acceleration_z); + } else { + result.acceleration = NAN; + } result.battery_voltage = (power_info >> 5) == 0x7FF ? NAN : battery_voltage; result.tx_power = (power_info & 0x1F) == 0x1F ? NAN : tx_power; result.movement_counter = movement_counter; diff --git a/esphome/components/scd4x/scd4x.cpp b/esphome/components/scd4x/scd4x.cpp index a265386cc2..0c108fba9d 100644 --- a/esphome/components/scd4x/scd4x.cpp +++ b/esphome/components/scd4x/scd4x.cpp @@ -307,7 +307,7 @@ bool SCD4XComponent::start_measurement_() { break; } - static uint8_t remaining_retries = 3; + uint8_t remaining_retries = 3; while (remaining_retries) { if (!this->write_command(measurement_command)) { ESP_LOGE(TAG, "Error starting measurements"); @@ -316,6 +316,7 @@ bool SCD4XComponent::start_measurement_() { if (--remaining_retries == 0) return false; delay(50); // NOLINT wait 50 ms and try again + continue; } this->status_clear_warning(); return true; diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp b/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp index d33fb80f78..44de986f9a 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp @@ -161,7 +161,7 @@ void USBCDCACMInstance::setup() { // Create a simple, unique task name per interface char task_name[] = "usb_tx_0"; - task_name[sizeof(task_name) - 1] = format_hex_char(static_cast(this->itf_)); + task_name[sizeof(task_name) - 2] = format_hex_char(static_cast(this->itf_)); xTaskCreate(usb_tx_task_fn, task_name, stack_size, this, 4, &this->usb_tx_task_handle_); if (this->usb_tx_task_handle_ == nullptr) { From 5c5ea8824edebb74e80a3ff9306add66b76b4b95 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 5 Mar 2026 13:51:08 -0600 Subject: [PATCH 377/609] [audio_file] New component for embedding files into firmware (#14434) Co-authored-by: Claude Opus 4.6 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- CODEOWNERS | 1 + esphome/components/audio_file/__init__.py | 255 ++++++++++++++++++ esphome/components/audio_file/audio_file.h | 28 ++ esphome/core/defines.h | 1 + script/ci-custom.py | 1 + tests/components/audio_file/common.yaml | 5 + .../components/audio_file/test.esp32-idf.yaml | 1 + tests/components/audio_file/test.wav | Bin 0 -> 46 bytes 8 files changed, 292 insertions(+) create mode 100644 esphome/components/audio_file/__init__.py create mode 100644 esphome/components/audio_file/audio_file.h create mode 100644 tests/components/audio_file/common.yaml create mode 100644 tests/components/audio_file/test.esp32-idf.yaml create mode 100644 tests/components/audio_file/test.wav diff --git a/CODEOWNERS b/CODEOWNERS index b22f85b71d..7c37b20e09 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -54,6 +54,7 @@ esphome/components/atm90e32/* @circuitsetup @descipher esphome/components/audio/* @kahrendt esphome/components/audio_adc/* @kbx81 esphome/components/audio_dac/* @kbx81 +esphome/components/audio_file/* @kahrendt esphome/components/axs15231/* @clydebarrow esphome/components/b_parasite/* @rbaron esphome/components/ballu/* @bazuchan diff --git a/esphome/components/audio_file/__init__.py b/esphome/components/audio_file/__init__.py new file mode 100644 index 0000000000..3ed6c1cd92 --- /dev/null +++ b/esphome/components/audio_file/__init__.py @@ -0,0 +1,255 @@ +from dataclasses import dataclass, field +import hashlib +import logging +from pathlib import Path + +import puremagic + +from esphome import external_files +import esphome.codegen as cg +from esphome.components import audio +import esphome.config_validation as cv +from esphome.const import ( + CONF_FILE, + CONF_ID, + CONF_PATH, + CONF_RAW_DATA_ID, + CONF_TYPE, + CONF_URL, +) +from esphome.core import CORE, ID, HexInt +from esphome.cpp_generator import MockObj +from esphome.external_files import download_content +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) + +CODEOWNERS = ["@kahrendt"] + +AUTO_LOAD = ["audio"] + +DOMAIN = "audio_file" + +audio_file_ns = cg.esphome_ns.namespace("audio_file") + +TYPE_LOCAL = "local" +TYPE_WEB = "web" + + +@dataclass +class AudioFileData: + file_ids: dict[str, ID] = field(default_factory=dict) + file_cache: dict[str, tuple[bytes, MockObj]] = field(default_factory=dict) + + +def _get_data() -> AudioFileData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = AudioFileData() + return CORE.data[DOMAIN] + + +def get_audio_file_ids() -> dict[str, ID]: + """Get all registered audio file IDs for cross-component access.""" + return _get_data().file_ids + + +def _compute_local_file_path(value: ConfigType) -> Path: + url = value[CONF_URL] + h = hashlib.new("sha256") + h.update(url.encode()) + key = h.hexdigest()[:8] + base_dir = external_files.compute_local_file_dir(DOMAIN) + _LOGGER.debug("_compute_local_file_path: base_dir=%s", base_dir / key) + return base_dir / key + + +def _download_web_file(value: ConfigType) -> ConfigType: + url = value[CONF_URL] + path = _compute_local_file_path(value) + + download_content(url, path) + _LOGGER.debug("download_web_file: path=%s", path) + return value + + +def _file_schema(value: ConfigType | str) -> ConfigType: + if isinstance(value, str): + return _validate_file_shorthand(value) + return TYPED_FILE_SCHEMA(value) + + +def _validate_file_shorthand(value: str) -> ConfigType: + value = cv.string_strict(value) + if value.startswith("http://") or value.startswith("https://"): + return _file_schema( + { + CONF_TYPE: TYPE_WEB, + CONF_URL: value, + } + ) + return _file_schema( + { + CONF_TYPE: TYPE_LOCAL, + CONF_PATH: value, + } + ) + + +def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]: + """Read an audio file and determine its type. Used by this component and media_source platform.""" + conf_file = file_config[CONF_FILE] + file_source = conf_file[CONF_TYPE] + if file_source == TYPE_LOCAL: + path = CORE.relative_config_path(conf_file[CONF_PATH]) + elif file_source == TYPE_WEB: + path = _compute_local_file_path(conf_file) + else: + raise cv.Invalid("Unsupported file source") + + with open(path, "rb") as f: + data = f.read() + + try: + file_type: str = puremagic.from_string(data) + file_type = file_type.removeprefix(".") + except puremagic.PureError as e: + raise cv.Invalid( + f"Unable to determine audio file type of '{path}'. " + f"Try re-encoding the file into a supported format. Details: {e}" + ) + + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"] + if file_type == "wav": + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["WAV"] + elif file_type in ("mp3", "mpeg", "mpga"): + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["MP3"] + elif file_type == "flac": + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["FLAC"] + elif ( + file_type == "ogg" + and len(data) >= 36 + and data.startswith(b"OggS") + and data[28:36] == b"OpusHead" + ): + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["OPUS"] + + return data, media_file_type + + +LOCAL_SCHEMA = cv.Schema( + { + cv.Required(CONF_PATH): cv.file_, + } +) + +WEB_SCHEMA = cv.All( + { + cv.Required(CONF_URL): cv.url, + }, + _download_web_file, +) + + +TYPED_FILE_SCHEMA = cv.typed_schema( + { + TYPE_LOCAL: LOCAL_SCHEMA, + TYPE_WEB: WEB_SCHEMA, + }, +) + + +MEDIA_FILE_TYPE_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(audio.AudioFile), + cv.Required(CONF_FILE): _file_schema, + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + } +) + + +MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB + + +def _validate_supported_local_file(config: list[ConfigType]) -> list[ConfigType]: + for file_config in config: + data, media_file_type = read_audio_file_and_type(file_config) + + if len(data) > MAX_FILE_SIZE: + file_info = file_config.get(CONF_FILE, {}) + source = ( + file_info.get(CONF_PATH) or file_info.get(CONF_URL) or "unknown source" + ) + raise cv.Invalid( + f"Audio file {source!r} is too large ({len(data)} bytes, max {MAX_FILE_SIZE} bytes)" + ) + + if str(media_file_type) == str(audio.AUDIO_FILE_TYPE_ENUM["NONE"]): + file_info = file_config.get(CONF_FILE, {}) + source = ( + file_info.get(CONF_PATH) or file_info.get(CONF_URL) or "unknown source" + ) + raise cv.Invalid( + f"Unsupported media file from {source!r} (detected type: {media_file_type})" + ) + + # Cache the file data so to_code() doesn't need to re-read it + _get_data().file_cache[str(file_config[CONF_ID])] = (data, media_file_type) + + media_file_type_str = str(media_file_type) + if media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["FLAC"]): + audio.request_flac_support() + elif media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["MP3"]): + audio.request_mp3_support() + elif media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["OPUS"]): + audio.request_opus_support() + + return config + + +CONFIG_SCHEMA = cv.All( + cv.only_on_esp32, + cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), + _validate_supported_local_file, +) + + +async def to_code(config: list[ConfigType]) -> None: + cache = _get_data().file_cache + + for file_config in config: + file_id = str(file_config[CONF_ID]) + data, media_file_type = cache[file_id] + + rhs = [HexInt(x) for x in data] + prog_arr = cg.progmem_array(file_config[CONF_RAW_DATA_ID], rhs) + + media_files_struct = cg.StructInitializer( + audio.AudioFile, + ( + "data", + prog_arr, + ), + ( + "length", + len(rhs), + ), + ( + "file_type", + media_file_type, + ), + ) + + cg.new_Pvariable( + file_config[CONF_ID], + media_files_struct, + ) + + # Store file ID for cross-component access + _get_data().file_ids[file_id] = file_config[CONF_ID] + + # Register all files in the shared C++ registry + cg.add_define("AUDIO_FILE_MAX_FILES", len(config)) + for file_config in config: + file_id = str(file_config[CONF_ID]) + file_var = await cg.get_variable(file_config[CONF_ID]) + cg.add(audio_file_ns.add_named_audio_file(file_var, file_id)) diff --git a/esphome/components/audio_file/audio_file.h b/esphome/components/audio_file/audio_file.h new file mode 100644 index 0000000000..537e19fb3c --- /dev/null +++ b/esphome/components/audio_file/audio_file.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef AUDIO_FILE_MAX_FILES + +#include "esphome/components/audio/audio.h" +#include "esphome/core/helpers.h" + +namespace esphome::audio_file { + +struct NamedAudioFile { + audio::AudioFile *file; + const char *file_id; +}; + +inline StaticVector + named_audio_files; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +inline void add_named_audio_file(audio::AudioFile *file, const char *file_id) { + named_audio_files.push_back({file, file_id}); +} + +inline const StaticVector &get_named_audio_files() { return named_audio_files; } + +} // namespace esphome::audio_file + +#endif // AUDIO_FILE_MAX_FILES diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 07afefd91a..1a6d9b3a80 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -137,6 +137,7 @@ // Feature flags which do not work for zephyr #ifndef USE_ZEPHYR +#define AUDIO_FILE_MAX_FILES 4 #define USE_AUDIO_DAC #define USE_AUDIO_FLAC_SUPPORT #define USE_AUDIO_MP3_SUPPORT diff --git a/script/ci-custom.py b/script/ci-custom.py index b60d7d7740..8e1652b505 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -72,6 +72,7 @@ ignore_types = ( ".gif", ".webp", ".bin", + ".wav", ) LINT_FILE_CHECKS = [] diff --git a/tests/components/audio_file/common.yaml b/tests/components/audio_file/common.yaml new file mode 100644 index 0000000000..9404208094 --- /dev/null +++ b/tests/components/audio_file/common.yaml @@ -0,0 +1,5 @@ +audio_file: + - id: test_audio + file: + type: local + path: $component_dir/test.wav diff --git a/tests/components/audio_file/test.esp32-idf.yaml b/tests/components/audio_file/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/audio_file/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/audio_file/test.wav b/tests/components/audio_file/test.wav new file mode 100644 index 0000000000000000000000000000000000000000..f9d07ef2238eb2fcb355055466d3789ee1a1fe0b GIT binary patch literal 46 ycmWIYbaPW Date: Thu, 5 Mar 2026 14:51:32 -0500 Subject: [PATCH 378/609] [wled][lcd_base][touchscreen][ee895] Fix off-by-one, buffer overrun, empty deref, and uninitialized pointers (#14513) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/ee895/ee895.h | 6 +++--- esphome/components/lcd_base/lcd_display.cpp | 2 +- esphome/components/touchscreen/touchscreen.h | 7 ++++++- esphome/components/wled/wled_light_effect.cpp | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/esphome/components/ee895/ee895.h b/esphome/components/ee895/ee895.h index 259b7c524b..ff1085e05d 100644 --- a/esphome/components/ee895/ee895.h +++ b/esphome/components/ee895/ee895.h @@ -22,9 +22,9 @@ class EE895Component : public PollingComponent, public i2c::I2CDevice { void write_command_(uint16_t addr, uint16_t reg_cnt); float read_float_(); uint16_t calc_crc16_(const uint8_t buf[], uint8_t len); - sensor::Sensor *co2_sensor_; - sensor::Sensor *temperature_sensor_; - sensor::Sensor *pressure_sensor_; + sensor::Sensor *co2_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE}; }; diff --git a/esphome/components/lcd_base/lcd_display.cpp b/esphome/components/lcd_base/lcd_display.cpp index cd08a739eb..1f0ba482d7 100644 --- a/esphome/components/lcd_base/lcd_display.cpp +++ b/esphome/components/lcd_base/lcd_display.cpp @@ -99,7 +99,7 @@ void HOT LCDDisplay::display() { this->send(this->buffer_[this->columns_ * 2 + i], true); } - if (this->rows_ >= 1) { + if (this->rows_ >= 2) { this->command_(LCD_DISPLAY_COMMAND_SET_DDRAM_ADDR | 0x40); for (uint8_t i = 0; i < this->columns_; i++) diff --git a/esphome/components/touchscreen/touchscreen.h b/esphome/components/touchscreen/touchscreen.h index 8016323d49..7451c207ec 100644 --- a/esphome/components/touchscreen/touchscreen.h +++ b/esphome/components/touchscreen/touchscreen.h @@ -65,7 +65,12 @@ class Touchscreen : public PollingComponent { void register_listener(TouchListener *listener) { this->touch_listeners_.push_back(listener); } - optional get_touch() { return this->touches_.begin()->second; } + optional get_touch() { + if (this->touches_.empty()) { + return {}; + } + return this->touches_.begin()->second; + } TouchPoints_t get_touches() { TouchPoints_t touches; diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp index 87bae5b1da..db2708d6d0 100644 --- a/esphome/components/wled/wled_light_effect.cpp +++ b/esphome/components/wled/wled_light_effect.cpp @@ -161,7 +161,7 @@ bool WLEDLightEffect::parse_notifier_frame_(light::AddressableLight &it, const u // https://kno.wled.ge/interfaces/udp-notifier/ // https://github.com/Aircoookie/WLED/blob/main/wled00/udp.cpp - if (size < 34) { + if (size <= 34) { return false; } From 99a805cba675ab2399d432dd39d142a0852fafc8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 09:52:10 -1000 Subject: [PATCH 379/609] Bump the docker-actions group across 1 directory with 2 updates (#14520) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-docker.yml | 2 +- .github/workflows/release.yml | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index a83bcae0b0..4009ac1e17 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -49,7 +49,7 @@ jobs: with: python-version: "3.11" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Set TAG run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 17a2616dff..8f68e9c873 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -99,15 +99,15 @@ jobs: python-version: "3.11" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Log in to docker hub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to the GitHub container registry - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -178,17 +178,17 @@ jobs: merge-multiple: true - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Log in to docker hub if: matrix.registry == 'dockerhub' - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to the GitHub container registry if: matrix.registry == 'ghcr' - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ghcr.io username: ${{ github.actor }} From 291679126f532a6b036c10984c97c47db0be9cd3 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:55:54 -0500 Subject: [PATCH 380/609] [nfc] Fix off-by-one in NDEF message parsing (#14485) Co-authored-by: Claude Opus 4.6 --- esphome/components/nfc/ndef_message.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/esphome/components/nfc/ndef_message.cpp b/esphome/components/nfc/ndef_message.cpp index e7304445c5..35028555c5 100644 --- a/esphome/components/nfc/ndef_message.cpp +++ b/esphome/components/nfc/ndef_message.cpp @@ -8,8 +8,14 @@ static const char *const TAG = "nfc.ndef_message"; NdefMessage::NdefMessage(std::vector &data) { ESP_LOGV(TAG, "Building NdefMessage with %zu bytes", data.size()); - uint8_t index = 0; - while (index <= data.size()) { + size_t index = 0; + while (index < data.size()) { + // Minimum record: TNF byte + type length byte + payload length (1 or 4 bytes) + if (index + 2 >= data.size()) { + ESP_LOGE(TAG, "Truncated record header; aborting"); + break; + } + uint8_t tnf_byte = data[index++]; bool me = tnf_byte & 0x40; // Message End bit (is set if this is the last record of the message) bool sr = tnf_byte & 0x10; // Short record bit (is set if payload size is less or equal to 255 bytes) @@ -23,6 +29,10 @@ NdefMessage::NdefMessage(std::vector &data) { if (sr) { payload_length = data[index++]; } else { + if (index + 4 > data.size()) { + ESP_LOGE(TAG, "Truncated payload length; aborting"); + break; + } payload_length = (static_cast(data[index]) << 24) | (static_cast(data[index + 1]) << 16) | (static_cast(data[index + 2]) << 8) | static_cast(data[index + 3]); index += 4; @@ -30,6 +40,10 @@ NdefMessage::NdefMessage(std::vector &data) { uint8_t id_length = 0; if (il) { + if (index >= data.size()) { + ESP_LOGE(TAG, "Truncated ID length; aborting"); + break; + } id_length = data[index++]; } From e25d740968f656b8e50dedb793fbc5370573f3ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Mar 2026 09:58:58 -1000 Subject: [PATCH 381/609] [wifi] Cache is_connected() for cheap inline access (#14463) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/wifi/wifi_component.cpp | 8 +++++--- esphome/components/wifi/wifi_component.h | 5 ++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 852ff922f1..8b60810d28 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -725,6 +725,7 @@ void WiFiComponent::restart_adapter() { void WiFiComponent::loop() { this->wifi_loop_(); const uint32_t now = App.get_loop_component_start_time(); + this->update_connected_state_(); if (this->has_sta()) { #if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER) @@ -776,7 +777,7 @@ void WiFiComponent::loop() { } case WIFI_COMPONENT_STATE_STA_CONNECTED: { - if (!this->is_connected()) { + if (!this->is_connected_()) { ESP_LOGW(TAG, "Connection lost; reconnecting"); this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; this->retry_connect(); @@ -2118,15 +2119,16 @@ bool WiFiComponent::can_proceed() { if (!this->has_sta() || this->state_ == WIFI_COMPONENT_STATE_DISABLED || this->ap_setup_) { return true; } - return this->is_connected(); + return this->is_connected_(); } #endif void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } -bool WiFiComponent::is_connected() const { +bool WiFiComponent::is_connected_() const { return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_; } +void WiFiComponent::update_connected_state_() { this->connected_ = this->is_connected_(); } void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->power_save_ = power_save; #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index a6f03a08d9..f340b708c9 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -443,7 +443,7 @@ class WiFiComponent : public Component { void set_reboot_timeout(uint32_t reboot_timeout); - bool is_connected() const; + bool is_connected() const { return this->connected_; } void set_power_save_mode(WiFiPowerSaveMode power_save); void set_min_auth_mode(WifiMinAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; } @@ -678,6 +678,8 @@ class WiFiComponent : public Component { bool wifi_sta_connect_(const WiFiAP &ap); void wifi_pre_setup_(); WiFiSTAConnectStatus wifi_sta_connect_status_() const; + bool is_connected_() const; + void update_connected_state_(); bool wifi_scan_start_(bool passive); #ifdef USE_WIFI_AP @@ -854,6 +856,7 @@ class WiFiComponent : public Component { bool has_completed_scan_after_captive_portal_start_{ false}; // Tracks if we've completed a scan after captive portal started bool skip_cooldown_next_cycle_{false}; + bool connected_{false}; bool post_connect_roaming_{true}; // Enabled by default #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) bool is_high_performance_mode_{false}; From d11e7cab464a007bccf37cf0cf3da4d8b1f71861 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:18:54 -0500 Subject: [PATCH 382/609] [xiaomi_ble][pvvx_mithermometer][atc_mithermometer] Add BLE service data bounds checks (#14514) Co-authored-by: Claude Opus 4.6 --- .../components/atc_mithermometer/atc_mithermometer.cpp | 4 ++++ .../components/pvvx_mithermometer/pvvx_mithermometer.cpp | 4 ++++ esphome/components/xiaomi_ble/xiaomi_ble.cpp | 9 +++++++++ 3 files changed, 17 insertions(+) diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.cpp b/esphome/components/atc_mithermometer/atc_mithermometer.cpp index b4d2929742..9afd6334f5 100644 --- a/esphome/components/atc_mithermometer/atc_mithermometer.cpp +++ b/esphome/components/atc_mithermometer/atc_mithermometer.cpp @@ -61,6 +61,10 @@ optional ATCMiThermometer::parse_header_(const esp32_ble_tracker::S } auto raw = service_data.data; + if (raw.size() < 13) { + ESP_LOGVV(TAG, "parse_header_(): service data too short (%zu).", raw.size()); + return {}; + } static uint8_t last_frame_count = 0; if (last_frame_count == raw[12]) { diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp index 5712447909..239a1e74fe 100644 --- a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp @@ -61,6 +61,10 @@ optional PVVXMiThermometer::parse_header_(const esp32_ble_tracker:: } auto raw = service_data.data; + if (raw.size() < 14) { + ESP_LOGVV(TAG, "parse_header_(): service data too short (%zu).", raw.size()); + return {}; + } static uint8_t last_frame_count = 0; if (last_frame_count == raw[13]) { diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 0018d35f1f..97a660f0e3 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -121,6 +121,11 @@ bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult // Byte 2: length // Byte 3..3+len-1: data point value + if (result.raw_offset < 0 || static_cast(result.raw_offset) >= message.size()) { + ESP_LOGVV(TAG, "parse_xiaomi_message(): raw_offset (%d) exceeds message size (%d)!", result.raw_offset, + message.size()); + return false; + } const uint8_t *payload = message.data() + result.raw_offset; uint8_t payload_length = message.size() - result.raw_offset; uint8_t payload_offset = 0; @@ -165,6 +170,10 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service } auto raw = service_data.data; + if (raw.size() < 5) { + ESP_LOGVV(TAG, "parse_xiaomi_header(): service data too short (%d).", raw.size()); + return {}; + } result.has_data = raw[0] & 0x40; result.has_capability = raw[0] & 0x20; result.has_encryption = raw[0] & 0x08; From b0be02e16d8b53efee692543c9be2c6da2118da7 Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Thu, 5 Mar 2026 12:54:17 -0800 Subject: [PATCH 383/609] [modbus] Fix timing bugs and better adhere to spec (#8032) Co-authored-by: brambo123 <52667932+brambo123@users.noreply.github.com> Co-authored-by: Keith Burzinski Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- .../growatt_solar/growatt_solar.cpp | 2 +- esphome/components/modbus/__init__.py | 5 + esphome/components/modbus/modbus.cpp | 294 ++++++++++++------ esphome/components/modbus/modbus.h | 55 +++- .../components/modbus/modbus_definitions.h | 2 + .../components/modbus_controller/__init__.py | 3 +- .../modbus_controller/modbus_controller.cpp | 2 +- tests/components/modbus/common.yaml | 2 + .../external_components/uart_mock/__init__.py | 4 +- .../uart_mock/uart_mock.cpp | 26 +- .../external_components/uart_mock/uart_mock.h | 5 +- .../fixtures/uart_mock_modbus.yaml | 48 ++- .../fixtures/uart_mock_modbus_timing.yaml | 2 + tests/integration/test_uart_mock_modbus.py | 67 +++- 14 files changed, 396 insertions(+), 121 deletions(-) diff --git a/esphome/components/growatt_solar/growatt_solar.cpp b/esphome/components/growatt_solar/growatt_solar.cpp index 686c1c232e..2997425872 100644 --- a/esphome/components/growatt_solar/growatt_solar.cpp +++ b/esphome/components/growatt_solar/growatt_solar.cpp @@ -26,7 +26,7 @@ void GrowattSolar::update() { } // The bus might be slow, or there might be other devices, or other components might be talking to our device. - if (this->waiting_for_response()) { + if (!this->ready_for_immediate_send()) { this->waiting_to_update_ = true; return; } diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py index 2bd85c6121..f6e0f98857 100644 --- a/esphome/components/modbus/__init__.py +++ b/esphome/components/modbus/__init__.py @@ -20,6 +20,7 @@ MULTI_CONF = True CONF_ROLE = "role" CONF_MODBUS_ID = "modbus_id" CONF_SEND_WAIT_TIME = "send_wait_time" +CONF_TURNAROUND_TIME = "turnaround_time" ModbusRole = modbus_ns.enum("ModbusRole") MODBUS_ROLES = { @@ -36,6 +37,9 @@ CONFIG_SCHEMA = ( cv.Optional( CONF_SEND_WAIT_TIME, default="250ms" ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_TURNAROUND_TIME, default="100ms" + ): cv.positive_time_period_milliseconds, cv.Optional(CONF_DISABLE_CRC, default=False): cv.boolean, } ) @@ -57,6 +61,7 @@ async def to_code(config): cg.add(var.set_flow_control_pin(pin)) cg.add(var.set_send_wait_time(config[CONF_SEND_WAIT_TIME])) + cg.add(var.set_turnaround_time(config[CONF_TURNAROUND_TIME])) cg.add(var.set_disable_crc(config[CONF_DISABLE_CRC])) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index d40343db33..28e26e307e 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -15,10 +15,69 @@ void Modbus::setup() { if (this->flow_control_pin_ != nullptr) { this->flow_control_pin_->setup(); } -} -void Modbus::loop() { - const uint32_t now = App.get_loop_component_start_time(); + this->frame_delay_ms_ = + std::max(2, // 1750us minimum per spec - rounded up to 2ms. + // 3.5 characters * 11 bits per character * 1000ms/sec / (bits/sec) (Standard modbus frame delay) + (uint16_t) (3.5 * 11 * 1000 / this->parent_->get_baud_rate()) + 1); + + this->long_rx_buffer_delay_ms_ = + (this->parent_->get_rx_full_threshold() * 11 * 1000 / this->parent_->get_baud_rate()) + 1; +} + +void Modbus::loop() { + // First process all available incoming data. + this->receive_and_parse_modbus_bytes_(); + + // If the response frame is finished (including interframe delay) - we timeout. + // The long_rx_buffer_delay accounts for long responses (larger than the UART rx_full_threshold) to avoid timeouts + // when the buffer is filling the back half of the response + const uint16_t timeout = std::max( + (uint16_t) this->frame_delay_ms_, + (uint16_t) (this->rx_buffer_.size() >= this->parent_->get_rx_full_threshold() ? this->long_rx_buffer_delay_ms_ + : 0)); + // We use millis() here and elsewhere instead of App.get_loop_component_start_time() to avoid stale timestamps + // It's critical in all timestamp comparisons that the left timestamp comes before the right one in time + // If we use a cached value in place of millis() and last_modbus_byte_ is updated inside our loop + // then the comparison is backwards (small negative which wraps to large positive) and will cause a false timeout + // So in this component we don't use any cached timestamp values to avoid these annoying bugs + if (millis() - this->last_modbus_byte_ > timeout) { + this->clear_rx_buffer_(LOG_STR("timeout after partial response"), true); + } + + // If we're past the send_wait_time timeout and response buffer doesn't have the start of the expected response + if (this->waiting_for_response_ != 0 && + millis() - this->last_send_ > this->last_send_tx_offset_ + this->send_wait_time_ && + (this->rx_buffer_.empty() || this->rx_buffer_[0] != this->waiting_for_response_)) { + ESP_LOGW(TAG, "Stop waiting for response from %" PRIu8 " %" PRIu32 "ms after last send", + this->waiting_for_response_, millis() - this->last_send_); + this->waiting_for_response_ = 0; + } + + // If there's no response pending and there's commands in the buffer + this->send_next_frame_(); +} + +bool Modbus::tx_blocked() { + const uint32_t now = millis(); + + // We block transmission in any of these case: + // 1. There are bytes in the UART Rx buffer + // 2. There are bytes in our Rx buffer + // 3. We're waiting for a response + // 4. The last sent byte isn't more than frame_delay ms ago (i.e. wait to tell receivers that our previous Tx is done) + // 5. The last received byte isn't more than frame_delay ms ago (i.e. wait to be sure there isn't more Rx coming) + // 6. If we're a client - also wait for the turnaround delay, to give the servers time to process the previous message + return this->available() || !this->rx_buffer_.empty() || (this->waiting_for_response_ != 0) || + (now - this->last_send_ < this->last_send_tx_offset_ + this->frame_delay_ms_ + + (this->role == ModbusRole::CLIENT ? this->turnaround_delay_ms_ : 0)) || + (now - this->last_modbus_byte_ < + this->frame_delay_ms_ + (this->role == ModbusRole::CLIENT ? this->turnaround_delay_ms_ : 0)); +} + +bool Modbus::tx_buffer_empty() { return this->tx_buffer_.empty(); } + +void Modbus::receive_and_parse_modbus_bytes_() { // Read all available bytes in batches to reduce UART call overhead. size_t avail = this->available(); uint8_t buf[64]; @@ -28,33 +87,20 @@ void Modbus::loop() { break; } avail -= to_read; - for (size_t i = 0; i < to_read; i++) { - if (this->parse_modbus_byte_(buf[i])) { - this->last_modbus_byte_ = now; + if (this->rx_buffer_.empty()) { + ESP_LOGV(TAG, "Received first byte %" PRIu8 " (0X%x) %" PRIu32 "ms after last send", buf[i], buf[i], + millis() - this->last_send_); } else { - size_t at = this->rx_buffer_.size(); - if (at > 0) { - ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse failed", at); - this->rx_buffer_.clear(); - } + ESP_LOGVV(TAG, "Received byte %" PRIu8 " (0X%x) %" PRIu32 "ms after last send", buf[i], buf[i], + millis() - this->last_send_); } - } - } - if (now - this->last_modbus_byte_ > 50) { - size_t at = this->rx_buffer_.size(); - if (at > 0) { - ESP_LOGV(TAG, "Clearing buffer of %d bytes - timeout", at); - this->rx_buffer_.clear(); - } - - // stop blocking new send commands after sent_wait_time_ ms after response received - if (now - this->last_send_ > send_wait_time_) { - if (waiting_for_response > 0) { - ESP_LOGV(TAG, "Stop waiting for response from %d", waiting_for_response); + // If the bytes in the rx buffer do not parse, clear out the buffer + if (!this->parse_modbus_byte_(buf[i])) { + this->clear_rx_buffer_(LOG_STR("parse failed"), true); } - waiting_for_response = 0; + this->last_modbus_byte_ = millis(); } } } @@ -63,7 +109,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { size_t at = this->rx_buffer_.size(); this->rx_buffer_.push_back(byte); const uint8_t *raw = &this->rx_buffer_[0]; - ESP_LOGVV(TAG, "Modbus received Byte %d (0X%x)", byte, byte); + // Byte 0: modbus address (match all) if (at == 0) return true; @@ -101,7 +147,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { if (computed_crc != remote_crc) return true; - ESP_LOGD(TAG, "Modbus user-defined function %02X found", function_code); + ESP_LOGD(TAG, "User-defined function %02X found", function_code); } else { // data starts at 2 and length is 4 for read registers commands @@ -152,9 +198,19 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8); if (computed_crc != remote_crc) { if (this->disable_crc_) { - ESP_LOGD(TAG, "Modbus CRC Check failed, but ignored! %02X!=%02X", computed_crc, remote_crc); + ESP_LOGD(TAG, "CRC check failed %" PRIu32 "ms after last send; ignoring", millis() - this->last_send_); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, " (%02X != %02X) %s", computed_crc, remote_crc, + format_hex_pretty_to(hex_buf, this->rx_buffer_.data(), this->rx_buffer_.size())); } else { - ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc); + ESP_LOGW(TAG, "CRC check failed %" PRIu32 "ms after last send", millis() - this->last_send_); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, " (%02X != %02X) %s", computed_crc, remote_crc, + format_hex_pretty_to(hex_buf, this->rx_buffer_.data(), this->rx_buffer_.size())); return false; } } @@ -164,52 +220,101 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { for (auto *device : this->devices_) { if (device->address_ == address) { found = true; - // Is it an error response? - if ((function_code & FUNCTION_CODE_EXCEPTION_MASK) == FUNCTION_CODE_EXCEPTION_MASK) { - ESP_LOGD(TAG, "Modbus error function code: 0x%X exception: %d", function_code, raw[2]); - if (waiting_for_response != 0) { - device->on_modbus_error(function_code & FUNCTION_CODE_MASK, raw[2]); - } else { - // Ignore modbus exception not related to a pending command - ESP_LOGD(TAG, "Ignoring Modbus error - not expecting a response"); - } - continue; - } if (this->role == ModbusRole::SERVER) { if (function_code == ModbusFunctionCode::READ_HOLDING_REGISTERS || function_code == ModbusFunctionCode::READ_INPUT_REGISTERS) { device->on_modbus_read_registers(function_code, uint16_t(data[1]) | (uint16_t(data[0]) << 8), uint16_t(data[3]) | (uint16_t(data[2]) << 8)); - continue; - } - if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER || - function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { + } else if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER || + function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { device->on_modbus_write_registers(function_code, data); - continue; + } + } else { // We're a client + // Is it an error response? + if ((function_code & FUNCTION_CODE_EXCEPTION_MASK) == FUNCTION_CODE_EXCEPTION_MASK) { + uint8_t exception = raw[2]; + ESP_LOGW(TAG, + "Error function code: 0x%X exception: %" PRIu8 ", address: %" PRIu8 ", %" PRIu32 + "ms after last send", + function_code, exception, address, millis() - this->last_send_); + if (this->waiting_for_response_ == address) { + device->on_modbus_error(function_code & FUNCTION_CODE_MASK, exception); + } else { + // Ignore modbus exception not related to a pending command + ESP_LOGD(TAG, "Ignoring error - not expecting a response from %" PRIu8 "", address); + } + } else { // Not an error response + if (this->waiting_for_response_ == address) { + device->on_modbus_data(data); + } else { + // Ignore modbus response not related to a pending command + ESP_LOGW(TAG, "Ignoring response - not expecting a response from %" PRIu8 ", %" PRIu32 "ms after last send", + address, millis() - this->last_send_); + } } } - // fallthrough for other function codes - device->on_modbus_data(data); } } - waiting_for_response = 0; - if (!found) { - ESP_LOGW(TAG, "Got Modbus frame from unknown address 0x%02X! ", address); + if (!found && this->role == ModbusRole::CLIENT) { + ESP_LOGW(TAG, "Got frame from unknown address %" PRIu8 ", %" PRIu32 "ms after last send", address, + millis() - this->last_send_); } - // reset buffer - ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse succeeded", at); - this->rx_buffer_.clear(); + this->clear_rx_buffer_(LOG_STR("parse succeeded")); + + if (this->waiting_for_response_ == address) + this->waiting_for_response_ = 0; + return true; } +void Modbus::send_next_frame_() { + if (this->tx_buffer_.empty()) + return; + + if (this->tx_blocked()) + return; + + const ModbusDeviceCommand &frame = this->tx_buffer_.front(); + + if (this->role == ModbusRole::CLIENT) { + this->waiting_for_response_ = frame.data.get()[0]; + } + + if (this->flow_control_pin_ != nullptr) { + this->flow_control_pin_->digital_write(true); + this->write_array(frame.data.get(), frame.size); + this->flush(); + this->flow_control_pin_->digital_write(false); + this->last_send_tx_offset_ = 0; + } else { + this->write_array(frame.data.get(), frame.size); + this->last_send_tx_offset_ = frame.size * 11 * 1000 / this->parent_->get_baud_rate() + 1; + } + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "Write: %s %" PRIu32 "ms after last send", format_hex_pretty_to(hex_buf, frame.data.get(), frame.size), + millis() - this->last_send_); + this->last_send_ = millis(); + this->tx_buffer_.pop_front(); + if (!this->tx_buffer_.empty()) { + ESP_LOGV(TAG, "Write queue contains %" PRIu32 " items.", this->tx_buffer_.size()); + } +} + void Modbus::dump_config() { ESP_LOGCONFIG(TAG, "Modbus:\n" " Send Wait Time: %d ms\n" + " Turnaround Time: %d ms\n" + " Frame Delay: %d ms\n" + " Long Rx Buffer Delay: %d ms\n" " CRC Disabled: %s", - this->send_wait_time_, YESNO(this->disable_crc_)); + this->send_wait_time_, this->turnaround_delay_ms_, this->frame_delay_ms_, + this->long_rx_buffer_delay_ms_, YESNO(this->disable_crc_)); LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_); } float Modbus::get_setup_priority() const { @@ -228,15 +333,6 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address return; } - static constexpr size_t ADDR_SIZE = 1; - static constexpr size_t FC_SIZE = 1; - static constexpr size_t START_ADDR_SIZE = 2; - static constexpr size_t NUM_ENTITIES_SIZE = 2; - static constexpr size_t BYTE_COUNT_SIZE = 1; - static constexpr size_t MAX_PAYLOAD_SIZE = std::numeric_limits::max(); - static constexpr size_t CRC_SIZE = 2; - static constexpr size_t MAX_FRAME_SIZE = - ADDR_SIZE + FC_SIZE + START_ADDR_SIZE + NUM_ENTITIES_SIZE + BYTE_COUNT_SIZE + MAX_PAYLOAD_SIZE + CRC_SIZE; uint8_t data[MAX_FRAME_SIZE]; size_t pos = 0; @@ -259,29 +355,16 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address } else { payload_len = 2; // Write single register or coil } + if (payload_len + pos + 2 > MAX_FRAME_SIZE) { // Check if payload fits (accounting for CRC) + ESP_LOGE(TAG, "Payload too large to send: %d bytes", payload_len); + return; + } for (int i = 0; i < payload_len; i++) { data[pos++] = payload[i]; } } - auto crc = crc16(data, pos); - data[pos++] = crc >> 0; - data[pos++] = crc >> 8; - - if (this->flow_control_pin_ != nullptr) - this->flow_control_pin_->digital_write(true); - - this->write_array(data, pos); - this->flush(); - - if (this->flow_control_pin_ != nullptr) - this->flow_control_pin_->digital_write(false); - waiting_for_response = address; - last_send_ = millis(); -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; -#endif - ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data, pos)); + this->queue_raw_(data, pos); } // Helper function for lambdas @@ -290,23 +373,44 @@ void Modbus::send_raw(const std::vector &payload) { if (payload.empty()) { return; } + // Frame size: payload + CRC(2) + if (payload.size() + 2 > MAX_FRAME_SIZE) { + ESP_LOGE(TAG, "Attempted to send frame larger than max frame size of %d bytes", MAX_FRAME_SIZE); + return; + } + // Use stack buffer - Modbus frames are small and bounded + uint8_t data[MAX_FRAME_SIZE]; - if (this->flow_control_pin_ != nullptr) - this->flow_control_pin_->digital_write(true); + std::memcpy(data, payload.data(), payload.size()); - auto crc = crc16(payload.data(), payload.size()); - this->write_array(payload); - this->write_byte(crc & 0xFF); - this->write_byte((crc >> 8) & 0xFF); - this->flush(); - if (this->flow_control_pin_ != nullptr) - this->flow_control_pin_->digital_write(false); - waiting_for_response = payload[0]; -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; + this->queue_raw_(data, payload.size()); +} + +// Assume data and length is valid and append CRC, then queue for sending. Used internally to avoid unnecessary copying +// of data into vectors +void Modbus::queue_raw_(const uint8_t *data, uint16_t len) { + if (this->tx_buffer_.size() < MODBUS_TX_BUFFER_SIZE) { + this->tx_buffer_.emplace_back(data, len); + } else { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR + char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; #endif - ESP_LOGV(TAG, "Modbus write raw: %s", format_hex_pretty_to(hex_buf, payload.data(), payload.size())); - last_send_ = millis(); + ESP_LOGE(TAG, "Write buffer full, dropped: %s", format_hex_pretty_to(hex_buf, data, len)); + } +} + +void Modbus::clear_rx_buffer_(const LogString *reason, bool warn) { + size_t at = this->rx_buffer_.size(); + if (at > 0) { + if (warn) { + ESP_LOGW(TAG, "Clearing buffer of %" PRIu32 " bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason), + millis() - this->last_send_); + } else { + ESP_LOGV(TAG, "Clearing buffer of %" PRIu32 " bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason), + millis() - this->last_send_); + } + this->rx_buffer_.clear(); + } } } // namespace modbus diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index fac74aaadf..c90d4c78ae 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -5,11 +5,16 @@ #include "esphome/components/modbus/modbus_definitions.h" +#include +#include #include +#include namespace esphome { namespace modbus { +static constexpr uint16_t MODBUS_TX_BUFFER_SIZE = 15; + enum ModbusRole { CLIENT, SERVER, @@ -17,6 +22,19 @@ enum ModbusRole { class ModbusDevice; +struct ModbusDeviceCommand { + // Frame with exact-size allocation to avoid std::vector overhead + std::unique_ptr data; + uint16_t size; // Modbus RTU max is 256 bytes + + ModbusDeviceCommand(const uint8_t *src, uint16_t len) : data(std::make_unique(len + 2)), size(len + 2) { + std::memcpy(this->data.get(), src, len); + auto crc = crc16(data.get(), len); + data[len + 0] = crc >> 0; + data[len + 1] = crc >> 8; + } +}; + class Modbus : public uart::UARTDevice, public Component { public: Modbus() = default; @@ -30,28 +48,45 @@ class Modbus : public uart::UARTDevice, public Component { void register_device(ModbusDevice *device) { this->devices_.push_back(device); } float get_setup_priority() const override; + bool tx_buffer_empty(); + bool tx_blocked(); void send(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len = 0, const uint8_t *payload = nullptr); void send_raw(const std::vector &payload); void set_role(ModbusRole role) { this->role = role; } void set_flow_control_pin(GPIOPin *flow_control_pin) { this->flow_control_pin_ = flow_control_pin; } - uint8_t waiting_for_response{0}; - void set_send_wait_time(uint16_t time_in_ms) { send_wait_time_ = time_in_ms; } - void set_disable_crc(bool disable_crc) { disable_crc_ = disable_crc; } + void set_send_wait_time(uint16_t time_in_ms) { this->send_wait_time_ = time_in_ms; } + void set_turnaround_time(uint16_t time_in_ms) { this->turnaround_delay_ms_ = time_in_ms; } + void set_disable_crc(bool disable_crc) { this->disable_crc_ = disable_crc; } ModbusRole role; protected: - GPIOPin *flow_control_pin_{nullptr}; - bool parse_modbus_byte_(uint8_t byte); - uint16_t send_wait_time_{250}; - bool disable_crc_; - std::vector rx_buffer_; + void receive_and_parse_modbus_bytes_(); + void clear_rx_buffer_(const LogString *reason, bool warn = false); + void send_next_frame_(); + void queue_raw_(const uint8_t *data, uint16_t len); + uint32_t last_modbus_byte_{0}; uint32_t last_send_{0}; + uint32_t last_send_tx_offset_{0}; + uint16_t frame_delay_ms_{5}; + uint16_t long_rx_buffer_delay_ms_{0}; + uint16_t send_wait_time_{250}; + uint16_t turnaround_delay_ms_{100}; + uint8_t waiting_for_response_{0}; + bool disable_crc_{false}; + + GPIOPin *flow_control_pin_{nullptr}; + + std::vector rx_buffer_; std::vector devices_; + // std::deque is appropriate here since we need a FIFO buffer, and we can't know ahead of time how many + // requests will be queued. Each modbus component may queue multiple requests, and the sequence of scheduling + // may change at run time. + std::deque tx_buffer_; }; class ModbusDevice { @@ -76,7 +111,9 @@ class ModbusDevice { this->send_raw(error_response); } // If more than one device is connected block sending a new command before a response is received - bool waiting_for_response() { return parent_->waiting_for_response != 0; } + ESPDEPRECATED("Use ready_for_immediate_send() instead. Removed in 2026.9.0", "2026.3.0") + bool waiting_for_response() { return !ready_for_immediate_send(); } + bool ready_for_immediate_send() { return parent_->tx_buffer_empty() && !parent_->tx_blocked(); } protected: friend Modbus; diff --git a/esphome/components/modbus/modbus_definitions.h b/esphome/components/modbus/modbus_definitions.h index 07f101ae4c..c86d548578 100644 --- a/esphome/components/modbus/modbus_definitions.h +++ b/esphome/components/modbus/modbus_definitions.h @@ -81,6 +81,8 @@ const uint8_t MAX_NUM_OF_REGISTERS_TO_WRITE = 123; // 0x7B // 6.3 03 (0x03) Read Holding Registers // 6.4 04 (0x04) Read Input Registers const uint8_t MAX_NUM_OF_REGISTERS_TO_READ = 125; // 0x7D + +static constexpr uint16_t MAX_FRAME_SIZE = 256; /// End of Modbus definitions } // namespace modbus } // namespace esphome diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index c45c338bb3..aea79b2053 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -48,6 +48,7 @@ CONF_SERVER_REGISTERS = "server_registers" MULTI_CONF = True modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller") +modbus_ns = cg.esphome_ns.namespace("modbus") ModbusController = modbus_controller_ns.class_( "ModbusController", cg.PollingComponent, modbus.ModbusDevice ) @@ -56,7 +57,7 @@ SensorItem = modbus_controller_ns.struct("SensorItem") ServerCourtesyResponse = modbus_controller_ns.struct("ServerCourtesyResponse") ServerRegister = modbus_controller_ns.struct("ServerRegister") -ModbusFunctionCode_ns = modbus_controller_ns.namespace("ModbusFunctionCode") +ModbusFunctionCode_ns = modbus_ns.namespace("ModbusFunctionCode") ModbusFunctionCode = ModbusFunctionCode_ns.enum("ModbusFunctionCode") MODBUS_FUNCTION_CODE = { "read_coils": ModbusFunctionCode.READ_COILS, diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 50bd9f45cb..7f0eb230e0 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -18,7 +18,7 @@ void ModbusController::setup() { this->create_register_ranges_(); } bool ModbusController::send_next_command_() { uint32_t last_send = millis() - this->last_command_timestamp_; - if ((last_send > this->command_throttle_) && !waiting_for_response() && !this->command_queue_.empty()) { + if ((last_send > this->command_throttle_) && this->ready_for_immediate_send() && !this->command_queue_.empty()) { auto &command = this->command_queue_.front(); // remove from queue if command was sent too often diff --git a/tests/components/modbus/common.yaml b/tests/components/modbus/common.yaml index d636143ec9..221aab4ed8 100644 --- a/tests/components/modbus/common.yaml +++ b/tests/components/modbus/common.yaml @@ -1,3 +1,5 @@ modbus: id: mod_bus1 flow_control_pin: ${flow_control_pin} + send_wait_time: 500ms + turnaround_time: 100ms diff --git a/tests/integration/fixtures/external_components/uart_mock/__init__.py b/tests/integration/fixtures/external_components/uart_mock/__init__.py index abb3abcc41..c10d73354e 100644 --- a/tests/integration/fixtures/external_components/uart_mock/__init__.py +++ b/tests/integration/fixtures/external_components/uart_mock/__init__.py @@ -71,6 +71,7 @@ RESPONSE_SCHEMA = cv.Schema( { cv.Required(CONF_EXPECT_TX): [cv.hex_uint8_t], cv.Required(CONF_INJECT_RX): [cv.hex_uint8_t], + cv.Optional(CONF_DELAY, default="0ms"): cv.positive_time_period_milliseconds, } ) @@ -151,7 +152,8 @@ async def to_code(config): for response in config[CONF_RESPONSES]: tx_data = response[CONF_EXPECT_TX] rx_data = response[CONF_INJECT_RX] - cg.add(var.add_response(tx_data, rx_data)) + delay_ms = response[CONF_DELAY] + cg.add(var.add_response(tx_data, rx_data, delay_ms)) for periodic in config[CONF_PERIODIC_RX]: data = periodic[CONF_DATA] diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp index 83a13793be..affcc8d908 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -36,8 +36,8 @@ void MockUartComponent::loop() { // component (e.g., LD2410) a chance to process each batch independently. if (this->injection_index_ < this->injections_.size()) { auto &injection = this->injections_[this->injection_index_]; - uint32_t target_time = this->scenario_start_ms_ + this->cumulative_delay_ms_ + injection.delay_ms; - if (now >= target_time) { + uint32_t total_delay = this->cumulative_delay_ms_ + injection.delay_ms; + if (now - this->scenario_start_ms_ >= total_delay) { ESP_LOGD(TAG, "Injecting %zu RX bytes (injection %u)", injection.rx_data.size(), this->injection_index_); this->inject_to_rx_buffer(injection.rx_data); this->cumulative_delay_ms_ += injection.delay_ms; @@ -52,6 +52,15 @@ void MockUartComponent::loop() { periodic.last_inject_ms = now; } } + + // Process delayed responses + for (auto &response : this->responses_) { + if (response.delay_ms > 0 && response.last_match_ms > 0 && now - response.last_match_ms >= response.delay_ms) { + ESP_LOGD(TAG, "Injecting %zu RX bytes for delayed response", response.inject_rx.size()); + this->inject_to_rx_buffer(response.inject_rx); + response.last_match_ms = 0; // Reset to prevent repeated injection + } + } } void MockUartComponent::start_scenario() { @@ -149,8 +158,9 @@ void MockUartComponent::add_injection(const std::vector &rx_data, uint3 this->injections_.push_back({rx_data, delay_ms}); } -void MockUartComponent::add_response(const std::vector &expect_tx, const std::vector &inject_rx) { - this->responses_.push_back({expect_tx, inject_rx}); +void MockUartComponent::add_response(const std::vector &expect_tx, const std::vector &inject_rx, + uint32_t delay_ms) { + this->responses_.push_back({expect_tx, inject_rx, delay_ms, 0}); } void MockUartComponent::add_periodic_rx(const std::vector &data, uint32_t interval_ms) { @@ -166,7 +176,13 @@ void MockUartComponent::try_match_response_() { size_t offset = this->tx_buffer_.size() - response.expect_tx.size(); if (std::equal(response.expect_tx.begin(), response.expect_tx.end(), this->tx_buffer_.begin() + offset)) { ESP_LOGD(TAG, "TX match found, injecting %zu RX bytes", response.inject_rx.size()); - this->inject_to_rx_buffer(response.inject_rx); + if (response.delay_ms > 0) { + ESP_LOGD(TAG, "Delaying response by %u ms", response.delay_ms); + // Schedule the response injection as a future injection + response.last_match_ms = App.get_loop_component_start_time(); + } else { + this->inject_to_rx_buffer(response.inject_rx); + } this->tx_buffer_.clear(); return; } diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h index b721512f96..901e371dec 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h @@ -34,7 +34,8 @@ class MockUartComponent : public uart::UARTComponent, public Component { // Scenario configuration - called from generated code void add_injection(const std::vector &rx_data, uint32_t delay_ms); - void add_response(const std::vector &expect_tx, const std::vector &inject_rx); + void add_response(const std::vector &expect_tx, const std::vector &inject_rx, + uint32_t delay_ms = 0); void add_periodic_rx(const std::vector &data, uint32_t interval_ms); void start_scenario(); @@ -64,6 +65,8 @@ class MockUartComponent : public uart::UARTComponent, public Component { struct Response { std::vector expect_tx; std::vector inject_rx; + uint32_t delay_ms; + uint32_t last_match_ms{0}; }; std::vector responses_; std::vector tx_buffer_; diff --git a/tests/integration/fixtures/uart_mock_modbus.yaml b/tests/integration/fixtures/uart_mock_modbus.yaml index 0a3492a0d2..3ff7ab01bd 100644 --- a/tests/integration/fixtures/uart_mock_modbus.yaml +++ b/tests/integration/fixtures/uart_mock_modbus.yaml @@ -25,20 +25,64 @@ uart_mock: auto_start: false debug: responses: - - expect_tx: [0x01, 0x03, 0x00, 0x03, 0x00, 0x01, 0x74, 0x0A] # Read holding register 1 on device 1 + - expect_tx: [0x01, 0x03, 0x00, 0x03, 0x00, 0x01, 0x74, 0x0A] # Read holding register 3 on device 1 (basic_register) inject_rx: [0x01, 0x03, 0x02, 0x01, 0x03, 0xF9, 0xD5] # Return value 0x0103 (hex) = 259 (dec) + - expect_tx: [0x01, 0x03, 0x00, 0x05, 0x00, 0x01, 0x94, 0x0B] # Read holding register 5 on device 1 (delayed_response) + delay: 100ms # Shorter than modbus send_wait_time of 200ms, should succeed + inject_rx: [0x01, 0x03, 0x02, 0x00, 0xFF, 0xF8, 0x04] # Return value 0x00FF (hex) = 255 (dec) + - expect_tx: [0x02, 0x03, 0x00, 0x07, 0x00, 0x01, 0x35, 0xF8] # Read holding register 7 on device 2 (late_response) + delay: 300ms # Longer than modbus send_wait_time of 200ms, should cause timeout + inject_rx: [0x02, 0x03, 0x02, 0x00, 0xF0, 0xFC, 0x00] # Return value 0x00F0 (hex) = 240 (dec) + - expect_tx: [0x03, 0x03, 0x00, 0x09, 0x00, 0x01, 0x55, 0xEA] # Read holding register 9 on device 3 (no_response) + inject_rx: [] # No response, should cause timeout + - expect_tx: [0x01, 0x03, 0x00, 0x0A, 0x00, 0x01, 0xA4, 0x08] # Read holding register A on device 1 (exception_response) + inject_rx: [0x01, 0x83, 0x02, 0xC0, 0xF1] # Exception response with code 2 (illegal data address) modbus: uart_id: virtual_uart_dev + send_wait_time: 200ms + turnaround_time: 10ms modbus_controller: - address: 1 + - address: 1 + id: modbus_controller_ok + max_cmd_retries: 0 + update_interval: 1s + - address: 2 + id: modbus_controller_slow + max_cmd_retries: 0 + update_interval: 1s + - address: 3 + id: modbus_controller_offline + max_cmd_retries: 0 + update_interval: 1s sensor: - platform: modbus_controller name: "basic_register" address: 0x03 register_type: holding + modbus_controller_id: modbus_controller_ok + - platform: modbus_controller + name: "delayed_response" + address: 0x05 + register_type: holding + modbus_controller_id: modbus_controller_ok + - platform: modbus_controller + name: "late_response" + address: 0x07 + register_type: holding + modbus_controller_id: modbus_controller_slow + - platform: modbus_controller + name: "no_response" + address: 0x09 + register_type: holding + modbus_controller_id: modbus_controller_offline + - platform: modbus_controller + name: "exception_response" + address: 0x0A + register_type: holding + modbus_controller_id: modbus_controller_ok button: - platform: template diff --git a/tests/integration/fixtures/uart_mock_modbus_timing.yaml b/tests/integration/fixtures/uart_mock_modbus_timing.yaml index c4e29e5fe8..f4cf0bde37 100644 --- a/tests/integration/fixtures/uart_mock_modbus_timing.yaml +++ b/tests/integration/fixtures/uart_mock_modbus_timing.yaml @@ -46,10 +46,12 @@ uart_mock: modbus: uart_id: virtual_uart_dev + turnaround_time: 10ms sensor: - platform: sdm_meter address: 2 + update_interval: 1s phase_a: voltage: name: sdm_voltage diff --git a/tests/integration/test_uart_mock_modbus.py b/tests/integration/test_uart_mock_modbus.py index bf3c069750..6901dc27fe 100644 --- a/tests/integration/test_uart_mock_modbus.py +++ b/tests/integration/test_uart_mock_modbus.py @@ -39,9 +39,17 @@ async def test_uart_mock_modbus( # Track sensor state updates (after initial state is swallowed) sensor_states: dict[str, list[float]] = { "basic_register": [], + "delayed_response": [], + "late_response": [], + "no_response": [], + "exception_response": [], } basic_register_changed = loop.create_future() + delayed_response_changed = loop.create_future() + late_response_changed = loop.create_future() + no_response_changed = loop.create_future() + exception_response_changed = loop.create_future() def on_state(state: EntityState) -> None: if isinstance(state, SensorState) and not state.missing_state: @@ -54,6 +62,23 @@ async def test_uart_mock_modbus( and not basic_register_changed.done() ): basic_register_changed.set_result(True) + elif ( + sensor_name == "delayed_response" + and state.state == 255.0 + and not delayed_response_changed.done() + ): + delayed_response_changed.set_result(True) + elif ( + sensor_name == "late_response" and not late_response_changed.done() + ): + late_response_changed.set_result(True) + elif sensor_name == "no_response" and not no_response_changed.done(): + no_response_changed.set_result(True) + elif ( + sensor_name == "exception_response" + and not exception_response_changed.done() + ): + exception_response_changed.set_result(True) async with ( run_compiled(yaml_config), @@ -79,20 +104,52 @@ async def test_uart_mock_modbus( assert start_btn is not None, "Start Scenario button not found" client.button_command(start_btn.key) + try: + await asyncio.wait_for(delayed_response_changed, timeout=2.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for delayed_response change. Received sensor states:\n" + f" delayed_response: {sensor_states['delayed_response']}\n" + ) + + try: + await asyncio.wait_for(late_response_changed, timeout=2.0) + pytest.fail( + f"late_response change should not have been triggered, but was. Received sensor states:\n" + f" late_response: {sensor_states['late_response']}\n" + ) + except TimeoutError: + pass # Expected timeout since we never inject a response for late_response + + try: + await asyncio.wait_for(no_response_changed, timeout=2.0) + pytest.fail( + f"no_response change should not have been triggered, but was. Received sensor states:\n" + f" no_response: {sensor_states['no_response']}\n" + ) + except TimeoutError: + pass # Expected timeout since we never inject a response for no_response + # Wait for basic register to be updated with successful parse try: - await asyncio.wait_for(basic_register_changed, timeout=15.0) + await asyncio.wait_for(basic_register_changed, timeout=2.0) except TimeoutError: pytest.fail( f"Timeout waiting for Basic Register change. Received sensor states:\n" f" basic_register: {sensor_states['basic_register']}\n" ) + try: + await asyncio.wait_for(exception_response_changed, timeout=2.0) + pytest.fail( + f"exception_response change should not have been triggered, but was. Received sensor states:\n" + f" exception_response: {sensor_states['exception_response']}\n" + ) + except TimeoutError: + pass + @pytest.mark.asyncio -@pytest.mark.xfail( - reason="There is a bug in UART which will timeout for long responses." -) async def test_uart_mock_modbus_timing( yaml_config: str, run_compiled: RunCompiledFunction, @@ -155,7 +212,7 @@ async def test_uart_mock_modbus_timing( # Wait for voltage to be updated with successful parse try: - await asyncio.wait_for(voltage_changed, timeout=15.0) + await asyncio.wait_for(voltage_changed, timeout=2.0) except TimeoutError: pytest.fail( f"Timeout waiting for SDM voltage change. Received sensor states:\n" From 3392e4d73b564877f1a7d9b4c80f534d24a48d56 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:08:58 -0500 Subject: [PATCH 384/609] [usb_uart][nextion][feedback][whirlpool][packet_transport][he60r][hc8][runtime_stats] Fix millis() wrapping bugs (#14474) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/feedback/feedback_cover.cpp | 13 +++++++++---- esphome/components/hc8/hc8.cpp | 16 ++++++++++------ esphome/components/hc8/hc8.h | 1 + esphome/components/he60r/he60r.cpp | 2 +- esphome/components/nextion/nextion.cpp | 6 +++--- .../packet_transport/packet_transport.cpp | 11 ++++++----- .../components/runtime_stats/runtime_stats.cpp | 14 +++----------- esphome/components/runtime_stats/runtime_stats.h | 10 +++++++--- esphome/components/usb_uart/usb_uart.cpp | 4 ++-- esphome/components/whirlpool/whirlpool.h | 2 +- esphome/core/component.cpp | 2 +- 11 files changed, 44 insertions(+), 37 deletions(-) diff --git a/esphome/components/feedback/feedback_cover.cpp b/esphome/components/feedback/feedback_cover.cpp index d247bada33..1dff210cd6 100644 --- a/esphome/components/feedback/feedback_cover.cpp +++ b/esphome/components/feedback/feedback_cover.cpp @@ -437,10 +437,15 @@ void FeedbackCover::recompute_position_() { } // check if we have an acceleration_wait_time, and remove from position computation - if (now > (this->start_dir_time_ + this->acceleration_wait_time_)) { - this->position += - dir * (now - std::max(this->start_dir_time_ + this->acceleration_wait_time_, this->last_recompute_time_)) / - (action_dur - this->acceleration_wait_time_); + if (now - this->start_dir_time_ > this->acceleration_wait_time_) { + uint32_t accel_end_time = this->start_dir_time_ + this->acceleration_wait_time_; + uint32_t effective_start; + if (static_cast(accel_end_time - this->last_recompute_time_) >= 0) { + effective_start = accel_end_time; + } else { + effective_start = this->last_recompute_time_; + } + this->position += dir * (now - effective_start) / (action_dur - this->acceleration_wait_time_); this->position = clamp(this->position, min_pos, max_pos); } this->last_recompute_time_ = now; diff --git a/esphome/components/hc8/hc8.cpp b/esphome/components/hc8/hc8.cpp index 4c2d367b24..900acca691 100644 --- a/esphome/components/hc8/hc8.cpp +++ b/esphome/components/hc8/hc8.cpp @@ -24,12 +24,16 @@ void HC8Component::setup() { } void HC8Component::update() { - uint32_t now_ms = App.get_loop_component_start_time(); - uint32_t warmup_ms = this->warmup_seconds_ * 1000; - if (now_ms < warmup_ms) { - ESP_LOGW(TAG, "HC8 warming up, %" PRIu32 " s left", (warmup_ms - now_ms) / 1000); - this->status_set_warning(); - return; + if (!this->warmup_complete_) { + uint32_t now_ms = App.get_loop_component_start_time(); + uint32_t warmup_ms = this->warmup_seconds_ * 1000; + if (now_ms < warmup_ms) { + ESP_LOGW(TAG, "HC8 warming up, %" PRIu32 " s left", (warmup_ms - now_ms) / 1000); + this->status_set_warning(); + return; + } + this->warmup_complete_ = true; + this->status_clear_warning(); } while (this->available()) diff --git a/esphome/components/hc8/hc8.h b/esphome/components/hc8/hc8.h index 74257fab14..b060f38a80 100644 --- a/esphome/components/hc8/hc8.h +++ b/esphome/components/hc8/hc8.h @@ -23,6 +23,7 @@ class HC8Component : public PollingComponent, public uart::UARTDevice { protected: sensor::Sensor *co2_sensor_{nullptr}; uint32_t warmup_seconds_{0}; + bool warmup_complete_{false}; }; template class HC8CalibrateAction : public Action, public Parented { diff --git a/esphome/components/he60r/he60r.cpp b/esphome/components/he60r/he60r.cpp index fdcd1a29c0..47440cc1f7 100644 --- a/esphome/components/he60r/he60r.cpp +++ b/esphome/components/he60r/he60r.cpp @@ -239,7 +239,7 @@ void HE60rCover::recompute_position_() { return; const uint32_t now = millis(); - if (now > this->last_recompute_time_) { + if (now != this->last_recompute_time_) { auto diff = (unsigned) (now - last_recompute_time_); float delta; switch (this->current_operation) { diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 9f1ce47837..c8c1b6fa41 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -337,7 +337,7 @@ void Nextion::loop() { this->started_ms_ = App.get_loop_component_start_time(); if (this->startup_override_ms_ > 0 && - this->started_ms_ + this->startup_override_ms_ < App.get_loop_component_start_time()) { + App.get_loop_component_start_time() - this->started_ms_ > this->startup_override_ms_) { ESP_LOGV(TAG, "Manual ready set"); this->connection_state_.nextion_reports_is_setup_ = true; } @@ -853,10 +853,10 @@ void Nextion::process_nextion_commands_() { const uint32_t ms = App.get_loop_component_start_time(); if (this->max_q_age_ms_ > 0 && !this->nextion_queue_.empty() && - this->nextion_queue_.front()->queue_time + this->max_q_age_ms_ < ms) { + ms - this->nextion_queue_.front()->queue_time > this->max_q_age_ms_) { for (size_t i = 0; i < this->nextion_queue_.size(); i++) { NextionComponentBase *component = this->nextion_queue_[i]->component; - if (this->nextion_queue_[i]->queue_time + this->max_q_age_ms_ < ms) { + if (ms - this->nextion_queue_[i]->queue_time > this->max_q_age_ms_) { if (this->nextion_queue_[i]->queue_time == 0) { ESP_LOGD(TAG, "Remove old queue '%s':'%s' (t=0)", component->get_queue_type_string().c_str(), component->get_variable_name().c_str()); diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index f241cc6114..6f1286b469 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -330,15 +330,16 @@ void PacketTransport::update() { if (!this->ping_pong_enable_) { return; } - auto now = millis() / 1000; - if (this->last_key_time_ + this->ping_pong_recyle_time_ < now) { + uint32_t now = millis(); + uint32_t ping_request_age = now - this->last_key_time_; + if (ping_request_age > this->ping_pong_recyle_time_ * 1000u) { this->resend_ping_key_ = this->ping_pong_enable_; - ESP_LOGV(TAG, "Ping request, age %" PRIu32, now - this->last_key_time_); + ESP_LOGV(TAG, "Ping request, age %" PRIu32, ping_request_age); this->last_key_time_ = now; } for (const auto &provider : this->providers_) { uint32_t key_response_age = now - provider.second.last_key_response_time; - if (key_response_age > (this->ping_pong_recyle_time_ * 2u)) { + if (key_response_age > (this->ping_pong_recyle_time_ * 2000u)) { #ifdef USE_STATUS_SENSOR if (provider.second.status_sensor != nullptr && provider.second.status_sensor->state) { ESP_LOGI(TAG, "Ping status for %s timeout at %" PRIu32 " with age %" PRIu32, provider.first.c_str(), now, @@ -496,7 +497,7 @@ void PacketTransport::process_(std::span data) { if (decoder.decode(PING_KEY, key) == DECODE_OK) { if (key == this->ping_key_) { ping_key_seen = true; - provider.last_key_response_time = millis() / 1000; + provider.last_key_response_time = millis(); ESP_LOGV(TAG, "Found good ping key %X at timestamp %" PRIu32, (unsigned) key, provider.last_key_response_time); } else { ESP_LOGV(TAG, "Unknown ping key %X", (unsigned) key); diff --git a/esphome/components/runtime_stats/runtime_stats.cpp b/esphome/components/runtime_stats/runtime_stats.cpp index d9fa22d949..cb28acc96c 100644 --- a/esphome/components/runtime_stats/runtime_stats.cpp +++ b/esphome/components/runtime_stats/runtime_stats.cpp @@ -9,21 +9,16 @@ namespace esphome { namespace runtime_stats { -RuntimeStatsCollector::RuntimeStatsCollector() : log_interval_(60000), next_log_time_(0) { +RuntimeStatsCollector::RuntimeStatsCollector() : log_interval_(60000), next_log_time_(60000) { global_runtime_stats = this; } -void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_us, uint32_t current_time) { +void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_us) { if (component == nullptr) return; // Record stats using component pointer as key this->component_stats_[component].record_time(duration_us); - - if (this->next_log_time_ == 0) { - this->next_log_time_ = current_time + this->log_interval_; - return; - } } void RuntimeStatsCollector::log_stats_() { @@ -88,10 +83,7 @@ void RuntimeStatsCollector::log_stats_() { } void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) { - if (this->next_log_time_ == 0) - return; - - if (current_time >= this->next_log_time_) { + if ((int32_t) (current_time - this->next_log_time_) >= 0) { this->log_stats_(); this->reset_stats_(); this->next_log_time_ = current_time + this->log_interval_; diff --git a/esphome/components/runtime_stats/runtime_stats.h b/esphome/components/runtime_stats/runtime_stats.h index 0847529720..303d895985 100644 --- a/esphome/components/runtime_stats/runtime_stats.h +++ b/esphome/components/runtime_stats/runtime_stats.h @@ -7,6 +7,7 @@ #include #include #include +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -80,10 +81,13 @@ class RuntimeStatsCollector { public: RuntimeStatsCollector(); - void set_log_interval(uint32_t log_interval) { this->log_interval_ = log_interval; } + void set_log_interval(uint32_t log_interval) { + this->log_interval_ = log_interval; + this->next_log_time_ = millis() + log_interval; + } uint32_t get_log_interval() const { return this->log_interval_; } - void record_component_time(Component *component, uint32_t duration_us, uint32_t current_time); + void record_component_time(Component *component, uint32_t duration_us); // Process any pending stats printing (should be called after component loop) void process_pending_stats(uint32_t current_time); @@ -101,7 +105,7 @@ class RuntimeStatsCollector { // We use Component* as the key since each component is unique std::map component_stats_; uint32_t log_interval_; - uint32_t next_log_time_; + uint32_t next_log_time_{0}; }; } // namespace runtime_stats diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 5c0397b2cb..e20bbd02db 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -171,8 +171,8 @@ void USBUartChannel::flush() { // Safe to call from the main loop only. // The 100 ms timeout guards against a device that stops responding mid-flush; // in that case the main loop is blocked for the full duration. - uint32_t deadline = millis() + 100; // 100 ms safety timeout - while ((!this->output_queue_.empty() || this->output_started_.load()) && millis() < deadline) { + uint32_t start = millis(); // 100 ms safety timeout + while ((!this->output_queue_.empty() || this->output_started_.load()) && millis() - start < 100) { // Kick start_output() in case data arrived but no transfer is in flight yet. this->parent_->start_output(this); yield(); diff --git a/esphome/components/whirlpool/whirlpool.h b/esphome/components/whirlpool/whirlpool.h index 907a21225c..992b2a7adf 100644 --- a/esphome/components/whirlpool/whirlpool.h +++ b/esphome/components/whirlpool/whirlpool.h @@ -48,7 +48,7 @@ class WhirlpoolClimate : public climate_ir::ClimateIR { /// Handle received IR Buffer bool on_receive(remote_base::RemoteReceiveData data) override; /// Set the time of the last transmission. - int32_t last_transmit_time_{}; + uint32_t last_transmit_time_{}; bool send_swing_cmd_{false}; Model model_; diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 8c2c8d38e8..a9ff3ec1eb 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -534,7 +534,7 @@ uint32_t WarnIfComponentBlockingGuard::finish() { // 1ms granularity, so results were essentially random noise. if (global_runtime_stats != nullptr) { uint32_t duration_us = micros() - this->started_us_; - global_runtime_stats->record_component_time(this->component_, duration_us, curr_time); + global_runtime_stats->record_component_time(this->component_, duration_us); } #endif if (blocking_time > WARN_IF_BLOCKING_OVER_MS) { From de14e7055e969d1e94302fdec544a2aa39d20e89 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:10:26 -0500 Subject: [PATCH 385/609] [cse7761][ads1115][tmp1075][matrix_keypad][seeed_mr60bha2] Fix assorted bugs (#14518) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/ads1115/ads1115.cpp | 15 ++------------- esphome/components/cse7761/cse7761.cpp | 10 +++++++--- .../components/matrix_keypad/matrix_keypad.cpp | 4 ++-- .../components/seeed_mr60bha2/seeed_mr60bha2.cpp | 12 ++++++++---- esphome/components/tmp1075/tmp1075.cpp | 4 ++-- 5 files changed, 21 insertions(+), 24 deletions(-) diff --git a/esphome/components/ads1115/ads1115.cpp b/esphome/components/ads1115/ads1115.cpp index f4996cd3b1..d493a6a6d3 100644 --- a/esphome/components/ads1115/ads1115.cpp +++ b/esphome/components/ads1115/ads1115.cpp @@ -173,19 +173,8 @@ float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1 } if (resolution == ADS1015_12_BITS) { - bool negative = (raw_conversion >> 15) == 1; - - // shift raw_conversion as it's only 12-bits, left justified - raw_conversion = raw_conversion >> (16 - ADS1015_12_BITS); - - // check if number was negative in order to keep the sign - if (negative) { - // the number was negative - // 1) set the negative bit back - raw_conversion |= 0x8000; - // 2) reset the former (shifted) negative bit - raw_conversion &= 0xF7FF; - } + // ADS1015 returns 12-bit value left-justified in 16 bits; shift right and sign-extend + raw_conversion = static_cast(static_cast(raw_conversion) >> (16 - ADS1015_12_BITS)); } auto signed_conversion = static_cast(raw_conversion); diff --git a/esphome/components/cse7761/cse7761.cpp b/esphome/components/cse7761/cse7761.cpp index f4966357d4..7525b901f8 100644 --- a/esphome/components/cse7761/cse7761.cpp +++ b/esphome/components/cse7761/cse7761.cpp @@ -147,13 +147,17 @@ uint32_t CSE7761Component::read_(uint8_t reg, uint8_t size) { } uint32_t CSE7761Component::coefficient_by_unit_(uint32_t unit) { + uint32_t coeff = 0; switch (unit) { case RMS_UC: - return 0x400000 * 100 / this->data_.coefficient[RMS_UC]; + coeff = this->data_.coefficient[RMS_UC]; + return coeff ? 0x400000 * 100 / coeff : 0; case RMS_IAC: - return (0x800000 * 100 / this->data_.coefficient[RMS_IAC]) * 10; // Stay within 32 bits + coeff = this->data_.coefficient[RMS_IAC]; + return coeff ? (0x800000 * 100 / coeff) * 10 : 0; // Stay within 32 bits case POWER_PAC: - return 0x80000000 / this->data_.coefficient[POWER_PAC]; + coeff = this->data_.coefficient[POWER_PAC]; + return coeff ? 0x80000000 / coeff : 0; } return 0; } diff --git a/esphome/components/matrix_keypad/matrix_keypad.cpp b/esphome/components/matrix_keypad/matrix_keypad.cpp index 43a20c49d1..febbe794e4 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.cpp +++ b/esphome/components/matrix_keypad/matrix_keypad.cpp @@ -61,7 +61,7 @@ void MatrixKeypad::loop() { ESP_LOGD(TAG, "key @ row %d, col %d released", row, col); for (auto &listener : this->listeners_) listener->button_released(row, col); - if (!this->keys_.empty()) { + if (this->pressed_key_ < (int) this->keys_.size()) { uint8_t keycode = this->keys_[this->pressed_key_]; ESP_LOGD(TAG, "key '%c' released", keycode); for (auto &listener : this->listeners_) @@ -84,7 +84,7 @@ void MatrixKeypad::loop() { ESP_LOGD(TAG, "key @ row %d, col %d pressed", row, col); for (auto &listener : this->listeners_) listener->button_pressed(row, col); - if (!this->keys_.empty()) { + if (key < (int) this->keys_.size()) { uint8_t keycode = this->keys_[key]; ESP_LOGD(TAG, "key '%c' pressed", keycode); for (auto &trigger : this->key_triggers_) diff --git a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp index 12f188fe03..8628faac5a 100644 --- a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp +++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp @@ -177,10 +177,14 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c uint16_t has_target_int = encode_uint16(data[1], data[0]); this->has_target_binary_sensor_->publish_state(has_target_int); if (has_target_int == 0) { - this->breath_rate_sensor_->publish_state(0.0); - this->heart_rate_sensor_->publish_state(0.0); - this->distance_sensor_->publish_state(0.0); - this->num_targets_sensor_->publish_state(0); + if (this->breath_rate_sensor_ != nullptr) + this->breath_rate_sensor_->publish_state(0.0); + if (this->heart_rate_sensor_ != nullptr) + this->heart_rate_sensor_->publish_state(0.0); + if (this->distance_sensor_ != nullptr) + this->distance_sensor_->publish_state(0.0); + if (this->num_targets_sensor_ != nullptr) + this->num_targets_sensor_->publish_state(0); } } break; diff --git a/esphome/components/tmp1075/tmp1075.cpp b/esphome/components/tmp1075/tmp1075.cpp index 9eb1e86c75..3c7ed01970 100644 --- a/esphome/components/tmp1075/tmp1075.cpp +++ b/esphome/components/tmp1075/tmp1075.cpp @@ -118,8 +118,8 @@ void TMP1075Sensor::send_alert_limit_high_() { } static uint16_t temp2regvalue(const float temp) { - const uint16_t regvalue = temp / 0.0625f; - return regvalue << 4; + const int16_t regvalue = static_cast(temp / 0.0625f); + return static_cast(regvalue << 4); } static float regvalue2temp(const uint16_t regvalue) { From 06d6322fe3ce5d4c36a24a7fafb1711c647c4439 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 5 Mar 2026 15:19:45 -0600 Subject: [PATCH 386/609] [audio] Extract detect_audio_file_type helper (#14507) Co-authored-by: J. Nick Koston --- esphome/components/audio/audio.cpp | 56 +++++++++++++++++++++++ esphome/components/audio/audio.h | 7 +++ esphome/components/audio/audio_reader.cpp | 50 ++------------------ esphome/components/audio/audio_reader.h | 5 -- 4 files changed, 66 insertions(+), 52 deletions(-) diff --git a/esphome/components/audio/audio.cpp b/esphome/components/audio/audio.cpp index 40592f6107..3d675109e4 100644 --- a/esphome/components/audio/audio.cpp +++ b/esphome/components/audio/audio.cpp @@ -1,5 +1,9 @@ #include "audio.h" +#include "esphome/core/helpers.h" + +#include + namespace esphome { namespace audio { @@ -58,6 +62,58 @@ const char *audio_file_type_to_string(AudioFileType file_type) { } } +AudioFileType detect_audio_file_type(const char *content_type, const char *url) { + // Try Content-Type header first + if (content_type != nullptr && content_type[0] != '\0') { +#ifdef USE_AUDIO_MP3_SUPPORT + if (strcasecmp(content_type, "mp3") == 0 || strcasecmp(content_type, "audio/mp3") == 0 || + strcasecmp(content_type, "audio/mpeg") == 0) { + return AudioFileType::MP3; + } +#endif + if (strcasecmp(content_type, "audio/wav") == 0) { + return AudioFileType::WAV; + } +#ifdef USE_AUDIO_FLAC_SUPPORT + if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) { + return AudioFileType::FLAC; + } +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + // Match "audio/ogg" with a codecs parameter containing "opus" + // Valid forms: audio/ogg;codecs=opus, audio/ogg; codecs="opus", etc. + // Plain "audio/ogg" without opus is not matched (almost always Ogg Vorbis) + if (strncasecmp(content_type, "audio/ogg", 9) == 0 && strcasestr(content_type + 9, "opus") != nullptr) { + return AudioFileType::OPUS; + } +#endif + } + + // Fallback to URL extension + if (url != nullptr && url[0] != '\0') { + if (str_endswith_ignore_case(url, ".wav")) { + return AudioFileType::WAV; + } +#ifdef USE_AUDIO_MP3_SUPPORT + if (str_endswith_ignore_case(url, ".mp3")) { + return AudioFileType::MP3; + } +#endif +#ifdef USE_AUDIO_FLAC_SUPPORT + if (str_endswith_ignore_case(url, ".flac")) { + return AudioFileType::FLAC; + } +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + if (str_endswith_ignore_case(url, ".opus")) { + return AudioFileType::OPUS; + } +#endif + } + + return AudioFileType::NONE; +} + void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor, size_t samples_to_scale) { // Note the assembly dsps_mulc function has audio glitches if the input and output buffers are the same. diff --git a/esphome/components/audio/audio.h b/esphome/components/audio/audio.h index 7d7db9e944..d3b41a362f 100644 --- a/esphome/components/audio/audio.h +++ b/esphome/components/audio/audio.h @@ -130,6 +130,13 @@ struct AudioFile { /// @return const char pointer to the readable file type const char *audio_file_type_to_string(AudioFileType file_type); +/// @brief Detect audio file type from a Content-Type header value and/or URL extension. +/// Tries Content-Type first, then falls back to URL extension. Either parameter may be null. +/// @param content_type Content-Type header value (may be null or empty) +/// @param url URL to inspect for file extension (may be null or empty) +/// @return The detected AudioFileType, or NONE if unknown +AudioFileType detect_audio_file_type(const char *content_type, const char *url); + /// @brief Scales Q15 fixed point audio samples. Scales in place if audio_samples == output_buffer. /// @param audio_samples PCM int16 audio samples /// @param output_buffer Buffer to store the scaled samples diff --git a/esphome/components/audio/audio_reader.cpp b/esphome/components/audio/audio_reader.cpp index 78d69d7a39..79ebf58889 100644 --- a/esphome/components/audio/audio_reader.cpp +++ b/esphome/components/audio/audio_reader.cpp @@ -185,26 +185,8 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { return err; } - if (str_endswith_ignore_case(url, ".wav")) { - file_type = AudioFileType::WAV; - } -#ifdef USE_AUDIO_MP3_SUPPORT - else if (str_endswith_ignore_case(url, ".mp3")) { - file_type = AudioFileType::MP3; - } -#endif -#ifdef USE_AUDIO_FLAC_SUPPORT - else if (str_endswith_ignore_case(url, ".flac")) { - file_type = AudioFileType::FLAC; - } -#endif -#ifdef USE_AUDIO_OPUS_SUPPORT - else if (str_endswith_ignore_case(url, ".opus")) { - file_type = AudioFileType::OPUS; - } -#endif - else { - file_type = AudioFileType::NONE; + file_type = detect_audio_file_type(nullptr, url); + if (file_type == AudioFileType::NONE) { this->cleanup_connection_(); return ESP_ERR_NOT_SUPPORTED; } @@ -232,32 +214,6 @@ AudioReaderState AudioReader::read() { return AudioReaderState::FAILED; } -AudioFileType AudioReader::get_audio_type(const char *content_type) { -#ifdef USE_AUDIO_MP3_SUPPORT - if (strcasecmp(content_type, "mp3") == 0 || strcasecmp(content_type, "audio/mp3") == 0 || - strcasecmp(content_type, "audio/mpeg") == 0) { - return AudioFileType::MP3; - } -#endif - if (strcasecmp(content_type, "audio/wav") == 0) { - return AudioFileType::WAV; - } -#ifdef USE_AUDIO_FLAC_SUPPORT - if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) { - return AudioFileType::FLAC; - } -#endif -#ifdef USE_AUDIO_OPUS_SUPPORT - // Match "audio/ogg" with a codecs parameter containing "opus" - // Valid forms: audio/ogg;codecs=opus, audio/ogg; codecs="opus", etc. - // Plain "audio/ogg" without a codecs parameter is not matched, as those are almost always Ogg Vorbis streams - if (strncasecmp(content_type, "audio/ogg", 9) == 0 && strcasestr(content_type + 9, "opus") != nullptr) { - return AudioFileType::OPUS; - } -#endif - return AudioFileType::NONE; -} - esp_err_t AudioReader::http_event_handler(esp_http_client_event_t *evt) { // Based on https://github.com/maroc81/WeatherLily/tree/main/main/net accessed 20241224 AudioReader *this_reader = (AudioReader *) evt->user_data; @@ -265,7 +221,7 @@ esp_err_t AudioReader::http_event_handler(esp_http_client_event_t *evt) { switch (evt->event_id) { case HTTP_EVENT_ON_HEADER: if (strcasecmp(evt->header_key, "Content-Type") == 0) { - this_reader->audio_file_type_ = get_audio_type(evt->header_value); + this_reader->audio_file_type_ = detect_audio_file_type(evt->header_value, nullptr); } break; default: diff --git a/esphome/components/audio/audio_reader.h b/esphome/components/audio/audio_reader.h index 0b73923e84..753b310213 100644 --- a/esphome/components/audio/audio_reader.h +++ b/esphome/components/audio/audio_reader.h @@ -58,11 +58,6 @@ class AudioReader { /// @brief Monitors the http client events to attempt determining the file type from the Content-Type header static esp_err_t http_event_handler(esp_http_client_event_t *evt); - /// @brief Determines the audio file type from the http header's Content-Type key - /// @param content_type string with the Content-Type key - /// @return AudioFileType of the url, if it can be determined. If not, return AudioFileType::NONE. - static AudioFileType get_audio_type(const char *content_type); - AudioReaderState file_read_(); AudioReaderState http_read_(); From fbf63d8e3bec76734ae5eb2e487aca43e0747913 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Mar 2026 11:23:00 -1000 Subject: [PATCH 387/609] [rp2040] Update arduino-pico to 5.5.1 and fix WiFi AP fallback (#14500) --- .clang-tidy.hash | 2 +- esphome/components/rp2040/__init__.py | 6 +- esphome/components/wifi/__init__.py | 8 +- .../components/wifi/wifi_component_pico_w.cpp | 81 ++++++++++++------- platformio.ini | 2 +- 5 files changed, 65 insertions(+), 34 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 767da3f33e..adcebadeb4 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -b97e16a84153b2a4cfc51137cd6121db3c32374504b2bea55144413b3e573052 +b6f8c16c1ddd222134bf4a71910b4c832e764e23caf49f9bce3280b079955fcf diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index ea269a47c5..1442a0a7f7 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -91,7 +91,7 @@ def _parse_platform_version(value): # The default/recommended arduino framework version # - https://github.com/earlephilhower/arduino-pico/releases # - https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(5, 5, 0) +RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(5, 5, 1) # The raspberrypi platform version to use for arduino frameworks # - https://github.com/maxgerhardt/platform-raspberrypi/tags @@ -101,8 +101,8 @@ RECOMMENDED_ARDUINO_PLATFORM_VERSION = "v1.4.0-gcc14-arduinopico460" def _arduino_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(5, 5, 0), "https://github.com/earlephilhower/arduino-pico"), - "latest": (cv.Version(5, 5, 0), None), + "dev": (cv.Version(5, 5, 1), "https://github.com/earlephilhower/arduino-pico"), + "latest": (cv.Version(5, 5, 1), None), "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 2aa63b87cc..0f86ec059e 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -210,7 +210,13 @@ WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend( def wifi_network_ap(value): if value is None: value = {} - return WIFI_NETWORK_AP(value) + config = WIFI_NETWORK_AP(value) + if CONF_MANUAL_IP in config and CORE.is_rp2040: + raise cv.Invalid( + "Manual AP IP configuration is not supported on RP2040. " + "The AP uses the default IP 192.168.4.1" + ) + return config WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend( diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 270425d8c2..5140fb285e 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -18,6 +18,25 @@ namespace esphome::wifi { static const char *const TAG = "wifi_pico_w"; +// Check if STA is fully connected (WiFi joined + has IP address). +// Do NOT use WiFi.status() or WiFi.connected() for this — in AP-only mode they +// unconditionally return true regardless of STA state, causing false positives +// when the fallback AP is active. +static bool wifi_sta_connected() { + int link = cyw43_wifi_link_status(&cyw43_state, CYW43_ITF_STA); + IPAddress local = WiFi.localIP(); + if (link == CYW43_LINK_JOIN && local.isSet()) { + // Verify the IP is a real STA IP, not the AP's IP leaking through + IPAddress ap_ip = WiFi.softAPIP(); + if (local == ap_ip) { + ESP_LOGV(TAG, "wifi_sta_connected: localIP %s matches AP IP, ignoring", local.toString().c_str()); + return false; + } + return true; + } + return false; +} + // Track previous state for detecting changes static bool s_sta_was_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_sta_had_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -27,17 +46,21 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) { if (sta.has_value()) { if (sta.value()) { cyw43_wifi_set_up(&cyw43_state, CYW43_ITF_STA, true, CYW43_COUNTRY_WORLDWIDE); + } else { + // Leave the STA network so the radio is free for scanning. + // Use cyw43_wifi_leave directly to avoid corrupting Arduino framework state. + cyw43_wifi_leave(&cyw43_state, CYW43_ITF_STA); } } - bool ap_state = false; if (ap.has_value()) { if (ap.value()) { cyw43_wifi_set_up(&cyw43_state, CYW43_ITF_AP, true, CYW43_COUNTRY_WORLDWIDE); - ap_state = true; + } else { + cyw43_wifi_set_up(&cyw43_state, CYW43_ITF_AP, false, CYW43_COUNTRY_WORLDWIDE); } + this->ap_started_ = ap.value(); } - this->ap_started_ = ap_state; return true; } @@ -129,8 +152,8 @@ WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { int status = cyw43_wifi_link_status(&cyw43_state, CYW43_ITF_STA); switch (status) { case CYW43_LINK_JOIN: - // WiFi joined, check if we have an IP address via the Arduino framework's WiFi class - if (WiFi.status() == WL_CONNECTED) { + // WiFi joined, check if STA has an IP address via wifi_sta_connected() + if (wifi_sta_connected()) { return WiFiSTAConnectStatus::CONNECTED; } return WiFiSTAConnectStatus::CONNECTING; @@ -188,19 +211,9 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { #ifdef USE_WIFI_AP bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { - esphome::network::IPAddress ip_address, gateway, subnet, dns; - if (manual_ip.has_value()) { - ip_address = manual_ip->static_ip; - gateway = manual_ip->gateway; - subnet = manual_ip->subnet; - dns = manual_ip->static_ip; - } else { - ip_address = network::IPAddress(192, 168, 4, 1); - gateway = network::IPAddress(192, 168, 4, 1); - subnet = network::IPAddress(255, 255, 255, 0); - dns = network::IPAddress(192, 168, 4, 1); - } - WiFi.config(ip_address, dns, gateway, subnet); + // AP IP is configured by WiFi.beginAP() internally using defaults (192.168.4.1). + // Manual AP IP has never worked on RP2040 — WiFi.config() configures the STA + // interface, not the AP. This is now rejected at config validation time. return true; } @@ -219,18 +232,25 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { } #endif - WiFi.beginAP(ap.ssid_.c_str(), ap.password_.c_str(), ap.has_channel() ? ap.get_channel() : 1); + // Pass nullptr for empty password — CYW43 uses the password pointer (not length) + // to choose between OPEN and WPA2 auth mode. + const char *ap_password = ap.password_.empty() ? nullptr : ap.password_.c_str(); + WiFi.beginAP(ap.ssid_.c_str(), ap_password, ap.has_channel() ? ap.get_channel() : 1); return true; } -network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {(const ip_addr_t *) WiFi.localIP()}; } +network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {(const ip_addr_t *) WiFi.softAPIP()}; } #endif // USE_WIFI_AP bool WiFiComponent::wifi_disconnect_() { - // Use Arduino WiFi.disconnect() instead of raw cyw43_wifi_leave() to properly - // clean up the lwIP netif, DHCP client, and internal Arduino state. - WiFi.disconnect(); + // Use cyw43_wifi_leave() directly instead of WiFi.disconnect(). + // WiFi.disconnect() sets _wifiHWInitted=false in the Arduino framework. beginAP() + // uses _wifiHWInitted to determine AP+STA vs AP-only mode — with it false, + // beginAP() enters AP-only mode (IP 192.168.42.1) instead of AP_STA mode + // (IP 192.168.4.1). In AP-only mode, _beginInternal() redirects all subsequent + // STA connect attempts to beginAP(), creating an infinite loop. + cyw43_wifi_leave(&cyw43_state, CYW43_ITF_STA); return true; } @@ -251,14 +271,21 @@ const char *WiFiComponent::wifi_ssid_to(std::span buffer buffer[len] = '\0'; return buffer.data(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } +int8_t WiFiComponent::wifi_rssi() { return this->is_connected_() ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { network::IPAddresses addresses; uint8_t index = 0; + // Filter out AP interface addresses — addrList includes all lwIP netifs. + // The AP netif IP lingers even after the AP radio is disabled. + IPAddress ap_ip = WiFi.softAPIP(); for (auto addr : addrList) { - addresses[index++] = addr.ipFromNetifNum(); + IPAddress ip(addr.ipFromNetifNum()); + if (ip == ap_ip) { + continue; + } + addresses[index++] = ip; } return addresses; } @@ -288,9 +315,7 @@ void WiFiComponent::wifi_loop_() { // Poll for connection state changes // The arduino-pico WiFi library doesn't have event callbacks like ESP8266/ESP32, // so we need to poll the link status to detect state changes. - // Use WiFi.connected() which checks both the WiFi link and IP address via the - // Arduino framework's own netif (not the SDK's uninitialized one). - bool is_connected = WiFi.connected(); + bool is_connected = wifi_sta_connected(); // Detect connection state change if (is_connected && !s_sta_was_connected) { diff --git a/platformio.ini b/platformio.ini index 16a1b18211..87f992759c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -196,7 +196,7 @@ board_build.filesystem_size = 0.5m platform = https://github.com/maxgerhardt/platform-raspberrypi.git#v1.4.0-gcc14-arduinopico460 platform_packages = ; earlephilhower/framework-arduinopico@~1.20602.0 ; Cannot use the platformio package until old releases stop getting deleted - earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/5.5.0/rp2040-5.5.0.zip + earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/5.5.1/rp2040-5.5.1.zip framework = arduino lib_deps = From e8b1dce67b2d31c1d8f9b2e44dc0d292d6e9d381 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:28:41 -0500 Subject: [PATCH 388/609] [st7735][st7789v][st7920] Fix display buffer overflows and dead code (#14511) Co-authored-by: Claude Opus 4.6 --- esphome/components/st7735/st7735.cpp | 14 +------------ esphome/components/st7735/st7735.h | 1 - esphome/components/st7789v/st7789v.cpp | 29 +++++++++++++++++--------- esphome/components/st7920/st7920.cpp | 15 +++++++------ 4 files changed, 29 insertions(+), 30 deletions(-) diff --git a/esphome/components/st7735/st7735.cpp b/esphome/components/st7735/st7735.cpp index 58459b79bb..0fcfdd6c71 100644 --- a/esphome/components/st7735/st7735.cpp +++ b/esphome/components/st7735/st7735.cpp @@ -466,7 +466,7 @@ void HOT ST7735::write_display_data_() { } void ST7735::spi_master_write_addr_(uint16_t addr1, uint16_t addr2) { - static uint8_t byte[4]; + uint8_t byte[4]; byte[0] = (addr1 >> 8) & 0xFF; byte[1] = addr1 & 0xFF; byte[2] = (addr2 >> 8) & 0xFF; @@ -476,17 +476,5 @@ void ST7735::spi_master_write_addr_(uint16_t addr1, uint16_t addr2) { this->write_array(byte, 4); } -void ST7735::spi_master_write_color_(uint16_t color, uint16_t size) { - static uint8_t byte[1024]; - int index = 0; - for (int i = 0; i < size; i++) { - byte[index++] = (color >> 8) & 0xFF; - byte[index++] = color & 0xFF; - } - - this->dc_pin_->digital_write(true); - write_array(byte, size * 2); -} - } // namespace st7735 } // namespace esphome diff --git a/esphome/components/st7735/st7735.h b/esphome/components/st7735/st7735.h index 37fe673962..e81be520ed 100644 --- a/esphome/components/st7735/st7735.h +++ b/esphome/components/st7735/st7735.h @@ -68,7 +68,6 @@ class ST7735 : public display::DisplayBuffer, void set_addr_window_(uint16_t x, uint16_t y, uint16_t w, uint16_t h); void draw_absolute_pixel_internal(int x, int y, Color color) override; void spi_master_write_addr_(uint16_t addr1, uint16_t addr2); - void spi_master_write_color_(uint16_t color, uint16_t size); int get_width_internal() override; int get_height_internal() override; diff --git a/esphome/components/st7789v/st7789v.cpp b/esphome/components/st7789v/st7789v.cpp index cd0b6cabc3..6e4360ae74 100644 --- a/esphome/components/st7789v/st7789v.cpp +++ b/esphome/components/st7789v/st7789v.cpp @@ -1,11 +1,16 @@ #include "st7789v.h" #include "esphome/core/log.h" +#include namespace esphome { namespace st7789v { static const char *const TAG = "st7789v"; -static const size_t TEMP_BUFFER_SIZE = 128; +#ifdef USE_ESP32 +static constexpr size_t TEMP_BUFFER_SIZE = 1024; +#else +static constexpr size_t TEMP_BUFFER_SIZE = 512; +#endif void ST7789V::setup() { #ifdef USE_POWER_SUPPLY @@ -236,7 +241,7 @@ void ST7789V::write_data_(uint8_t value) { } void ST7789V::write_addr_(uint16_t addr1, uint16_t addr2) { - static uint8_t byte[4]; + uint8_t byte[4]; byte[0] = (addr1 >> 8) & 0xFF; byte[1] = addr1 & 0xFF; byte[2] = (addr2 >> 8) & 0xFF; @@ -247,15 +252,19 @@ void ST7789V::write_addr_(uint16_t addr1, uint16_t addr2) { } void ST7789V::write_color_(uint16_t color, uint16_t size) { - static uint8_t byte[1024]; - int index = 0; - for (int i = 0; i < size; i++) { - byte[index++] = (color >> 8) & 0xFF; - byte[index++] = color & 0xFF; - } - + uint8_t byte[TEMP_BUFFER_SIZE]; + uint16_t remaining = size; this->dc_pin_->digital_write(true); - write_array(byte, size * 2); + while (remaining > 0) { + uint16_t batch = std::min(remaining, static_cast(sizeof(byte) / 2)); + int index = 0; + for (int i = 0; i < batch; i++) { + byte[index++] = (color >> 8) & 0xFF; + byte[index++] = color & 0xFF; + } + this->write_array(byte, batch * 2); + remaining -= batch; + } } size_t ST7789V::get_buffer_length_() { diff --git a/esphome/components/st7920/st7920.cpp b/esphome/components/st7920/st7920.cpp index afd7cd61bd..a840f98152 100644 --- a/esphome/components/st7920/st7920.cpp +++ b/esphome/components/st7920/st7920.cpp @@ -72,16 +72,19 @@ void ST7920::goto_xy_(uint16_t x, uint16_t y) { } void HOT ST7920::write_display_data() { - uint8_t i, j, b; - for (j = 0; j < (uint8_t) (this->get_height_internal() / 2); j++) { + int i, j; + uint8_t b; + int width_bytes = this->get_width_internal() / 8; + int half_height = this->get_height_internal() / 2; + for (j = 0; j < half_height; j++) { this->goto_xy_(0, j); this->enable(); - for (i = 0; i < 16; i++) { // 16 bytes from line #0+ - b = this->buffer_[i + j * 16]; + for (i = 0; i < width_bytes; i++) { + b = this->buffer_[i + j * width_bytes]; this->send_(LCD_DATA, b); } - for (i = 0; i < 16; i++) { // 16 bytes from line #32+ - b = this->buffer_[i + (j + 32) * 16]; + for (i = 0; i < width_bytes; i++) { + b = this->buffer_[i + (j + half_height) * width_bytes]; this->send_(LCD_DATA, b); } this->disable(); From b2c12d88fe15d31710365a0b8759317527abc2d7 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 5 Mar 2026 23:24:11 +0100 Subject: [PATCH 389/609] [uart] init tx_pin, rx_pin, flow control, rx_buffer_size (#14524) --- esphome/components/uart/uart_component.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index b6ffbbd51f..078ce64b30 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -183,10 +183,10 @@ class UARTComponent { virtual void check_logger_conflict() = 0; bool check_read_timeout_(size_t len = 1); - InternalGPIOPin *tx_pin_; - InternalGPIOPin *rx_pin_; - InternalGPIOPin *flow_control_pin_; - size_t rx_buffer_size_; + InternalGPIOPin *tx_pin_{}; + InternalGPIOPin *rx_pin_{}; + InternalGPIOPin *flow_control_pin_{}; + size_t rx_buffer_size_{}; size_t rx_full_threshold_{1}; size_t rx_timeout_{0}; uint32_t baud_rate_{0}; From 8a8f6824a200a8396a66f1873a7731504e75cc0b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:29:44 -0500 Subject: [PATCH 390/609] [openthread][ethernet][wifi] Add IPv6 address array bounds assert (#14488) Co-authored-by: Claude Opus 4.6 --- esphome/components/ethernet/ethernet_component.cpp | 1 + esphome/components/openthread/openthread.h | 2 +- esphome/components/openthread/openthread_esp.cpp | 1 + esphome/components/wifi/wifi_component_esp8266.cpp | 2 ++ esphome/components/wifi/wifi_component_esp_idf.cpp | 1 + esphome/components/wifi/wifi_component_pico_w.cpp | 3 +++ 6 files changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 098f7be972..0bc67c8b03 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -470,6 +470,7 @@ network::IPAddresses EthernetComponent::get_ip_addresses() { uint8_t count = 0; count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s); assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); + assert(count < addresses.size()); for (int i = 0; i < count; i++) { addresses[i + 1] = network::IPAddress(&if_ip6s[i]); } diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index d853c58f95..c87f4fa7c1 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -90,7 +90,7 @@ class InstanceLock { otInstance *get_instance(); private: - // Use a private constructor in order to force thehandling + // Use a private constructor in order to force the handling // of acquisition failure InstanceLock() {} }; diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 2296e32b7f..cdc7a404b2 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -197,6 +197,7 @@ network::IPAddresses OpenThreadComponent::get_ip_addresses() { esp_netif_t *netif = esp_netif_get_default_netif(); count = esp_netif_get_all_ip6(netif, if_ip6s); assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); + assert(count < addresses.size()); for (int i = 0; i < count; i++) { addresses[i + 1] = network::IPAddress(&if_ip6s[i]); } diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 02ce59502b..355832b434 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -6,6 +6,7 @@ #include +#include #include #include #ifdef USE_WIFI_WPA2_EAP @@ -205,6 +206,7 @@ network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { network::IPAddresses addresses; uint8_t index = 0; for (auto &addr : addrList) { + assert(index < addresses.size()); addresses[index++] = addr.ipFromNetifNum(); } return addresses; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index bf432cea6e..eca3f19249 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -585,6 +585,7 @@ network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { uint8_t count = 0; count = esp_netif_get_all_ip6(s_sta_netif, if_ip6s); assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); + assert(count < addresses.size()); for (int i = 0; i < count; i++) { addresses[i + 1] = network::IPAddress(&if_ip6s[i]); } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 5140fb285e..1cfeee3c1b 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -3,6 +3,8 @@ #ifdef USE_WIFI #ifdef USE_RP2040 +#include + #include "lwip/dns.h" #include "lwip/err.h" #include "lwip/netif.h" @@ -285,6 +287,7 @@ network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { if (ip == ap_ip) { continue; } + assert(index < addresses.size()); addresses[index++] = ip; } return addresses; From 64098122e77cc893d5e2cc288a5b30e6a1744724 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 5 Mar 2026 16:30:13 -0600 Subject: [PATCH 391/609] [audio_file] Add media source platform (#14436) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- CODEOWNERS | 1 + .../audio_file/media_source/__init__.py | 38 +++ .../media_source/audio_file_media_source.cpp | 283 ++++++++++++++++++ .../media_source/audio_file_media_source.h | 50 ++++ tests/components/audio_file/common.yaml | 4 + 5 files changed, 376 insertions(+) create mode 100644 esphome/components/audio_file/media_source/__init__.py create mode 100644 esphome/components/audio_file/media_source/audio_file_media_source.cpp create mode 100644 esphome/components/audio_file/media_source/audio_file_media_source.h diff --git a/CODEOWNERS b/CODEOWNERS index 7c37b20e09..8bf896d159 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -55,6 +55,7 @@ esphome/components/audio/* @kahrendt esphome/components/audio_adc/* @kbx81 esphome/components/audio_dac/* @kbx81 esphome/components/audio_file/* @kahrendt +esphome/components/audio_file/media_source/* @kahrendt esphome/components/axs15231/* @clydebarrow esphome/components/b_parasite/* @rbaron esphome/components/ballu/* @bazuchan diff --git a/esphome/components/audio_file/media_source/__init__.py b/esphome/components/audio_file/media_source/__init__.py new file mode 100644 index 0000000000..e9e292a2b2 --- /dev/null +++ b/esphome/components/audio_file/media_source/__init__.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +from esphome.components import media_source, psram +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_TASK_STACK_IN_PSRAM +from esphome.types import ConfigType + +CODEOWNERS = ["@kahrendt"] +AUTO_LOAD = ["audio"] +DEPENDENCIES = ["audio_file"] + +audio_file_ns = cg.esphome_ns.namespace("audio_file") +AudioFileMediaSource = audio_file_ns.class_( + "AudioFileMediaSource", cg.Component, media_source.MediaSource +) + +CONFIG_SCHEMA = cv.All( + media_source.media_source_schema( + AudioFileMediaSource, + ) + .extend( + { + cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All( + cv.boolean, cv.requires_component(psram.DOMAIN) + ), + } + ) + .extend(cv.COMPONENT_SCHEMA), + cv.only_on_esp32, +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await media_source.register_media_source(var, config) + + if CONF_TASK_STACK_IN_PSRAM in config: + cg.add(var.set_task_stack_in_psram(config[CONF_TASK_STACK_IN_PSRAM])) diff --git a/esphome/components/audio_file/media_source/audio_file_media_source.cpp b/esphome/components/audio_file/media_source/audio_file_media_source.cpp new file mode 100644 index 0000000000..120f871d2f --- /dev/null +++ b/esphome/components/audio_file/media_source/audio_file_media_source.cpp @@ -0,0 +1,283 @@ +#include "audio_file_media_source.h" + +#ifdef USE_ESP32 + +#include "esphome/components/audio/audio_decoder.h" + +#include + +namespace esphome::audio_file { + +namespace { // anonymous namespace for internal linkage +struct AudioSinkAdapter : public audio::AudioSinkCallback { + media_source::MediaSource *source; + audio::AudioStreamInfo stream_info; + + size_t audio_sink_write(uint8_t *data, size_t length, TickType_t ticks_to_wait) override { + return this->source->write_output(data, length, pdTICKS_TO_MS(ticks_to_wait), this->stream_info); + } +}; +} // namespace + +#if defined(USE_AUDIO_OPUS_SUPPORT) +static constexpr uint32_t DECODE_TASK_STACK_SIZE = 5 * 1024; +#else +static constexpr uint32_t DECODE_TASK_STACK_SIZE = 3 * 1024; +#endif + +static const char *const TAG = "audio_file_media_source"; + +enum EventGroupBits : uint32_t { + // Requests to start playback (set by play_uri, handled by loop) + REQUEST_START = (1 << 0), + // Commands from main loop to decode task + COMMAND_STOP = (1 << 1), + COMMAND_PAUSE = (1 << 2), + // Decode task lifecycle signals (one-shot, cleared by loop) + TASK_STARTING = (1 << 7), + TASK_RUNNING = (1 << 8), + TASK_STOPPING = (1 << 9), + TASK_STOPPED = (1 << 10), + TASK_ERROR = (1 << 11), + // Decode task state (level-triggered, set/cleared by decode task) + TASK_PAUSED = (1 << 12), + ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits +}; + +void AudioFileMediaSource::dump_config() { + ESP_LOGCONFIG(TAG, "Audio File Media Source:"); + ESP_LOGCONFIG(TAG, " Task Stack in PSRAM: %s", this->task_stack_in_psram_ ? "Yes" : "No"); +} + +void AudioFileMediaSource::setup() { + this->disable_loop(); + + this->event_group_ = xEventGroupCreate(); + if (this->event_group_ == nullptr) { + ESP_LOGE(TAG, "Failed to create event group"); + this->mark_failed(); + return; + } +} + +void AudioFileMediaSource::loop() { + EventBits_t event_bits = xEventGroupGetBits(this->event_group_); + + if (event_bits & REQUEST_START) { + xEventGroupClearBits(this->event_group_, REQUEST_START); + this->decoding_state_ = AudioFileDecodingState::START_TASK; + } + + switch (this->decoding_state_) { + case AudioFileDecodingState::START_TASK: { + if (!this->decode_task_.is_created()) { + xEventGroupClearBits(this->event_group_, ALL_BITS); + if (!this->decode_task_.create(decode_task, "AudioFileDec", DECODE_TASK_STACK_SIZE, this, 1, + this->task_stack_in_psram_)) { + ESP_LOGE(TAG, "Failed to create task"); + this->status_momentary_error("task_create", 1000); + this->set_state_(media_source::MediaSourceState::ERROR); + this->decoding_state_ = AudioFileDecodingState::IDLE; + return; + } + } + this->decoding_state_ = AudioFileDecodingState::DECODING; + break; + } + case AudioFileDecodingState::DECODING: { + if (event_bits & TASK_STARTING) { + ESP_LOGD(TAG, "Starting"); + xEventGroupClearBits(this->event_group_, TASK_STARTING); + } + + if (event_bits & TASK_RUNNING) { + ESP_LOGV(TAG, "Started"); + xEventGroupClearBits(this->event_group_, TASK_RUNNING); + this->set_state_(media_source::MediaSourceState::PLAYING); + } + + if ((event_bits & TASK_PAUSED) && this->get_state() != media_source::MediaSourceState::PAUSED) { + this->set_state_(media_source::MediaSourceState::PAUSED); + } else if (!(event_bits & TASK_PAUSED) && this->get_state() == media_source::MediaSourceState::PAUSED) { + this->set_state_(media_source::MediaSourceState::PLAYING); + } + + if (event_bits & TASK_STOPPING) { + ESP_LOGV(TAG, "Stopping"); + xEventGroupClearBits(this->event_group_, TASK_STOPPING); + } + + if (event_bits & TASK_ERROR) { + // Report error so the orchestrator knows playback failed; task will have already logged the specific error + this->set_state_(media_source::MediaSourceState::ERROR); + } + + if (event_bits & TASK_STOPPED) { + ESP_LOGD(TAG, "Stopped"); + xEventGroupClearBits(this->event_group_, ALL_BITS); + + this->decode_task_.deallocate(); + this->set_state_(media_source::MediaSourceState::IDLE); + this->decoding_state_ = AudioFileDecodingState::IDLE; + } + break; + } + case AudioFileDecodingState::IDLE: { + if (this->get_state() == media_source::MediaSourceState::ERROR && !this->status_has_error()) { + this->set_state_(media_source::MediaSourceState::IDLE); + } + break; + } + } + + if ((this->decoding_state_ == AudioFileDecodingState::IDLE) && + (this->get_state() == media_source::MediaSourceState::IDLE)) { + this->disable_loop(); + } +} + +// Called from the orchestrator's main loop, so no synchronization needed with loop() +bool AudioFileMediaSource::play_uri(const std::string &uri) { + if (!this->is_ready() || this->is_failed() || this->status_has_error() || !this->has_listener() || + xEventGroupGetBits(this->event_group_) & REQUEST_START) { + return false; + } + + // Check if source is already playing + if (this->get_state() != media_source::MediaSourceState::IDLE) { + ESP_LOGE(TAG, "Cannot play '%s': source is busy", uri.c_str()); + return false; + } + + // Validate URI starts with "audio-file://" + if (!uri.starts_with("audio-file://")) { + ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str()); + return false; + } + + // Strip "audio-file://" prefix and find the file + const char *file_id = uri.c_str() + 13; // "audio-file://" is 13 characters + + for (const auto &named_file : get_named_audio_files()) { + if (strcmp(named_file.file_id, file_id) == 0) { + this->current_file_ = named_file.file; + xEventGroupSetBits(this->event_group_, EventGroupBits::REQUEST_START); + this->enable_loop(); + return true; + } + } + + ESP_LOGE(TAG, "Unknown file: '%s'", file_id); + return false; +} + +// Called from the orchestrator's main loop, so no synchronization needed with loop() +void AudioFileMediaSource::handle_command(media_source::MediaSourceCommand command) { + if (this->decoding_state_ != AudioFileDecodingState::DECODING) { + return; + } + + switch (command) { + case media_source::MediaSourceCommand::STOP: + xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_STOP); + break; + case media_source::MediaSourceCommand::PAUSE: + xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_PAUSE); + break; + case media_source::MediaSourceCommand::PLAY: + xEventGroupClearBits(this->event_group_, EventGroupBits::COMMAND_PAUSE); + break; + default: + break; + } +} + +void AudioFileMediaSource::decode_task(void *params) { + AudioFileMediaSource *this_source = static_cast(params); + + do { // do-while(false) ensures RAII objects are destroyed on all exit paths via break + + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STARTING); + + // 0 bytes for input transfer buffer makes it an inplace buffer + std::unique_ptr decoder = make_unique(0, 4096); + + esp_err_t err = decoder->start(this_source->current_file_->file_type); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to start decoder: %s", esp_err_to_name(err)); + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR | EventGroupBits::TASK_STOPPING); + break; + } + + // Add the file as a const data source + decoder->add_source(this_source->current_file_->data, this_source->current_file_->length); + + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_RUNNING); + + AudioSinkAdapter audio_sink; + bool has_stream_info = false; + + while (true) { + EventBits_t event_bits = xEventGroupGetBits(this_source->event_group_); + + if (event_bits & EventGroupBits::COMMAND_STOP) { + break; + } + + bool paused = event_bits & EventGroupBits::COMMAND_PAUSE; + decoder->set_pause_output_state(paused); + if (paused) { + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_PAUSED); + vTaskDelay(pdMS_TO_TICKS(20)); + } else { + xEventGroupClearBits(this_source->event_group_, EventGroupBits::TASK_PAUSED); + } + + // Will stop gracefully once finished with the current file + audio::AudioDecoderState decoder_state = decoder->decode(true); + + if (decoder_state == audio::AudioDecoderState::FINISHED) { + break; + } else if (decoder_state == audio::AudioDecoderState::FAILED) { + ESP_LOGE(TAG, "Decoder failed"); + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR); + break; + } + + if (!has_stream_info && decoder->get_audio_stream_info().has_value()) { + has_stream_info = true; + + audio::AudioStreamInfo stream_info = decoder->get_audio_stream_info().value(); + + ESP_LOGD(TAG, "Bits per sample: %d, Channels: %d, Sample rate: %d", stream_info.get_bits_per_sample(), + stream_info.get_channels(), stream_info.get_sample_rate()); + + if (stream_info.get_bits_per_sample() != 16 || stream_info.get_channels() > 2) { + ESP_LOGE(TAG, "Incompatible audio stream. Only 16 bits per sample and 1 or 2 channels are supported"); + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR); + break; + } + + audio_sink.source = this_source; + audio_sink.stream_info = stream_info; + esp_err_t err = decoder->add_sink(&audio_sink); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to add sink: %s", esp_err_to_name(err)); + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR); + break; + } + } + } + + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STOPPING); + } while (false); + + // All RAII objects from the do-while block (decoder, audio_sink, etc.) are now destroyed. + + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STOPPED); + vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it +} + +} // namespace esphome::audio_file + +#endif // USE_ESP32 diff --git a/esphome/components/audio_file/media_source/audio_file_media_source.h b/esphome/components/audio_file/media_source/audio_file_media_source.h new file mode 100644 index 0000000000..75e18c13b8 --- /dev/null +++ b/esphome/components/audio_file/media_source/audio_file_media_source.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_ESP32 + +#include "esphome/components/audio/audio.h" +#include "esphome/components/audio_file/audio_file.h" +#include "esphome/components/media_source/media_source.h" +#include "esphome/core/component.h" +#include "esphome/core/static_task.h" + +#include +#include + +namespace esphome::audio_file { + +enum class AudioFileDecodingState : uint8_t { + START_TASK, + DECODING, + IDLE, +}; + +class AudioFileMediaSource : public Component, public media_source::MediaSource { + public: + void setup() override; + void loop() override; + void dump_config() override; + + // MediaSource interface implementation + bool play_uri(const std::string &uri) override; + void handle_command(media_source::MediaSourceCommand command) override; + bool can_handle(const std::string &uri) const override { return uri.starts_with("audio-file://"); } + + void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; } + + protected: + static void decode_task(void *params); + + audio::AudioFile *current_file_{nullptr}; + AudioFileDecodingState decoding_state_{AudioFileDecodingState::IDLE}; + EventGroupHandle_t event_group_{nullptr}; + StaticTask decode_task_; + + bool task_stack_in_psram_{false}; +}; + +} // namespace esphome::audio_file + +#endif // USE_ESP32 diff --git a/tests/components/audio_file/common.yaml b/tests/components/audio_file/common.yaml index 9404208094..e7f55b4806 100644 --- a/tests/components/audio_file/common.yaml +++ b/tests/components/audio_file/common.yaml @@ -3,3 +3,7 @@ audio_file: file: type: local path: $component_dir/test.wav + +media_source: + - platform: audio_file + id: audio_file_source From 58ab63096587dcfd75d7ad74a397d8b0687fb41f Mon Sep 17 00:00:00 2001 From: Gnuspice Date: Fri, 6 Mar 2026 12:37:07 +1300 Subject: [PATCH 392/609] [ethernet] add get_eth_handle() function (#14527) --- esphome/components/ethernet/ethernet_component.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index f5a31d78eb..e54e1543e3 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -115,6 +115,7 @@ class EthernetComponent : public Component { const char *get_eth_mac_address_pretty_into_buffer(std::span buf); eth_duplex_t get_duplex_mode(); eth_speed_t get_link_speed(); + esp_eth_handle_t get_eth_handle() const { return this->eth_handle_; } bool powerdown(); #ifdef USE_ETHERNET_IP_STATE_LISTENERS From 44870323dab606efe85a21d93ac0f32eaab22aee Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:47:47 -0500 Subject: [PATCH 393/609] [host] Add null checks for getenv and fopen in preferences (#14531) Co-authored-by: Claude Opus 4.6 --- esphome/components/host/preferences.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/esphome/components/host/preferences.cpp b/esphome/components/host/preferences.cpp index 5ad87c1f2a..275c202e3e 100644 --- a/esphome/components/host/preferences.cpp +++ b/esphome/components/host/preferences.cpp @@ -4,6 +4,7 @@ #include #include "preferences.h" #include "esphome/core/application.h" +#include "esphome/core/log.h" namespace esphome { namespace host { @@ -14,7 +15,12 @@ static const char *const TAG = "host.preferences"; void HostPreferences::setup_() { if (this->setup_complete_) return; - this->filename_.append(getenv("HOME")); + const char *home = getenv("HOME"); + if (home == nullptr) { + ESP_LOGE(TAG, "HOME environment variable is not set"); + abort(); + } + this->filename_.append(home); this->filename_.append("/.esphome"); this->filename_.append("/prefs"); fs::create_directories(this->filename_); @@ -44,9 +50,12 @@ void HostPreferences::setup_() { bool HostPreferences::sync() { this->setup_(); FILE *fp = fopen(this->filename_.c_str(), "wb"); - std::map>::iterator it; + if (fp == nullptr) { + ESP_LOGE(TAG, "Failed to open preferences file for writing: %s", this->filename_.c_str()); + return false; + } - for (it = this->data.begin(); it != this->data.end(); ++it) { + for (auto it = this->data.begin(); it != this->data.end(); ++it) { fwrite(&it->first, sizeof(uint32_t), 1, fp); uint8_t len = it->second.size(); fwrite(&len, sizeof(len), 1, fp); From 80fe54ed6948386a9b6218737b164c9e61c7d71e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 23:30:39 -0500 Subject: [PATCH 394/609] [bluetooth_proxy] Add null checks for api_connection (#14536) Co-authored-by: Claude Opus 4.6 --- .../bluetooth_proxy/bluetooth_connection.cpp | 25 +++++++++++++++---- .../bluetooth_proxy/bluetooth_proxy.cpp | 4 +++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 60f56fda54..b2000fbd94 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -415,11 +415,14 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status); break; } + auto *api_connection = this->proxy_->get_api_connection(); + if (api_connection == nullptr) + break; api::BluetoothGATTReadResponse resp; resp.address = this->address_; resp.handle = param->read.handle; resp.set_data(param->read.value, param->read.value_len); - this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTReadResponse::MESSAGE_TYPE); + api_connection->send_message(resp, api::BluetoothGATTReadResponse::MESSAGE_TYPE); break; } case ESP_GATTC_WRITE_CHAR_EVT: @@ -429,10 +432,13 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status); break; } + auto *api_connection = this->proxy_->get_api_connection(); + if (api_connection == nullptr) + break; api::BluetoothGATTWriteResponse resp; resp.address = this->address_; resp.handle = param->write.handle; - this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); + api_connection->send_message(resp, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); break; } case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { @@ -442,10 +448,13 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status); break; } + auto *api_connection = this->proxy_->get_api_connection(); + if (api_connection == nullptr) + break; api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->unreg_for_notify.handle; - this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); + api_connection->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { @@ -455,20 +464,26 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status); break; } + auto *api_connection = this->proxy_->get_api_connection(); + if (api_connection == nullptr) + break; api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->reg_for_notify.handle; - this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); + api_connection->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); break; } case ESP_GATTC_NOTIFY_EVT: { ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_, param->notify.handle); + auto *api_connection = this->proxy_->get_api_connection(); + if (api_connection == nullptr) + break; api::BluetoothGATTNotifyDataResponse resp; resp.address = this->address_; resp.handle = param->notify.handle; resp.set_data(param->notify.value, param->notify.value_len); - this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyDataResponse::MESSAGE_TYPE); + api_connection->send_message(resp, api::BluetoothGATTNotifyDataResponse::MESSAGE_TYPE); break; } default: diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index d45377b3f6..cab328e2f5 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -420,6 +420,8 @@ void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_ } void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) { + if (this->api_connection_ == nullptr) + return; api::BluetoothDevicePairingResponse call; call.address = address; call.paired = paired; @@ -429,6 +431,8 @@ void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_ } void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) { + if (this->api_connection_ == nullptr) + return; api::BluetoothDeviceUnpairingResponse call; call.address = address; call.success = success; From a2c0d70c2c1983cf996f045ba7801216ddcea9ac Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Fri, 6 Mar 2026 08:00:17 +0100 Subject: [PATCH 395/609] [ble_nus] Add uart support (#14320) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/components/ble_nus/__init__.py | 53 +++++++- esphome/components/ble_nus/ble_nus.cpp | 125 +++++++++++++++--- esphome/components/ble_nus/ble_nus.h | 16 ++- esphome/core/defines.h | 2 + .../ble_nus/test-uart.nrf52-adafruit.yaml | 4 + 5 files changed, 178 insertions(+), 22 deletions(-) create mode 100644 tests/components/ble_nus/test-uart.nrf52-adafruit.yaml diff --git a/esphome/components/ble_nus/__init__.py b/esphome/components/ble_nus/__init__.py index 6581ce1cfa..c0837da402 100644 --- a/esphome/components/ble_nus/__init__.py +++ b/esphome/components/ble_nus/__init__.py @@ -1,29 +1,64 @@ import esphome.codegen as cg from esphome.components.logger import request_log_listener +from esphome.components.uart import ( + UARTComponent, + debug_to_code, + maybe_empty_debug, + uart_ns, +) from esphome.components.zephyr import zephyr_add_prj_conf import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_LOGS, CONF_TYPE +from esphome.const import ( + CONF_DEBUG, + CONF_ID, + CONF_LOGS, + CONF_RX_BUFFER_SIZE, + CONF_TX_BUFFER_SIZE, + CONF_TYPE, +) +from esphome.types import ConfigType -AUTO_LOAD = ["zephyr_ble_server"] +AUTO_LOAD = ["zephyr_ble_server", "uart"] CODEOWNERS = ["@tomaszduda23"] ble_nus_ns = cg.esphome_ns.namespace("ble_nus") -BLENUS = ble_nus_ns.class_("BLENUS", cg.Component) +BLENUS = ble_nus_ns.class_("BLENUS", cg.Component, UARTComponent) + +CONF_UART = "uart" + + +def validate_rx_buffer(config: ConfigType) -> ConfigType: + config = config.copy() + if config[CONF_TYPE] == CONF_LOGS: + if CONF_RX_BUFFER_SIZE in config: + raise cv.Invalid("logs does not support rx_buffer_size") + elif CONF_RX_BUFFER_SIZE not in config: + config[CONF_RX_BUFFER_SIZE] = 512 + return config + CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(BLENUS), cv.Optional(CONF_TYPE, default=CONF_LOGS): cv.one_of( - *[CONF_LOGS], lower=True + *[CONF_LOGS, CONF_UART], lower=True ), + cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.All( + cv.validate_bytes, cv.int_range(min=160, max=8192) + ), + cv.Optional(CONF_RX_BUFFER_SIZE): cv.All( + cv.validate_bytes, cv.int_range(min=160, max=8192) + ), + cv.Optional(CONF_DEBUG): maybe_empty_debug, } ).extend(cv.COMPONENT_SCHEMA), cv.only_with_framework("zephyr"), + validate_rx_buffer, ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) zephyr_add_prj_conf("BT_NUS", True) expose_log = config[CONF_TYPE] == CONF_LOGS @@ -31,3 +66,11 @@ async def to_code(config): if expose_log: request_log_listener() # Request a log listener slot for BLE NUS log streaming await cg.register_component(var, config) + cg.add_define("ESPHOME_BLE_NUS_TX_RING_BUFFER_SIZE", config[CONF_TX_BUFFER_SIZE]) + if CONF_RX_BUFFER_SIZE in config: + cg.add_define( + "ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE", config[CONF_RX_BUFFER_SIZE] + ) + if CONF_DEBUG in config: + cg.add_global(uart_ns.using) + await debug_to_code(config[CONF_DEBUG], var) diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index a10132eb3e..d1710100a0 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -11,25 +11,111 @@ namespace esphome::ble_nus { -constexpr size_t BLE_TX_BUF_SIZE = 2048; - // NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables) BLENUS *global_ble_nus; -RING_BUF_DECLARE(global_ble_tx_ring_buf, BLE_TX_BUF_SIZE); +RING_BUF_DECLARE(global_ble_tx_ring_buf, ESPHOME_BLE_NUS_TX_RING_BUFFER_SIZE); +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE +RING_BUF_DECLARE(global_ble_rx_ring_buf, ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE); +#endif // NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables) static const char *const TAG = "ble_nus"; -size_t BLENUS::write_array(const uint8_t *data, size_t len) { +void BLENUS::write_array(const uint8_t *data, size_t len) { if (atomic_get(&this->tx_status_) == TX_DISABLED) { - return 0; + return; + } + auto sent = ring_buf_put(&global_ble_tx_ring_buf, data, len); + if (sent < len) { + ESP_LOGE(TAG, "TX dropping %u bytes", len - sent); + return; + } +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(uart::UART_DIRECTION_TX, data[i]); + } +#endif +} + +bool BLENUS::peek_byte(uint8_t *data) { +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + if (this->has_peek_) { + *data = this->peek_buffer_; + return true; + } + + if (this->read_byte(&this->peek_buffer_)) { + *data = this->peek_buffer_; + this->has_peek_ = true; + return true; + } + + return false; +#else + return false; +#endif +} + +bool BLENUS::read_array(uint8_t *data, size_t len) { +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + if (len == 0) { + return true; + } + if (this->available() < len) { + return false; + } + + // First, use the peek buffer if available + if (this->has_peek_) { + data[0] = this->peek_buffer_; + this->has_peek_ = false; + data++; + if (--len == 0) { // Decrement len first, then check it... + return true; // No more to read + } + } + + if (ring_buf_get(&global_ble_rx_ring_buf, data, len) != len) { + ESP_LOGE(TAG, "UART BLE unexpected size"); + return false; + } +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(uart::UART_DIRECTION_RX, data[i]); + } +#endif + return true; +#else + return false; +#endif +} + +size_t BLENUS::available() { +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + uint32_t size = ring_buf_size_get(&global_ble_rx_ring_buf); + ESP_LOGVV(TAG, "UART BLE available %u", size); + return size + (this->has_peek_ ? 1 : 0); +#else + return 0; +#endif +} + +void BLENUS::flush() { + constexpr uint32_t timeout_5sec = 5000; + uint32_t start = millis(); + while (atomic_get(&this->tx_status_) != TX_DISABLED && !ring_buf_is_empty(&global_ble_tx_ring_buf)) { + if (millis() - start > timeout_5sec) { + ESP_LOGW(TAG, "Flush timeout"); + return; + } + delay(1); } - return ring_buf_put(&global_ble_tx_ring_buf, data, len); } void BLENUS::connected(bt_conn *conn, uint8_t err) { if (err == 0) { global_ble_nus->conn_.store(bt_conn_ref(conn)); + global_ble_nus->connected_ = true; } } @@ -38,6 +124,7 @@ void BLENUS::disconnected(bt_conn *conn, uint8_t reason) { bt_conn_unref(global_ble_nus->conn_.load()); // Connection array is global static. // Reference can be kept even if disconnected. + global_ble_nus->connected_ = false; } } @@ -63,12 +150,19 @@ void BLENUS::send_enabled_callback(bt_nus_send_status status) { break; } } - void BLENUS::rx_callback(bt_conn *conn, const uint8_t *const data, uint16_t len) { - ESP_LOGD(TAG, "Received %d bytes.", len); + ESP_LOGV(TAG, "Received %d bytes.", len); +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + auto recv_len = ring_buf_put(&global_ble_rx_ring_buf, data, len); + if (recv_len < len) { + ESP_LOGE(TAG, "RX dropping %u bytes", len - recv_len); + } +#endif } - void BLENUS::setup() { +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + this->rx_buffer_size_ = ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE; +#endif bt_nus_cb callbacks = { .received = rx_callback, .sent = tx_callback, @@ -106,16 +200,17 @@ void BLENUS::on_log(uint8_t level, const char *tag, const char *message, size_t #endif void BLENUS::dump_config() { - ESP_LOGCONFIG(TAG, - "ble nus:\n" - " log: %s", - YESNO(this->expose_log_)); uint32_t mtu = 0; bt_conn *conn = this->conn_.load(); - if (conn) { + if (conn && this->connected_) { mtu = bt_nus_get_mtu(conn); } - ESP_LOGCONFIG(TAG, " MTU: %u", mtu); + ESP_LOGCONFIG(TAG, + "ble nus:\n" + " log: %s\n" + " connected: %s\n" + " MTU: %u", + YESNO(this->expose_log_), YESNO(this->connected_.load()), mtu); } void BLENUS::loop() { diff --git a/esphome/components/ble_nus/ble_nus.h b/esphome/components/ble_nus/ble_nus.h index b2b0ee7713..67e9ae9f97 100644 --- a/esphome/components/ble_nus/ble_nus.h +++ b/esphome/components/ble_nus/ble_nus.h @@ -2,6 +2,7 @@ #ifdef USE_ZEPHYR #include "esphome/core/defines.h" #include "esphome/core/component.h" +#include "esphome/components/uart/uart_component.h" #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" #endif @@ -10,7 +11,7 @@ namespace esphome::ble_nus { -class BLENUS : public Component { +class BLENUS : public uart::UARTComponent, public Component { enum TxStatus { TX_DISABLED, TX_ENABLED, @@ -21,7 +22,12 @@ class BLENUS : public Component { void setup() override; void dump_config() override; void loop() override; - size_t write_array(const uint8_t *data, size_t len); + void write_array(const uint8_t *data, size_t len) override; + bool peek_byte(uint8_t *data) override; + bool read_array(uint8_t *data, size_t len) override; + size_t available() override; + void flush() override; + void check_logger_conflict() override {} void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } #ifdef USE_LOGGER void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); @@ -37,6 +43,12 @@ class BLENUS : public Component { std::atomic conn_ = nullptr; bool expose_log_ = false; atomic_t tx_status_ = ATOMIC_INIT(TX_DISABLED); + std::atomic connected_{}; +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + // RX buffer for peek functionality + uint8_t peek_buffer_{0}; + bool has_peek_{false}; +#endif }; } // namespace esphome::ble_nus diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 1a6d9b3a80..be5fdc9006 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -356,6 +356,8 @@ #endif #ifdef USE_NRF52 +#define ESPHOME_BLE_NUS_TX_RING_BUFFER_SIZE 512 +#define ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE 512 #define USE_ESPHOME_TASK_LOG_BUFFER #define USE_LOGGER_EARLY_MESSAGE #define USE_LOGGER_UART_SELECTION_USB_CDC diff --git a/tests/components/ble_nus/test-uart.nrf52-adafruit.yaml b/tests/components/ble_nus/test-uart.nrf52-adafruit.yaml new file mode 100644 index 0000000000..0d917ec115 --- /dev/null +++ b/tests/components/ble_nus/test-uart.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +ble_nus: + type: uart + tx_buffer_size: 160 + rx_buffer_size: 160 From 666fb7cf39aeaf2e2a7890695099535ed8b5a67c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 07:18:28 -0500 Subject: [PATCH 396/609] [sx127x][sx126x][max6956] Fix null deref, unterminated string, and pin bounds check (#14529) Co-authored-by: Claude Opus 4.6 --- esphome/components/max6956/max6956.cpp | 2 ++ esphome/components/sx126x/sx126x.cpp | 3 ++- esphome/components/sx127x/sx127x.cpp | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/components/max6956/max6956.cpp b/esphome/components/max6956/max6956.cpp index a350e66ee0..ce45541b63 100644 --- a/esphome/components/max6956/max6956.cpp +++ b/esphome/components/max6956/max6956.cpp @@ -111,6 +111,8 @@ void MAX6956::write_brightness_mode() { } void MAX6956::set_pin_brightness(uint8_t pin, float brightness) { + if (pin < MAX6956_MIN || pin > MAX6956_MAX) + return; uint8_t reg_addr = MAX6956_CURRENT_START + (pin - MAX6956_MIN) / 2; uint8_t config = 0; uint8_t shift = 4 * (pin % 2); diff --git a/esphome/components/sx126x/sx126x.cpp b/esphome/components/sx126x/sx126x.cpp index 64cd24b171..ec62fad10a 100644 --- a/esphome/components/sx126x/sx126x.cpp +++ b/esphome/components/sx126x/sx126x.cpp @@ -155,7 +155,8 @@ void SX126x::configure() { } // check silicon version to make sure hw is ok - this->read_register_(REG_VERSION_STRING, (uint8_t *) this->version_, 16); + this->read_register_(REG_VERSION_STRING, (uint8_t *) this->version_, sizeof(this->version_)); + this->version_[sizeof(this->version_) - 1] = '\0'; if (strncmp(this->version_, "SX126", 5) != 0 && strncmp(this->version_, "LLCC68", 6) != 0) { this->mark_failed(); return; diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index f6aa11b634..66957a7342 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -260,6 +260,11 @@ SX127xError SX127x::transmit_packet(const std::vector &packet) { return SX127xError::INVALID_PARAMS; } + if (this->dio0_pin_ == nullptr) { + ESP_LOGE(TAG, "DIO0 pin not configured, cannot wait for transmit completion"); + return SX127xError::INVALID_PARAMS; + } + SX127xError ret = SX127xError::NONE; if (this->modulation_ == MOD_LORA) { this->set_mode_standby(); From 6c07c15c504c7f0a66ab1a9e2c2f91943526ffa2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 07:18:56 -0500 Subject: [PATCH 397/609] [mipi_dsi][e131] Fix semaphore cast, missing return, and light count overread (#14530) Co-authored-by: Claude Opus 4.6 --- esphome/components/e131/e131_addressable_light_effect.cpp | 4 +++- esphome/components/mipi_dsi/mipi_dsi.cpp | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp index 7d62f739a2..f6010a7cc9 100644 --- a/esphome/components/e131/e131_addressable_light_effect.cpp +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -54,8 +54,10 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet int32_t output_offset = (universe - first_universe_) * get_lights_per_universe(); // limit amount of lights per universe and received + // packet.count is the number of DMX bytes including start code; divide by channels to get the number of lights + int lights_in_packet = (packet.count > 0) ? (packet.count - 1) / channels_ : 0; int output_end = - std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + packet.count - 1)); + std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + lights_in_packet)); auto *input_data = packet.values + 1; auto effect_name = get_name(); diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index 4d45cfb799..815b9d75a1 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -10,7 +10,7 @@ namespace mipi_dsi { static constexpr size_t MIPI_DSI_MAX_CMD_LOG_BYTES = 64; static bool notify_refresh_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel_event_data_t *edata, void *user_ctx) { - auto *sem = static_cast(user_ctx); + auto sem = static_cast(user_ctx); BaseType_t need_yield = pdFALSE; xSemaphoreGiveFromISR(sem, &need_yield); return (need_yield == pdTRUE); @@ -190,6 +190,7 @@ void MIPI_DSI::draw_pixels_at(int x_start, int y_start, int w, int h, const uint if (bitness != this->color_depth_) { display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad); + return; } this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad); } From c0b7f41397e41ff53ed2d551fcd0e3cf03f89147 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Fri, 6 Mar 2026 13:21:44 +0100 Subject: [PATCH 398/609] [esp32] Fix wrong variable usage in P4 pin validation error msg (#14539) --- esphome/components/esp32/gpio_esp32_p4.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32/gpio_esp32_p4.py b/esphome/components/esp32/gpio_esp32_p4.py index b98b567da2..2726c5932f 100644 --- a/esphome/components/esp32/gpio_esp32_p4.py +++ b/esphome/components/esp32/gpio_esp32_p4.py @@ -33,7 +33,7 @@ def esp32_p4_validate_supports(value): is_input = mode[CONF_INPUT] if num < 0 or num > 54: - raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)") + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-54)") if is_input: # All ESP32 pins support input mode pass From 5084c32f3c3da1bb857b79f6200825ec456000c8 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Fri, 6 Mar 2026 13:22:11 +0100 Subject: [PATCH 399/609] [esp32] Fix ESP32-S3 pin validation error message (#14540) --- esphome/components/esp32/gpio_esp32_s3.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/gpio_esp32_s3.py b/esphome/components/esp32/gpio_esp32_s3.py index 7120504693..aea378f499 100644 --- a/esphome/components/esp32/gpio_esp32_s3.py +++ b/esphome/components/esp32/gpio_esp32_s3.py @@ -29,7 +29,7 @@ _LOGGER = logging.getLogger(__name__) def esp32_s3_validate_gpio_pin(value): if value < 0 or value > 48: - raise cv.Invalid(f"Invalid pin number: {value} (must be 0-46)") + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-48)") if value in _ESP_32S3_SPI_PSRAM_PINS: raise cv.Invalid( @@ -55,7 +55,7 @@ def esp32_s3_validate_supports(value): is_input = mode[CONF_INPUT] if num < 0 or num > 48: - raise cv.Invalid(f"Invalid pin number: {num} (must be 0-46)") + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-48)") if is_input: # All ESP32 pins support input mode pass From e59a2b3eded37c9de46d8148b2f8cb40b46d06ac Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Fri, 6 Mar 2026 17:25:44 +0100 Subject: [PATCH 400/609] [nrf52] prepare for usb cdc (#14174) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/esp32/__init__.py | 2 ++ esphome/core/event_pool.h | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index a14d3af69e..8ad84656ed 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -464,6 +464,8 @@ def only_on_variant(*, supported=None, unsupported=None, msg_prefix="This featur unsupported = [unsupported] def validator_(obj): + if not CORE.is_esp32: + raise cv.Invalid(f"{msg_prefix} is only available on ESP32") variant = get_esp32_variant() if supported is not None and variant not in supported: raise cv.Invalid( diff --git a/esphome/core/event_pool.h b/esphome/core/event_pool.h index 928a4e7dee..99541d4a17 100644 --- a/esphome/core/event_pool.h +++ b/esphome/core/event_pool.h @@ -1,6 +1,6 @@ #pragma once -#if defined(USE_ESP32) +#if defined(USE_ESP32) || defined(USE_ZEPHYR) #include #include From 07e51886f3563061c09e6a8c9c36ab94300b0b1f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Mar 2026 06:57:52 -1000 Subject: [PATCH 401/609] [core] Move entity icon strings to PROGMEM on ESP8266 (#14437) --- esphome/components/api/api_connection.h | 3 +- esphome/components/mqtt/mqtt_component.cpp | 10 ++--- esphome/components/mqtt/mqtt_component.h | 4 +- esphome/components/web_server/web_server.cpp | 3 +- esphome/config_validation.py | 17 ++++--- esphome/core/config.py | 4 ++ esphome/core/entity_base.cpp | 38 ++++++++++++++-- esphome/core/entity_base.h | 33 +++++++++++--- esphome/core/entity_helpers.py | 47 +++++++++++++++++--- tests/unit_tests/core/test_entity_helpers.py | 17 +++++++ tests/unit_tests/test_config_validation.py | 12 +++++ 11 files changed, 158 insertions(+), 30 deletions(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index aae8db3c68..88f0ef82d6 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -354,7 +354,8 @@ class APIConnection final : public APIServerConnectionBase { // Set common EntityBase properties #ifdef USE_ENTITY_ICON - msg.icon = entity->get_icon_ref(); + char icon_buf[MAX_ICON_LENGTH]; + msg.icon = StringRef(entity->get_icon_to(icon_buf)); #endif msg.disabled_by_default = entity->is_disabled_by_default(); msg.entity_category = static_cast(entity->get_entity_category()); diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index f49069960b..98fa10def9 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -209,12 +209,11 @@ bool MQTTComponent::send_discovery_() { if (this->is_disabled_by_default_()) root[MQTT_ENABLED_BY_DEFAULT] = false; - // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto icon_ref = this->get_icon_ref_(); - if (!icon_ref.empty()) { - root[MQTT_ICON] = icon_ref; + char icon_buf[MAX_ICON_LENGTH]; + const char *icon = this->get_icon_to_(icon_buf); + if (icon[0] != '\0') { + root[MQTT_ICON] = icon; } - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) const auto entity_category = this->get_entity()->get_entity_category(); if (entity_category != ENTITY_CATEGORY_NONE) { @@ -413,7 +412,6 @@ const StringRef &MQTTComponent::friendly_name_() const { return this->get_entity StringRef MQTTComponent::get_default_object_id_to_(std::span buf) const { return this->get_entity()->get_object_id_to(buf); } -StringRef MQTTComponent::get_icon_ref_() const { return this->get_entity()->get_icon_ref(); } bool MQTTComponent::is_disabled_by_default_() const { return this->get_entity()->is_disabled_by_default(); } bool MQTTComponent::compute_is_internal_() { if (this->custom_state_topic_.has_value()) { diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 0ffe6341d3..2403ef64ea 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -298,8 +298,8 @@ class MQTTComponent : public Component { /// Get the friendly name of this MQTT component. const StringRef &friendly_name_() const; - /// Get the icon field of this component as StringRef - StringRef get_icon_ref_() const; + /// Get the icon field of this component into a stack buffer + const char *get_icon_to_(std::span buf) const { return this->get_entity()->get_icon_to(buf); } /// Get whether the underlying Entity is disabled by default bool is_disabled_by_default_() const; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 6b94a103cc..bc90c88e57 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -568,7 +568,8 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J } #endif #ifdef USE_ENTITY_ICON - root[ESPHOME_F("icon")] = obj->get_icon_ref().c_str(); + char icon_buf[MAX_ICON_LENGTH]; + root[ESPHOME_F("icon")] = obj->get_icon_to(icon_buf); #endif root[ESPHOME_F("entity_category")] = obj->get_entity_category(); bool is_disabled = obj->is_disabled_by_default(); diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 3b0e4da298..1eac53e9b2 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -400,14 +400,21 @@ def string_strict(value): def icon(value): """Validate that a given config value is a valid icon.""" + from esphome.core.config import ICON_MAX_LENGTH + value = string_strict(value) if not value: return value - if re.match("^[\\w\\-]+:[\\w\\-]+$", value): - return value - raise Invalid( - 'Icons must match the format "[icon pack]:[icon]", e.g. "mdi:home-assistant"' - ) + if not re.match("^[\\w\\-]+:[\\w\\-]+$", value): + raise Invalid( + 'Icons must match the format "[icon pack]:[icon]", e.g. "mdi:home-assistant"' + ) + if len(value) > ICON_MAX_LENGTH: + raise Invalid( + f"Icon string is too long ({len(value)} chars, max {ICON_MAX_LENGTH}). " + "Icons are stored in PROGMEM with a 64-byte buffer limit." + ) + return value def sub_device_id(value: str | None) -> core.ID | None: diff --git a/esphome/core/config.py b/esphome/core/config.py index 4f526404fe..9093ab3fe9 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -188,6 +188,10 @@ else: # Keep in sync with ESPHOME_FRIENDLY_NAME_MAX_LEN in esphome/core/entity_base.h FRIENDLY_NAME_MAX_LEN = 120 +# Max icon string length (63 chars + null = 64-byte PROGMEM buffer) +# Keep in sync with MAX_ICON_LENGTH in esphome/core/entity_base.h +ICON_MAX_LENGTH = 63 + AREA_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(Area), diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index eafc04f92a..1265277572 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -1,6 +1,7 @@ #include "esphome/core/entity_base.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" +#include "esphome/core/progmem.h" #include "esphome/core/string_ref.h" namespace esphome { @@ -72,7 +73,27 @@ std::string EntityBase::get_unit_of_measurement() const { return std::string(this->get_unit_of_measurement_ref().c_str()); } -// Entity icon (from index) +// Entity icon — buffer-based API for PROGMEM safety on ESP8266 +const char *EntityBase::get_icon_to([[maybe_unused]] std::span buffer) const { +#ifdef USE_ENTITY_ICON + const uint8_t idx = this->icon_idx_; +#else + const uint8_t idx = 0; +#endif +#ifdef USE_ESP8266 + if (idx == 0) + return ""; + const char *icon = entity_icon_lookup(idx); + ESPHOME_strncpy_P(buffer.data(), icon, buffer.size() - 1); + buffer[buffer.size() - 1] = '\0'; + return buffer.data(); +#else + return entity_icon_lookup(idx); +#endif +} + +#ifndef USE_ESP8266 +// Deprecated icon accessors — not available on ESP8266 (rodata is RAM) StringRef EntityBase::get_icon_ref() const { #ifdef USE_ENTITY_ICON return StringRef(entity_icon_lookup(this->icon_idx_)); @@ -80,7 +101,14 @@ StringRef EntityBase::get_icon_ref() const { return StringRef(entity_icon_lookup(0)); #endif } -std::string EntityBase::get_icon() const { return std::string(this->get_icon_ref().c_str()); } +std::string EntityBase::get_icon() const { +#ifdef USE_ENTITY_ICON + return std::string(entity_icon_lookup(this->icon_idx_)); +#else + return std::string(entity_icon_lookup(0)); +#endif +} +#endif // !USE_ESP8266 // Entity Object ID - computed on-demand from name std::string EntityBase::get_object_id() const { @@ -154,8 +182,10 @@ ESPPreferenceObject EntityBase::make_entity_preference_(size_t size, uint32_t ve #ifdef USE_ENTITY_ICON void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) { - if (!obj.get_icon_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj.get_icon_ref().c_str()); + char icon_buf[MAX_ICON_LENGTH]; + const char *icon = obj.get_icon_to(icon_buf); + if (icon[0] != '\0') { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, icon); } } #endif diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 54d4ae311f..1ce1e658e0 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -36,6 +36,10 @@ static constexpr size_t OBJECT_ID_MAX_LEN = 128; // Maximum state length that Home Assistant will accept without raising ValueError static constexpr size_t MAX_STATE_LEN = 255; +// Maximum icon string buffer size (63 chars + null terminator) +// Icons are stored in PROGMEM; on ESP8266 they must be copied to a stack buffer. +static constexpr size_t MAX_ICON_LENGTH = 64; + enum EntityCategory : uint8_t { ENTITY_CATEGORY_NONE = 0, ENTITY_CATEGORY_CONFIG = 1, @@ -124,12 +128,31 @@ class EntityBase { "2026.3.0") std::string get_unit_of_measurement() const; - // Get/set this entity's icon - ESPDEPRECATED( - "Use get_icon_ref() instead for better performance (avoids string copy). Will be removed in ESPHome 2026.5.0", - "2025.11.0") - std::string get_icon() const; + // Get this entity's icon into a stack buffer. + // On ESP32: returns pointer to PROGMEM string directly (buffer unused). + // On ESP8266: copies from PROGMEM to buffer, returns buffer pointer. + const char *get_icon_to(std::span buffer) const; + +#ifdef USE_ESP8266 + // On ESP8266, rodata is RAM. Icons are in PROGMEM and cannot be accessed + // directly as const char*. Use get_icon_to() with a stack buffer instead. + template StringRef get_icon_ref() const { + static_assert(sizeof(T) == 0, + "get_icon_ref() unavailable on ESP8266 (rodata is RAM). Use get_icon_to() with a stack buffer."); + return StringRef(""); + } + template std::string get_icon() const { + static_assert(sizeof(T) == 0, + "get_icon() unavailable on ESP8266 (rodata is RAM). Use get_icon_to() with a stack buffer."); + return ""; + } +#else + // Deprecated: use get_icon_to() instead. Icons are in PROGMEM. + ESPDEPRECATED("Use get_icon_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0") StringRef get_icon_ref() const; + ESPDEPRECATED("Use get_icon_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0") + std::string get_icon() const; +#endif #ifdef USE_DEVICES // Get/set this entity's device id diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 551e35df65..01fa27b833 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -17,6 +17,7 @@ from esphome.const import ( CONF_UNIT_OF_MEASUREMENT, ) from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority +from esphome.core.config import ICON_MAX_LENGTH from esphome.cpp_generator import MockObj, RawStatement, add, get_variable import esphome.final_validate as fv from esphome.helpers import cpp_string_escape, fnv1_hash_object_id, sanitize, snake_case @@ -78,6 +79,8 @@ def _generate_category_code( table_var: str, lookup_fn: str, strings: dict[str, int], + *, + progmem_strings: bool = False, ) -> str: """Generate C++ code for one string category (PROGMEM pointer table + lookup). @@ -85,14 +88,40 @@ def _generate_category_code( in flash (via PROGMEM) and read with progmem_read_ptr(). String literals themselves remain in RAM but benefit from linker string deduplication. Index 0 means "not set" and returns empty string. + + When progmem_strings=True, each string is declared as a separate PROGMEM + char array. This ensures the string data itself is in flash on ESP8266 + (where .rodata is RAM). On other platforms PROGMEM is a no-op. """ if not strings: return "" sorted_strings = sorted(strings.items(), key=lambda x: x[1]) - entries = ", ".join(cpp_string_escape(s) for s, _ in sorted_strings) count = len(sorted_strings) + if progmem_strings: + # Emit individual PROGMEM char arrays so string data lives in flash + lines: list[str] = [] + var_names: list[str] = [] + for i, (s, _) in enumerate(sorted_strings): + var_name = f"{table_var}_STR_{i}" + var_names.append(var_name) + lines.append( + f"static const char {var_name}[] PROGMEM = {cpp_string_escape(s)};" + ) + entries = ", ".join(var_names) + # Empty string must also be PROGMEM — on ESP8266, callers use strncpy_P + empty_var = f"{table_var}_EMPTY" + lines.append(f'static const char {empty_var}[] PROGMEM = "";') + lines.append(f"static const char *const {table_var}[] PROGMEM = {{{entries}}};") + lines.append(f"const char *{lookup_fn}(uint8_t index) {{") + lines.append(f" if (index == 0 || index > {count}) return {empty_var};") + lines.append(f" return progmem_read_ptr(&{table_var}[index - 1]);") + lines.append("}") + return "\n".join(lines) + "\n" + + entries = ", ".join(cpp_string_escape(s) for s, _ in sorted_strings) + return ( f"static const char *const {table_var}[] PROGMEM = {{{entries}}};\n" f"const char *{lookup_fn}(uint8_t index) {{\n" @@ -103,9 +132,9 @@ def _generate_category_code( _CATEGORY_CONFIGS = ( - ("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes"), - ("ENTITY_UOM_TABLE", "entity_uom_lookup", "units"), - ("ENTITY_ICON_TABLE", "entity_icon_lookup", "icons"), + ("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes", False), + ("ENTITY_UOM_TABLE", "entity_uom_lookup", "units", False), + ("ENTITY_ICON_TABLE", "entity_icon_lookup", "icons", True), ) @@ -117,8 +146,10 @@ async def _generate_tables_job() -> None: """ pool = _get_pool() parts = ["namespace esphome {"] - for table_var, lookup_fn, attr in _CATEGORY_CONFIGS: - code = _generate_category_code(table_var, lookup_fn, getattr(pool, attr)) + for table_var, lookup_fn, attr, progmem_strs in _CATEGORY_CONFIGS: + code = _generate_category_code( + table_var, lookup_fn, getattr(pool, attr), progmem_strings=progmem_strs + ) if code: parts.append(code) parts.append("} // namespace esphome") @@ -160,6 +191,10 @@ def register_unit_of_measurement(value: str) -> int: def register_icon(value: str) -> int: """Register an icon string and return its 1-based index.""" + if value and len(value) > ICON_MAX_LENGTH: + raise ValueError( + f"Icon string too long ({len(value)} chars, max {ICON_MAX_LENGTH}): '{value}'" + ) return _register_string(value, _get_pool().icons, _MAX_ICONS, "icon") diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index a5cfad5ab6..79bc3095b9 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -23,6 +23,7 @@ from esphome.core.entity_helpers import ( _setup_entity_impl, entity_duplicate_validator, get_base_entity_object_id, + register_icon, setup_entity, ) from esphome.cpp_generator import MockObj @@ -909,6 +910,22 @@ def test_register_string_overflow() -> None: _register_string("overflow", category, 3, "test") +def test_register_icon_max_length() -> None: + """Test register_icon rejects icons exceeding 63 characters.""" + # 63 chars should succeed + max_icon = "mdi:" + "a" * 59 # 63 total + idx = register_icon(max_icon) + assert idx > 0 + + # 64 chars should fail + too_long = "mdi:" + "a" * 60 # 64 total + with pytest.raises(ValueError, match="Icon string too long"): + register_icon(too_long) + + # Empty string returns 0 + assert register_icon("") == 0 + + @pytest.mark.asyncio async def test_setup_entity_with_entity_category( setup_test_environment: list[str], diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 9602010ad3..c1849daf4b 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -148,6 +148,18 @@ def test_icon__invalid(): config_validation.icon("foo") +def test_icon__max_length(): + """Test that icons exceeding 63 characters are rejected.""" + # Exactly 63 chars should pass + max_icon = "mdi:" + "a" * 59 # 63 chars total + assert config_validation.icon(max_icon) == max_icon + + # 64 chars should fail + too_long = "mdi:" + "a" * 60 # 64 chars total + with pytest.raises(Invalid, match="Icon string is too long"): + config_validation.icon(too_long) + + @pytest.mark.parametrize("value", ("True", "YES", "on", "enAblE", True)) def test_boolean__valid_true(value): assert config_validation.boolean(value) is True From 74e4b69654c2dd77f81cd712fe1c7ece3db78bfa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Mar 2026 06:58:13 -1000 Subject: [PATCH 402/609] [core] Replace Application name/friendly_name std::string with StringRef (#14532) Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> --- .../components/api/api_frame_helper_noise.cpp | 2 +- esphome/components/esp32_ble/ble.cpp | 2 +- esphome/components/mdns/mdns_component.cpp | 2 +- esphome/components/mqtt/mqtt_component.cpp | 6 +- esphome/components/openthread/openthread.cpp | 2 +- .../components/web_server/web_server_v1.cpp | 2 +- esphome/components/wifi/wifi_component.cpp | 2 +- .../wifi/wifi_component_esp8266.cpp | 2 +- esphome/core/application.h | 54 +++++++------ esphome/core/config.py | 60 +++++++++++++-- esphome/core/defines.h | 1 + esphome/core/entity_base.cpp | 6 +- tests/dummy_main.cpp | 4 +- tests/unit_tests/core/test_config.py | 77 +++++++++++++++++++ 14 files changed, 181 insertions(+), 41 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 3ae35e9be8..ba4f2f0642 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -269,7 +269,7 @@ APIError APINoiseFrameHelper::state_action_() { } if (state_ == State::SERVER_HELLO) { // send server hello - const std::string &name = App.get_name(); + const auto &name = App.get_name(); char mac[MAC_ADDRESS_BUFFER_SIZE]; get_mac_address_into_buffer(mac); diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 9d26018800..bbe972b9f3 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -273,7 +273,7 @@ bool ESP32BLE::ble_setup_() { device_name = this->name_; } } else { - const std::string &app_name = App.get_name(); + const auto &app_name = App.get_name(); size_t name_len = app_name.length(); if (name_len > 20) { if (App.is_name_add_mac_suffix_enabled()) { diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 5e5e1279d9..342a6e6c64 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -59,7 +59,7 @@ void MDNSComponent::compile_records_(StaticVectorget_port(); - const std::string &friendly_name = App.get_friendly_name(); + const auto &friendly_name = App.get_friendly_name(); bool friendly_name_empty = friendly_name.empty(); // Calculate exact capacity for txt_records diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 98fa10def9..d31a78b090 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -267,7 +267,7 @@ bool MQTTComponent::send_discovery_() { root[MQTT_UNIQUE_ID] = unique_id_buf; } - const std::string &node_name = App.get_name(); + const auto &node_name = App.get_name(); if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR) { // node_name (max 31) + "_" (1) + object_id (max 128) + null char object_id_full[ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1]; @@ -275,8 +275,8 @@ bool MQTTComponent::send_discovery_() { root[MQTT_OBJECT_ID] = object_id_full; } - const std::string &friendly_name_ref = App.get_friendly_name(); - const std::string &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref; + const auto &friendly_name_ref = App.get_friendly_name(); + const auto &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref; const char *node_area = App.get_area(); JsonObject device_info = root[MQTT_DEVICE].to(); diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 9452f5a41e..fb81481299 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -132,7 +132,7 @@ void OpenThreadSrpComponent::setup() { // set the host name uint16_t size; char *existing_host_name = otSrpClientBuffersGetHostNameString(instance, &size); - const std::string &host_name = App.get_name(); + const auto &host_name = App.get_name(); uint16_t host_name_len = host_name.size(); if (host_name_len > size) { ESP_LOGW(TAG, "Hostname is too long, choose a shorter project name"); diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index f7b90018dc..85a4e80541 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -75,7 +75,7 @@ void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; } void WebServer::handle_index_request(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("text/html")); - const std::string &title = App.get_name(); + const auto &title = App.get_name(); stream->print(ESPHOME_F("")); stream->print(title.c_str()); diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8b60810d28..60764955cc 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -913,7 +913,7 @@ void WiFiComponent::setup_ap_config_() { static constexpr size_t AP_SSID_PREFIX_LEN = 25; static constexpr size_t AP_SSID_SUFFIX_LEN = 7; - const std::string &app_name = App.get_name(); + const auto &app_name = App.get_name(); const char *name_ptr = app_name.c_str(); size_t name_len = app_name.length(); diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 355832b434..a9b26c5935 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -212,7 +212,7 @@ network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { return addresses; } bool WiFiComponent::wifi_apply_hostname_() { - const std::string &hostname = App.get_name(); + const auto &hostname = App.get_name(); bool ret = wifi_station_set_hostname(const_cast<char *>(hostname.c_str())); if (!ret) { ESP_LOGV(TAG, "Set hostname failed"); diff --git a/esphome/core/application.h b/esphome/core/application.h index 40f8a00edd..87f9fdf59a 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -138,26 +138,36 @@ static constexpr uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for qu class Application { public: - void pre_setup(const std::string &name, const std::string &friendly_name, bool name_add_mac_suffix) { +#ifdef ESPHOME_NAME_ADD_MAC_SUFFIX + /// Pre-setup with MAC suffix: overwrites placeholder in mutable static buffers with actual MAC. + void pre_setup(char *name, size_t name_len, char *friendly_name, size_t friendly_name_len) { arch_init(); - this->name_add_mac_suffix_ = name_add_mac_suffix; - if (name_add_mac_suffix) { - // MAC address length: 12 hex chars + null terminator - constexpr size_t mac_address_len = 13; - // MAC address suffix length (last 6 characters of 12-char MAC address string) - constexpr size_t mac_address_suffix_len = 6; - char mac_addr[mac_address_len]; - get_mac_address_into_buffer(mac_addr); - const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len; - this->name_ = make_name_with_suffix(name, '-', mac_suffix_ptr, mac_address_suffix_len); - if (!friendly_name.empty()) { - this->friendly_name_ = make_name_with_suffix(friendly_name, ' ', mac_suffix_ptr, mac_address_suffix_len); - } - } else { - this->name_ = name; - this->friendly_name_ = friendly_name; + this->name_add_mac_suffix_ = true; + // MAC address length: 12 hex chars + null terminator + constexpr size_t mac_address_len = 13; + // MAC address suffix length (last 6 characters of 12-char MAC address string) + constexpr size_t mac_address_suffix_len = 6; + char mac_addr[mac_address_len]; + get_mac_address_into_buffer(mac_addr); + // Overwrite the placeholder suffix in the mutable static buffers with actual MAC + // name is always non-empty (validated by validate_hostname in Python config) + memcpy(name + name_len - mac_address_suffix_len, mac_addr + mac_address_suffix_len, mac_address_suffix_len); + if (friendly_name_len > 0) { + memcpy(friendly_name + friendly_name_len - mac_address_suffix_len, mac_addr + mac_address_suffix_len, + mac_address_suffix_len); } + this->name_ = StringRef(name, name_len); + this->friendly_name_ = StringRef(friendly_name, friendly_name_len); } +#else + /// Pre-setup without MAC suffix: StringRef points directly at const string literals in flash. + void pre_setup(const char *name, size_t name_len, const char *friendly_name, size_t friendly_name_len) { + arch_init(); + this->name_add_mac_suffix_ = false; + this->name_ = StringRef(name, name_len); + this->friendly_name_ = StringRef(friendly_name, friendly_name_len); + } +#endif #ifdef USE_DEVICES void register_device(Device *device) { this->devices_.push_back(device); } @@ -274,10 +284,10 @@ class Application { void loop(); /// Get the name of this Application set by pre_setup(). - const std::string &get_name() const { return this->name_; } + const StringRef &get_name() const { return this->name_; } /// Get the friendly name of this Application set by pre_setup(). - const std::string &get_friendly_name() const { return this->friendly_name_; } + const StringRef &get_friendly_name() const { return this->friendly_name_; } /// Get the area of this Application set by pre_setup(). const char *get_area() const { @@ -627,9 +637,9 @@ class Application { #endif #endif - // std::string members (typically 24-32 bytes each) - std::string name_; - std::string friendly_name_; + // StringRef members (8 bytes each: pointer + size) + StringRef name_; + StringRef friendly_name_; // 4-byte members uint32_t last_loop_{0}; diff --git a/esphome/core/config.py b/esphome/core/config.py index 9093ab3fe9..8631726a02 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -50,6 +50,7 @@ from esphome.core import ( ) from esphome.helpers import ( copy_file_if_changed, + cpp_string_escape, fnv1a_32bit_hash, get_str_env, walk_files, @@ -58,6 +59,38 @@ from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) +# C++ variable names and separators for app name buffers (used with MAC suffix) +_APP_NAME_BUF_VAR = "esphome_app_name_buf" +_APP_NAME_MAC_SEP = "-" +_APP_FRIENDLY_NAME_BUF_VAR = "esphome_app_friendly_name_buf" +_APP_FRIENDLY_NAME_MAC_SEP = " " +# Placeholder suffix for MAC address (last 6 hex chars) +_MAC_SUFFIX_PLACEHOLDER = "XXXXXX" + + +def make_app_name_cpp( + value: str, var_name: str, sep: str, *, add_mac_suffix: bool +) -> tuple[str, str | None, int]: + """Compute C++ expression and optional global declaration for an app name. + + Returns (cpp_expr, global_decl_or_none, byte_length). + - cpp_expr: The C++ expression to pass to pre_setup (var name or string literal). + - global_decl: A static char[] declaration string, or None if not needed. + - byte_length: The UTF-8 byte length of the string value. + """ + if add_mac_suffix: + buf_value = "" if not value else f"{value}{sep}{_MAC_SUFFIX_PLACEHOLDER}" + escaped = cpp_string_escape(buf_value) + return ( + var_name, + f"static char {var_name}[] = {escaped};", + len(buf_value.encode("utf-8")), + ) + if not value: + return '""', None, 0 + return cpp_string_escape(value), None, len(value.encode("utf-8")) + + StartupTrigger = cg.esphome_ns.class_( "StartupTrigger", cg.Component, automation.Trigger.template() ) @@ -78,6 +111,8 @@ VALID_INCLUDE_EXTS = {".h", ".hpp", ".tcc", ".ino", ".cpp", ".c"} def validate_hostname(config): # Keep in sync with ESPHOME_DEVICE_NAME_MAX_LEN in esphome/core/entity_base.h + if not config[CONF_NAME]: + raise cv.Invalid("Hostname must not be empty", path=[CONF_NAME]) max_length = 31 if config[CONF_NAME_ADD_MAC_SUFFIX]: max_length -= 7 # "-AABBCC" is appended when add mac suffix option is used @@ -555,13 +590,28 @@ async def to_code(config: ConfigType) -> None: # Construct App via placement new — see application.cpp for storage details cg.add_global(cg.RawStatement("#include <new>")) cg.add(cg.RawExpression("new (&App) Application()")) - cg.add( - cg.App.pre_setup( - config[CONF_NAME], - config[CONF_FRIENDLY_NAME], - config[CONF_NAME_ADD_MAC_SUFFIX], + name = config[CONF_NAME] + friendly_name = config[CONF_FRIENDLY_NAME] + name_add_mac_suffix = config[CONF_NAME_ADD_MAC_SUFFIX] + + def _emit_app_name( + value: str, var_name: str, sep: str + ) -> tuple[cg.Expression, int]: + """Emit codegen for an app name and return (expression, byte_length).""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + value, var_name, sep, add_mac_suffix=name_add_mac_suffix ) + if global_decl is not None: + cg.add_global(cg.RawStatement(global_decl)) + return cg.RawExpression(cpp_expr), byte_len + + name_expr, name_len = _emit_app_name(name, _APP_NAME_BUF_VAR, _APP_NAME_MAC_SEP) + friendly_expr, friendly_len = _emit_app_name( + friendly_name, _APP_FRIENDLY_NAME_BUF_VAR, _APP_FRIENDLY_NAME_MAC_SEP ) + if name_add_mac_suffix: + cg.add_define("ESPHOME_NAME_ADD_MAC_SUFFIX") + cg.add(cg.App.pre_setup(name_expr, name_len, friendly_expr, friendly_len)) # Define component count for static allocation cg.add_define("ESPHOME_COMPONENT_COUNT", len(CORE.component_ids)) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index be5fdc9006..c5f38ab9aa 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -13,6 +13,7 @@ #define ESPHOME_PROJECT_VERSION "v2" #define ESPHOME_PROJECT_VERSION_30 "v2" #define ESPHOME_VARIANT "ESP32" +#define ESPHOME_NAME_ADD_MAC_SUFFIX #define ESPHOME_DEBUG_SCHEDULER #define ESPHOME_DEBUG_API diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 1265277572..37e7fcc998 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -23,13 +23,13 @@ void EntityBase::set_name(const char *name, uint32_t object_id_hash) { // Bug-for-bug compatibility with OLD behavior: // - With MAC suffix: OLD code used App.get_friendly_name() directly (no fallback) // - Without MAC suffix: OLD code used pre-computed object_id with fallback to device name - const std::string &friendly = App.get_friendly_name(); + const auto &friendly = App.get_friendly_name(); if (App.is_name_add_mac_suffix_enabled()) { // MAC suffix enabled - use friendly_name directly (even if empty) for compatibility - this->name_ = StringRef(friendly); + this->name_ = friendly; } else { // No MAC suffix - fallback to device name if friendly_name is empty - this->name_ = StringRef(!friendly.empty() ? friendly : App.get_name()); + this->name_ = !friendly.empty() ? friendly : App.get_name(); } } this->flags_.has_own_name = false; diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 3ccf35e04d..6fa0c08aa3 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -12,7 +12,9 @@ using namespace esphome; void setup() { - App.pre_setup("livingroom", "LivingRoom", false); + static char name[] = "livingroom"; + static char friendly_name[] = "LivingRoom"; + App.pre_setup(name, sizeof(name) - 1, friendly_name, sizeof(friendly_name) - 1); auto *log = new logger::Logger(115200); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index 88801a9ca0..474d31a90a 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -23,6 +23,7 @@ from esphome.const import ( from esphome.core import CORE, config from esphome.core.config import ( Area, + make_app_name_cpp, preload_core_config, valid_include, valid_project_name, @@ -969,3 +970,79 @@ def test_config_hash_different_for_different_configs() -> None: hash2 = CORE.config_hash assert hash1 != hash2 + + +def test_make_app_name_cpp_no_mac_simple() -> None: + """Test simple name without MAC suffix returns string literal.""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + "my-device", "buf", "-", add_mac_suffix=False + ) + assert cpp_expr == '"my-device"' + assert global_decl is None + assert byte_len == 9 + + +def test_make_app_name_cpp_no_mac_empty() -> None: + """Test empty name without MAC suffix.""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + "", "buf", "-", add_mac_suffix=False + ) + assert cpp_expr == '""' + assert global_decl is None + assert byte_len == 0 + + +def test_make_app_name_cpp_mac_suffix() -> None: + """Test name with MAC suffix emits static buffer.""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + "my-device", "esphome_app_name_buf", "-", add_mac_suffix=True + ) + assert cpp_expr == "esphome_app_name_buf" + assert global_decl is not None + assert "static char esphome_app_name_buf[]" in global_decl + assert "my-device-XXXXXX" in global_decl + assert byte_len == len("my-device-XXXXXX") + + +def test_make_app_name_cpp_mac_suffix_empty() -> None: + """Test empty name with MAC suffix emits empty static buffer.""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + "", "esphome_app_name_buf", "-", add_mac_suffix=True + ) + assert cpp_expr == "esphome_app_name_buf" + assert global_decl is not None + assert "static char esphome_app_name_buf[]" in global_decl + assert byte_len == 0 + + +def test_make_app_name_cpp_mac_suffix_space_sep() -> None: + """Test friendly name uses space separator for MAC suffix.""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + "My Device", "esphome_app_friendly_name_buf", " ", add_mac_suffix=True + ) + assert cpp_expr == "esphome_app_friendly_name_buf" + assert global_decl is not None + assert "My Device XXXXXX" in global_decl + assert byte_len == len("My Device XXXXXX") + + +def test_make_app_name_cpp_non_ascii_utf8_length() -> None: + """Test non-ASCII characters use UTF-8 byte length.""" + _, global_decl, byte_len = make_app_name_cpp( + "café", "buf", "-", add_mac_suffix=False + ) + assert byte_len == len("café".encode()) # 5 bytes, not 4 chars + assert global_decl is None + + +def test_make_app_name_cpp_non_ascii_mac_suffix_utf8_length() -> None: + """Test non-ASCII with MAC suffix uses UTF-8 byte length.""" + _, _, byte_len = make_app_name_cpp("café", "buf", "-", add_mac_suffix=True) + assert byte_len == len("café-XXXXXX".encode()) + + +def test_make_app_name_cpp_special_chars_escaped() -> None: + """Test special characters are properly escaped in C++ string.""" + cpp_expr, _, _ = make_app_name_cpp('my "device"', "buf", "-", add_mac_suffix=False) + # cpp_string_escape uses octal escapes for quotes + assert '"' not in cpp_expr[1:-1] # no unescaped quotes inside the outer quotes From a16b8fc0ac30a015df61555d59fb98e19a9efe6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Mar 2026 07:00:31 -1000 Subject: [PATCH 403/609] [rp2040] Fix Pico W LED pin and auto-generate board definitions for arduino-pico 5.5.x (#14528) --- esphome/components/rp2040/boards.jinja2 | 25 + esphome/components/rp2040/boards.py | 2199 ++++++++++++++++- esphome/components/rp2040/generate_boards.py | 186 ++ esphome/components/rp2040/gpio.py | 22 +- .../components/test_rp2040_generate_boards.py | 273 ++ 5 files changed, 2688 insertions(+), 17 deletions(-) create mode 100644 esphome/components/rp2040/boards.jinja2 create mode 100644 esphome/components/rp2040/generate_boards.py create mode 100644 tests/unit_tests/components/test_rp2040_generate_boards.py diff --git a/esphome/components/rp2040/boards.jinja2 b/esphome/components/rp2040/boards.jinja2 new file mode 100644 index 0000000000..989fb83701 --- /dev/null +++ b/esphome/components/rp2040/boards.jinja2 @@ -0,0 +1,25 @@ +# Auto-generated by generate_boards.py — do not edit manually +# To regenerate: python esphome/components/rp2040/generate_boards.py <arduino-pico-path> + +# arduino-pico maps pins >= {{ cyw43_gpio_offset }} to CYW43 wireless chip GPIOs +CYW43_GPIO_OFFSET = {{ cyw43_gpio_offset }} +CYW43_MAX_GPIO = {{ cyw43_max_gpio }} +DEFAULT_MAX_PIN = {{ default_max_pin }} + +RP2040_BASE_PINS = {} + +RP2040_BOARD_PINS = { +{%- for name, pins in board_pins %} + {{ name | repr }}: {{ pins | format_pins }}, +{%- endfor %} +} + +BOARDS = { +{%- for name, info in boards %} + {{ name | repr }}: { + {%- for key, value in info.items() %} + {{ key | repr }}: {{ value | repr }}, + {%- endfor %} + }, +{%- endfor %} +} diff --git a/esphome/components/rp2040/boards.py b/esphome/components/rp2040/boards.py index c761efba58..c99934567a 100644 --- a/esphome/components/rp2040/boards.py +++ b/esphome/components/rp2040/boards.py @@ -1,28 +1,2205 @@ +# Auto-generated by generate_boards.py — do not edit manually +# To regenerate: python esphome/components/rp2040/generate_boards.py <arduino-pico-path> + +# arduino-pico maps pins >= 64 to CYW43 wireless chip GPIOs +CYW43_GPIO_OFFSET = 64 +CYW43_MAX_GPIO = 66 +DEFAULT_MAX_PIN = 29 + RP2040_BASE_PINS = {} RP2040_BOARD_PINS = { - "pico": { - "SDA": 4, - "SCL": 5, - "LED": 25, - "SDA1": 26, - "SCL1": 27, + "0xcb_helios": { + "LED": 17, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL1": 3, + "SDA1": 2, + "SS": 21, + "TX": 0, }, - "rpipico": "pico", - "rpipicow": { - "SDA": 4, + "DudesCab": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 19, + "SCL1": 11, + "SDA": 18, + "SDA1": 10, + "SS": 5, + "TX": 0, + }, + "MyRP_2350B": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, "SCL": 5, - "LED": 32, - "SDA1": 26, "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "MyRP_bot": { + "LED": 25, + "MISO": 12, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 17, + "SCL1": 4, + "SDA": 16, + "SDA1": 5, + "SS": 13, + }, + "adafruit_feather": { + "LED": 13, + "MISO": 20, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 3, + "SCL1": 25, + "SDA": 2, + "SDA1": 24, + "SS": 17, + "TX": 0, + }, + "adafruit_feather_adalogger": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_can": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_dvi": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_prop_maker": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_rfm": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_rp2350_adalogger": { + "LED": 7, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 3, + "SCL1": 31, + "SDA": 2, + "SDA1": 31, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_rp2350_hstx": { + "LED": 7, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 3, + "SCL1": 31, + "SDA": 2, + "SDA1": 31, + "SS": 21, + "TX": 0, + }, + "adafruit_feather_scorpio": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_thinkink": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_usb_host": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_floppsy": { + "LED": 28, + "MISO": 20, + "MOSI": 19, + "SCK": 18, + "SCL": 17, + "SDA": 16, + "SS": 24, + }, + "adafruit_fruitjam": { + "LED": 29, + "MISO": 36, + "MOSI": 35, + "RX": 9, + "SCK": 34, + "SCL": 21, + "SDA": 20, + "SS": 39, + "TX": 8, + }, + "adafruit_itsybitsy": { + "LED": 11, + "MISO": 20, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 25, + "SCL1": 3, + "SDA": 24, + "SDA1": 2, + "TX": 0, + }, + "adafruit_kb2040": { + "MISO": 20, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 13, + "SCL1": 3, + "SDA": 12, + "SDA1": 2, + "TX": 0, + }, + "adafruit_macropad2040": {"LED": 13, "SCL": 21, "SDA": 20}, + "adafruit_metro": { + "LED": 13, + "MISO": 20, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 17, + "SCL1": 3, + "SDA": 16, + "SDA1": 2, + "SS": 23, + "TX": 0, + }, + "adafruit_metro_rp2350": { + "LED": 23, + "MISO": 28, + "MOSI": 31, + "RX": 1, + "SCK": 30, + "SCL": 21, + "SDA": 20, + "SS": 29, + "TX": 0, + }, + "adafruit_qtpy": { + "MISO": 4, + "MOSI": 3, + "RX": 29, + "SCK": 6, + "SCL": 25, + "SCL1": 23, + "SDA": 24, + "SDA1": 22, + "TX": 28, + }, + "adafruit_stemmafriend": { + "LED": 12, + "MISO": 4, + "MOSI": 7, + "RX": 27, + "SCK": 2, + "SCL": 21, + "SCL1": 27, + "SDA": 20, + "SDA1": 26, + "SS": 1, + "TX": 26, + }, + "adafruit_trinkeyrp2040qt": {"RX": 17, "SCL": 17, "SDA": 16, "TX": 16}, + "akana_r1": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 7, + "SDA": 4, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "amken_bunny": {"LED": 24, "RX": 1, "TX": 0}, + "amken_revelop": {"LED": 24, "RX": 1, "SCL": 29, "SDA": 28, "TX": 0}, + "amken_revelop_es": {"LED": 5, "MISO": 0, "MOSI": 3, "SCK": 2, "SS": 1, "TX": 20}, + "amken_revelop_plus": {"LED": 24, "RX": 1, "SCL": 29, "SDA": 28, "TX": 0}, + "artronshop_rp2_nano": { + "LED": 13, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 17, + "SCL1": 19, + "SDA": 16, + "SDA1": 18, + "SS": 5, + "TX": 0, + }, + "bigtreetech_SKR_Pico": {"LED": 13, "RX": 1, "TX": 0}, + "breadstick_raspberry": { + "RX": 21, + "SCL": 13, + "SCL1": 23, + "SDA": 12, + "SDA1": 22, + "TX": 20, + }, + "bridgetek_idm2040_43a": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "bridgetek_idm2040_7a": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "challenger_2040_lora": { + "LED": 24, + "MISO": 20, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2040_lte": { + "LED": 19, + "MISO": 20, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2040_nfc": { + "LED": 24, + "MISO": 20, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SCL1": 11, + "SDA": 0, + "SDA1": 10, + "SS": 21, + "TX": 16, + }, + "challenger_2040_sdrtc": { + "LED": 24, + "MISO": 20, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2040_subghz": { + "LED": 24, + "MISO": 20, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2040_uwb": { + "LED": 24, + "MISO": 20, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2040_wifi": { + "LED": 12, + "MISO": 24, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2040_wifi6_ble": { + "LED": 10, + "MISO": 24, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2040_wifi_ble": { + "LED": 10, + "MISO": 24, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2350_bconnect": { + "LED": 7, + "MISO": 16, + "MOSI": 19, + "RX": 13, + "SCK": 18, + "SCL": 21, + "SCL1": 11, + "SDA": 20, + "SDA1": 10, + "SS": 17, + "TX": 12, + }, + "challenger_2350_wifi6_ble5": { + "LED": 7, + "MISO": 16, + "MOSI": 19, + "RX": 13, + "SCK": 18, + "SCL": 21, + "SCL1": 31, + "SDA": 20, + "SDA1": 31, + "SS": 17, + "TX": 12, + }, + "challenger_nb_2040_wifi": { + "LED": 12, + "MISO": 24, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "connectivity_2040_lte_wifi_ble": { + "LED": 19, + "MISO": 20, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "cytron_iriv_io_controller": { + "LED": 29, + "MISO": 20, + "MOSI": 19, + "RX": 31, + "SCK": 22, + "SCL": 17, + "SCL1": 31, + "SDA": 16, + "SDA1": 31, + "SS": 21, + "TX": 31, + }, + "cytron_maker_nano_rp2040": { + "LED": 2, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 1, + "SCL1": 27, + "SDA": 0, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "cytron_maker_pi_rp2040": { + "LED": 3, + "RX": 1, + "SCL": 17, + "SCL1": 3, + "SDA": 16, + "SDA1": 2, + "TX": 0, + }, + "cytron_maker_uno_rp2040": { + "LED": 3, + "MISO": 12, + "MOSI": 11, + "RX": 1, + "SCK": 10, + "SCL": 21, + "SCL1": 27, + "SDA": 20, + "SDA1": 26, + "SS": 13, + "TX": 0, + }, + "cytron_motion_2350_pro": { + "LED": 2, + "MISO": 4, + "MOSI": 7, + "RX": 1, + "SCK": 6, + "SCL": 17, + "SCL1": 27, + "SDA": 16, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "datanoisetv_picoadk": { + "LED": 15, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "datanoisetv_picoadk_v2": { + "LED": 2, + "MISO": 8, + "MOSI": 7, + "RX": 13, + "SCK": 6, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 5, + "TX": 12, + }, + "degz_suibo": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "dfrobot_beetle_rp2040": { + "LED": 13, + "MISO": 0, + "MOSI": 3, + "RX": 29, + "SCK": 2, + "SCL": 5, + "SCL1": 3, + "SDA": 4, + "SDA1": 2, + "SS": 1, + "TX": 28, + }, + "electroniccats_huntercat_nfc": {"LED": 8, "RX": 1, "SCL": 5, "SDA": 4, "TX": 0}, + "evn_alpha": { + "LED": 25, + "MISO": 0, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 7, + "SDA": 4, + "SDA1": 6, + "SS": 1, + "TX": 0, + }, + "extelec_rc2040": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SDA": 4, + "SS": 5, + "TX": 0, + }, + "flyboard2040_core": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 17, + "SCL1": 15, + "SDA": 16, + "SDA1": 14, + "SS": 5, + "TX": 0, + }, + "geeekpi_rp2040_plus": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "generic": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "generic_rp2350": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "groundstudio_marble_pico": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "ilabs_rpico32": { + "MISO": 24, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 5, + "SDA": 4, + "SS": 21, + "TX": 0, + }, + "jumperless_v1": { + "LED": 25, + "MISO": 0, + "MOSI": 3, + "RX": 17, + "SCK": 2, + "SCL": 5, + "SCL1": 19, + "SDA": 4, + "SDA1": 18, + "SS": 1, + "TX": 16, + }, + "jumperless_v5": { + "LED": 17, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 5, + "SCL1": 23, + "SDA": 4, + "SDA1": 22, + "SS": 21, + "TX": 0, + }, + "melopero_cookie_rp2040": { + "LED": 21, + "MISO": 20, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 13, + "SCL1": 3, + "SDA": 12, + "SDA1": 2, + "SS": 1, + "TX": 0, + }, + "melopero_shake_rp2040": { + "LED": 25, + "MISO": 20, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 3, + "SDA": 8, + "SDA1": 2, + "SS": 1, + "TX": 0, + }, + "mksthr36": { + "MISO": 16, + "MOSI": 19, + "SCK": 18, + "SCL": 23, + "SDA": 22, + "SS": 17, + "TX": 6, + }, + "mksthr42": { + "MISO": 16, + "MOSI": 19, + "SCK": 18, + "SCL": 23, + "SDA": 22, + "SS": 17, + "TX": 6, + }, + "nekosystems_bl2040_mini": { + "LED": 6, + "MISO": 16, + "MOSI": 19, + "RX": 13, + "SCK": 18, + "SCL": 25, + "SCL1": 23, + "SDA": 24, + "SDA1": 22, + "SS": 17, + "TX": 12, + }, + "newsan_archi": { + "MISO": 4, + "MOSI": 3, + "RX": 17, + "SCK": 2, + "SCL": 1, + "SCL1": 7, + "SDA": 0, + "SDA1": 6, + "SS": 5, + "TX": 16, + }, + "nullbits_bit_c_pro": { + "LED": 18, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 3, + "SCL1": 5, + "SDA": 2, + "SDA1": 4, + "SS": 21, + "TX": 0, + }, + "olimex_pico2bb48": { + "LED": 25, + "MISO": 4, + "MOSI": 7, + "RX": 1, + "SCK": 6, + "SCL": 13, + "SCL1": 3, + "SDA": 12, + "SDA1": 2, + "SS": 5, + "TX": 0, + }, + "olimex_pico2xl": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "olimex_pico2xxl": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "picolume": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "pimoroni_explorer": { + "MISO": 10, + "MOSI": 10, + "RX": 10, + "SCK": 10, + "SCL": 21, + "SCL1": 10, + "SDA": 20, + "SDA1": 10, + "SS": 10, + "TX": 10, + }, + "pimoroni_pico_plus_2": { + "LED": 25, + "MISO": 32, + "MOSI": 35, + "RX": 1, + "SCK": 34, + "SCL": 5, + "SCL1": 7, + "SDA": 4, + "SDA1": 6, + "SS": 33, + "TX": 0, + }, + "pimoroni_pico_plus_2w": { + "LED": 64, + "MISO": 32, + "MOSI": 35, + "RX": 1, + "SCK": 34, + "SCL": 5, + "SCL1": 7, + "SDA": 4, + "SDA1": 6, + "SS": 33, + "TX": 0, + }, + "pimoroni_plasma2040": {"LED": 16, "SCL": 21, "SDA": 20}, + "pimoroni_plasma2350": { + "LED": 16, + "MISO": 31, + "MOSI": 31, + "RX": 31, + "SCK": 31, + "SCL": 21, + "SCL1": 31, + "SDA": 20, + "SDA1": 31, + "SS": 31, + "TX": 31, + }, + "pimoroni_plasma2350w": { + "LED": 16, + "MISO": 24, + "MOSI": 24, + "RX": 31, + "SCK": 29, + "SCL": 21, + "SCL1": 31, + "SDA": 20, + "SDA1": 31, + "SS": 25, + "TX": 31, + }, + "pimoroni_servo2040": {"LED": 18, "SCL": 21, "SDA": 20}, + "pimoroni_tiny2040": { + "LED": 19, + "MISO": 4, + "MOSI": 7, + "RX": 1, + "SCK": 6, + "SCL": 5, + "SCL1": 7, + "SDA": 4, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "pimoroni_tiny2350": { + "LED": 19, + "MISO": 4, + "MOSI": 7, + "RX": 1, + "SCK": 6, + "SCL": 13, + "SCL1": 7, + "SDA": 12, + "SDA1": 6, + "SS": 5, + "TX": 0, + }, + "pintronix_pinmax": { + "LED": 27, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SDA": 4, + "SS": 17, + "TX": 0, + }, + "rakwireless_rak11300": { + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 3, + "SCL1": 21, + "SDA": 2, + "SDA1": 20, + "SS": 17, + "TX": 0, + }, + "rpipico": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "rpipico2": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "rpipico2w": { + "LED": 64, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "rpipicow": { + "LED": 64, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "sea_picro": { + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 5, + "SCL1": 3, + "SDA": 4, + "SDA1": 2, + "SS": 21, + "TX": 0, + }, + "seeed_indicator_rp2040": { + "MISO": 0, + "MOSI": 3, + "RX": 17, + "SCK": 2, + "SCL": 21, + "SCL1": 15, + "SDA": 20, + "SDA1": 14, + "SS": 1, + "TX": 16, + }, + "seeed_xiao_rp2040": { + "LED": 17, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 7, + "SDA": 6, + "TX": 0, + }, + "seeed_xiao_rp2350": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 17, + "SCL1": 7, + "SDA": 16, + "SDA1": 6, + "SS": 5, + "TX": 0, + }, + "silicognition_rp2040_shim": { + "MISO": 12, + "MOSI": 11, + "RX": 1, + "SCK": 10, + "SCL": 17, + "SDA": 16, + "SS": 21, + "TX": 0, + }, + "soldered_nula_rp2350": { + "MISO": 2, + "MOSI": 3, + "RX": 1, + "SCK": 4, + "SCL": 9, + "SCL1": 31, + "SDA": 8, + "SDA1": 30, + "SS": 5, + "TX": 0, + }, + "solderparty_rp2040_stamp": { + "LED": 20, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 3, + "SDA": 4, + "SDA1": 2, + "SS": 17, + "TX": 0, + }, + "solderparty_rp2350_stamp": { + "LED": 3, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 5, + "SCL1": 3, + "SDA": 4, + "SDA1": 2, + "SS": 21, + "TX": 0, + }, + "solderparty_rp2350_stamp_xl": { + "LED": 3, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 5, + "SCL1": 3, + "SDA": 4, + "SDA1": 2, + "SS": 21, + "TX": 0, + }, + "sparkfun_iotnode_lorawanrp2350": { + "LED": 25, + "MISO": 12, + "MOSI": 15, + "RX": 19, + "SCK": 14, + "SCL": 21, + "SCL1": 31, + "SDA": 20, + "SDA1": 31, + "SS": 13, + "TX": 18, + }, + "sparkfun_iotredboard_rp2350": { + "LED": 25, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 5, + "SCL1": 31, + "SDA": 4, + "SDA1": 30, + "SS": 21, + "TX": 0, + }, + "sparkfun_micromodrp2040": { + "LED": 25, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 5, + "SDA": 4, + "SS": 21, + "TX": 0, + }, + "sparkfun_promicrorp2040": { + "LED": 25, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 17, + "SDA": 16, + "SS": 21, + "TX": 0, + }, + "sparkfun_promicrorp2350": { + "LED": 25, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 17, + "SCL1": 31, + "SDA": 16, + "SDA1": 31, + "SS": 21, + "TX": 0, + }, + "sparkfun_thingplusrp2040": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 17, + "SCL1": 7, + "SDA": 16, + "SDA1": 6, + "TX": 0, + }, + "sparkfun_thingplusrp2350": { + "LED": 64, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 7, + "SCL1": 31, + "SDA": 6, + "SDA1": 31, + "SS": 9, + "TX": 0, + }, + "sparkfun_xrp_controller": { + "LED": 64, + "MISO": 16, + "MOSI": 19, + "RX": 13, + "SCK": 18, + "SCL": 5, + "SCL1": 39, + "SDA": 4, + "SDA1": 38, + "SS": 17, + "TX": 12, + }, + "sparkfun_xrp_controller_beta": {"LED": 64, "SCL": 19, "SDA": 18}, + "upesy_rp2040_devkit": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 23, + "SDA": 4, + "SDA1": 22, + "SS": 17, + "TX": 0, + }, + "vccgnd_yd_rp2040": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "vicharak_shrike-lite": { + "LED": 4, + "MISO": 20, + "MOSI": 19, + "RX": 17, + "SCK": 18, + "SCL": 25, + "SCL1": 7, + "SDA": 24, + "SDA1": 6, + "SS": 21, + "TX": 16, + }, + "viyalab_mizu": { + "LED": 25, + "MISO": 16, + "MOSI": 7, + "RX": 1, + "SCK": 6, + "SCL": 5, + "SCL1": 3, + "SDA": 4, + "SDA1": 2, + "SS": 17, + "TX": 0, + }, + "waveshare_rp2040_lcd_0_96": { + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "waveshare_rp2040_lcd_1_28": { + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "waveshare_rp2040_lora": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 7, + "SDA": 4, + "SDA1": 6, + "SS": 5, + "TX": 0, + }, + "waveshare_rp2040_matrix": { + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "waveshare_rp2040_one": { + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "waveshare_rp2040_pizero": { + "MISO": 20, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 21, + "TX": 0, + }, + "waveshare_rp2040_plus": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "waveshare_rp2040_zero": { + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "waveshare_rp2350_lcd_0_96": { + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "waveshare_rp2350_pizero": { + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "waveshare_rp2350_plus": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "waveshare_rp2350_zero": { + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "waveshare_rp2350b_plus_w": { + "LED": 23, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "wiznet_55rp20_evb_pico": { + "LED": 19, + "MISO": 2, + "MOSI": 3, + "RX": 1, + "SCK": 4, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "wiznet_wizfi360_evb_pico": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 27, + "SDA": 8, + "SDA1": 26, + "SS": 17, + "TX": 0, }, } BOARDS = { + "0xcb_helios": { + "name": "0xCB Helios", + "mcu": "rp2040", + "max_pin": 29, + }, + "DudesCab": { + "name": "L'atelier d'Arnoz DudesCab", + "mcu": "rp2040", + "max_pin": 29, + }, + "MyRP_2350B": { + "name": "MyMakers RP2350B", + "mcu": "rp2350", + "max_pin": 47, + }, + "MyRP_bot": { + "name": "MyMakers RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather": { + "name": "Adafruit Feather RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_adalogger": { + "name": "Adafruit Feather RP2040 Adalogger", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_can": { + "name": "Adafruit Feather RP2040 CAN", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_dvi": { + "name": "Adafruit Feather RP2040 DVI", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_prop_maker": { + "name": "Adafruit Feather RP2040 Prop-Maker", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_rfm": { + "name": "Adafruit Feather RP2040 RFM", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_rp2350_adalogger": { + "name": "Adafruit Feather RP2350 Adalogger", + "mcu": "rp2350", + "max_pin": 47, + }, + "adafruit_feather_rp2350_hstx": { + "name": "Adafruit Feather RP2350 HSTX", + "mcu": "rp2350", + "max_pin": 47, + }, + "adafruit_feather_scorpio": { + "name": "Adafruit Feather RP2040 SCORPIO", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_thinkink": { + "name": "Adafruit Feather RP2040 ThinkINK", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_usb_host": { + "name": "Adafruit Feather RP2040 USB Host", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_floppsy": { + "name": "Adafruit Floppsy", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_fruitjam": { + "name": "Adafruit Fruit Jam RP2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "adafruit_itsybitsy": { + "name": "Adafruit ItsyBitsy RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_kb2040": { + "name": "Adafruit KB2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_macropad2040": { + "name": "Adafruit MacroPad RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_metro": { + "name": "Adafruit Metro RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_metro_rp2350": { + "name": "Adafruit Metro RP2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "adafruit_qtpy": { + "name": "Adafruit QT Py RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_stemmafriend": { + "name": "Adafruit STEMMA Friend RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_trinkeyrp2040qt": { + "name": "Adafruit Trinkey RP2040 QT", + "mcu": "rp2040", + "max_pin": 29, + }, + "akana_r1": { + "name": "METE HOCA Akana R1", + "mcu": "rp2040", + "max_pin": 29, + }, + "amken_bunny": { + "name": "Amken BunnyBoard", + "mcu": "rp2040", + "max_pin": 29, + }, + "amken_revelop": { + "name": "Amken Revelop", + "mcu": "rp2040", + "max_pin": 29, + }, + "amken_revelop_es": { + "name": "Amken Revelop eS", + "mcu": "rp2040", + "max_pin": 29, + }, + "amken_revelop_plus": { + "name": "Amken Revelop Plus", + "mcu": "rp2040", + "max_pin": 29, + }, + "arduino_nano_connect": { + "name": "Arduino Nano RP2040 Connect", + "mcu": "rp2040", + "max_pin": 29, + }, + "artronshop_rp2_nano": { + "name": "ArtronShop RP2 Nano", + "mcu": "rp2040", + "max_pin": 29, + }, + "bigtreetech_SKR_Pico": { + "name": "BIGTREETECH SKR-Pico", + "mcu": "rp2040", + "max_pin": 29, + }, + "breadstick_raspberry": { + "name": "Breadstick Raspberry", + "mcu": "rp2040", + "max_pin": 29, + }, + "bridgetek_idm2040_43a": { + "name": "BridgeTek IDM2040-43A", + "mcu": "rp2040", + "max_pin": 29, + }, + "bridgetek_idm2040_7a": { + "name": "BridgeTek IDM2040-7A", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_lora": { + "name": "iLabs Challenger 2040 LoRa", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_lte": { + "name": "iLabs Challenger 2040 LTE", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_nfc": { + "name": "iLabs Challenger 2040 NFC", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_sdrtc": { + "name": "iLabs Challenger 2040 SD/RTC", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_subghz": { + "name": "iLabs Challenger 2040 SubGHz", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_uwb": { + "name": "iLabs Challenger 2040 UWB", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_wifi": { + "name": "iLabs Challenger 2040 WiFi", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_wifi6_ble": { + "name": "iLabs Challenger 2040 WiFi6/BLE", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_wifi_ble": { + "name": "iLabs Challenger 2040 WiFi/BLE", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2350_bconnect": { + "name": "iLabs Challenger 2350 BConnect", + "mcu": "rp2350", + "max_pin": 47, + }, + "challenger_2350_wifi6_ble5": { + "name": "iLabs Challenger 2350 WiFi/BLE", + "mcu": "rp2350", + "max_pin": 47, + }, + "challenger_nb_2040_wifi": { + "name": "iLabs Challenger NB 2040 WiFi", + "mcu": "rp2040", + "max_pin": 29, + }, + "connectivity_2040_lte_wifi_ble": { + "name": "iLabs Connectivity 2040 LTE/WiFi/BLE", + "mcu": "rp2040", + "max_pin": 29, + }, + "cytron_iriv_io_controller": { + "name": "Cytron IRIV IO Controller", + "mcu": "rp2350", + "max_pin": 47, + }, + "cytron_maker_nano_rp2040": { + "name": "Cytron Maker Nano RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "cytron_maker_pi_rp2040": { + "name": "Cytron Maker Pi RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "cytron_maker_uno_rp2040": { + "name": "Cytron Maker Uno RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "cytron_motion_2350_pro": { + "name": "Cytron Motion 2350 Pro", + "mcu": "rp2350", + "max_pin": 47, + }, + "datanoisetv_picoadk": { + "name": "DatanoiseTV PicoADK", + "mcu": "rp2040", + "max_pin": 29, + }, + "datanoisetv_picoadk_v2": { + "name": "DatanoiseTV PicoADK v2", + "mcu": "rp2350", + "max_pin": 47, + }, + "degz_suibo": { + "name": "Degz Robotics Suibo RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "dfrobot_beetle_rp2040": { + "name": "DFRobot Beetle RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "electroniccats_huntercat_nfc": { + "name": "ElectronicCats HunterCat NFC RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "evn_alpha": { + "name": "EVN Alpha", + "mcu": "rp2040", + "max_pin": 29, + }, + "extelec_rc2040": { + "name": "ExtremeElectronics RC2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "flyboard2040_core": { + "name": "DeRuiLab FlyBoard2040Core", + "mcu": "rp2040", + "max_pin": 29, + }, + "geeekpi_rp2040_plus": { + "name": "GeeekPi RP2040 Plus", + "mcu": "rp2040", + "max_pin": 29, + }, + "generic": { + "name": "Generic RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "generic_rp2350": { + "name": "Generic RP2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "groundstudio_marble_pico": { + "name": "GroundStudio Marble Pico", + "mcu": "rp2040", + "max_pin": 29, + }, + "ilabs_rpico32": { + "name": "iLabs RPICO32", + "mcu": "rp2040", + "max_pin": 29, + }, + "jumperless_v1": { + "name": "Architeuthis Flux Jumperless", + "mcu": "rp2040", + "max_pin": 29, + }, + "jumperless_v5": { + "name": "Architeuthis Flux Jumperless V5", + "mcu": "rp2350", + "max_pin": 47, + }, + "melopero_cookie_rp2040": { + "name": "Melopero Cookie RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "melopero_shake_rp2040": { + "name": "Melopero Shake RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "mksthr36": { + "name": "Makerbase MKS THR36", + "mcu": "rp2040", + "max_pin": 29, + }, + "mksthr42": { + "name": "Makerbase MKS THR42", + "mcu": "rp2040", + "max_pin": 29, + }, + "nekosystems_bl2040_mini": { + "name": "Neko Systems BL2040 Mini", + "mcu": "rp2040", + "max_pin": 29, + }, + "newsan_archi": { + "name": "Newsan Archi", + "mcu": "rp2040", + "max_pin": 29, + }, + "nullbits_bit_c_pro": { + "name": "nullbits Bit-C PRO", + "mcu": "rp2040", + "max_pin": 29, + }, + "olimex_pico2bb48": { + "name": "Olimex Pico2BB48", + "mcu": "rp2350", + "max_pin": 47, + }, + "olimex_pico2xl": { + "name": "Olimex Pico2XL", + "mcu": "rp2350", + "max_pin": 47, + }, + "olimex_pico2xxl": { + "name": "Olimex Pico2XXL", + "mcu": "rp2350", + "max_pin": 47, + }, + "olimex_rp2040pico30": { + "name": "Olimex RP2040-Pico30", + "mcu": "rp2040", + "max_pin": 29, + }, + "picolume": { + "name": "PicoLume Transceiver", + "mcu": "rp2040", + "max_pin": 29, + }, + "pimoroni_explorer": { + "name": "Pimoroni Explorer", + "mcu": "rp2350", + "max_pin": 47, + }, + "pimoroni_pga2040": { + "name": "Pimoroni PGA2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "pimoroni_pga2350": { + "name": "Pimoroni PGA2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "pimoroni_pico_plus_2": { + "name": "Pimoroni PicoPlus2", + "mcu": "rp2350", + "max_pin": 47, + }, + "pimoroni_pico_plus_2w": { + "name": "Pimoroni PicoPlus2W", + "mcu": "rp2350", + "max_pin": 47, + "max_virtual_pin": 64, + }, + "pimoroni_plasma2040": { + "name": "Pimoroni Plasma2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "pimoroni_plasma2350": { + "name": "Pimoroni Plasma2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "pimoroni_plasma2350w": { + "name": "Pimoroni Plasma2350W", + "mcu": "rp2350", + "max_pin": 47, + }, + "pimoroni_servo2040": { + "name": "Pimoroni Servo2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "pimoroni_tiny2040": { + "name": "Pimoroni Tiny2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "pimoroni_tiny2350": { + "name": "Pimoroni Tiny2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "pintronix_pinmax": { + "name": "Pintronix PinMax", + "mcu": "rp2040", + "max_pin": 29, + }, + "rakwireless_rak11300": { + "name": "RAKwireless RAK11300", + "mcu": "rp2040", + "max_pin": 29, + }, + "redscorp_rp2040_eins": { + "name": "redscorp RP2040-Eins", + "mcu": "rp2040", + "max_pin": 29, + }, + "redscorp_rp2040_promini": { + "name": "redscorp RP2040-ProMini", + "mcu": "rp2040", + "max_pin": 29, + }, "rpipico": { "name": "Raspberry Pi Pico", + "mcu": "rp2040", + "max_pin": 29, + }, + "rpipico2": { + "name": "Raspberry Pi Pico 2", + "mcu": "rp2350", + "max_pin": 47, + }, + "rpipico2w": { + "name": "Raspberry Pi Pico 2W", + "mcu": "rp2350", + "max_pin": 47, + "max_virtual_pin": 64, }, "rpipicow": { "name": "Raspberry Pi Pico W", + "mcu": "rp2040", + "max_pin": 29, + "max_virtual_pin": 64, + }, + "sea_picro": { + "name": "Generic Sea-Picro", + "mcu": "rp2040", + "max_pin": 29, + }, + "seeed_indicator_rp2040": { + "name": "Seeed INDICATOR RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "seeed_xiao_rp2040": { + "name": "Seeed XIAO RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "seeed_xiao_rp2350": { + "name": "Seeed XIAO RP2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "silicognition_rp2040_shim": { + "name": "Silicognition RP2040-Shim", + "mcu": "rp2040", + "max_pin": 29, + }, + "soldered_nula_rp2350": { + "name": "Soldered Electronics NULA RP2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "solderparty_rp2040_stamp": { + "name": "Solder Party RP2040 Stamp", + "mcu": "rp2040", + "max_pin": 29, + }, + "solderparty_rp2350_stamp": { + "name": "Solder Party RP2350 Stamp", + "mcu": "rp2350", + "max_pin": 47, + }, + "solderparty_rp2350_stamp_xl": { + "name": "Solder Party RP2350 Stamp XL", + "mcu": "rp2350", + "max_pin": 47, + }, + "sparkfun_iotnode_lorawanrp2350": { + "name": "SparkFun IoT Node LoRaWAN", + "mcu": "rp2350", + "max_pin": 47, + }, + "sparkfun_iotredboard_rp2350": { + "name": "SparkFun IoT RedBoard RP2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "sparkfun_micromodrp2040": { + "name": "SparkFun MicroMod RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "sparkfun_promicrorp2040": { + "name": "SparkFun ProMicro RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "sparkfun_promicrorp2350": { + "name": "SparkFun ProMicro RP2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "sparkfun_thingplusrp2040": { + "name": "SparkFun Thing Plus RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "sparkfun_thingplusrp2350": { + "name": "SparkFun Thing Plus RP2350", + "mcu": "rp2350", + "max_pin": 47, + "max_virtual_pin": 64, + }, + "sparkfun_xrp_controller": { + "name": "SparkFun XRP Controller", + "mcu": "rp2350", + "max_pin": 47, + "max_virtual_pin": 64, + }, + "sparkfun_xrp_controller_beta": { + "name": "SparkFun XRP Controller (Beta)", + "mcu": "rp2040", + "max_pin": 29, + "max_virtual_pin": 64, + }, + "upesy_rp2040_devkit": { + "name": "uPesy RP2040 DevKit", + "mcu": "rp2040", + "max_pin": 29, + }, + "vccgnd_yd_rp2040": { + "name": "VCC-GND YD RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "vicharak_shrike-lite": { + "name": "Vicharak Shrike-Lite", + "mcu": "rp2040", + "max_pin": 29, + }, + "viyalab_mizu": { + "name": "Viyalab Mizu RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_lcd_0_96": { + "name": "Waveshare RP2040 LCD 0.96", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_lcd_1_28": { + "name": "Waveshare RP2040 LCD 1.28", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_lora": { + "name": "Waveshare RP2040 LoRa", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_matrix": { + "name": "Waveshare RP2040 Matrix", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_one": { + "name": "Waveshare RP2040 One", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_pizero": { + "name": "Waveshare RP2040 PiZero", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_plus": { + "name": "Waveshare RP2040 Plus", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_zero": { + "name": "Waveshare RP2040 Zero", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2350_lcd_0_96": { + "name": "Waveshare RP2350 LCD 0.96", + "mcu": "rp2350", + "max_pin": 47, + }, + "waveshare_rp2350_pizero": { + "name": "Waveshare RP2350 PiZero", + "mcu": "rp2350", + "max_pin": 47, + }, + "waveshare_rp2350_plus": { + "name": "Waveshare RP2350 Plus", + "mcu": "rp2350", + "max_pin": 47, + }, + "waveshare_rp2350_zero": { + "name": "Waveshare RP2350 Zero", + "mcu": "rp2350", + "max_pin": 47, + }, + "waveshare_rp2350b_plus_w": { + "name": "Waveshare RP2350B Plus W", + "mcu": "rp2350", + "max_pin": 47, + }, + "wiznet_5100s_evb_pico": { + "name": "WIZnet W5100S-EVB-Pico", + "mcu": "rp2040", + "max_pin": 29, + }, + "wiznet_5100s_evb_pico2": { + "name": "WIZnet W5100S-EVB-Pico2", + "mcu": "rp2350", + "max_pin": 47, + }, + "wiznet_5500_evb_pico": { + "name": "WIZnet W5500-EVB-Pico", + "mcu": "rp2040", + "max_pin": 29, + }, + "wiznet_5500_evb_pico2": { + "name": "WIZnet W5500-EVB-Pico2", + "mcu": "rp2350", + "max_pin": 47, + }, + "wiznet_55rp20_evb_pico": { + "name": "WIZnet W55RP20-EVB-Pico", + "mcu": "rp2040", + "max_pin": 29, + }, + "wiznet_6300_evb_pico": { + "name": "WIZnet W6300-EVB-Pico", + "mcu": "rp2040", + "max_pin": 29, + }, + "wiznet_6300_evb_pico2": { + "name": "WIZnet W6300-EVB-Pico2", + "mcu": "rp2350", + "max_pin": 47, + }, + "wiznet_wizfi360_evb_pico": { + "name": "WIZnet WizFi360-EVB-Pico", + "mcu": "rp2040", + "max_pin": 29, }, } diff --git a/esphome/components/rp2040/generate_boards.py b/esphome/components/rp2040/generate_boards.py new file mode 100644 index 0000000000..a0e3699f37 --- /dev/null +++ b/esphome/components/rp2040/generate_boards.py @@ -0,0 +1,186 @@ +"""Generate boards.py from arduino-pico board definitions. + +Usage: python esphome/components/rp2040/generate_boards.py <arduino-pico-path> +""" + +import json +from pathlib import Path +import re +import sys + +from jinja2 import Environment, FileSystemLoader + +# Map arduino-pico pin defines to ESPHome-friendly names +PIN_NAME_MAP = { + "LED": "LED", + "WIRE0_SDA": "SDA", + "WIRE0_SCL": "SCL", + "WIRE1_SDA": "SDA1", + "WIRE1_SCL": "SCL1", + "SPI0_MISO": "MISO", + "SPI0_MOSI": "MOSI", + "SPI0_SCK": "SCK", + "SPI0_SS": "SS", + "SERIAL1_TX": "TX", + "SERIAL1_RX": "RX", +} + +# arduino-pico maps pins >= 64 to CYW43 wireless chip GPIOs (pin - 64) +CYW43_GPIO_OFFSET = 64 +# CYW43 has 3 GPIOs: 0=LED, 1=VBUS_SENSE, 2=REG_ON +CYW43_GPIO_COUNT = 3 + +# Max GPIO pin per MCU (hardware specs from datasheets) +MCU_MAX_PIN = { + "rp2040": 29, # GPIO 0-29 + "rp2350": 47, # GPIO 0-47 (RP2350A) +} +DEFAULT_MAX_PIN = 29 + +PIN_DEFINE_RE = re.compile(r"#define\s+PIN_(\w+)\s+\((\d+)u\)") + + +def parse_variant_pins(variant_dir: Path) -> dict[str, int]: + """Parse pins_arduino.h and return mapped pin names.""" + header = variant_dir / "pins_arduino.h" + if not header.exists(): + return {} + + pins = {} + for match in PIN_DEFINE_RE.finditer(header.read_text(encoding="utf-8")): + raw_name = match.group(1) + value = int(match.group(2)) + if raw_name in PIN_NAME_MAP: + pins[PIN_NAME_MAP[raw_name]] = value + return pins + + +def load_boards(arduino_pico_path: Path) -> tuple[dict, dict]: + """Load all board definitions and return (board_pins, boards) dicts.""" + json_dir = arduino_pico_path / "tools" / "json" + variants_dir = arduino_pico_path / "variants" + + board_pins = {} + boards = {} + variant_pins_cache: dict[str, dict[str, int]] = {} + + for json_file in sorted(json_dir.glob("*.json")): + board_name = json_file.stem + with open(json_file, encoding="utf-8") as f: + data = json.load(f) + + build = data.get("build", {}) + mcu = build.get("mcu", "rp2040") + variant = build.get("variant", board_name) + name = data.get("name", board_name) + vendor = data.get("vendor", "") + + display_name = f"{vendor} {name}".strip() if vendor else name + + boards[board_name] = { + "name": display_name, + "mcu": mcu, + "max_pin": MCU_MAX_PIN.get(mcu, DEFAULT_MAX_PIN), + } + + # Get pins for this variant + if variant not in variant_pins_cache: + variant_dir = variants_dir / variant + variant_pins_cache[variant] = parse_variant_pins(variant_dir) + + pins = variant_pins_cache[variant] + if pins: + max_pin = boards[board_name]["max_pin"] + cyw43_max = CYW43_GPIO_OFFSET + CYW43_GPIO_COUNT - 1 + # Filter out placeholder values (e.g. 99 = "not connected") + filtered = { + name: value + for name, value in pins.items() + if value <= max_pin or CYW43_GPIO_OFFSET <= value <= cyw43_max + } + if filtered: + board_pins[board_name] = filtered + + # Compute max_virtual_pin per board from pin maps + for board_name, pins in board_pins.items(): + if isinstance(pins, str): + continue + virtual_pins = [v for v in pins.values() if v >= CYW43_GPIO_OFFSET] + if virtual_pins and board_name in boards: + boards[board_name]["max_virtual_pin"] = max(virtual_pins) + + # Deduplicate: if board pins match its variant's pins, use string alias + for board_name in list(board_pins.keys()): + if board_name not in boards: + continue + build_variant = _get_variant(json_dir / f"{board_name}.json") + if ( + build_variant + and build_variant != board_name + and build_variant in board_pins + and board_pins[board_name] == board_pins[build_variant] + ): + board_pins[board_name] = build_variant + + return board_pins, boards + + +def _get_variant(json_file: Path) -> str | None: + """Get variant name from a board JSON file.""" + if not json_file.exists(): + return None + with open(json_file, encoding="utf-8") as f: + data = json.load(f) + return data.get("build", {}).get("variant") + + +_TEMPLATE_DIR = Path(__file__).parent + + +def _format_pins(pins: dict[str, int] | str) -> str: + """Jinja2 filter to format a pin dict or alias as Python source.""" + if isinstance(pins, str): + return repr(pins) + items = ", ".join(f"{k!r}: {v}" for k, v in sorted(pins.items())) + return f"{{{items}}}" + + +_jinja_env = Environment( + loader=FileSystemLoader(_TEMPLATE_DIR), keep_trailing_newline=True +) +_jinja_env.filters["format_pins"] = _format_pins +_jinja_env.filters["repr"] = repr + + +def generate(arduino_pico_path: Path) -> str: + """Generate boards.py content.""" + board_pins, boards = load_boards(arduino_pico_path) + + template = _jinja_env.get_template("boards.jinja2") + return template.render( + cyw43_gpio_offset=CYW43_GPIO_OFFSET, + cyw43_max_gpio=CYW43_GPIO_OFFSET + CYW43_GPIO_COUNT - 1, + default_max_pin=DEFAULT_MAX_PIN, + board_pins=sorted(board_pins.items()), + boards=sorted(boards.items()), + ) + + +def main(): + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} <arduino-pico-path>", file=sys.stderr) + sys.exit(1) + + arduino_pico_path = Path(sys.argv[1]) + if not (arduino_pico_path / "tools" / "json").exists(): + print(f"Error: {arduino_pico_path}/tools/json not found", file=sys.stderr) + sys.exit(1) + + output = generate(arduino_pico_path) + output_file = Path(__file__).parent / "boards.py" + output_file.write_text(output, encoding="utf-8") + print(f"Generated {output_file}") + + +if __name__ == "__main__": + main() diff --git a/esphome/components/rp2040/gpio.py b/esphome/components/rp2040/gpio.py index 193e567d17..18fb09f76a 100644 --- a/esphome/components/rp2040/gpio.py +++ b/esphome/components/rp2040/gpio.py @@ -54,19 +54,29 @@ def _translate_pin(value): return _lookup_pin(value) +def _board_max_virtual_pin(board): + """Get the max CYW43 virtual pin for this board, or None if no virtual pins.""" + return boards.BOARDS.get(board, {}).get("max_virtual_pin") + + def validate_gpio_pin(value): value = _translate_pin(value) board = CORE.data[KEY_RP2040][KEY_BOARD] - if board == "rpipicow" and value == 32: - return value # Special case for Pico-w LED pin - if value < 0 or value > 29: - raise cv.Invalid(f"RP2040: Invalid pin number: {value}") + max_virtual = _board_max_virtual_pin(board) + if max_virtual is not None and boards.CYW43_GPIO_OFFSET <= value <= max_virtual: + return value + max_pin = boards.BOARDS.get(board, {}).get("max_pin", boards.DEFAULT_MAX_PIN) + if value < 0 or value > max_pin: + raise cv.Invalid(f"Invalid pin number: {value} (max {max_pin} for this board)") return value def validate_supports(value): board = CORE.data[KEY_RP2040][KEY_BOARD] - if board != "rpipicow" or value[CONF_NUMBER] != 32: + if ( + _board_max_virtual_pin(board) is None + or value[CONF_NUMBER] < boards.CYW43_GPIO_OFFSET + ): return value mode = value[CONF_MODE] is_input = mode[CONF_INPUT] @@ -75,7 +85,7 @@ def validate_supports(value): is_pullup = mode[CONF_PULLUP] is_pulldown = mode[CONF_PULLDOWN] if not is_output or is_input or is_open_drain or is_pullup or is_pulldown: - raise cv.Invalid("Only output mode is supported for Pico-w LED pin") + raise cv.Invalid("Only output mode is supported for CYW43 virtual pins") return value diff --git a/tests/unit_tests/components/test_rp2040_generate_boards.py b/tests/unit_tests/components/test_rp2040_generate_boards.py new file mode 100644 index 0000000000..2e40ed08ba --- /dev/null +++ b/tests/unit_tests/components/test_rp2040_generate_boards.py @@ -0,0 +1,273 @@ +"""Tests for rp2040 generate_boards.py.""" + +from __future__ import annotations + +import json +from pathlib import Path +import textwrap + +import pytest + +from esphome.components.rp2040.generate_boards import load_boards, parse_variant_pins + +PICO_PINS_HEADER = textwrap.dedent("""\ + #pragma once + #define PIN_LED (25u) + #define PIN_SERIAL1_TX (0u) + #define PIN_SERIAL1_RX (1u) + #define PIN_WIRE0_SDA (4u) + #define PIN_WIRE0_SCL (5u) + #define PIN_WIRE1_SDA (26u) + #define PIN_WIRE1_SCL (27u) + #define PIN_SPI0_MISO (16u) + #define PIN_SPI0_MOSI (19u) + #define PIN_SPI0_SCK (18u) + #define PIN_SPI0_SS (17u) + #include "../generic/common.h" +""") + +PICOW_PINS_HEADER = textwrap.dedent("""\ + #pragma once + #include <cyw43_wrappers.h> + #define PIN_LED (64u) + #define PIN_WIRE0_SDA (4u) + #define PIN_WIRE0_SCL (5u) + #include "../generic/common.h" +""") + + +@pytest.fixture() +def arduino_pico(tmp_path: Path) -> Path: + """Create a minimal arduino-pico directory structure.""" + json_dir = tmp_path / "tools" / "json" + json_dir.mkdir(parents=True) + variants_dir = tmp_path / "variants" + variants_dir.mkdir() + + generic_dir = variants_dir / "generic" + generic_dir.mkdir() + (generic_dir / "common.h").write_text("#pragma once\n") + + return tmp_path + + +def _add_board( + arduino_pico: Path, + board_name: str, + mcu: str = "rp2040", + variant: str | None = None, + vendor: str = "", + name: str | None = None, + pins_header: str | None = None, +) -> None: + """Add a board JSON and variant to the fake arduino-pico tree.""" + if variant is None: + variant = board_name + if name is None: + name = board_name + + json_dir = arduino_pico / "tools" / "json" + variants_dir = arduino_pico / "variants" + + board_json = { + "build": { + "mcu": mcu, + "variant": variant, + }, + "name": name, + "vendor": vendor, + } + (json_dir / f"{board_name}.json").write_text(json.dumps(board_json)) + + variant_dir = variants_dir / variant + variant_dir.mkdir(exist_ok=True) + if pins_header is not None: + (variant_dir / "pins_arduino.h").write_text(pins_header) + + +def test_parse_basic_pins(tmp_path: Path) -> None: + variant_dir = tmp_path / "rpipico" + variant_dir.mkdir() + (variant_dir / "pins_arduino.h").write_text(PICO_PINS_HEADER) + + pins = parse_variant_pins(variant_dir) + assert pins["LED"] == 25 + assert pins["SDA"] == 4 + assert pins["SCL"] == 5 + assert pins["SDA1"] == 26 + assert pins["SCL1"] == 27 + assert pins["MISO"] == 16 + assert pins["MOSI"] == 19 + assert pins["SCK"] == 18 + assert pins["SS"] == 17 + assert pins["TX"] == 0 + assert pins["RX"] == 1 + + +def test_parse_cyw43_led_pin(tmp_path: Path) -> None: + variant_dir = tmp_path / "rpipicow" + variant_dir.mkdir() + (variant_dir / "pins_arduino.h").write_text(PICOW_PINS_HEADER) + + pins = parse_variant_pins(variant_dir) + assert pins["LED"] == 64 + + +def test_parse_missing_header(tmp_path: Path) -> None: + variant_dir = tmp_path / "noheader" + variant_dir.mkdir() + assert parse_variant_pins(variant_dir) == {} + + +def test_parse_unmapped_defines_ignored(tmp_path: Path) -> None: + variant_dir = tmp_path / "custom" + variant_dir.mkdir() + (variant_dir / "pins_arduino.h").write_text( + "#define PIN_NEOPIXEL (16u)\n#define PIN_LED (25u)\n" + ) + + pins = parse_variant_pins(variant_dir) + assert "NEOPIXEL" not in pins + assert pins["LED"] == 25 + + +def test_load_basic_board(arduino_pico: Path) -> None: + _add_board( + arduino_pico, + "rpipico", + vendor="Raspberry Pi", + name="Pico", + pins_header=PICO_PINS_HEADER, + ) + + board_pins, boards = load_boards(arduino_pico) + + assert "rpipico" in boards + assert boards["rpipico"]["name"] == "Raspberry Pi Pico" + assert boards["rpipico"]["mcu"] == "rp2040" + assert boards["rpipico"]["max_pin"] == 29 + + assert "rpipico" in board_pins + assert board_pins["rpipico"]["LED"] == 25 + assert board_pins["rpipico"]["SDA"] == 4 + + +def test_load_rp2350_board(arduino_pico: Path) -> None: + _add_board( + arduino_pico, + "rpipico2", + mcu="rp2350", + vendor="Raspberry Pi", + name="Pico 2", + pins_header=PICO_PINS_HEADER, + ) + + _, boards = load_boards(arduino_pico) + + assert boards["rpipico2"]["mcu"] == "rp2350" + assert boards["rpipico2"]["max_pin"] == 47 + + +def test_cyw43_board_has_max_virtual_pin(arduino_pico: Path) -> None: + _add_board( + arduino_pico, + "rpipicow", + vendor="Raspberry Pi", + name="Pico W", + pins_header=PICOW_PINS_HEADER, + ) + + _, boards = load_boards(arduino_pico) + + assert boards["rpipicow"]["max_virtual_pin"] == 64 + + +def test_non_cyw43_board_has_no_max_virtual_pin(arduino_pico: Path) -> None: + _add_board( + arduino_pico, + "rpipico", + vendor="Raspberry Pi", + name="Pico", + pins_header=PICO_PINS_HEADER, + ) + + _, boards = load_boards(arduino_pico) + + assert "max_virtual_pin" not in boards["rpipico"] + + +def test_board_without_variant_header(arduino_pico: Path) -> None: + _add_board(arduino_pico, "novariant", name="No Variant") + + board_pins, boards = load_boards(arduino_pico) + + assert "novariant" in boards + assert "novariant" not in board_pins + + +def test_shared_variant_deduplicates(arduino_pico: Path) -> None: + """Two boards sharing the same variant should alias.""" + _add_board(arduino_pico, "base_board", pins_header=PICO_PINS_HEADER) + _add_board(arduino_pico, "alias_board", variant="base_board") + + board_pins, _ = load_boards(arduino_pico) + + assert board_pins["base_board"] == parse_variant_pins( + arduino_pico / "variants" / "base_board" + ) + assert board_pins["alias_board"] == "base_board" + + +def test_display_name_with_vendor(arduino_pico: Path) -> None: + _add_board(arduino_pico, "testboard", vendor="Acme", name="Widget") + _, boards = load_boards(arduino_pico) + assert boards["testboard"]["name"] == "Acme Widget" + + +def test_display_name_without_vendor(arduino_pico: Path) -> None: + _add_board(arduino_pico, "testboard", vendor="", name="Widget") + _, boards = load_boards(arduino_pico) + assert boards["testboard"]["name"] == "Widget" + + +def test_unknown_mcu_gets_default_max_pin(arduino_pico: Path) -> None: + _add_board(arduino_pico, "future", mcu="rp2450", pins_header=PICO_PINS_HEADER) + _, boards = load_boards(arduino_pico) + assert boards["future"]["max_pin"] == 29 + + +def test_placeholder_pins_filtered_out(arduino_pico: Path) -> None: + """Pins with placeholder values like 99 should be filtered out.""" + header = textwrap.dedent("""\ + #pragma once + #define PIN_LED (25u) + #define PIN_WIRE0_SDA (4u) + #define PIN_WIRE0_SCL (5u) + #define PIN_WIRE1_SDA (99u) + #define PIN_WIRE1_SCL (99u) + """) + _add_board(arduino_pico, "placeholder", pins_header=header) + + board_pins, boards = load_boards(arduino_pico) + + assert "SDA1" not in board_pins["placeholder"] + assert "SCL1" not in board_pins["placeholder"] + assert board_pins["placeholder"]["LED"] == 25 + assert "max_virtual_pin" not in boards["placeholder"] + + +def test_placeholder_pins_not_treated_as_virtual(arduino_pico: Path) -> None: + """Pin 99 should not cause max_virtual_pin to be set.""" + header = textwrap.dedent("""\ + #pragma once + #define PIN_LED (64u) + #define PIN_WIRE0_SDA (4u) + #define PIN_WIRE0_SCL (5u) + #define PIN_SPI0_MISO (99u) + """) + _add_board(arduino_pico, "badpin", pins_header=header) + + board_pins, boards = load_boards(arduino_pico) + + assert "MISO" not in board_pins["badpin"] + assert boards["badpin"]["max_virtual_pin"] == 64 From 82629c397f699af8b263e66e7cc904bebcc297d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Mar 2026 07:01:50 -1000 Subject: [PATCH 404/609] [hlk_fm22x] Fix oversized response rejection breaking GET_ALL_FACE_IDS (#14506) --- esphome/components/hlk_fm22x/hlk_fm22x.cpp | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.cpp b/esphome/components/hlk_fm22x/hlk_fm22x.cpp index 18d26f057a..7c7c8782de 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.cpp +++ b/esphome/components/hlk_fm22x/hlk_fm22x.cpp @@ -133,24 +133,22 @@ void HlkFm22xComponent::recv_command_() { checksum ^= byte; length |= byte; - if (length > HLK_FM22X_MAX_RESPONSE_SIZE) { - ESP_LOGE(TAG, "Response too large: %u bytes", length); - // Discard exactly the remaining payload and checksum for this frame - for (uint16_t i = 0; i < length + 1 && this->available() > 0; ++i) - this->read(); - return; - } - + // Read up to buffer size; discard excess bytes while still computing checksum + // GET_ALL_FACE_IDS can return all enrolled face data (hundreds of bytes) + // but handlers only need the first few bytes + size_t to_store = std::min(static_cast<size_t>(length), HLK_FM22X_MAX_RESPONSE_SIZE); for (uint16_t idx = 0; idx < length; ++idx) { byte = this->read(); checksum ^= byte; - this->recv_buf_[idx] = byte; + if (idx < to_store) { + this->recv_buf_[idx] = byte; + } } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_pretty_size(HLK_FM22X_MAX_RESPONSE_SIZE)]; ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, - format_hex_pretty_to(hex_buf, this->recv_buf_.data(), length)); + format_hex_pretty_to(hex_buf, this->recv_buf_.data(), to_store)); #endif byte = this->read(); @@ -160,10 +158,10 @@ void HlkFm22xComponent::recv_command_() { } switch (response_type) { case HlkFm22xResponseType::NOTE: - this->handle_note_(this->recv_buf_.data(), length); + this->handle_note_(this->recv_buf_.data(), to_store); break; case HlkFm22xResponseType::REPLY: - this->handle_reply_(this->recv_buf_.data(), length); + this->handle_reply_(this->recv_buf_.data(), to_store); break; default: ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type); From 6e3bc7b1ddb5b8ac91ecf0653087dc835264ca8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Mar 2026 07:33:05 -1000 Subject: [PATCH 405/609] [ci] Use pull_request_target for codeowner approved label workflow (#14561) --- .github/scripts/codeowners.js | 2 +- .../codeowner-approved-label-update.yml | 63 +++++---------- .../workflows/codeowner-approved-label.yml | 78 ------------------- 3 files changed, 21 insertions(+), 122 deletions(-) delete mode 100644 .github/workflows/codeowner-approved-label.yml diff --git a/.github/scripts/codeowners.js b/.github/scripts/codeowners.js index 5d69c11b1a..9b2f2922c0 100644 --- a/.github/scripts/codeowners.js +++ b/.github/scripts/codeowners.js @@ -2,7 +2,7 @@ // // Used by: // - codeowner-review-request.yml -// - codeowner-approved-label.yml + codeowner-approved-label-update.yml +// - codeowner-approved-label-update.yml // - auto-label-pr/detectors.js (detectCodeOwner) /** diff --git a/.github/workflows/codeowner-approved-label-update.yml b/.github/workflows/codeowner-approved-label-update.yml index 9168cce1d6..c2eb886913 100644 --- a/.github/workflows/codeowner-approved-label-update.yml +++ b/.github/workflows/codeowner-approved-label-update.yml @@ -1,13 +1,15 @@ -# Fallback for fork PRs: phase 1 (codeowner-approved-label.yml) handles -# non-fork PRs directly but can't write labels on fork PRs (read-only token). -# This workflow re-determines the action and applies it if needed. +# Adds/removes a 'code-owner-approved' label when a component-specific +# codeowner approves (or dismisses) a PR. +# +# Uses pull_request_target so that fork PRs do not require workflow approval. +# The label is reconciled on every PR update; for review events specifically, +# this means the label is applied on the next push after a codeowner review. -name: Codeowner Approved Label Update +name: Codeowner Approved Label on: - workflow_run: - workflows: ["Codeowner Approved Label"] - types: [completed] + pull_request_target: + types: [opened, synchronize, reopened, ready_for_review] permissions: issues: write @@ -15,51 +17,23 @@ permissions: contents: read jobs: - update-label: + codeowner-approved: name: Run - if: > - github.event.workflow_run.conclusion == 'success' && - github.event.workflow_run.event == 'pull_request_review' + if: ${{ github.repository == 'esphome/esphome' }} runs-on: ubuntu-latest steps: - - name: Get PR details - id: pr - env: - GH_TOKEN: ${{ github.token }} - HEAD_SHA: ${{ github.event.workflow_run.head_sha }} - REPO: ${{ github.repository }} - run: | - pr_data=$(gh pr list --repo "$REPO" --state open --search "$HEAD_SHA" \ - --json number,baseRefName --jq '.[0] // empty') - - if [ -z "$pr_data" ]; then - echo "No open PR found for SHA $HEAD_SHA, skipping" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - - pr_number=$(echo "$pr_data" | jq -r '.number') - base_ref=$(echo "$pr_data" | jq -r '.baseRefName') - - echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT" - echo "base_ref=$base_ref" >> "$GITHUB_OUTPUT" - echo "Found PR #$pr_number targeting $base_ref" - - - name: Checkout base repository - if: steps.pr.outputs.skip != 'true' + - name: Checkout base branch uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - repository: ${{ github.repository }} - ref: ${{ steps.pr.outputs.base_ref }} + ref: ${{ github.event.pull_request.base.sha }} sparse-checkout: | .github/scripts/codeowners.js CODEOWNERS - - name: Update label - if: steps.pr.outputs.skip != 'true' + - name: Check codeowner approval and update label uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: - PR_NUMBER: ${{ steps.pr.outputs.pr_number }} + PR_NUMBER: ${{ github.event.pull_request.number }} with: script: | const { loadCodeowners, determineLabelAction, LabelAction } = require('./.github/scripts/codeowners.js'); @@ -76,6 +50,11 @@ jobs: github, owner, repo, pr_number, codeownersPatterns, LABEL_NAME ); + if (action === LabelAction.NONE) { + console.log('No label change needed'); + return; + } + if (action === LabelAction.ADD) { await github.rest.issues.addLabels({ owner, repo, issue_number: pr_number, labels: [LABEL_NAME] @@ -90,6 +69,4 @@ jobs: } catch (error) { if (error.status !== 404) throw error; } - } else { - console.log('No label change needed'); } diff --git a/.github/workflows/codeowner-approved-label.yml b/.github/workflows/codeowner-approved-label.yml deleted file mode 100644 index 12199bd0b0..0000000000 --- a/.github/workflows/codeowner-approved-label.yml +++ /dev/null @@ -1,78 +0,0 @@ -# Adds/removes a 'code-owner-approved' label when a component-specific -# codeowner approves (or dismisses) a PR. -# -# Handles non-fork PRs directly. For fork PRs the GITHUB_TOKEN is read-only, -# so label writes are deferred to codeowner-approved-label-update.yml which -# triggers via workflow_run with write permissions. - -name: Codeowner Approved Label - -on: - pull_request_review: - types: [submitted, dismissed] - -permissions: - issues: write - pull-requests: read - contents: read - -jobs: - codeowner-approved: - name: Run - if: ${{ github.repository == 'esphome/esphome' }} - runs-on: ubuntu-latest - steps: - - name: Checkout base branch - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.event.pull_request.base.sha }} - sparse-checkout: | - .github/scripts/codeowners.js - CODEOWNERS - - - name: Check codeowner approval and update label - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - with: - script: | - const { loadCodeowners, determineLabelAction, LabelAction } = require('./.github/scripts/codeowners.js'); - - const owner = context.repo.owner; - const repo = context.repo.repo; - const pr_number = parseInt(process.env.PR_NUMBER, 10); - const LABEL_NAME = 'code-owner-approved'; - - console.log(`Processing PR #${pr_number} for codeowner approval label`); - - const codeownersPatterns = loadCodeowners(); - const action = await determineLabelAction( - github, owner, repo, pr_number, codeownersPatterns, LABEL_NAME - ); - - if (action === LabelAction.NONE) { - console.log('No label change needed'); - return; - } - - try { - if (action === LabelAction.ADD) { - await github.rest.issues.addLabels({ - owner, repo, issue_number: pr_number, labels: [LABEL_NAME] - }); - console.log(`Added '${LABEL_NAME}' label`); - } else if (action === LabelAction.REMOVE) { - await github.rest.issues.removeLabel({ - owner, repo, issue_number: pr_number, name: LABEL_NAME - }); - console.log(`Removed '${LABEL_NAME}' label`); - } - } catch (error) { - if (error.status === 403) { - console.log('Fork PR: deferring label write to phase 2 workflow'); - } else if (error.status === 404) { - console.log('Label already removed'); - } else { - throw error; - } - } From 65b7c73bf3fdbbe9260040b96e758561dc9be548 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Mar 2026 08:02:34 -1000 Subject: [PATCH 406/609] [sgp4x] Fix undefined behavior from mutating entity config at runtime (#14562) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/sgp4x/sgp4x.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index 23589265ca..44d0a54080 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -35,13 +35,9 @@ void SGP4xComponent::setup() { this->self_test_time_ = SPG40_SELFTEST_TIME; this->measure_time_ = SGP40_MEASURE_TIME; if (this->nox_sensor_) { - ESP_LOGE(TAG, "SGP41 required for NOx"); - // disable the sensor - this->nox_sensor_->set_disabled_by_default(true); - // make sure it's not visible in HA - this->nox_sensor_->set_internal(true); - this->nox_sensor_->state = NAN; - // remove pointer to sensor + ESP_LOGE(TAG, "SGP41 required for NOx, disabling NOx sensor"); + // Drop the pointer so update() never publishes to it. + // The entity remains registered but will never receive state updates. this->nox_sensor_ = nullptr; } } else if (featureset == SGP41_FEATURESET) { From b2378e830e947ecc8d79f197abf838868927053e Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht <rupprecht.thomas@gmail.com> Date: Fri, 6 Mar 2026 19:11:52 +0100 Subject: [PATCH 407/609] [rtttl] Add AudioStreamInfo and set volume (#14439) Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/rtttl/rtttl.cpp | 40 ++++++++++-------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 4ccfc539ea..9bf0450993 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -29,11 +29,6 @@ static constexpr uint8_t REPEATING_NOTE_GAP_MS = 10; static constexpr uint16_t SAMPLE_BUFFER_SIZE = 2048; static constexpr uint16_t SAMPLE_RATE = 16000; -struct SpeakerSample { - int8_t left{0}; - int8_t right{0}; -}; - inline double deg2rad(double degrees) { static constexpr double PI_ON_180 = M_PI / 180.0; return degrees * PI_ON_180; @@ -108,6 +103,9 @@ void Rtttl::loop() { } } else if (this->state_ == State::INIT) { if (this->speaker_->is_stopped()) { + audio::AudioStreamInfo audio_stream_info = audio::AudioStreamInfo(16, 1, SAMPLE_RATE); + this->speaker_->set_audio_stream_info(audio_stream_info); + this->speaker_->set_volume(this->gain_); this->speaker_->start(); this->set_state_(State::STARTING); } @@ -120,35 +118,27 @@ void Rtttl::loop() { return; } if (this->samples_sent_ != this->samples_count_) { - SpeakerSample sample[SAMPLE_BUFFER_SIZE + 2]; + int16_t sample[SAMPLE_BUFFER_SIZE]; uint16_t sample_index = 0; double rem = 0.0; - while (true) { + while (sample_index < SAMPLE_BUFFER_SIZE && this->samples_sent_ < this->samples_count_) { // Try and send out the remainder of the existing note, one per `loop()` if (this->samples_per_wave_ != 0 && this->samples_sent_ >= this->samples_gap_) { // Play note rem = ((this->samples_sent_ << 10) % this->samples_per_wave_) * (360.0 / this->samples_per_wave_); - - int8_t val = (127 * this->gain_) * sin(deg2rad(rem)); - - sample[sample_index].left = val; - sample[sample_index].right = val; + sample[sample_index] = INT16_MAX * sin(deg2rad(rem)); } else { - sample[sample_index].left = 0; - sample[sample_index].right = 0; - } - - if (sample_index >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) { - break; + sample[sample_index] = 0; } this->samples_sent_++; sample_index++; } if (sample_index > 0) { - size_t bytes_to_send = sample_index * sizeof(SpeakerSample); - size_t send = this->speaker_->play((uint8_t *) (&sample), bytes_to_send); - if (send != bytes_to_send) { - this->samples_sent_ -= (sample_index - (send / sizeof(SpeakerSample))); + size_t bytes = sample_index * sizeof(int16_t); + size_t sent_bytes = this->speaker_->play((uint8_t *) (&sample), bytes); + size_t samples_sent = sent_bytes / sizeof(int16_t); + if (samples_sent != sample_index) { + this->samples_sent_ -= (sample_index - samples_sent); } return; } @@ -408,11 +398,7 @@ void Rtttl::finish_() { #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { - SpeakerSample sample[2]; - sample[0].left = 0; - sample[0].right = 0; - sample[1].left = 0; - sample[1].right = 0; + int16_t sample[2] = {0, 0}; this->speaker_->play((uint8_t *) (&sample), sizeof(sample)); this->speaker_->finish(); this->set_state_(State::STOPPING); From 8a915dcbbed3af2e285dce91f32c03743310ca21 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Mar 2026 08:34:27 -1000 Subject: [PATCH 408/609] [core] Move device class strings to PROGMEM on ESP8266 (#14443) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/api/api_connection.cpp | 84 ++++++++++++++----- esphome/components/api/api_connection.h | 34 ++------ .../components/mqtt/mqtt_binary_sensor.cpp | 6 +- esphome/components/mqtt/mqtt_button.cpp | 6 -- esphome/components/mqtt/mqtt_component.cpp | 5 ++ esphome/components/mqtt/mqtt_cover.cpp | 7 +- esphome/components/mqtt/mqtt_event.cpp | 7 -- esphome/components/mqtt/mqtt_number.cpp | 4 - esphome/components/mqtt/mqtt_sensor.cpp | 5 -- esphome/components/mqtt/mqtt_text_sensor.cpp | 6 -- esphome/components/mqtt/mqtt_valve.cpp | 7 +- esphome/components/web_server/web_server.cpp | 3 +- esphome/core/config.py | 6 ++ esphome/core/entity_base.cpp | 37 +++++++- esphome/core/entity_base.h | 33 ++++++-- esphome/core/entity_helpers.py | 8 +- tests/unit_tests/core/test_entity_helpers.py | 17 ++++ 17 files changed, 167 insertions(+), 108 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 98ba1abe0b..77920432c0 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -396,6 +396,48 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess return static_cast<uint16_t>(header_padding + calculated_size + footer_size); } +uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, + uint8_t message_type, APIConnection *conn, + uint32_t remaining_size) { + // Set common fields that are shared by all entity types + msg.key = entity->get_object_id_hash(); + + // API 1.14+ clients compute object_id client-side from the entity name + // For older clients, we must send object_id for backward compatibility + // See: https://github.com/esphome/backlog/issues/76 + // TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then + // Buffer must remain in scope until encode_message_to_buffer is called + char object_id_buf[OBJECT_ID_MAX_LEN]; + if (!conn->client_supports_api_version(1, 14)) { + msg.object_id = entity->get_object_id_to(object_id_buf); + } + + if (entity->has_own_name()) { + msg.name = entity->get_name(); + } + + // Set common EntityBase properties +#ifdef USE_ENTITY_ICON + char icon_buf[MAX_ICON_LENGTH]; + msg.icon = StringRef(entity->get_icon_to(icon_buf)); +#endif + msg.disabled_by_default = entity->is_disabled_by_default(); + msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category()); +#ifdef USE_DEVICES + msg.device_id = entity->get_device_id(); +#endif + return encode_message_to_buffer(msg, message_type, conn, remaining_size); +} + +uint16_t APIConnection::fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg, + StringRef &device_class_field, + uint8_t message_type, APIConnection *conn, + uint32_t remaining_size) { + char dc_buf[MAX_DEVICE_CLASS_LENGTH]; + device_class_field = StringRef(entity->get_device_class_to(dc_buf)); + return fill_and_encode_entity_info(entity, msg, message_type, conn, remaining_size); +} + #ifdef USE_BINARY_SENSOR bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor) { return this->send_message_smart_(binary_sensor, BinarySensorStateResponse::MESSAGE_TYPE, @@ -414,10 +456,9 @@ uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConn uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *binary_sensor = static_cast<binary_sensor::BinarySensor *>(entity); ListEntitiesBinarySensorResponse msg; - msg.device_class = binary_sensor->get_device_class_ref(); msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); - return fill_and_encode_entity_info(binary_sensor, msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, - remaining_size); + return fill_and_encode_entity_info_with_device_class( + binary_sensor, msg, msg.device_class, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, remaining_size); } #endif @@ -443,8 +484,8 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c msg.supports_position = traits.get_supports_position(); msg.supports_tilt = traits.get_supports_tilt(); msg.supports_stop = traits.get_supports_stop(); - msg.device_class = cover->get_device_class_ref(); - return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(cover, msg, msg.device_class, + ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) @@ -609,9 +650,9 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection * msg.unit_of_measurement = sensor->get_unit_of_measurement_ref(); msg.accuracy_decimals = sensor->get_accuracy_decimals(); msg.force_update = sensor->get_force_update(); - msg.device_class = sensor->get_device_class_ref(); msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class()); - return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(sensor, msg, msg.device_class, + ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size); } #endif @@ -631,8 +672,8 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection * auto *a_switch = static_cast<switch_::Switch *>(entity); ListEntitiesSwitchResponse msg; msg.assumed_state = a_switch->assumed_state(); - msg.device_class = a_switch->get_device_class_ref(); - return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(a_switch, msg, msg.device_class, + ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) { ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch) @@ -661,9 +702,8 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity); ListEntitiesTextSensorResponse msg; - msg.device_class = text_sensor->get_device_class_ref(); - return fill_and_encode_entity_info(text_sensor, msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn, - remaining_size); + return fill_and_encode_entity_info_with_device_class( + text_sensor, msg, msg.device_class, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn, remaining_size); } #endif @@ -776,11 +816,11 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection * ListEntitiesNumberResponse msg; msg.unit_of_measurement = number->get_unit_of_measurement_ref(); msg.mode = static_cast<enums::NumberMode>(number->traits.get_mode()); - msg.device_class = number->get_device_class_ref(); msg.min_value = number->traits.get_min_value(); msg.max_value = number->traits.get_max_value(); msg.step = number->traits.get_step(); - return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(number, msg, msg.device_class, + ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::on_number_command_request(const NumberCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(number::Number, number, number) @@ -925,8 +965,8 @@ void APIConnection::on_select_command_request(const SelectCommandRequest &msg) { uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *button = static_cast<button::Button *>(entity); ListEntitiesButtonResponse msg; - msg.device_class = button->get_device_class_ref(); - return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(button, msg, msg.device_class, + ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size); } void esphome::api::APIConnection::on_button_command_request(const ButtonCommandRequest &msg) { ENTITY_COMMAND_GET(button::Button, button, button) @@ -986,11 +1026,11 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c auto *valve = static_cast<valve::Valve *>(entity); ListEntitiesValveResponse msg; auto traits = valve->get_traits(); - msg.device_class = valve->get_device_class_ref(); msg.assumed_state = traits.get_is_assumed_state(); msg.supports_position = traits.get_supports_position(); msg.supports_stop = traits.get_supports_stop(); - return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(valve, msg, msg.device_class, + ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::on_valve_command_request(const ValveCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve) @@ -1434,9 +1474,9 @@ uint16_t APIConnection::try_send_event_response(event::Event *event, StringRef e uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *event = static_cast<event::Event *>(entity); ListEntitiesEventResponse msg; - msg.device_class = event->get_device_class_ref(); msg.event_types = &event->get_event_types(); - return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(event, msg, msg.device_class, + ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size); } #endif @@ -1492,8 +1532,8 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *update = static_cast<update::UpdateEntity *>(entity); ListEntitiesUpdateResponse msg; - msg.device_class = update->get_device_class_ref(); - return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(update, msg, msg.device_class, + ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::on_update_command_request(const UpdateCommandRequest &msg) { ENTITY_COMMAND_GET(update::UpdateEntity, update, update) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 88f0ef82d6..2c66a194a6 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -334,36 +334,12 @@ class APIConnection final : public APIServerConnectionBase { // Helper to fill entity info base and encode message static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, uint8_t message_type, - APIConnection *conn, uint32_t remaining_size) { - // Set common fields that are shared by all entity types - msg.key = entity->get_object_id_hash(); + APIConnection *conn, uint32_t remaining_size); - // API 1.14+ clients compute object_id client-side from the entity name - // For older clients, we must send object_id for backward compatibility - // See: https://github.com/esphome/backlog/issues/76 - // TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then - // Buffer must remain in scope until encode_message_to_buffer is called - char object_id_buf[OBJECT_ID_MAX_LEN]; - if (!conn->client_supports_api_version(1, 14)) { - msg.object_id = entity->get_object_id_to(object_id_buf); - } - - if (entity->has_own_name()) { - msg.name = entity->get_name(); - } - - // Set common EntityBase properties -#ifdef USE_ENTITY_ICON - char icon_buf[MAX_ICON_LENGTH]; - msg.icon = StringRef(entity->get_icon_to(icon_buf)); -#endif - msg.disabled_by_default = entity->is_disabled_by_default(); - msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category()); -#ifdef USE_DEVICES - msg.device_id = entity->get_device_id(); -#endif - return encode_message_to_buffer(msg, message_type, conn, remaining_size); - } + // Wrapper for entity types that have a device_class field + static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg, + StringRef &device_class_field, uint8_t message_type, + APIConnection *conn, uint32_t remaining_size); #ifdef USE_VOICE_ASSISTANT // Helper to check voice assistant validity and connection ownership diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 75995f61e0..ebb29db44f 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -30,15 +30,11 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto device_class = this->binary_sensor_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) if (this->binary_sensor_->is_status_binary_sensor()) root[MQTT_PAYLOAD_ON] = mqtt::global_mqtt_client->get_availability().payload_available; if (this->binary_sensor_->is_status_binary_sensor()) root[MQTT_PAYLOAD_OFF] = mqtt::global_mqtt_client->get_availability().payload_not_available; + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) config.command_topic = false; } bool MQTTBinarySensorComponent::send_initial_state() { diff --git a/esphome/components/mqtt/mqtt_button.cpp b/esphome/components/mqtt/mqtt_button.cpp index 718fe93016..7e0ae7d06e 100644 --- a/esphome/components/mqtt/mqtt_button.cpp +++ b/esphome/components/mqtt/mqtt_button.cpp @@ -30,13 +30,7 @@ void MQTTButtonComponent::dump_config() { } void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson config.state_topic = false; - const auto device_class = this->button_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } MQTT_COMPONENT_TYPE(MQTTButtonComponent, "button") diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index d31a78b090..afc514609c 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -214,6 +214,11 @@ bool MQTTComponent::send_discovery_() { if (icon[0] != '\0') { root[MQTT_ICON] = icon; } + char dc_buf[MAX_DEVICE_CLASS_LENGTH]; + const char *dc = this->get_entity()->get_device_class_to(dc_buf); + if (dc[0] != '\0') { + root[MQTT_DEVICE_CLASS] = dc; + } const auto entity_category = this->get_entity()->get_entity_category(); if (entity_category != ENTITY_CATEGORY_NONE) { diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 9752004094..ddb4b2d69d 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -91,12 +91,6 @@ void MQTTCoverComponent::dump_config() { } void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto device_class = this->cover_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) - auto traits = this->cover_->get_traits(); if (traits.get_is_assumed_state()) { root[MQTT_OPTIMISTIC] = true; @@ -129,6 +123,7 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic_to(topic_buf); } } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) if (traits.get_supports_tilt() && !traits.get_supports_position()) { config.command_topic = false; } diff --git a/esphome/components/mqtt/mqtt_event.cpp b/esphome/components/mqtt/mqtt_event.cpp index 37d5c2551a..93ff6971b3 100644 --- a/esphome/components/mqtt/mqtt_event.cpp +++ b/esphome/components/mqtt/mqtt_event.cpp @@ -20,13 +20,6 @@ void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf for (const auto &event_type : this->event_->get_event_types()) event_types.add(event_type); - // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto device_class = this->event_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) - config.command_topic = false; } diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index a2734f2beb..b0bac8b3d7 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -57,10 +57,6 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_MODE] = NumberMqttModeStrings::get_progmem_str(static_cast<uint8_t>(mode), static_cast<uint8_t>(NUMBER_MODE_BOX)); } - const auto device_class = this->number_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) config.command_topic = true; diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index a7d311d194..c66465dd16 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -44,11 +44,6 @@ void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto device_class = this->sensor_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } - if (this->sensor_->has_accuracy_decimals()) { root[MQTT_SUGGESTED_DISPLAY_PRECISION] = this->sensor_->get_accuracy_decimals(); } diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index a6b9f90b68..3acd71b50d 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -14,12 +14,6 @@ using namespace esphome::text_sensor; MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {} void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto device_class = this->sensor_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) config.command_topic = false; } void MQTTTextSensor::setup() { diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index 2b9f02858b..b155a4c897 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -64,12 +64,6 @@ void MQTTValveComponent::dump_config() { } void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto device_class = this->valve_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) - auto traits = this->valve_->get_traits(); if (traits.get_is_assumed_state()) { root[MQTT_OPTIMISTIC] = true; @@ -78,6 +72,7 @@ void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf root[MQTT_POSITION_TOPIC] = this->get_position_state_topic(); root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic(); } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } MQTT_COMPONENT_TYPE(MQTTValveComponent, "valve") diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index bc90c88e57..5590e67b82 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -2137,7 +2137,8 @@ json::SerializationBuffer<> WebServer::event_json_(event::Event *obj, StringRef for (const char *event_type : obj->get_event_types()) { event_types.add(event_type); } - root[ESPHOME_F("device_class")] = obj->get_device_class_ref(); + char dc_buf[MAX_DEVICE_CLASS_LENGTH]; + root[ESPHOME_F("device_class")] = obj->get_device_class_to(dc_buf); this->add_sorting_info_(root, obj); } diff --git a/esphome/core/config.py b/esphome/core/config.py index 8631726a02..d4a839cb79 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -223,6 +223,12 @@ else: # Keep in sync with ESPHOME_FRIENDLY_NAME_MAX_LEN in esphome/core/entity_base.h FRIENDLY_NAME_MAX_LEN = 120 +# Max device class string length (47 chars + null = 48-byte PROGMEM buffer) +# Keep in sync with MAX_DEVICE_CLASS_LENGTH in esphome/core/entity_base.h: +# DEVICE_CLASS_MAX_LENGTH == MAX_DEVICE_CLASS_LENGTH - 1 (C++ includes the null) +DEVICE_CLASS_MAX_LENGTH = 47 + + # Max icon string length (63 chars + null = 64-byte PROGMEM buffer) # Keep in sync with MAX_ICON_LENGTH in esphome/core/entity_base.h ICON_MAX_LENGTH = 63 diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 37e7fcc998..5c4e1c4445 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -51,7 +51,27 @@ __attribute__((weak)) const char *entity_device_class_lookup(uint8_t) { return " __attribute__((weak)) const char *entity_uom_lookup(uint8_t) { return ""; } __attribute__((weak)) const char *entity_icon_lookup(uint8_t) { return ""; } -// Entity device class (from index) +// Entity device class — buffer-based API for PROGMEM safety on ESP8266 +const char *EntityBase::get_device_class_to([[maybe_unused]] std::span<char, MAX_DEVICE_CLASS_LENGTH> buffer) const { +#ifdef USE_ENTITY_DEVICE_CLASS + const uint8_t idx = this->device_class_idx_; +#else + const uint8_t idx = 0; +#endif +#ifdef USE_ESP8266 + if (idx == 0) + return ""; + const char *dc = entity_device_class_lookup(idx); + ESPHOME_strncpy_P(buffer.data(), dc, buffer.size() - 1); + buffer[buffer.size() - 1] = '\0'; + return buffer.data(); +#else + return entity_device_class_lookup(idx); +#endif +} + +#ifndef USE_ESP8266 +// Deprecated device class accessors — not available on ESP8266 (rodata is RAM) StringRef EntityBase::get_device_class_ref() const { #ifdef USE_ENTITY_DEVICE_CLASS return StringRef(entity_device_class_lookup(this->device_class_idx_)); @@ -59,7 +79,14 @@ StringRef EntityBase::get_device_class_ref() const { return StringRef(entity_device_class_lookup(0)); #endif } -std::string EntityBase::get_device_class() const { return std::string(this->get_device_class_ref().c_str()); } +std::string EntityBase::get_device_class() const { +#ifdef USE_ENTITY_DEVICE_CLASS + return std::string(entity_device_class_lookup(this->device_class_idx_)); +#else + return std::string(entity_device_class_lookup(0)); +#endif +} +#endif // !USE_ESP8266 // Entity unit of measurement (from index) StringRef EntityBase::get_unit_of_measurement_ref() const { @@ -191,8 +218,10 @@ void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) #endif void log_entity_device_class(const char *tag, const char *prefix, const EntityBase &obj) { - if (!obj.get_device_class_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj.get_device_class_ref().c_str()); + char dc_buf[MAX_DEVICE_CLASS_LENGTH]; + const char *dc = obj.get_device_class_to(dc_buf); + if (dc[0] != '\0') { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, dc); } } diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 1ce1e658e0..20eb68b67a 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -36,6 +36,11 @@ static constexpr size_t OBJECT_ID_MAX_LEN = 128; // Maximum state length that Home Assistant will accept without raising ValueError static constexpr size_t MAX_STATE_LEN = 255; +// Maximum device class string buffer size (47 chars + null terminator) +// Longest standard device class: "volatile_organic_compounds_parts" (32 chars) +// Device classes are stored in PROGMEM; on ESP8266 they must be copied to a stack buffer. +static constexpr size_t MAX_DEVICE_CLASS_LENGTH = 48; + // Maximum icon string buffer size (63 chars + null terminator) // Icons are stored in PROGMEM; on ESP8266 they must be copied to a stack buffer. static constexpr size_t MAX_ICON_LENGTH = 64; @@ -113,13 +118,31 @@ class EntityBase { #endif } - // Get device class as StringRef (from packed index) + // Get this entity's device class into a stack buffer. + // On non-ESP8266: returns pointer to PROGMEM string directly (buffer unused). + // On ESP8266: copies from PROGMEM to buffer, returns buffer pointer. + const char *get_device_class_to(std::span<char, MAX_DEVICE_CLASS_LENGTH> buffer) const; + +#ifdef USE_ESP8266 + // On ESP8266, rodata is RAM. Device classes are in PROGMEM and cannot be accessed + // directly as const char*. Use get_device_class_to() with a stack buffer instead. + template<typename T = int> StringRef get_device_class_ref() const { + static_assert(sizeof(T) == 0, "get_device_class_ref() unavailable on ESP8266 (rodata is RAM). " + "Use get_device_class_to() with a stack buffer."); + return StringRef(""); + } + template<typename T = int> std::string get_device_class() const { + static_assert(sizeof(T) == 0, "get_device_class() unavailable on ESP8266 (rodata is RAM). " + "Use get_device_class_to() with a stack buffer."); + return ""; + } +#else + // Deprecated: use get_device_class_to() instead. Device classes are in PROGMEM. + ESPDEPRECATED("Use get_device_class_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0") StringRef get_device_class_ref() const; - /// Get the device class as std::string (deprecated, prefer get_device_class_ref()) - ESPDEPRECATED("Use get_device_class_ref() instead for better performance (avoids string copy). Will be removed in " - "ESPHome 2026.9.0", - "2026.3.0") + ESPDEPRECATED("Use get_device_class_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0") std::string get_device_class() const; +#endif // Get unit of measurement as StringRef (from packed index) StringRef get_unit_of_measurement_ref() const; /// Get the unit of measurement as std::string (deprecated, prefer get_unit_of_measurement_ref()) diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 01fa27b833..a46d2466fd 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -17,7 +17,7 @@ from esphome.const import ( CONF_UNIT_OF_MEASUREMENT, ) from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority -from esphome.core.config import ICON_MAX_LENGTH +from esphome.core.config import DEVICE_CLASS_MAX_LENGTH, ICON_MAX_LENGTH from esphome.cpp_generator import MockObj, RawStatement, add, get_variable import esphome.final_validate as fv from esphome.helpers import cpp_string_escape, fnv1_hash_object_id, sanitize, snake_case @@ -132,7 +132,7 @@ def _generate_category_code( _CATEGORY_CONFIGS = ( - ("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes", False), + ("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes", True), ("ENTITY_UOM_TABLE", "entity_uom_lookup", "units", False), ("ENTITY_ICON_TABLE", "entity_icon_lookup", "icons", True), ) @@ -179,6 +179,10 @@ def _register_string( def register_device_class(value: str) -> int: """Register a device_class string and return its 1-based index.""" + if value and len(value) > DEVICE_CLASS_MAX_LENGTH: + raise ValueError( + f"Device class string too long ({len(value)} chars, max {DEVICE_CLASS_MAX_LENGTH}): '{value}'" + ) return _register_string( value, _get_pool().device_classes, _MAX_DEVICE_CLASSES, "device_class" ) diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index 79bc3095b9..1392a1d043 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -23,6 +23,7 @@ from esphome.core.entity_helpers import ( _setup_entity_impl, entity_duplicate_validator, get_base_entity_object_id, + register_device_class, register_icon, setup_entity, ) @@ -926,6 +927,22 @@ def test_register_icon_max_length() -> None: assert register_icon("") == 0 +def test_register_device_class_max_length() -> None: + """Test register_device_class rejects device classes exceeding 47 characters.""" + # 47 chars should succeed + max_dc = "a" * 47 + idx = register_device_class(max_dc) + assert idx > 0 + + # 48 chars should fail + too_long = "a" * 48 + with pytest.raises(ValueError, match="Device class string too long"): + register_device_class(too_long) + + # Empty string returns 0 + assert register_device_class("") == 0 + + @pytest.mark.asyncio async def test_setup_entity_with_entity_category( setup_test_environment: list[str], From 9654140c00fecc7c86c554c7d733436c2505d2b5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:00:46 -0500 Subject: [PATCH 409/609] [tm1638][rp2040_pio_led_strip][atm90e32] Fix bounds checks and off-by-one (#14559) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/atm90e32/atm90e32.cpp | 4 +-- .../rp2040_pio_led_strip/led_strip.cpp | 2 +- esphome/components/tm1638/tm1638.cpp | 29 ++++++++++--------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index 412964d0f8..ee7fe5ce75 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -619,7 +619,7 @@ void ATM90E32Component::run_gain_calibrations() { ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: measured voltage is 0.", cs, phase_labels[phase]); } else { - uint32_t new_voltage_gain = static_cast<uint16_t>((ref_voltage / measured_voltage) * current_voltage_gain); + uint32_t new_voltage_gain = static_cast<uint32_t>((ref_voltage / measured_voltage) * current_voltage_gain); if (new_voltage_gain == 0) { ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", cs, phase_labels[phase]); @@ -644,7 +644,7 @@ void ATM90E32Component::run_gain_calibrations() { ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: measured current is 0.", cs, phase_labels[phase]); } else { - uint32_t new_current_gain = static_cast<uint16_t>((ref_current / measured_current) * current_current_gain); + uint32_t new_current_gain = static_cast<uint32_t>((ref_current / measured_current) * current_current_gain); if (new_current_gain == 0) { ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain would be 0. Check reference and measured current.", cs, phase_labels[phase]); diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.cpp b/esphome/components/rp2040_pio_led_strip/led_strip.cpp index dc0d3c315a..fdb49fb3ef 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.cpp +++ b/esphome/components/rp2040_pio_led_strip/led_strip.cpp @@ -70,7 +70,7 @@ void RP2040PIOLEDStripLightOutput::setup() { // but there are only 4 state machines on each PIO so we can only have 4 strips per PIO uint offset = 0; - if (RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1] > 4) { + if (RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1] >= 4) { ESP_LOGE(TAG, "Too many instances of PIO program"); this->mark_failed(); return; diff --git a/esphome/components/tm1638/tm1638.cpp b/esphome/components/tm1638/tm1638.cpp index 8ef546ff32..c67ff1adbc 100644 --- a/esphome/components/tm1638/tm1638.cpp +++ b/esphome/components/tm1638/tm1638.cpp @@ -147,35 +147,38 @@ void TM1638Component::set_intensity(uint8_t brightness_level) { uint8_t TM1638Component::print(uint8_t start_pos, const char *str) { uint8_t pos = start_pos; - bool last_was_dot = false; for (; *str != '\0'; str++) { uint8_t data = TM1638_UNKNOWN_CHAR; if (*str >= ' ' && *str <= '~') { - data = progmem_read_byte(&TM1638Translation::SEVEN_SEG[*str - 32]); // subract 32 to account for ASCII offset - } else if (data == TM1638_UNKNOWN_CHAR) { + // Subtract 32 to account for ASCII offset + data = progmem_read_byte(&TM1638Translation::SEVEN_SEG[*str - 32]); + } else { ESP_LOGW(TAG, "Encountered character '%c' with no TM1638 representation while translating string!", *str); } - if (*str == '.') // handle dots - { - if (pos != start_pos && - !last_was_dot) // if we are not at the first position, backup by one unless last char was a dot - { + if (*str == '.') { + // Merge dot onto previous character unless we're at the start or last was also a dot + if (pos != start_pos && !last_was_dot) { pos--; } - this->buffer_[pos] |= 0b10000000; // turn on the dot on the previous position - last_was_dot = true; // set a bit in case the next chracter is also a dot - } else // if not a dot, then just write the character to display - { + if (pos >= 8) { + ESP_LOGI(TAG, "TM1638 String is too long for the display!"); + break; + } + // Turn on the dot on the previous position + this->buffer_[pos] |= 0b10000000; + last_was_dot = true; + } else { + // Not a dot, write the character to display if (pos >= 8) { ESP_LOGI(TAG, "TM1638 String is too long for the display!"); break; } this->buffer_[pos] = data; - last_was_dot = false; // clear dot tracking bit + last_was_dot = false; } pos++; From 42dbb51022a61e07b26d82b6d348f470d2afa72f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Mar 2026 09:03:54 -1000 Subject: [PATCH 410/609] [api] Devirtualize protobuf encode/calculate_size (#14449) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/api/api_connection.cpp | 274 ++-- esphome/components/api/api_connection.h | 94 +- esphome/components/api/api_pb2.cpp | 1436 ++++++++++------- esphome/components/api/api_pb2.h | 352 ++-- esphome/components/api/api_pb2_service.h | 8 - esphome/components/api/api_server.cpp | 8 +- esphome/components/api/api_server.h | 2 +- esphome/components/api/list_entities.cpp | 2 +- esphome/components/api/proto.h | 421 +---- .../bluetooth_proxy/bluetooth_connection.cpp | 21 +- .../bluetooth_proxy/bluetooth_proxy.cpp | 18 +- .../voice_assistant/voice_assistant.cpp | 11 +- .../components/zwave_proxy/zwave_proxy.cpp | 6 +- script/api_protobuf/api_protobuf.py | 107 +- 14 files changed, 1373 insertions(+), 1387 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 77920432c0..8721072e49 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -275,7 +275,7 @@ void APIConnection::check_keepalive_(uint32_t now) { // Only send ping if we're not disconnecting ESP_LOGVV(TAG, "Sending keepalive PING"); PingRequest req; - this->flags_.sent_ping = this->send_message(req, PingRequest::MESSAGE_TYPE); + this->flags_.sent_ping = this->send_message(req); if (!this->flags_.sent_ping) { // If we can't send the ping request directly (tx_buffer full), // schedule it at the front of the batch so it will be sent with priority @@ -336,7 +336,7 @@ bool APIConnection::send_disconnect_response_() { this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("disconnected")); this->flags_.next_close = true; DisconnectResponse resp; - return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); + return this->send_message(resp); } void APIConnection::on_disconnect_response() { // Don't close socket here, let APIServer::loop() do it @@ -344,61 +344,19 @@ void APIConnection::on_disconnect_response() { this->flags_.remove = true; } -// Encodes a message to the buffer and returns the total number of bytes used, -// including header and footer overhead. Returns 0 if the message doesn't fit. -uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, - uint32_t remaining_size) { -#ifdef HAS_PROTO_MESSAGE_DUMP - // If in log-only mode, just log and return - if (conn->flags_.log_only_mode) { - DumpBuffer dump_buf; - conn->log_send_message_(msg.message_name(), msg.dump_to(dump_buf)); - return 1; // Return non-zero to indicate "success" for logging - } +uint16_t APIConnection::fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, + CalculateSizeFn size_fn, MessageEncodeFn encode_fn, + APIConnection *conn, uint32_t remaining_size) { + msg.key = entity->get_object_id_hash(); +#ifdef USE_DEVICES + msg.device_id = entity->get_device_id(); #endif - - // Calculate size - uint32_t calculated_size = msg.calculated_size(); - - // Cache frame sizes to avoid repeated virtual calls - const uint8_t header_padding = conn->helper_->frame_header_padding(); - const uint8_t footer_size = conn->helper_->frame_footer_size(); - - // Calculate total size with padding for buffer allocation - size_t total_calculated_size = calculated_size + header_padding + footer_size; - - // Check if it fits - if (total_calculated_size > remaining_size) { - return 0; // Doesn't fit - } - - // Get buffer size after allocation (which includes header padding) - std::vector<uint8_t> &shared_buf = conn->parent_->get_shared_buffer_ref(); - - if (conn->flags_.batch_first_message) { - // First message - buffer already prepared by caller, just clear flag - conn->flags_.batch_first_message = false; - } else { - // Batch message second or later - // Add padding for previous message footer + this message header - size_t current_size = shared_buf.size(); - shared_buf.reserve(current_size + total_calculated_size); - shared_buf.resize(current_size + footer_size + header_padding); - } - - // Pre-resize buffer to include payload, then encode through raw pointer - size_t write_start = shared_buf.size(); - shared_buf.resize(write_start + calculated_size); - ProtoWriteBuffer buffer{&shared_buf, write_start}; - msg.encode(buffer); - - // Return total size (header + payload + footer) - return static_cast<uint16_t>(header_padding + calculated_size + footer_size); + return encode_to_buffer(size_fn(&msg), encode_fn, &msg, conn, remaining_size); } uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, - uint8_t message_type, APIConnection *conn, - uint32_t remaining_size) { + CalculateSizeFn size_fn, MessageEncodeFn encode_fn, + APIConnection *conn, uint32_t remaining_size) { // Set common fields that are shared by all entity types msg.key = entity->get_object_id_hash(); @@ -406,7 +364,7 @@ uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResp // For older clients, we must send object_id for backward compatibility // See: https://github.com/esphome/backlog/issues/76 // TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then - // Buffer must remain in scope until encode_message_to_buffer is called + // Buffer must remain in scope until encode_to_buffer is called char object_id_buf[OBJECT_ID_MAX_LEN]; if (!conn->client_supports_api_version(1, 14)) { msg.object_id = entity->get_object_id_to(object_id_buf); @@ -426,16 +384,17 @@ uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResp #ifdef USE_DEVICES msg.device_id = entity->get_device_id(); #endif - return encode_message_to_buffer(msg, message_type, conn, remaining_size); + return encode_to_buffer(size_fn(&msg), encode_fn, &msg, conn, remaining_size); } uint16_t APIConnection::fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg, StringRef &device_class_field, - uint8_t message_type, APIConnection *conn, + CalculateSizeFn size_fn, + MessageEncodeFn encode_fn, APIConnection *conn, uint32_t remaining_size) { char dc_buf[MAX_DEVICE_CLASS_LENGTH]; device_class_field = StringRef(entity->get_device_class_to(dc_buf)); - return fill_and_encode_entity_info(entity, msg, message_type, conn, remaining_size); + return fill_and_encode_entity_info(entity, msg, size_fn, encode_fn, conn, remaining_size); } #ifdef USE_BINARY_SENSOR @@ -449,16 +408,14 @@ uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConn BinarySensorStateResponse resp; resp.state = binary_sensor->state; resp.missing_state = !binary_sensor->has_state(); - return fill_and_encode_entity_state(binary_sensor, resp, BinarySensorStateResponse::MESSAGE_TYPE, conn, - remaining_size); + return fill_and_encode_entity_state(binary_sensor, resp, conn, remaining_size); } uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *binary_sensor = static_cast<binary_sensor::BinarySensor *>(entity); ListEntitiesBinarySensorResponse msg; msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); - return fill_and_encode_entity_info_with_device_class( - binary_sensor, msg, msg.device_class, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(binary_sensor, msg, msg.device_class, conn, remaining_size); } #endif @@ -474,7 +431,7 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection * if (traits.get_supports_tilt()) msg.tilt = cover->tilt; msg.current_operation = static_cast<enums::CoverOperation>(cover->current_operation); - return fill_and_encode_entity_state(cover, msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(cover, msg, conn, remaining_size); } uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *cover = static_cast<cover::Cover *>(entity); @@ -484,8 +441,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c msg.supports_position = traits.get_supports_position(); msg.supports_tilt = traits.get_supports_tilt(); msg.supports_stop = traits.get_supports_stop(); - return fill_and_encode_entity_info_with_device_class(cover, msg, msg.device_class, - ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(cover, msg, msg.device_class, conn, remaining_size); } void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) @@ -517,7 +473,7 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co msg.direction = static_cast<enums::FanDirection>(fan->direction); if (traits.supports_preset_modes() && fan->has_preset_mode()) msg.preset_mode = fan->get_preset_mode(); - return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(fan, msg, conn, remaining_size); } uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *fan = static_cast<fan::Fan *>(entity); @@ -528,7 +484,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con msg.supports_direction = traits.supports_direction(); msg.supported_speed_count = traits.supported_speed_count(); msg.supported_preset_modes = &traits.supported_preset_modes(); - return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(fan, msg, conn, remaining_size); } void APIConnection::on_fan_command_request(const FanCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan) @@ -571,7 +527,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection * if (light->supports_effects()) { resp.effect = light->get_effect_name(); } - return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(light, resp, conn, remaining_size); } uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *light = static_cast<light::LightState *>(entity); @@ -596,7 +552,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c } } msg.effects = &effects_list; - return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(light, msg, conn, remaining_size); } void APIConnection::on_light_command_request(const LightCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light) @@ -641,7 +597,7 @@ uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection SensorStateResponse resp; resp.state = sensor->state; resp.missing_state = !sensor->has_state(); - return fill_and_encode_entity_state(sensor, resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(sensor, resp, conn, remaining_size); } uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { @@ -651,8 +607,7 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection * msg.accuracy_decimals = sensor->get_accuracy_decimals(); msg.force_update = sensor->get_force_update(); msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class()); - return fill_and_encode_entity_info_with_device_class(sensor, msg, msg.device_class, - ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(sensor, msg, msg.device_class, conn, remaining_size); } #endif @@ -665,15 +620,14 @@ uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection auto *a_switch = static_cast<switch_::Switch *>(entity); SwitchStateResponse resp; resp.state = a_switch->state; - return fill_and_encode_entity_state(a_switch, resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(a_switch, resp, conn, remaining_size); } uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *a_switch = static_cast<switch_::Switch *>(entity); ListEntitiesSwitchResponse msg; msg.assumed_state = a_switch->assumed_state(); - return fill_and_encode_entity_info_with_device_class(a_switch, msg, msg.device_class, - ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(a_switch, msg, msg.device_class, conn, remaining_size); } void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) { ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch) @@ -697,13 +651,12 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec TextSensorStateResponse resp; resp.state = StringRef(text_sensor->state); resp.missing_state = !text_sensor->has_state(); - return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(text_sensor, resp, conn, remaining_size); } uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity); ListEntitiesTextSensorResponse msg; - return fill_and_encode_entity_info_with_device_class( - text_sensor, msg, msg.device_class, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(text_sensor, msg, msg.device_class, conn, remaining_size); } #endif @@ -743,7 +696,7 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection resp.current_humidity = climate->current_humidity; if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) resp.target_humidity = climate->target_humidity; - return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(climate, resp, conn, remaining_size); } uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *climate = static_cast<climate::Climate *>(entity); @@ -770,7 +723,7 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection msg.supported_presets = &traits.get_supported_presets(); msg.supported_custom_presets = &traits.get_supported_custom_presets(); msg.supported_swing_modes = &traits.get_supported_swing_modes(); - return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(climate, msg, conn, remaining_size); } void APIConnection::on_climate_command_request(const ClimateCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate) @@ -808,7 +761,7 @@ uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection NumberStateResponse resp; resp.state = number->state; resp.missing_state = !number->has_state(); - return fill_and_encode_entity_state(number, resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(number, resp, conn, remaining_size); } uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { @@ -819,8 +772,7 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection * msg.min_value = number->traits.get_min_value(); msg.max_value = number->traits.get_max_value(); msg.step = number->traits.get_step(); - return fill_and_encode_entity_info_with_device_class(number, msg, msg.device_class, - ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(number, msg, msg.device_class, conn, remaining_size); } void APIConnection::on_number_command_request(const NumberCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(number::Number, number, number) @@ -840,12 +792,12 @@ uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *c resp.year = date->year; resp.month = date->month; resp.day = date->day; - return fill_and_encode_entity_state(date, resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(date, resp, conn, remaining_size); } uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *date = static_cast<datetime::DateEntity *>(entity); ListEntitiesDateResponse msg; - return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(date, msg, conn, remaining_size); } void APIConnection::on_date_command_request(const DateCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date) @@ -865,12 +817,12 @@ uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *c resp.hour = time->hour; resp.minute = time->minute; resp.second = time->second; - return fill_and_encode_entity_state(time, resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(time, resp, conn, remaining_size); } uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *time = static_cast<datetime::TimeEntity *>(entity); ListEntitiesTimeResponse msg; - return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(time, msg, conn, remaining_size); } void APIConnection::on_time_command_request(const TimeCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time) @@ -892,12 +844,12 @@ uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnectio ESPTime state = datetime->state_as_esptime(); resp.epoch_seconds = state.timestamp; } - return fill_and_encode_entity_state(datetime, resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(datetime, resp, conn, remaining_size); } uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *datetime = static_cast<datetime::DateTimeEntity *>(entity); ListEntitiesDateTimeResponse msg; - return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(datetime, msg, conn, remaining_size); } void APIConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime) @@ -916,7 +868,7 @@ uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *c TextStateResponse resp; resp.state = StringRef(text->state); resp.missing_state = !text->has_state(); - return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(text, resp, conn, remaining_size); } uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { @@ -926,7 +878,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co msg.min_length = text->traits.get_min_length(); msg.max_length = text->traits.get_max_length(); msg.pattern = text->traits.get_pattern_ref(); - return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(text, msg, conn, remaining_size); } void APIConnection::on_text_command_request(const TextCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(text::Text, text, text) @@ -945,14 +897,14 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection SelectStateResponse resp; resp.state = select->current_option(); resp.missing_state = !select->has_state(); - return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(select, resp, conn, remaining_size); } uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *select = static_cast<select::Select *>(entity); ListEntitiesSelectResponse msg; msg.options = &select->traits.get_options(); - return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(select, msg, conn, remaining_size); } void APIConnection::on_select_command_request(const SelectCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(select::Select, select, select) @@ -965,8 +917,7 @@ void APIConnection::on_select_command_request(const SelectCommandRequest &msg) { uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *button = static_cast<button::Button *>(entity); ListEntitiesButtonResponse msg; - return fill_and_encode_entity_info_with_device_class(button, msg, msg.device_class, - ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(button, msg, msg.device_class, conn, remaining_size); } void esphome::api::APIConnection::on_button_command_request(const ButtonCommandRequest &msg) { ENTITY_COMMAND_GET(button::Button, button, button) @@ -983,7 +934,7 @@ uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *c auto *a_lock = static_cast<lock::Lock *>(entity); LockStateResponse resp; resp.state = static_cast<enums::LockState>(a_lock->state); - return fill_and_encode_entity_state(a_lock, resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(a_lock, resp, conn, remaining_size); } uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { @@ -992,7 +943,7 @@ uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *co msg.assumed_state = a_lock->traits.get_assumed_state(); msg.supports_open = a_lock->traits.get_supports_open(); msg.requires_code = a_lock->traits.get_requires_code(); - return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(a_lock, msg, conn, remaining_size); } void APIConnection::on_lock_command_request(const LockCommandRequest &msg) { ENTITY_COMMAND_GET(lock::Lock, a_lock, lock) @@ -1020,7 +971,7 @@ uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection * ValveStateResponse resp; resp.position = valve->position; resp.current_operation = static_cast<enums::ValveOperation>(valve->current_operation); - return fill_and_encode_entity_state(valve, resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(valve, resp, conn, remaining_size); } uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *valve = static_cast<valve::Valve *>(entity); @@ -1029,8 +980,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c msg.assumed_state = traits.get_is_assumed_state(); msg.supports_position = traits.get_supports_position(); msg.supports_stop = traits.get_supports_stop(); - return fill_and_encode_entity_info_with_device_class(valve, msg, msg.device_class, - ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(valve, msg, msg.device_class, conn, remaining_size); } void APIConnection::on_valve_command_request(const ValveCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve) @@ -1056,7 +1006,7 @@ uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConne resp.state = static_cast<enums::MediaPlayerState>(report_state); resp.volume = media_player->volume; resp.muted = media_player->is_muted(); - return fill_and_encode_entity_state(media_player, resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(media_player, resp, conn, remaining_size); } uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *media_player = static_cast<media_player::MediaPlayer *>(entity); @@ -1073,8 +1023,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose); media_format.sample_bytes = supported_format.sample_bytes; } - return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, - remaining_size); + return fill_and_encode_entity_info(media_player, msg, conn, remaining_size); } void APIConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player) @@ -1115,7 +1064,7 @@ void APIConnection::try_send_camera_image_() { msg.device_id = camera::Camera::instance()->get_device_id(); #endif - if (!this->send_message_impl(msg, CameraImageResponse::MESSAGE_TYPE)) { + if (!this->send_message(msg)) { return; // Send failed, try again later } this->image_reader_->consume_data(to_send); @@ -1141,7 +1090,7 @@ void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image) uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *camera = static_cast<camera::Camera *>(entity); ListEntitiesCameraResponse msg; - return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(camera, msg, conn, remaining_size); } void APIConnection::on_camera_image_request(const CameraImageRequest &msg) { if (camera::Camera::instance() == nullptr) @@ -1296,7 +1245,7 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno bool APIConnection::send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg) { VoiceAssistantConfigurationResponse resp; if (!this->check_voice_assistant_api_connection_()) { - return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE); + return this->send_message(resp); } auto &config = voice_assistant::global_voice_assistant->get_configuration(); @@ -1328,7 +1277,7 @@ bool APIConnection::send_voice_assistant_get_configuration_response_(const Voice resp.active_wake_words = &config.active_wake_words; resp.max_active_wake_words = config.max_active_wake_words; - return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE); + return this->send_message(resp); } void APIConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { if (!this->send_voice_assistant_get_configuration_response_(msg)) { @@ -1363,8 +1312,7 @@ uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, A auto *a_alarm_control_panel = static_cast<alarm_control_panel::AlarmControlPanel *>(entity); AlarmControlPanelStateResponse resp; resp.state = static_cast<enums::AlarmControlPanelState>(a_alarm_control_panel->get_state()); - return fill_and_encode_entity_state(a_alarm_control_panel, resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, - remaining_size); + return fill_and_encode_entity_state(a_alarm_control_panel, resp, conn, remaining_size); } uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { @@ -1373,8 +1321,7 @@ uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, AP msg.supported_features = a_alarm_control_panel->get_supported_features(); msg.requires_code = a_alarm_control_panel->get_requires_code(); msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm(); - return fill_and_encode_entity_info(a_alarm_control_panel, msg, ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE, - conn, remaining_size); + return fill_and_encode_entity_info(a_alarm_control_panel, msg, conn, remaining_size); } void APIConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel) @@ -1421,7 +1368,7 @@ uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConne resp.target_temperature_high = wh->get_target_temperature_high(); resp.state = wh->get_state(); - return fill_and_encode_entity_state(wh, resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(wh, resp, conn, remaining_size); } uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *wh = static_cast<water_heater::WaterHeater *>(entity); @@ -1432,7 +1379,7 @@ uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnec msg.target_temperature_step = traits.get_target_temperature_step(); msg.supported_modes = &traits.get_supported_modes(); msg.supported_features = traits.get_feature_flags(); - return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(wh, msg, conn, remaining_size); } void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) { @@ -1468,15 +1415,14 @@ uint16_t APIConnection::try_send_event_response(event::Event *event, StringRef e uint32_t remaining_size) { EventResponse resp; resp.event_type = event_type; - return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(event, resp, conn, remaining_size); } uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *event = static_cast<event::Event *>(entity); ListEntitiesEventResponse msg; msg.event_types = &event->get_event_types(); - return fill_and_encode_entity_info_with_device_class(event, msg, msg.device_class, - ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(event, msg, msg.device_class, conn, remaining_size); } #endif @@ -1493,9 +1439,7 @@ void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRF #endif } -void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) { - this->send_message(msg, InfraredRFReceiveEvent::MESSAGE_TYPE); -} +void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) { this->send_message(msg); } #endif #ifdef USE_INFRARED @@ -1503,7 +1447,7 @@ uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection auto *infrared = static_cast<infrared::Infrared *>(entity); ListEntitiesInfraredResponse msg; msg.capabilities = infrared->get_capability_flags(); - return fill_and_encode_entity_info(infrared, msg, ListEntitiesInfraredResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(infrared, msg, conn, remaining_size); } #endif @@ -1527,13 +1471,12 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection resp.release_summary = StringRef(update->update_info.summary); resp.release_url = StringRef(update->update_info.release_url); } - return fill_and_encode_entity_state(update, resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(update, resp, conn, remaining_size); } uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *update = static_cast<update::UpdateEntity *>(entity); ListEntitiesUpdateResponse msg; - return fill_and_encode_entity_info_with_device_class(update, msg, msg.device_class, - ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(update, msg, msg.device_class, conn, remaining_size); } void APIConnection::on_update_command_request(const UpdateCommandRequest &msg) { ENTITY_COMMAND_GET(update::UpdateEntity, update, update) @@ -1559,7 +1502,7 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char SubscribeLogsResponse msg; msg.level = static_cast<enums::LogLevel>(level); msg.set_message(reinterpret_cast<const uint8_t *>(line), message_len); - return this->send_message_impl(msg, SubscribeLogsResponse::MESSAGE_TYPE); + return this->send_message(msg); } void APIConnection::complete_authentication_() { @@ -1616,12 +1559,12 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) { // Auto-authenticate - password auth was removed in ESPHome 2026.1.0 this->complete_authentication_(); - return this->send_message(resp, HelloResponse::MESSAGE_TYPE); + return this->send_message(resp); } bool APIConnection::send_ping_response_() { PingResponse resp; - return this->send_message(resp, PingResponse::MESSAGE_TYPE); + return this->send_message(resp); } bool APIConnection::send_device_info_response_() { @@ -1745,7 +1688,7 @@ bool APIConnection::send_device_info_response_() { } #endif - return this->send_message(resp, DeviceInfoResponse::MESSAGE_TYPE); + return this->send_message(resp); } void APIConnection::on_hello_request(const HelloRequest &msg) { if (!this->send_hello_response_(msg)) { @@ -1845,7 +1788,7 @@ void APIConnection::send_execute_service_response(uint32_t call_id, bool success resp.call_id = call_id; resp.success = success; resp.error_message = error_message; - this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE); + this->send_message(resp); } #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON void APIConnection::send_execute_service_response(uint32_t call_id, bool success, StringRef error_message, @@ -1856,7 +1799,7 @@ void APIConnection::send_execute_service_response(uint32_t call_id, bool success resp.error_message = error_message; resp.response_data = response_data; resp.response_data_len = response_data_len; - this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE); + this->send_message(resp); } #endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON #endif // USE_API_USER_DEFINED_ACTION_RESPONSES @@ -1895,7 +1838,7 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio resp.success = true; } - return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE); + return this->send_message(resp); } void APIConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { if (!this->send_noise_encryption_set_key_response_(msg)) { @@ -1924,16 +1867,73 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { } return false; } -bool APIConnection::send_message_impl(const ProtoMessage &msg, uint8_t message_type) { - uint32_t payload_size = msg.calculated_size(); - std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref(); +bool APIConnection::send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, + const void *msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + // Skip dump for log messages (recursive logging risk) and camera frames (high-frequency noise) + if (message_type != SubscribeLogsResponse::MESSAGE_TYPE +#ifdef USE_CAMERA + && message_type != CameraImageResponse::MESSAGE_TYPE +#endif + ) { + auto *proto_msg = static_cast<const ProtoMessage *>(msg); + DumpBuffer dump_buf; + this->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf)); + } +#endif + auto &shared_buf = this->parent_->get_shared_buffer_ref(); this->prepare_first_message_buffer(shared_buf, payload_size); size_t write_start = shared_buf.size(); shared_buf.resize(write_start + payload_size); ProtoWriteBuffer buffer{&shared_buf, write_start}; - msg.encode(buffer); + encode_fn(msg, buffer); return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type); } +// Encodes a message to the buffer and returns the total number of bytes used, +// including header and footer overhead. Returns 0 if the message doesn't fit. +uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg, + APIConnection *conn, uint32_t remaining_size) { +#ifdef HAS_PROTO_MESSAGE_DUMP + if (conn->flags_.log_only_mode) { + auto *proto_msg = static_cast<const ProtoMessage *>(msg); + DumpBuffer dump_buf; + conn->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf)); + return 1; + } +#endif + // Cache frame sizes to avoid repeated virtual calls + const uint8_t header_padding = conn->helper_->frame_header_padding(); + const uint8_t footer_size = conn->helper_->frame_footer_size(); + + // Calculate total size with padding for buffer allocation + size_t total_calculated_size = calculated_size + header_padding + footer_size; + + // Check if it fits + if (total_calculated_size > remaining_size) + return 0; // Doesn't fit + + std::vector<uint8_t> &shared_buf = conn->parent_->get_shared_buffer_ref(); + + if (conn->flags_.batch_first_message) { + // First message - buffer already prepared by caller, just clear flag + conn->flags_.batch_first_message = false; + } else { + // Batch message second or later + // Add padding for previous message footer + this message header + size_t current_size = shared_buf.size(); + shared_buf.reserve(current_size + total_calculated_size); + shared_buf.resize(current_size + footer_size + header_padding); + } + + // Pre-resize buffer to include payload, then encode through raw pointer + size_t write_start = shared_buf.size(); + shared_buf.resize(write_start + calculated_size); + ProtoWriteBuffer buffer{&shared_buf, write_start}; + encode_fn(msg, buffer); + + // Return total size (header + payload + footer) + return static_cast<uint16_t>(header_padding + calculated_size + footer_size); +} bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE); @@ -2292,17 +2292,17 @@ uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { ListEntitiesDoneResponse resp; - return encode_message_to_buffer(resp, ListEntitiesDoneResponse::MESSAGE_TYPE, conn, remaining_size); + return encode_message_to_buffer(resp, conn, remaining_size); } uint16_t APIConnection::try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { DisconnectRequest req; - return encode_message_to_buffer(req, DisconnectRequest::MESSAGE_TYPE, conn, remaining_size); + return encode_message_to_buffer(req, conn, remaining_size); } uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { PingRequest req; - return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size); + return encode_message_to_buffer(req, conn, remaining_size); } #ifdef USE_API_HOMEASSISTANT_STATES @@ -2321,7 +2321,7 @@ void APIConnection::process_state_subscriptions_() { resp.attribute = it.attribute != nullptr ? StringRef(it.attribute) : StringRef(""); resp.once = it.once; - if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) { + if (this->send_message(resp)) { this->state_subs_at_++; } } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 2c66a194a6..54b6db6800 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -129,7 +129,7 @@ class APIConnection final : public APIServerConnectionBase { void send_homeassistant_action(const HomeassistantActionRequest &call) { if (!this->flags_.service_call_subscription) return; - this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE); + this->send_message(call); } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override; @@ -153,7 +153,7 @@ class APIConnection final : public APIServerConnectionBase { #ifdef USE_HOMEASSISTANT_TIME void send_time_request() { GetTimeRequest req; - this->send_message(req, GetTimeRequest::MESSAGE_TYPE); + this->send_message(req); } #endif @@ -263,7 +263,19 @@ class APIConnection final : public APIServerConnectionBase { void on_fatal_error() override; void on_no_setup_connection() override; - bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) override; + + // Function pointer type for type-erased message encoding + using MessageEncodeFn = void (*)(const void *, ProtoWriteBuffer &); + // Function pointer type for type-erased size calculation + using CalculateSizeFn = uint32_t (*)(const void *); + + template<typename T> bool send_message(const T &msg) { + if constexpr (T::ESTIMATED_SIZE == 0) { + return this->send_message_(0, T::MESSAGE_TYPE, &encode_msg_noop, &msg); + } else { + return this->send_message_(msg.calculate_size(), T::MESSAGE_TYPE, &proto_encode_msg<T>, &msg); + } + } void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t header_padding, size_t total_size) { shared_buf.clear(); @@ -318,28 +330,68 @@ class APIConnection final : public APIServerConnectionBase { void process_state_subscriptions_(); #endif - // Non-template helper to encode any ProtoMessage - static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, - uint32_t remaining_size); - - // Helper to fill entity state base and encode message - static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, uint8_t message_type, - APIConnection *conn, uint32_t remaining_size) { - msg.key = entity->get_object_id_hash(); -#ifdef USE_DEVICES - msg.device_id = entity->get_device_id(); -#endif - return encode_message_to_buffer(msg, message_type, conn, remaining_size); + // Size thunk — converts void* back to concrete type for direct calculate_size() call + template<typename T> static uint32_t calc_size(const void *msg) { + return static_cast<const T *>(msg)->calculate_size(); } - // Helper to fill entity info base and encode message - static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, uint8_t message_type, - APIConnection *conn, uint32_t remaining_size); + // Shared no-op encode thunk for empty messages (ESTIMATED_SIZE == 0) + static void encode_msg_noop(const void *, ProtoWriteBuffer &) {} - // Wrapper for entity types that have a device_class field + // Non-template buffer management for send_message + bool send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg); + + // Non-template buffer management for batch encoding + static uint16_t encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg, + APIConnection *conn, uint32_t remaining_size); + + // Thin template wrapper — computes size, delegates buffer work to non-template helper + template<typename T> static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) { + if constexpr (T::ESTIMATED_SIZE == 0) { + return encode_to_buffer(0, &encode_msg_noop, &msg, conn, remaining_size); + } else { + return encode_to_buffer(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size); + } + } + + // Non-template core — fills state fields and encodes + static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, + CalculateSizeFn size_fn, MessageEncodeFn encode_fn, APIConnection *conn, + uint32_t remaining_size); + + // Thin template wrapper + template<typename T> + static uint16_t fill_and_encode_entity_state(EntityBase *entity, T &msg, APIConnection *conn, + uint32_t remaining_size) { + return fill_and_encode_entity_state(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size); + } + + // Non-template core — fills info fields, allocates buffers, and encodes + static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, + CalculateSizeFn size_fn, MessageEncodeFn encode_fn, APIConnection *conn, + uint32_t remaining_size); + + // Thin template wrapper + template<typename T> + static uint16_t fill_and_encode_entity_info(EntityBase *entity, T &msg, APIConnection *conn, + uint32_t remaining_size) { + return fill_and_encode_entity_info(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size); + } + + // Non-template core — fills device_class, then delegates to fill_and_encode_entity_info static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg, - StringRef &device_class_field, uint8_t message_type, - APIConnection *conn, uint32_t remaining_size); + StringRef &device_class_field, CalculateSizeFn size_fn, + MessageEncodeFn encode_fn, APIConnection *conn, + uint32_t remaining_size); + + // Thin template wrapper + template<typename T> + static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, T &msg, + StringRef &device_class_field, APIConnection *conn, + uint32_t remaining_size) { + return fill_and_encode_entity_info_with_device_class(entity, msg, device_class_field, &calc_size<T>, + &proto_encode_msg<T>, conn, remaining_size); + } #ifdef USE_VOICE_ASSISTANT // Helper to check voice assistant validity and connection ownership diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 9e74d5ddc7..d8703aa416 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -37,20 +37,24 @@ void HelloResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(3, this->server_info); buffer.encode_string(4, this->name); } -void HelloResponse::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->api_version_major); - size.add_uint32(1, this->api_version_minor); - size.add_length(1, this->server_info.size()); - size.add_length(1, this->name.size()); +uint32_t HelloResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->api_version_major); + size += ProtoSize::calc_uint32(1, this->api_version_minor); + size += ProtoSize::calc_length(1, this->server_info.size()); + size += ProtoSize::calc_length(1, this->name.size()); + return size; } #ifdef USE_AREAS void AreaInfo::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->area_id); buffer.encode_string(2, this->name); } -void AreaInfo::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->area_id); - size.add_length(1, this->name.size()); +uint32_t AreaInfo::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->area_id); + size += ProtoSize::calc_length(1, this->name.size()); + return size; } #endif #ifdef USE_DEVICES @@ -59,10 +63,12 @@ void DeviceInfo::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(2, this->name); buffer.encode_uint32(3, this->area_id); } -void DeviceInfo::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->device_id); - size.add_length(1, this->name.size()); - size.add_uint32(1, this->area_id); +uint32_t DeviceInfo::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->device_id); + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_uint32(1, this->area_id); + return size; } #endif void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { @@ -120,60 +126,62 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(24, this->zwave_home_id); #endif } -void DeviceInfoResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->name.size()); - size.add_length(1, this->mac_address.size()); - size.add_length(1, this->esphome_version.size()); - size.add_length(1, this->compilation_time.size()); - size.add_length(1, this->model.size()); +uint32_t DeviceInfoResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_length(1, this->mac_address.size()); + size += ProtoSize::calc_length(1, this->esphome_version.size()); + size += ProtoSize::calc_length(1, this->compilation_time.size()); + size += ProtoSize::calc_length(1, this->model.size()); #ifdef USE_DEEP_SLEEP - size.add_bool(1, this->has_deep_sleep); + size += ProtoSize::calc_bool(1, this->has_deep_sleep); #endif #ifdef ESPHOME_PROJECT_NAME - size.add_length(1, this->project_name.size()); + size += ProtoSize::calc_length(1, this->project_name.size()); #endif #ifdef ESPHOME_PROJECT_NAME - size.add_length(1, this->project_version.size()); + size += ProtoSize::calc_length(1, this->project_version.size()); #endif #ifdef USE_WEBSERVER - size.add_uint32(1, this->webserver_port); + size += ProtoSize::calc_uint32(1, this->webserver_port); #endif #ifdef USE_BLUETOOTH_PROXY - size.add_uint32(1, this->bluetooth_proxy_feature_flags); + size += ProtoSize::calc_uint32(1, this->bluetooth_proxy_feature_flags); #endif - size.add_length(1, this->manufacturer.size()); - size.add_length(1, this->friendly_name.size()); + size += ProtoSize::calc_length(1, this->manufacturer.size()); + size += ProtoSize::calc_length(1, this->friendly_name.size()); #ifdef USE_VOICE_ASSISTANT - size.add_uint32(2, this->voice_assistant_feature_flags); + size += ProtoSize::calc_uint32(2, this->voice_assistant_feature_flags); #endif #ifdef USE_AREAS - size.add_length(2, this->suggested_area.size()); + size += ProtoSize::calc_length(2, this->suggested_area.size()); #endif #ifdef USE_BLUETOOTH_PROXY - size.add_length(2, this->bluetooth_mac_address.size()); + size += ProtoSize::calc_length(2, this->bluetooth_mac_address.size()); #endif #ifdef USE_API_NOISE - size.add_bool(2, this->api_encryption_supported); + size += ProtoSize::calc_bool(2, this->api_encryption_supported); #endif #ifdef USE_DEVICES for (const auto &it : this->devices) { - size.add_message_object_force(2, it); + size += ProtoSize::calc_message_force(2, it.calculate_size()); } #endif #ifdef USE_AREAS for (const auto &it : this->areas) { - size.add_message_object_force(2, it); + size += ProtoSize::calc_message_force(2, it.calculate_size()); } #endif #ifdef USE_AREAS - size.add_message_object(2, this->area); + size += ProtoSize::calc_message(2, this->area.calculate_size()); #endif #ifdef USE_ZWAVE_PROXY - size.add_uint32(2, this->zwave_proxy_feature_flags); + size += ProtoSize::calc_uint32(2, this->zwave_proxy_feature_flags); #endif #ifdef USE_ZWAVE_PROXY - size.add_uint32(2, this->zwave_home_id); + size += ProtoSize::calc_uint32(2, this->zwave_home_id); #endif + return size; } #ifdef USE_BINARY_SENSOR void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { @@ -191,20 +199,22 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(10, this->device_id); #endif } -void ListEntitiesBinarySensorResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); - size.add_length(1, this->device_class.size()); - size.add_bool(1, this->is_status_binary_sensor); - size.add_bool(1, this->disabled_by_default); +uint32_t ListEntitiesBinarySensorResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->is_status_binary_sensor); + size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void BinarySensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -214,13 +224,15 @@ void BinarySensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void BinarySensorStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->state); - size.add_bool(1, this->missing_state); +uint32_t BinarySensorStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->state); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } #endif #ifdef USE_COVER @@ -242,23 +254,25 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(13, this->device_id); #endif } -void ListEntitiesCoverResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); - size.add_bool(1, this->assumed_state); - size.add_bool(1, this->supports_position); - size.add_bool(1, this->supports_tilt); - size.add_length(1, this->device_class.size()); - size.add_bool(1, this->disabled_by_default); +uint32_t ListEntitiesCoverResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_bool(1, this->assumed_state); + size += ProtoSize::calc_bool(1, this->supports_position); + size += ProtoSize::calc_bool(1, this->supports_tilt); + size += ProtoSize::calc_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_bool(1, this->supports_stop); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_bool(1, this->supports_stop); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void CoverStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -269,14 +283,16 @@ void CoverStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(6, this->device_id); #endif } -void CoverStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_float(1, this->position); - size.add_float(1, this->tilt); - size.add_uint32(1, static_cast<uint32_t>(this->current_operation)); +uint32_t CoverStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_float(1, this->position); + size += ProtoSize::calc_float(1, this->tilt); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->current_operation)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -337,27 +353,29 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(13, this->device_id); #endif } -void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); - size.add_bool(1, this->supports_oscillation); - size.add_bool(1, this->supports_speed); - size.add_bool(1, this->supports_direction); - size.add_int32(1, this->supported_speed_count); - size.add_bool(1, this->disabled_by_default); +uint32_t ListEntitiesFanResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_bool(1, this->supports_oscillation); + size += ProtoSize::calc_bool(1, this->supports_speed); + size += ProtoSize::calc_bool(1, this->supports_direction); + size += ProtoSize::calc_int32(1, this->supported_speed_count); + size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); if (!this->supported_preset_modes->empty()) { for (const char *it : *this->supported_preset_modes) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void FanStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -370,16 +388,18 @@ void FanStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(8, this->device_id); #endif } -void FanStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->state); - size.add_bool(1, this->oscillating); - size.add_uint32(1, static_cast<uint32_t>(this->direction)); - size.add_int32(1, this->speed_level); - size.add_length(1, this->preset_mode.size()); +uint32_t FanStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->state); + size += ProtoSize::calc_bool(1, this->oscillating); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->direction)); + size += ProtoSize::calc_int32(1, this->speed_level); + size += ProtoSize::calc_length(1, this->preset_mode.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -464,30 +484,32 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(16, this->device_id); #endif } -void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesLightResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); if (!this->supported_color_modes->empty()) { for (const auto &it : *this->supported_color_modes) { - size.add_uint32_force(1, static_cast<uint32_t>(it)); + size += ProtoSize::calc_uint32_force(1, static_cast<uint32_t>(it)); } } - size.add_float(1, this->min_mireds); - size.add_float(1, this->max_mireds); + size += ProtoSize::calc_float(1, this->min_mireds); + size += ProtoSize::calc_float(1, this->max_mireds); if (!this->effects->empty()) { for (const char *it : *this->effects) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } - size.add_bool(1, this->disabled_by_default); + size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(2, this->device_id); + size += ProtoSize::calc_uint32(2, this->device_id); #endif + return size; } void LightStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -507,23 +529,25 @@ void LightStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(14, this->device_id); #endif } -void LightStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->state); - size.add_float(1, this->brightness); - size.add_uint32(1, static_cast<uint32_t>(this->color_mode)); - size.add_float(1, this->color_brightness); - size.add_float(1, this->red); - size.add_float(1, this->green); - size.add_float(1, this->blue); - size.add_float(1, this->white); - size.add_float(1, this->color_temperature); - size.add_float(1, this->cold_white); - size.add_float(1, this->warm_white); - size.add_length(1, this->effect.size()); +uint32_t LightStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->state); + size += ProtoSize::calc_float(1, this->brightness); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->color_mode)); + size += ProtoSize::calc_float(1, this->color_brightness); + size += ProtoSize::calc_float(1, this->red); + size += ProtoSize::calc_float(1, this->green); + size += ProtoSize::calc_float(1, this->blue); + size += ProtoSize::calc_float(1, this->white); + size += ProtoSize::calc_float(1, this->color_temperature); + size += ProtoSize::calc_float(1, this->cold_white); + size += ProtoSize::calc_float(1, this->warm_white); + size += ProtoSize::calc_length(1, this->effect.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -653,23 +677,25 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(14, this->device_id); #endif } -void ListEntitiesSensorResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesSensorResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_length(1, this->unit_of_measurement.size()); - size.add_int32(1, this->accuracy_decimals); - size.add_bool(1, this->force_update); - size.add_length(1, this->device_class.size()); - size.add_uint32(1, static_cast<uint32_t>(this->state_class)); - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_length(1, this->unit_of_measurement.size()); + size += ProtoSize::calc_int32(1, this->accuracy_decimals); + size += ProtoSize::calc_bool(1, this->force_update); + size += ProtoSize::calc_length(1, this->device_class.size()); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->state_class)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void SensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -679,13 +705,15 @@ void SensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void SensorStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_float(1, this->state); - size.add_bool(1, this->missing_state); +uint32_t SensorStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_float(1, this->state); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } #endif #ifdef USE_SWITCH @@ -704,20 +732,22 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(10, this->device_id); #endif } -void ListEntitiesSwitchResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesSwitchResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->assumed_state); - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->assumed_state); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void SwitchStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -726,12 +756,14 @@ void SwitchStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(3, this->device_id); #endif } -void SwitchStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->state); +uint32_t SwitchStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool SwitchCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -774,19 +806,21 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(9, this->device_id); #endif } -void ListEntitiesTextSensorResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesTextSensorResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void TextSensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -796,13 +830,15 @@ void TextSensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void TextSensorStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_length(1, this->state.size()); - size.add_bool(1, this->missing_state); +uint32_t TextSensorStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->state.size()); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } #endif bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -822,9 +858,11 @@ void SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast<uint32_t>(this->level)); buffer.encode_bytes(3, this->message_ptr_, this->message_len_); } -void SubscribeLogsResponse::calculate_size(ProtoSize &size) const { - size.add_uint32(1, static_cast<uint32_t>(this->level)); - size.add_length(1, this->message_len_); +uint32_t SubscribeLogsResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->level)); + size += ProtoSize::calc_length(1, this->message_len_); + return size; } #ifdef USE_API_NOISE bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { @@ -840,16 +878,22 @@ bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthD return true; } void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->success); } -void NoiseEncryptionSetKeyResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); } +uint32_t NoiseEncryptionSetKeyResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_bool(1, this->success); + return size; +} #endif #ifdef USE_API_HOMEASSISTANT_SERVICES void HomeassistantServiceMap::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->key); buffer.encode_string(2, this->value); } -void HomeassistantServiceMap::calculate_size(ProtoSize &size) const { - size.add_length(1, this->key.size()); - size.add_length(1, this->value.size()); +uint32_t HomeassistantServiceMap::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->key.size()); + size += ProtoSize::calc_length(1, this->value.size()); + return size; } void HomeassistantActionRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->service); @@ -873,21 +917,35 @@ void HomeassistantActionRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(8, this->response_template); #endif } -void HomeassistantActionRequest::calculate_size(ProtoSize &size) const { - size.add_length(1, this->service.size()); - size.add_repeated_message(1, this->data); - size.add_repeated_message(1, this->data_template); - size.add_repeated_message(1, this->variables); - size.add_bool(1, this->is_event); +uint32_t HomeassistantActionRequest::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->service.size()); + if (!this->data.empty()) { + for (const auto &it : this->data) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + if (!this->data_template.empty()) { + for (const auto &it : this->data_template) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + if (!this->variables.empty()) { + for (const auto &it : this->variables) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + size += ProtoSize::calc_bool(1, this->is_event); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES - size.add_uint32(1, this->call_id); + size += ProtoSize::calc_uint32(1, this->call_id); #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - size.add_bool(1, this->wants_response); + size += ProtoSize::calc_bool(1, this->wants_response); #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - size.add_length(1, this->response_template.size()); + size += ProtoSize::calc_length(1, this->response_template.size()); #endif + return size; } #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES @@ -929,10 +987,12 @@ void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer &buffer) const buffer.encode_string(2, this->attribute); buffer.encode_bool(3, this->once); } -void SubscribeHomeAssistantStateResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->entity_id.size()); - size.add_length(1, this->attribute.size()); - size.add_bool(1, this->once); +uint32_t SubscribeHomeAssistantStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->entity_id.size()); + size += ProtoSize::calc_length(1, this->attribute.size()); + size += ProtoSize::calc_bool(1, this->once); + return size; } bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { @@ -1034,9 +1094,11 @@ void ListEntitiesServicesArgument::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); buffer.encode_uint32(2, static_cast<uint32_t>(this->type)); } -void ListEntitiesServicesArgument::calculate_size(ProtoSize &size) const { - size.add_length(1, this->name.size()); - size.add_uint32(1, static_cast<uint32_t>(this->type)); +uint32_t ListEntitiesServicesArgument::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->type)); + return size; } void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); @@ -1046,11 +1108,17 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { } buffer.encode_uint32(4, static_cast<uint32_t>(this->supports_response)); } -void ListEntitiesServicesResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->name.size()); - size.add_fixed32(1, this->key); - size.add_repeated_message(1, this->args); - size.add_uint32(1, static_cast<uint32_t>(this->supports_response)); +uint32_t ListEntitiesServicesResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_fixed32(1, this->key); + if (!this->args.empty()) { + for (const auto &it : this->args) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->supports_response)); + return size; } bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1165,13 +1233,15 @@ void ExecuteServiceResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(4, this->response_data, this->response_data_len); #endif } -void ExecuteServiceResponse::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->call_id); - size.add_bool(1, this->success); - size.add_length(1, this->error_message.size()); +uint32_t ExecuteServiceResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->call_id); + size += ProtoSize::calc_bool(1, this->success); + size += ProtoSize::calc_length(1, this->error_message.size()); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON - size.add_length(1, this->response_data_len); + size += ProtoSize::calc_length(1, this->response_data_len); #endif + return size; } #endif #ifdef USE_CAMERA @@ -1188,18 +1258,20 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(8, this->device_id); #endif } -void ListEntitiesCameraResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); - size.add_bool(1, this->disabled_by_default); +uint32_t ListEntitiesCameraResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void CameraImageResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -1209,13 +1281,15 @@ void CameraImageResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void CameraImageResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_length(1, this->data_len_); - size.add_bool(1, this->done); +uint32_t CameraImageResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->data_len_); + size += ProtoSize::calc_bool(1, this->done); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1275,60 +1349,62 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer) const { #endif buffer.encode_uint32(27, this->feature_flags); } -void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); - size.add_bool(1, this->supports_current_temperature); - size.add_bool(1, this->supports_two_point_target_temperature); +uint32_t ListEntitiesClimateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_bool(1, this->supports_current_temperature); + size += ProtoSize::calc_bool(1, this->supports_two_point_target_temperature); if (!this->supported_modes->empty()) { for (const auto &it : *this->supported_modes) { - size.add_uint32_force(1, static_cast<uint32_t>(it)); + size += ProtoSize::calc_uint32_force(1, static_cast<uint32_t>(it)); } } - size.add_float(1, this->visual_min_temperature); - size.add_float(1, this->visual_max_temperature); - size.add_float(1, this->visual_target_temperature_step); - size.add_bool(1, this->supports_action); + size += ProtoSize::calc_float(1, this->visual_min_temperature); + size += ProtoSize::calc_float(1, this->visual_max_temperature); + size += ProtoSize::calc_float(1, this->visual_target_temperature_step); + size += ProtoSize::calc_bool(1, this->supports_action); if (!this->supported_fan_modes->empty()) { for (const auto &it : *this->supported_fan_modes) { - size.add_uint32_force(1, static_cast<uint32_t>(it)); + size += ProtoSize::calc_uint32_force(1, static_cast<uint32_t>(it)); } } if (!this->supported_swing_modes->empty()) { for (const auto &it : *this->supported_swing_modes) { - size.add_uint32_force(1, static_cast<uint32_t>(it)); + size += ProtoSize::calc_uint32_force(1, static_cast<uint32_t>(it)); } } if (!this->supported_custom_fan_modes->empty()) { for (const char *it : *this->supported_custom_fan_modes) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } if (!this->supported_presets->empty()) { for (const auto &it : *this->supported_presets) { - size.add_uint32_force(2, static_cast<uint32_t>(it)); + size += ProtoSize::calc_uint32_force(2, static_cast<uint32_t>(it)); } } if (!this->supported_custom_presets->empty()) { for (const char *it : *this->supported_custom_presets) { - size.add_length_force(2, strlen(it)); + size += ProtoSize::calc_length_force(2, strlen(it)); } } - size.add_bool(2, this->disabled_by_default); + size += ProtoSize::calc_bool(2, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(2, this->icon.size()); + size += ProtoSize::calc_length(2, this->icon.size()); #endif - size.add_uint32(2, static_cast<uint32_t>(this->entity_category)); - size.add_float(2, this->visual_current_temperature_step); - size.add_bool(2, this->supports_current_humidity); - size.add_bool(2, this->supports_target_humidity); - size.add_float(2, this->visual_min_humidity); - size.add_float(2, this->visual_max_humidity); + size += ProtoSize::calc_uint32(2, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_float(2, this->visual_current_temperature_step); + size += ProtoSize::calc_bool(2, this->supports_current_humidity); + size += ProtoSize::calc_bool(2, this->supports_target_humidity); + size += ProtoSize::calc_float(2, this->visual_min_humidity); + size += ProtoSize::calc_float(2, this->visual_max_humidity); #ifdef USE_DEVICES - size.add_uint32(2, this->device_id); + size += ProtoSize::calc_uint32(2, this->device_id); #endif - size.add_uint32(2, this->feature_flags); + size += ProtoSize::calc_uint32(2, this->feature_flags); + return size; } void ClimateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -1349,24 +1425,26 @@ void ClimateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(16, this->device_id); #endif } -void ClimateStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_uint32(1, static_cast<uint32_t>(this->mode)); - size.add_float(1, this->current_temperature); - size.add_float(1, this->target_temperature); - size.add_float(1, this->target_temperature_low); - size.add_float(1, this->target_temperature_high); - size.add_uint32(1, static_cast<uint32_t>(this->action)); - size.add_uint32(1, static_cast<uint32_t>(this->fan_mode)); - size.add_uint32(1, static_cast<uint32_t>(this->swing_mode)); - size.add_length(1, this->custom_fan_mode.size()); - size.add_uint32(1, static_cast<uint32_t>(this->preset)); - size.add_length(1, this->custom_preset.size()); - size.add_float(1, this->current_humidity); - size.add_float(1, this->target_humidity); +uint32_t ClimateStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->mode)); + size += ProtoSize::calc_float(1, this->current_temperature); + size += ProtoSize::calc_float(1, this->target_temperature); + size += ProtoSize::calc_float(1, this->target_temperature_low); + size += ProtoSize::calc_float(1, this->target_temperature_high); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->action)); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->fan_mode)); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->swing_mode)); + size += ProtoSize::calc_length(1, this->custom_fan_mode.size()); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->preset)); + size += ProtoSize::calc_length(1, this->custom_preset.size()); + size += ProtoSize::calc_float(1, this->current_humidity); + size += ProtoSize::calc_float(1, this->target_humidity); #ifdef USE_DEVICES - size.add_uint32(2, this->device_id); + size += ProtoSize::calc_uint32(2, this->device_id); #endif + return size; } bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1481,27 +1559,29 @@ void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer) const { } buffer.encode_uint32(12, this->supported_features); } -void ListEntitiesWaterHeaterResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif - size.add_float(1, this->min_temperature); - size.add_float(1, this->max_temperature); - size.add_float(1, this->target_temperature_step); + size += ProtoSize::calc_float(1, this->min_temperature); + size += ProtoSize::calc_float(1, this->max_temperature); + size += ProtoSize::calc_float(1, this->target_temperature_step); if (!this->supported_modes->empty()) { for (const auto &it : *this->supported_modes) { - size.add_uint32_force(1, static_cast<uint32_t>(it)); + size += ProtoSize::calc_uint32_force(1, static_cast<uint32_t>(it)); } } - size.add_uint32(1, this->supported_features); + size += ProtoSize::calc_uint32(1, this->supported_features); + return size; } void WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -1515,17 +1595,19 @@ void WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_float(7, this->target_temperature_low); buffer.encode_float(8, this->target_temperature_high); } -void WaterHeaterStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_float(1, this->current_temperature); - size.add_float(1, this->target_temperature); - size.add_uint32(1, static_cast<uint32_t>(this->mode)); +uint32_t WaterHeaterStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_float(1, this->current_temperature); + size += ProtoSize::calc_float(1, this->target_temperature); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->mode)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif - size.add_uint32(1, this->state); - size.add_float(1, this->target_temperature_low); - size.add_float(1, this->target_temperature_high); + size += ProtoSize::calc_uint32(1, this->state); + size += ProtoSize::calc_float(1, this->target_temperature_low); + size += ProtoSize::calc_float(1, this->target_temperature_high); + return size; } bool WaterHeaterCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1588,24 +1670,26 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(14, this->device_id); #endif } -void ListEntitiesNumberResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesNumberResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_float(1, this->min_value); - size.add_float(1, this->max_value); - size.add_float(1, this->step); - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_length(1, this->unit_of_measurement.size()); - size.add_uint32(1, static_cast<uint32_t>(this->mode)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_float(1, this->min_value); + size += ProtoSize::calc_float(1, this->max_value); + size += ProtoSize::calc_float(1, this->step); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_length(1, this->unit_of_measurement.size()); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->mode)); + size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void NumberStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -1615,13 +1699,15 @@ void NumberStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void NumberStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_float(1, this->state); - size.add_bool(1, this->missing_state); +uint32_t NumberStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_float(1, this->state); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool NumberCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1666,23 +1752,25 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(9, this->device_id); #endif } -void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesSelectResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif if (!this->options->empty()) { for (const char *it : *this->options) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void SelectStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -1692,13 +1780,15 @@ void SelectStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void SelectStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_length(1, this->state.size()); - size.add_bool(1, this->missing_state); +uint32_t SelectStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->state.size()); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool SelectCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1753,25 +1843,27 @@ void ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(11, this->device_id); #endif } -void ListEntitiesSirenResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesSirenResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); + size += ProtoSize::calc_bool(1, this->disabled_by_default); if (!this->tones->empty()) { for (const char *it : *this->tones) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } - size.add_bool(1, this->supports_duration); - size.add_bool(1, this->supports_volume); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_bool(1, this->supports_duration); + size += ProtoSize::calc_bool(1, this->supports_volume); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void SirenStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -1780,12 +1872,14 @@ void SirenStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(3, this->device_id); #endif } -void SirenStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->state); +uint32_t SirenStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool SirenCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1860,22 +1954,24 @@ void ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(12, this->device_id); #endif } -void ListEntitiesLockResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesLockResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_bool(1, this->assumed_state); - size.add_bool(1, this->supports_open); - size.add_bool(1, this->requires_code); - size.add_length(1, this->code_format.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_bool(1, this->assumed_state); + size += ProtoSize::calc_bool(1, this->supports_open); + size += ProtoSize::calc_bool(1, this->requires_code); + size += ProtoSize::calc_length(1, this->code_format.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void LockStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -1884,12 +1980,14 @@ void LockStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(3, this->device_id); #endif } -void LockStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_uint32(1, static_cast<uint32_t>(this->state)); +uint32_t LockStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->state)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool LockCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1946,19 +2044,21 @@ void ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(9, this->device_id); #endif } -void ListEntitiesButtonResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesButtonResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool ButtonCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1991,12 +2091,14 @@ void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, static_cast<uint32_t>(this->purpose)); buffer.encode_uint32(5, this->sample_bytes); } -void MediaPlayerSupportedFormat::calculate_size(ProtoSize &size) const { - size.add_length(1, this->format.size()); - size.add_uint32(1, this->sample_rate); - size.add_uint32(1, this->num_channels); - size.add_uint32(1, static_cast<uint32_t>(this->purpose)); - size.add_uint32(1, this->sample_bytes); +uint32_t MediaPlayerSupportedFormat::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->format.size()); + size += ProtoSize::calc_uint32(1, this->sample_rate); + size += ProtoSize::calc_uint32(1, this->num_channels); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->purpose)); + size += ProtoSize::calc_uint32(1, this->sample_bytes); + return size; } void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); @@ -2016,21 +2118,27 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { #endif buffer.encode_uint32(11, this->feature_flags); } -void ListEntitiesMediaPlayerResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesMediaPlayerResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_bool(1, this->supports_pause); - size.add_repeated_message(1, this->supported_formats); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_bool(1, this->supports_pause); + if (!this->supported_formats.empty()) { + for (const auto &it : this->supported_formats) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif - size.add_uint32(1, this->feature_flags); + size += ProtoSize::calc_uint32(1, this->feature_flags); + return size; } void MediaPlayerStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -2041,14 +2149,16 @@ void MediaPlayerStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(5, this->device_id); #endif } -void MediaPlayerStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_uint32(1, static_cast<uint32_t>(this->state)); - size.add_float(1, this->volume); - size.add_bool(1, this->muted); +uint32_t MediaPlayerStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->state)); + size += ProtoSize::calc_float(1, this->volume); + size += ProtoSize::calc_bool(1, this->muted); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2122,21 +2232,25 @@ void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(3, this->address_type); buffer.encode_bytes(4, this->data, this->data_len); } -void BluetoothLERawAdvertisement::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_sint32(1, this->rssi); - size.add_uint32(1, this->address_type); - size.add_length(1, this->data_len); +uint32_t BluetoothLERawAdvertisement::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_sint32(1, this->rssi); + size += ProtoSize::calc_uint32(1, this->address_type); + size += ProtoSize::calc_length(1, this->data_len); + return size; } void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const { for (uint16_t i = 0; i < this->advertisements_len; i++) { buffer.encode_message(1, this->advertisements[i]); } } -void BluetoothLERawAdvertisementsResponse::calculate_size(ProtoSize &size) const { +uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const { + uint32_t size = 0; for (uint16_t i = 0; i < this->advertisements_len; i++) { - size.add_message_object_force(1, this->advertisements[i]); + size += ProtoSize::calc_message_force(1, this->advertisements[i].calculate_size()); } + return size; } bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2163,11 +2277,13 @@ void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(3, this->mtu); buffer.encode_int32(4, this->error); } -void BluetoothDeviceConnectionResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_bool(1, this->connected); - size.add_uint32(1, this->mtu); - size.add_int32(1, this->error); +uint32_t BluetoothDeviceConnectionResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_bool(1, this->connected); + size += ProtoSize::calc_uint32(1, this->mtu); + size += ProtoSize::calc_int32(1, this->error); + return size; } bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2187,13 +2303,15 @@ void BluetoothGATTDescriptor::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_uint32(3, this->short_uuid); } -void BluetoothGATTDescriptor::calculate_size(ProtoSize &size) const { +uint32_t BluetoothGATTDescriptor::calculate_size() const { + uint32_t size = 0; if (this->uuid[0] != 0 || this->uuid[1] != 0) { - size.add_uint64_force(1, this->uuid[0]); - size.add_uint64_force(1, this->uuid[1]); + size += ProtoSize::calc_uint64_force(1, this->uuid[0]); + size += ProtoSize::calc_uint64_force(1, this->uuid[1]); } - size.add_uint32(1, this->handle); - size.add_uint32(1, this->short_uuid); + size += ProtoSize::calc_uint32(1, this->handle); + size += ProtoSize::calc_uint32(1, this->short_uuid); + return size; } void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer &buffer) const { if (this->uuid[0] != 0 || this->uuid[1] != 0) { @@ -2207,15 +2325,21 @@ void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer &buffer) const { } buffer.encode_uint32(5, this->short_uuid); } -void BluetoothGATTCharacteristic::calculate_size(ProtoSize &size) const { +uint32_t BluetoothGATTCharacteristic::calculate_size() const { + uint32_t size = 0; if (this->uuid[0] != 0 || this->uuid[1] != 0) { - size.add_uint64_force(1, this->uuid[0]); - size.add_uint64_force(1, this->uuid[1]); + size += ProtoSize::calc_uint64_force(1, this->uuid[0]); + size += ProtoSize::calc_uint64_force(1, this->uuid[1]); } - size.add_uint32(1, this->handle); - size.add_uint32(1, this->properties); - size.add_repeated_message(1, this->descriptors); - size.add_uint32(1, this->short_uuid); + size += ProtoSize::calc_uint32(1, this->handle); + size += ProtoSize::calc_uint32(1, this->properties); + if (!this->descriptors.empty()) { + for (const auto &it : this->descriptors) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + size += ProtoSize::calc_uint32(1, this->short_uuid); + return size; } void BluetoothGATTService::encode(ProtoWriteBuffer &buffer) const { if (this->uuid[0] != 0 || this->uuid[1] != 0) { @@ -2228,14 +2352,20 @@ void BluetoothGATTService::encode(ProtoWriteBuffer &buffer) const { } buffer.encode_uint32(4, this->short_uuid); } -void BluetoothGATTService::calculate_size(ProtoSize &size) const { +uint32_t BluetoothGATTService::calculate_size() const { + uint32_t size = 0; if (this->uuid[0] != 0 || this->uuid[1] != 0) { - size.add_uint64_force(1, this->uuid[0]); - size.add_uint64_force(1, this->uuid[1]); + size += ProtoSize::calc_uint64_force(1, this->uuid[0]); + size += ProtoSize::calc_uint64_force(1, this->uuid[1]); } - size.add_uint32(1, this->handle); - size.add_repeated_message(1, this->characteristics); - size.add_uint32(1, this->short_uuid); + size += ProtoSize::calc_uint32(1, this->handle); + if (!this->characteristics.empty()) { + for (const auto &it : this->characteristics) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + size += ProtoSize::calc_uint32(1, this->short_uuid); + return size; } void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); @@ -2243,14 +2373,24 @@ void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_message(2, it); } } -void BluetoothGATTGetServicesResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_repeated_message(1, this->services); +uint32_t BluetoothGATTGetServicesResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + if (!this->services.empty()) { + for (const auto &it : this->services) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + return size; } void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); } -void BluetoothGATTGetServicesDoneResponse::calculate_size(ProtoSize &size) const { size.add_uint64(1, this->address); } +uint32_t BluetoothGATTGetServicesDoneResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + return size; +} bool BluetoothGATTReadRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: @@ -2269,10 +2409,12 @@ void BluetoothGATTReadResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_bytes(3, this->data_ptr_, this->data_len_); } -void BluetoothGATTReadResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_uint32(1, this->handle); - size.add_length(1, this->data_len_); +uint32_t BluetoothGATTReadResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_uint32(1, this->handle); + size += ProtoSize::calc_length(1, this->data_len_); + return size; } bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2361,10 +2503,12 @@ void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_bytes(3, this->data_ptr_, this->data_len_); } -void BluetoothGATTNotifyDataResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_uint32(1, this->handle); - size.add_length(1, this->data_len_); +uint32_t BluetoothGATTNotifyDataResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_uint32(1, this->handle); + size += ProtoSize::calc_length(1, this->data_len_); + return size; } void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->free); @@ -2375,80 +2519,96 @@ void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer &buffer) const { } } } -void BluetoothConnectionsFreeResponse::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->free); - size.add_uint32(1, this->limit); +uint32_t BluetoothConnectionsFreeResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->free); + size += ProtoSize::calc_uint32(1, this->limit); for (const auto &it : this->allocated) { if (it != 0) { - size.add_uint64_force(1, it); + size += ProtoSize::calc_uint64_force(1, it); } } + return size; } void BluetoothGATTErrorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); buffer.encode_int32(3, this->error); } -void BluetoothGATTErrorResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_uint32(1, this->handle); - size.add_int32(1, this->error); +uint32_t BluetoothGATTErrorResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_uint32(1, this->handle); + size += ProtoSize::calc_int32(1, this->error); + return size; } void BluetoothGATTWriteResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } -void BluetoothGATTWriteResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_uint32(1, this->handle); +uint32_t BluetoothGATTWriteResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_uint32(1, this->handle); + return size; } void BluetoothGATTNotifyResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } -void BluetoothGATTNotifyResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_uint32(1, this->handle); +uint32_t BluetoothGATTNotifyResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_uint32(1, this->handle); + return size; } void BluetoothDevicePairingResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->paired); buffer.encode_int32(3, this->error); } -void BluetoothDevicePairingResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_bool(1, this->paired); - size.add_int32(1, this->error); +uint32_t BluetoothDevicePairingResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_bool(1, this->paired); + size += ProtoSize::calc_int32(1, this->error); + return size; } void BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->success); buffer.encode_int32(3, this->error); } -void BluetoothDeviceUnpairingResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_bool(1, this->success); - size.add_int32(1, this->error); +uint32_t BluetoothDeviceUnpairingResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_bool(1, this->success); + size += ProtoSize::calc_int32(1, this->error); + return size; } void BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->success); buffer.encode_int32(3, this->error); } -void BluetoothDeviceClearCacheResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_bool(1, this->success); - size.add_int32(1, this->error); +uint32_t BluetoothDeviceClearCacheResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_bool(1, this->success); + size += ProtoSize::calc_int32(1, this->error); + return size; } void BluetoothScannerStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast<uint32_t>(this->state)); buffer.encode_uint32(2, static_cast<uint32_t>(this->mode)); buffer.encode_uint32(3, static_cast<uint32_t>(this->configured_mode)); } -void BluetoothScannerStateResponse::calculate_size(ProtoSize &size) const { - size.add_uint32(1, static_cast<uint32_t>(this->state)); - size.add_uint32(1, static_cast<uint32_t>(this->mode)); - size.add_uint32(1, static_cast<uint32_t>(this->configured_mode)); +uint32_t BluetoothScannerStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->state)); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->mode)); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->configured_mode)); + return size; } bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2480,10 +2640,12 @@ void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(2, this->auto_gain); buffer.encode_float(3, this->volume_multiplier); } -void VoiceAssistantAudioSettings::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->noise_suppression_level); - size.add_uint32(1, this->auto_gain); - size.add_float(1, this->volume_multiplier); +uint32_t VoiceAssistantAudioSettings::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->noise_suppression_level); + size += ProtoSize::calc_uint32(1, this->auto_gain); + size += ProtoSize::calc_float(1, this->volume_multiplier); + return size; } void VoiceAssistantRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->start); @@ -2492,12 +2654,14 @@ void VoiceAssistantRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_message(4, this->audio_settings, false); buffer.encode_string(5, this->wake_word_phrase); } -void VoiceAssistantRequest::calculate_size(ProtoSize &size) const { - size.add_bool(1, this->start); - size.add_length(1, this->conversation_id.size()); - size.add_uint32(1, this->flags); - size.add_message_object(1, this->audio_settings); - size.add_length(1, this->wake_word_phrase.size()); +uint32_t VoiceAssistantRequest::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_bool(1, this->start); + size += ProtoSize::calc_length(1, this->conversation_id.size()); + size += ProtoSize::calc_uint32(1, this->flags); + size += ProtoSize::calc_message(1, this->audio_settings.calculate_size()); + size += ProtoSize::calc_length(1, this->wake_word_phrase.size()); + return size; } bool VoiceAssistantResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2574,9 +2738,11 @@ void VoiceAssistantAudio::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(1, this->data, this->data_len); buffer.encode_bool(2, this->end); } -void VoiceAssistantAudio::calculate_size(ProtoSize &size) const { - size.add_length(1, this->data_len); - size.add_bool(1, this->end); +uint32_t VoiceAssistantAudio::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->data_len); + size += ProtoSize::calc_bool(1, this->end); + return size; } bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2642,7 +2808,11 @@ bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLength return true; } void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->success); } -void VoiceAssistantAnnounceFinished::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); } +uint32_t VoiceAssistantAnnounceFinished::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_bool(1, this->success); + return size; +} void VoiceAssistantWakeWord::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->id); buffer.encode_string(2, this->wake_word); @@ -2650,14 +2820,16 @@ void VoiceAssistantWakeWord::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(3, it, true); } } -void VoiceAssistantWakeWord::calculate_size(ProtoSize &size) const { - size.add_length(1, this->id.size()); - size.add_length(1, this->wake_word.size()); +uint32_t VoiceAssistantWakeWord::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->id.size()); + size += ProtoSize::calc_length(1, this->wake_word.size()); if (!this->trained_languages.empty()) { for (const auto &it : this->trained_languages) { - size.add_length_force(1, it.size()); + size += ProtoSize::calc_length_force(1, it.size()); } } + return size; } bool VoiceAssistantExternalWakeWord::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2719,14 +2891,20 @@ void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer &buffer) const } buffer.encode_uint32(3, this->max_active_wake_words); } -void VoiceAssistantConfigurationResponse::calculate_size(ProtoSize &size) const { - size.add_repeated_message(1, this->available_wake_words); - if (!this->active_wake_words->empty()) { - for (const auto &it : *this->active_wake_words) { - size.add_length_force(1, it.size()); +uint32_t VoiceAssistantConfigurationResponse::calculate_size() const { + uint32_t size = 0; + if (!this->available_wake_words.empty()) { + for (const auto &it : this->available_wake_words) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); } } - size.add_uint32(1, this->max_active_wake_words); + if (!this->active_wake_words->empty()) { + for (const auto &it : *this->active_wake_words) { + size += ProtoSize::calc_length_force(1, it.size()); + } + } + size += ProtoSize::calc_uint32(1, this->max_active_wake_words); + return size; } bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { @@ -2756,21 +2934,23 @@ void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer) con buffer.encode_uint32(11, this->device_id); #endif } -void ListEntitiesAlarmControlPanelResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesAlarmControlPanelResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_uint32(1, this->supported_features); - size.add_bool(1, this->requires_code); - size.add_bool(1, this->requires_code_to_arm); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_uint32(1, this->supported_features); + size += ProtoSize::calc_bool(1, this->requires_code); + size += ProtoSize::calc_bool(1, this->requires_code_to_arm); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -2779,12 +2959,14 @@ void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(3, this->device_id); #endif } -void AlarmControlPanelStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_uint32(1, static_cast<uint32_t>(this->state)); +uint32_t AlarmControlPanelStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->state)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2841,22 +3023,24 @@ void ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(12, this->device_id); #endif } -void ListEntitiesTextResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesTextResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_uint32(1, this->min_length); - size.add_uint32(1, this->max_length); - size.add_length(1, this->pattern.size()); - size.add_uint32(1, static_cast<uint32_t>(this->mode)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_uint32(1, this->min_length); + size += ProtoSize::calc_uint32(1, this->max_length); + size += ProtoSize::calc_length(1, this->pattern.size()); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->mode)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void TextStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -2866,13 +3050,15 @@ void TextStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void TextStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_length(1, this->state.size()); - size.add_bool(1, this->missing_state); +uint32_t TextStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->state.size()); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool TextCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2922,18 +3108,20 @@ void ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(8, this->device_id); #endif } -void ListEntitiesDateResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesDateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void DateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -2945,15 +3133,17 @@ void DateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(6, this->device_id); #endif } -void DateStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->missing_state); - size.add_uint32(1, this->year); - size.add_uint32(1, this->month); - size.add_uint32(1, this->day); +uint32_t DateStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->missing_state); + size += ProtoSize::calc_uint32(1, this->year); + size += ProtoSize::calc_uint32(1, this->month); + size += ProtoSize::calc_uint32(1, this->day); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool DateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -3001,18 +3191,20 @@ void ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(8, this->device_id); #endif } -void ListEntitiesTimeResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesTimeResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void TimeStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -3024,15 +3216,17 @@ void TimeStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(6, this->device_id); #endif } -void TimeStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->missing_state); - size.add_uint32(1, this->hour); - size.add_uint32(1, this->minute); - size.add_uint32(1, this->second); +uint32_t TimeStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->missing_state); + size += ProtoSize::calc_uint32(1, this->hour); + size += ProtoSize::calc_uint32(1, this->minute); + size += ProtoSize::calc_uint32(1, this->second); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool TimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -3084,24 +3278,26 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(10, this->device_id); #endif } -void ListEntitiesEventResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesEventResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); if (!this->event_types->empty()) { for (const char *it : *this->event_types) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void EventResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -3110,12 +3306,14 @@ void EventResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(3, this->device_id); #endif } -void EventResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_length(1, this->event_type.size()); +uint32_t EventResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->event_type.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } #endif #ifdef USE_VALVE @@ -3136,22 +3334,24 @@ void ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(12, this->device_id); #endif } -void ListEntitiesValveResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesValveResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_length(1, this->device_class.size()); - size.add_bool(1, this->assumed_state); - size.add_bool(1, this->supports_position); - size.add_bool(1, this->supports_stop); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->assumed_state); + size += ProtoSize::calc_bool(1, this->supports_position); + size += ProtoSize::calc_bool(1, this->supports_stop); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void ValveStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -3161,13 +3361,15 @@ void ValveStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void ValveStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_float(1, this->position); - size.add_uint32(1, static_cast<uint32_t>(this->current_operation)); +uint32_t ValveStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_float(1, this->position); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->current_operation)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool ValveCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -3215,18 +3417,20 @@ void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(8, this->device_id); #endif } -void ListEntitiesDateTimeResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesDateTimeResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void DateTimeStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -3236,13 +3440,15 @@ void DateTimeStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void DateTimeStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->missing_state); - size.add_fixed32(1, this->epoch_seconds); +uint32_t DateTimeStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->missing_state); + size += ProtoSize::calc_fixed32(1, this->epoch_seconds); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool DateTimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -3285,19 +3491,21 @@ void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(9, this->device_id); #endif } -void ListEntitiesUpdateResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesUpdateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void UpdateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -3314,20 +3522,22 @@ void UpdateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(11, this->device_id); #endif } -void UpdateStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->missing_state); - size.add_bool(1, this->in_progress); - size.add_bool(1, this->has_progress); - size.add_float(1, this->progress); - size.add_length(1, this->current_version.size()); - size.add_length(1, this->latest_version.size()); - size.add_length(1, this->title.size()); - size.add_length(1, this->release_summary.size()); - size.add_length(1, this->release_url.size()); +uint32_t UpdateStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->missing_state); + size += ProtoSize::calc_bool(1, this->in_progress); + size += ProtoSize::calc_bool(1, this->has_progress); + size += ProtoSize::calc_float(1, this->progress); + size += ProtoSize::calc_length(1, this->current_version.size()); + size += ProtoSize::calc_length(1, this->latest_version.size()); + size += ProtoSize::calc_length(1, this->title.size()); + size += ProtoSize::calc_length(1, this->release_summary.size()); + size += ProtoSize::calc_length(1, this->release_url.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -3369,7 +3579,11 @@ bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited valu return true; } void ZWaveProxyFrame::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(1, this->data, this->data_len); } -void ZWaveProxyFrame::calculate_size(ProtoSize &size) const { size.add_length(1, this->data_len); } +uint32_t ZWaveProxyFrame::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->data_len); + return size; +} bool ZWaveProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: @@ -3396,9 +3610,11 @@ void ZWaveProxyRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast<uint32_t>(this->type)); buffer.encode_bytes(2, this->data, this->data_len); } -void ZWaveProxyRequest::calculate_size(ProtoSize &size) const { - size.add_uint32(1, static_cast<uint32_t>(this->type)); - size.add_length(1, this->data_len); +uint32_t ZWaveProxyRequest::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->type)); + size += ProtoSize::calc_length(1, this->data_len); + return size; } #endif #ifdef USE_INFRARED @@ -3416,19 +3632,21 @@ void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { #endif buffer.encode_uint32(8, this->capabilities); } -void ListEntitiesInfraredResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesInfraredResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif - size.add_uint32(1, this->capabilities); + size += ProtoSize::calc_uint32(1, this->capabilities); + return size; } #endif #ifdef USE_IR_RF @@ -3482,16 +3700,18 @@ void InfraredRFReceiveEvent::encode(ProtoWriteBuffer &buffer) const { buffer.encode_sint32(3, it, true); } } -void InfraredRFReceiveEvent::calculate_size(ProtoSize &size) const { +uint32_t InfraredRFReceiveEvent::calculate_size() const { + uint32_t size = 0; #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif - size.add_fixed32(1, this->key); + size += ProtoSize::calc_fixed32(1, this->key); if (!this->timings->empty()) { for (const auto &it : *this->timings) { - size.add_sint32_force(1, it); + size += ProtoSize::calc_sint32_force(1, it); } } + return size; } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index a97f6c0a76..89cb1158f3 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -388,8 +388,8 @@ class HelloResponse final : public ProtoMessage { uint32_t api_version_minor{0}; StringRef server_info{}; StringRef name{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -453,8 +453,8 @@ class AreaInfo final : public ProtoMessage { public: uint32_t area_id{0}; StringRef name{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -468,8 +468,8 @@ class DeviceInfo final : public ProtoMessage { uint32_t device_id{0}; StringRef name{}; uint32_t area_id{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -533,8 +533,8 @@ class DeviceInfoResponse final : public ProtoMessage { #ifdef USE_ZWAVE_PROXY uint32_t zwave_home_id{0}; #endif - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -564,8 +564,8 @@ class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage { #endif StringRef device_class{}; bool is_status_binary_sensor{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -581,8 +581,8 @@ class BinarySensorStateResponse final : public StateResponseProtoMessage { #endif bool state{false}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -603,8 +603,8 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage { bool supports_tilt{false}; StringRef device_class{}; bool supports_stop{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -621,8 +621,8 @@ class CoverStateResponse final : public StateResponseProtoMessage { float position{0.0f}; float tilt{0.0f}; enums::CoverOperation current_operation{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -663,8 +663,8 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { bool supports_direction{false}; int32_t supported_speed_count{0}; const std::vector<const char *> *supported_preset_modes{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -683,8 +683,8 @@ class FanStateResponse final : public StateResponseProtoMessage { enums::FanDirection direction{}; int32_t speed_level{0}; StringRef preset_mode{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -730,8 +730,8 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage { float min_mireds{0.0f}; float max_mireds{0.0f}; const FixedVector<const char *> *effects{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -757,8 +757,8 @@ class LightStateResponse final : public StateResponseProtoMessage { float cold_white{0.0f}; float warm_white{0.0f}; StringRef effect{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -821,8 +821,8 @@ class ListEntitiesSensorResponse final : public InfoResponseProtoMessage { bool force_update{false}; StringRef device_class{}; enums::SensorStateClass state_class{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -838,8 +838,8 @@ class SensorStateResponse final : public StateResponseProtoMessage { #endif float state{0.0f}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -857,8 +857,8 @@ class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage { #endif bool assumed_state{false}; StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -873,8 +873,8 @@ class SwitchStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "switch_state_response"; } #endif bool state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -907,8 +907,8 @@ class ListEntitiesTextSensorResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_text_sensor_response"; } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -924,8 +924,8 @@ class TextSensorStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -963,8 +963,8 @@ class SubscribeLogsResponse final : public ProtoMessage { this->message_ptr_ = data; this->message_len_ = len; } - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -996,8 +996,8 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage { const char *message_name() const override { return "noise_encryption_set_key_response"; } #endif bool success{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1010,8 +1010,8 @@ class HomeassistantServiceMap final : public ProtoMessage { public: StringRef key{}; StringRef value{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1039,8 +1039,8 @@ class HomeassistantActionRequest final : public ProtoMessage { #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON StringRef response_template{}; #endif - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1083,8 +1083,8 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage { StringRef entity_id{}; StringRef attribute{}; bool once{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1174,8 +1174,8 @@ class ListEntitiesServicesArgument final : public ProtoMessage { public: StringRef name{}; enums::ServiceArgType type{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1193,8 +1193,8 @@ class ListEntitiesServicesResponse final : public ProtoMessage { uint32_t key{0}; FixedVector<ListEntitiesServicesArgument> args{}; enums::SupportsResponseType supports_response{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1263,8 +1263,8 @@ class ExecuteServiceResponse final : public ProtoMessage { const uint8_t *response_data{nullptr}; uint16_t response_data_len{0}; #endif - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1280,8 +1280,8 @@ class ListEntitiesCameraResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_camera_response"; } #endif - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1302,8 +1302,8 @@ class CameraImageResponse final : public StateResponseProtoMessage { this->data_len_ = len; } bool done{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1353,8 +1353,8 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { float visual_min_humidity{0.0f}; float visual_max_humidity{0.0f}; uint32_t feature_flags{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1381,8 +1381,8 @@ class ClimateStateResponse final : public StateResponseProtoMessage { StringRef custom_preset{}; float current_humidity{0.0f}; float target_humidity{0.0f}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1439,8 +1439,8 @@ class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage { float target_temperature_step{0.0f}; const water_heater::WaterHeaterModeMask *supported_modes{}; uint32_t supported_features{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1460,8 +1460,8 @@ class WaterHeaterStateResponse final : public StateResponseProtoMessage { uint32_t state{0}; float target_temperature_low{0.0f}; float target_temperature_high{0.0f}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1504,8 +1504,8 @@ class ListEntitiesNumberResponse final : public InfoResponseProtoMessage { StringRef unit_of_measurement{}; enums::NumberMode mode{}; StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1521,8 +1521,8 @@ class NumberStateResponse final : public StateResponseProtoMessage { #endif float state{0.0f}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1555,8 +1555,8 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_select_response"; } #endif const FixedVector<const char *> *options{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1572,8 +1572,8 @@ class SelectStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1609,8 +1609,8 @@ class ListEntitiesSirenResponse final : public InfoResponseProtoMessage { const FixedVector<const char *> *tones{}; bool supports_duration{false}; bool supports_volume{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1625,8 +1625,8 @@ class SirenStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "siren_state_response"; } #endif bool state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1670,8 +1670,8 @@ class ListEntitiesLockResponse final : public InfoResponseProtoMessage { bool supports_open{false}; bool requires_code{false}; StringRef code_format{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1686,8 +1686,8 @@ class LockStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "lock_state_response"; } #endif enums::LockState state{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1723,8 +1723,8 @@ class ListEntitiesButtonResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_button_response"; } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1755,8 +1755,8 @@ class MediaPlayerSupportedFormat final : public ProtoMessage { uint32_t num_channels{0}; enums::MediaPlayerFormatPurpose purpose{}; uint32_t sample_bytes{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1773,8 +1773,8 @@ class ListEntitiesMediaPlayerResponse final : public InfoResponseProtoMessage { bool supports_pause{false}; std::vector<MediaPlayerSupportedFormat> supported_formats{}; uint32_t feature_flags{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1791,8 +1791,8 @@ class MediaPlayerStateResponse final : public StateResponseProtoMessage { enums::MediaPlayerState state{}; float volume{0.0f}; bool muted{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1847,8 +1847,8 @@ class BluetoothLERawAdvertisement final : public ProtoMessage { uint32_t address_type{0}; uint8_t data[62]{}; uint8_t data_len{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1864,8 +1864,8 @@ class BluetoothLERawAdvertisementsResponse final : public ProtoMessage { #endif std::array<BluetoothLERawAdvertisement, BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE> advertisements{}; uint16_t advertisements_len{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1901,8 +1901,8 @@ class BluetoothDeviceConnectionResponse final : public ProtoMessage { bool connected{false}; uint32_t mtu{0}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1929,8 +1929,8 @@ class BluetoothGATTDescriptor final : public ProtoMessage { std::array<uint64_t, 2> uuid{}; uint32_t handle{0}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1944,8 +1944,8 @@ class BluetoothGATTCharacteristic final : public ProtoMessage { uint32_t properties{0}; FixedVector<BluetoothGATTDescriptor> descriptors{}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1958,8 +1958,8 @@ class BluetoothGATTService final : public ProtoMessage { uint32_t handle{0}; FixedVector<BluetoothGATTCharacteristic> characteristics{}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1975,8 +1975,8 @@ class BluetoothGATTGetServicesResponse final : public ProtoMessage { #endif uint64_t address{0}; std::vector<BluetoothGATTService> services{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1991,8 +1991,8 @@ class BluetoothGATTGetServicesDoneResponse final : public ProtoMessage { const char *message_name() const override { return "bluetooth_gatt_get_services_done_response"; } #endif uint64_t address{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2030,8 +2030,8 @@ class BluetoothGATTReadResponse final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2125,8 +2125,8 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2143,8 +2143,8 @@ class BluetoothConnectionsFreeResponse final : public ProtoMessage { uint32_t free{0}; uint32_t limit{0}; std::array<uint64_t, BLUETOOTH_PROXY_MAX_CONNECTIONS> allocated{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2161,8 +2161,8 @@ class BluetoothGATTErrorResponse final : public ProtoMessage { uint64_t address{0}; uint32_t handle{0}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2178,8 +2178,8 @@ class BluetoothGATTWriteResponse final : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2195,8 +2195,8 @@ class BluetoothGATTNotifyResponse final : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2213,8 +2213,8 @@ class BluetoothDevicePairingResponse final : public ProtoMessage { uint64_t address{0}; bool paired{false}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2231,8 +2231,8 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage { uint64_t address{0}; bool success{false}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2249,8 +2249,8 @@ class BluetoothDeviceClearCacheResponse final : public ProtoMessage { uint64_t address{0}; bool success{false}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2267,8 +2267,8 @@ class BluetoothScannerStateResponse final : public ProtoMessage { enums::BluetoothScannerState state{}; enums::BluetoothScannerMode mode{}; enums::BluetoothScannerMode configured_mode{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2313,8 +2313,8 @@ class VoiceAssistantAudioSettings final : public ProtoMessage { uint32_t noise_suppression_level{0}; uint32_t auto_gain{0}; float volume_multiplier{0.0f}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2333,8 +2333,8 @@ class VoiceAssistantRequest final : public ProtoMessage { uint32_t flags{0}; VoiceAssistantAudioSettings audio_settings{}; StringRef wake_word_phrase{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2395,8 +2395,8 @@ class VoiceAssistantAudio final : public ProtoDecodableMessage { const uint8_t *data{nullptr}; uint16_t data_len{0}; bool end{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2453,8 +2453,8 @@ class VoiceAssistantAnnounceFinished final : public ProtoMessage { const char *message_name() const override { return "voice_assistant_announce_finished"; } #endif bool success{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2466,8 +2466,8 @@ class VoiceAssistantWakeWord final : public ProtoMessage { StringRef id{}; StringRef wake_word{}; std::vector<std::string> trained_languages{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2516,8 +2516,8 @@ class VoiceAssistantConfigurationResponse final : public ProtoMessage { std::vector<VoiceAssistantWakeWord> available_wake_words{}; const std::vector<std::string> *active_wake_words{}; uint32_t max_active_wake_words{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2551,8 +2551,8 @@ class ListEntitiesAlarmControlPanelResponse final : public InfoResponseProtoMess uint32_t supported_features{0}; bool requires_code{false}; bool requires_code_to_arm{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2567,8 +2567,8 @@ class AlarmControlPanelStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "alarm_control_panel_state_response"; } #endif enums::AlarmControlPanelState state{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2606,8 +2606,8 @@ class ListEntitiesTextResponse final : public InfoResponseProtoMessage { uint32_t max_length{0}; StringRef pattern{}; enums::TextMode mode{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2623,8 +2623,8 @@ class TextStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2657,8 +2657,8 @@ class ListEntitiesDateResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_date_response"; } #endif - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2676,8 +2676,8 @@ class DateStateResponse final : public StateResponseProtoMessage { uint32_t year{0}; uint32_t month{0}; uint32_t day{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2711,8 +2711,8 @@ class ListEntitiesTimeResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_time_response"; } #endif - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2730,8 +2730,8 @@ class TimeStateResponse final : public StateResponseProtoMessage { uint32_t hour{0}; uint32_t minute{0}; uint32_t second{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2767,8 +2767,8 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage { #endif StringRef device_class{}; const FixedVector<const char *> *event_types{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2783,8 +2783,8 @@ class EventResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "event_response"; } #endif StringRef event_type{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2804,8 +2804,8 @@ class ListEntitiesValveResponse final : public InfoResponseProtoMessage { bool assumed_state{false}; bool supports_position{false}; bool supports_stop{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2821,8 +2821,8 @@ class ValveStateResponse final : public StateResponseProtoMessage { #endif float position{0.0f}; enums::ValveOperation current_operation{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2856,8 +2856,8 @@ class ListEntitiesDateTimeResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_date_time_response"; } #endif - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2873,8 +2873,8 @@ class DateTimeStateResponse final : public StateResponseProtoMessage { #endif bool missing_state{false}; uint32_t epoch_seconds{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2907,8 +2907,8 @@ class ListEntitiesUpdateResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_update_response"; } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2931,8 +2931,8 @@ class UpdateStateResponse final : public StateResponseProtoMessage { StringRef title{}; StringRef release_summary{}; StringRef release_url{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2966,8 +2966,8 @@ class ZWaveProxyFrame final : public ProtoDecodableMessage { #endif const uint8_t *data{nullptr}; uint16_t data_len{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2985,8 +2985,8 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage { enums::ZWaveProxyRequestType type{}; const uint8_t *data{nullptr}; uint16_t data_len{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -3005,8 +3005,8 @@ class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_infrared_response"; } #endif uint32_t capabilities{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -3052,8 +3052,8 @@ class InfraredRFReceiveEvent final : public ProtoMessage { #endif uint32_t key{0}; const std::vector<int32_t> *timings{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 1441507406..e70b97196b 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -19,14 +19,6 @@ class APIServerConnectionBase : public ProtoService { public: #endif - bool send_message(const ProtoMessage &msg, uint8_t message_type) { -#ifdef HAS_PROTO_MESSAGE_DUMP - DumpBuffer dump_buf; - this->log_send_message_(msg.message_name(), msg.dump_to(dump_buf)); -#endif - return this->send_message_impl(msg, message_type); - } - virtual void on_hello_request(const HelloRequest &value){}; virtual void on_disconnect_request(){}; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 0352d7347b..06816fe3e0 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -359,11 +359,11 @@ void APIServer::on_update(update::UpdateEntity *obj) { #endif #ifdef USE_ZWAVE_PROXY -void APIServer::on_zwave_proxy_request(const esphome::api::ProtoMessage &msg) { +void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) { // We could add code to manage a second subscription type, but, since this message type is // very infrequent and small, we simply send it to all clients for (auto &c : this->clients_) - c->send_message(msg, api::ZWaveProxyRequest::MESSAGE_TYPE); + c->send_message(msg); } #endif @@ -531,7 +531,7 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString this->set_noise_psk(active_psk); for (auto &c : this->clients_) { DisconnectRequest req; - c->send_message(req, DisconnectRequest::MESSAGE_TYPE); + c->send_message(req); } }); } @@ -631,7 +631,7 @@ void APIServer::on_shutdown() { // Send disconnect requests to all connected clients for (auto &c : this->clients_) { DisconnectRequest req; - if (!c->send_message(req, DisconnectRequest::MESSAGE_TYPE)) { + if (!c->send_message(req)) { // If we can't send the disconnect request directly (tx_buffer full), // schedule it at the front of the batch so it will be sent with priority c->schedule_message_front_(nullptr, DisconnectRequest::MESSAGE_TYPE, DisconnectRequest::ESTIMATED_SIZE); diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 6eff2005f8..e6c10d1595 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -179,7 +179,7 @@ class APIServer : public Component, void on_update(update::UpdateEntity *obj) override; #endif #ifdef USE_ZWAVE_PROXY - void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg); + void on_zwave_proxy_request(const ZWaveProxyRequest &msg); #endif #ifdef USE_IR_RF void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings); diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index fe43a47c3b..0a94c1699b 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -94,7 +94,7 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie #ifdef USE_API_USER_DEFINED_ACTIONS bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { auto resp = service->encode_list_service_response(); - return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE); + return this->client_->send_message(resp); } #endif diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 750fff0810..702208d9de 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -364,7 +364,11 @@ class ProtoWriteBuffer { /// Encode a packed repeated sint32 field (zero-copy from vector) void encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values); /// Encode a nested message field (force=true for repeated, false for singular) - void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = true); + /// Templated so concrete message type is preserved for direct encode/calculate_size calls. + template<typename T> void encode_message(uint32_t field_id, const T &value, bool force = true); + // Non-template core for encode_message — all buffer work happens here + void encode_message(uint32_t field_id, uint32_t msg_length_bytes, const void *value, + void (*encode_fn)(const void *, ProtoWriteBuffer &), bool force); std::vector<uint8_t> *get_buffer() const { return buffer_; } protected: @@ -452,20 +456,20 @@ class DumpBuffer { class ProtoMessage { public: - // Default implementation for messages with no fields - virtual void encode(ProtoWriteBuffer &buffer) const {} - // Default implementation for messages with no fields - virtual void calculate_size(ProtoSize &size) const {} - // Convenience: calculate and return size directly (defined after ProtoSize) - uint32_t calculated_size() const; + // Non-virtual defaults for messages with no fields. + // Concrete message classes hide these with their own implementations. + // All call sites use templates to preserve the concrete type, so virtual + // dispatch is not needed. This eliminates per-message vtable entries for + // encode/calculate_size, saving ~1.3 KB of flash across all message types. + void encode(ProtoWriteBuffer &buffer) const {} + uint32_t calculate_size() const { return 0; } #ifdef HAS_PROTO_MESSAGE_DUMP virtual const char *dump_to(DumpBuffer &out) const = 0; virtual const char *message_name() const { return "unknown"; } #endif protected: - // Non-virtual: messages are never deleted polymorphically. - // Protected prevents accidental `delete base_ptr` (compile error). + // Non-virtual destructor is protected to prevent polymorphic deletion. ~ProtoMessage() = default; }; @@ -494,32 +498,7 @@ class ProtoDecodableMessage : public ProtoMessage { }; class ProtoSize { - private: - uint32_t total_size_ = 0; - public: - /** - * @brief ProtoSize class for Protocol Buffer serialization size calculation - * - * This class provides methods to calculate the exact byte counts needed - * for encoding various Protocol Buffer field types. The class now uses an - * object-based approach to reduce parameter passing overhead while keeping - * varint calculation methods static for external use. - * - * Implements Protocol Buffer encoding size calculation according to: - * https://protobuf.dev/programming-guides/encoding/ - * - * Key features: - * - Object-based approach reduces flash usage by eliminating parameter passing - * - Early-return optimization for zero/default values - * - Static varint methods for external callers - * - Specialized handling for different field types according to protobuf spec - */ - - ProtoSize() = default; - - uint32_t get_size() const { return total_size_; } - /** * @brief Calculates the size in bytes needed to encode a uint32_t value as a varint * @@ -616,320 +595,77 @@ class ProtoSize { return varint(tag); } - /** - * @brief Common parameters for all add_*_field methods - * - * All add_*_field methods follow these common patterns: - * * @param field_id_size Pre-calculated size of the field ID in bytes - * @param value The value to calculate size for (type varies) - * @param force Whether to calculate size even if the value is default/zero/empty - * - * Each method follows this implementation pattern: - * 1. Skip calculation if value is default (0, false, empty) and not forced - * 2. Calculate the size based on the field's encoding rules - * 3. Add the field_id_size + calculated value size to total_size - */ - - /** - * @brief Calculates and adds the size of an int32 field to the total message size - */ - inline void add_int32(uint32_t field_id_size, int32_t value) { - if (value != 0) { - add_int32_force(field_id_size, value); - } + // Static methods that RETURN size contribution (no ProtoSize object needed). + // Used by generated calculate_size() methods to accumulate into a plain uint32_t register. + static constexpr uint32_t calc_int32(uint32_t field_id_size, int32_t value) { + return value ? field_id_size + (value < 0 ? 10 : varint(static_cast<uint32_t>(value))) : 0; } - - /** - * @brief Calculates and adds the size of an int32 field to the total message size (force version) - */ - inline void add_int32_force(uint32_t field_id_size, int32_t value) { - // Always calculate size when forced - // Negative values are encoded as 10-byte varints in protobuf - total_size_ += field_id_size + (value < 0 ? 10 : varint(static_cast<uint32_t>(value))); + static constexpr uint32_t calc_int32_force(uint32_t field_id_size, int32_t value) { + return field_id_size + (value < 0 ? 10 : varint(static_cast<uint32_t>(value))); } - - /** - * @brief Calculates and adds the size of a uint32 field to the total message size - */ - inline void add_uint32(uint32_t field_id_size, uint32_t value) { - if (value != 0) { - add_uint32_force(field_id_size, value); - } + static constexpr uint32_t calc_uint32(uint32_t field_id_size, uint32_t value) { + return value ? field_id_size + varint(value) : 0; } - - /** - * @brief Calculates and adds the size of a uint32 field to the total message size (force version) - */ - inline void add_uint32_force(uint32_t field_id_size, uint32_t value) { - // Always calculate size when force is true - total_size_ += field_id_size + varint(value); + static constexpr uint32_t calc_uint32_force(uint32_t field_id_size, uint32_t value) { + return field_id_size + varint(value); } - - /** - * @brief Calculates and adds the size of a boolean field to the total message size - */ - inline void add_bool(uint32_t field_id_size, bool value) { - if (value) { - // Boolean fields always use 1 byte when true - total_size_ += field_id_size + 1; - } + static constexpr uint32_t calc_bool(uint32_t field_id_size, bool value) { return value ? field_id_size + 1 : 0; } + static constexpr uint32_t calc_bool_force(uint32_t field_id_size) { return field_id_size + 1; } + static constexpr uint32_t calc_float(uint32_t field_id_size, float value) { + return value != 0.0f ? field_id_size + 4 : 0; } - - /** - * @brief Calculates and adds the size of a boolean field to the total message size (force version) - */ - inline void add_bool_force(uint32_t field_id_size, bool value) { - // Always calculate size when force is true - // Boolean fields always use 1 byte - total_size_ += field_id_size + 1; + static constexpr uint32_t calc_fixed32(uint32_t field_id_size, uint32_t value) { + return value ? field_id_size + 4 : 0; } - - /** - * @brief Calculates and adds the size of a float field to the total message size - */ - inline void add_float(uint32_t field_id_size, float value) { - if (value != 0.0f) { - total_size_ += field_id_size + 4; - } + static constexpr uint32_t calc_sfixed32(uint32_t field_id_size, int32_t value) { + return value ? field_id_size + 4 : 0; } - - // NOTE: add_double_field removed - wire type 1 (64-bit: double) not supported - // to reduce overhead on embedded systems - - /** - * @brief Calculates and adds the size of a fixed32 field to the total message size - */ - inline void add_fixed32(uint32_t field_id_size, uint32_t value) { - if (value != 0) { - total_size_ += field_id_size + 4; - } + static constexpr uint32_t calc_sint32(uint32_t field_id_size, int32_t value) { + return value ? field_id_size + varint(encode_zigzag32(value)) : 0; } - - // NOTE: add_fixed64_field removed - wire type 1 (64-bit: fixed64) not supported - // to reduce overhead on embedded systems - - /** - * @brief Calculates and adds the size of a sfixed32 field to the total message size - */ - inline void add_sfixed32(uint32_t field_id_size, int32_t value) { - if (value != 0) { - total_size_ += field_id_size + 4; - } + static constexpr uint32_t calc_sint32_force(uint32_t field_id_size, int32_t value) { + return field_id_size + varint(encode_zigzag32(value)); } - - // NOTE: add_sfixed64_field removed - wire type 1 (64-bit: sfixed64) not supported - // to reduce overhead on embedded systems - - /** - * @brief Calculates and adds the size of a sint32 field to the total message size - * - * Sint32 fields use ZigZag encoding, which is more efficient for negative values. - */ - inline void add_sint32(uint32_t field_id_size, int32_t value) { - if (value != 0) { - add_sint32_force(field_id_size, value); - } + static constexpr uint32_t calc_int64(uint32_t field_id_size, int64_t value) { + return value ? field_id_size + varint(value) : 0; } - - /** - * @brief Calculates and adds the size of a sint32 field to the total message size (force version) - * - * Sint32 fields use ZigZag encoding, which is more efficient for negative values. - */ - inline void add_sint32_force(uint32_t field_id_size, int32_t value) { - // Always calculate size when force is true - // ZigZag encoding for sint32 - total_size_ += field_id_size + varint(encode_zigzag32(value)); + static constexpr uint32_t calc_int64_force(uint32_t field_id_size, int64_t value) { + return field_id_size + varint(value); } - - /** - * @brief Calculates and adds the size of an int64 field to the total message size - */ - inline void add_int64(uint32_t field_id_size, int64_t value) { - if (value != 0) { - add_int64_force(field_id_size, value); - } + static constexpr uint32_t calc_uint64(uint32_t field_id_size, uint64_t value) { + return value ? field_id_size + varint(value) : 0; } - - /** - * @brief Calculates and adds the size of an int64 field to the total message size (force version) - */ - inline void add_int64_force(uint32_t field_id_size, int64_t value) { - // Always calculate size when force is true - total_size_ += field_id_size + varint(value); + static constexpr uint32_t calc_uint64_force(uint32_t field_id_size, uint64_t value) { + return field_id_size + varint(value); } - - /** - * @brief Calculates and adds the size of a uint64 field to the total message size - */ - inline void add_uint64(uint32_t field_id_size, uint64_t value) { - if (value != 0) { - add_uint64_force(field_id_size, value); - } + static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) { + return len ? field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len) : 0; } - - /** - * @brief Calculates and adds the size of a uint64 field to the total message size (force version) - */ - inline void add_uint64_force(uint32_t field_id_size, uint64_t value) { - // Always calculate size when force is true - total_size_ += field_id_size + varint(value); + static constexpr uint32_t calc_length_force(uint32_t field_id_size, size_t len) { + return field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len); } - - // NOTE: sint64 support functions (add_sint64_field, add_sint64_field_force) removed - // sint64 type is not supported by ESPHome API to reduce overhead on embedded systems - - /** - * @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size - */ - inline void add_length(uint32_t field_id_size, size_t len) { - if (len != 0) { - add_length_force(field_id_size, len); - } + static constexpr uint32_t calc_sint64(uint32_t field_id_size, int64_t value) { + return value ? field_id_size + varint(encode_zigzag64(value)) : 0; } - - /** - * @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size (repeated - * field version) - */ - inline void add_length_force(uint32_t field_id_size, size_t len) { - // Always calculate size when force is true - // Field ID + length varint + data bytes - total_size_ += field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len); + static constexpr uint32_t calc_sint64_force(uint32_t field_id_size, int64_t value) { + return field_id_size + varint(encode_zigzag64(value)); } - - /** - * @brief Adds a pre-calculated size directly to the total - * - * This is used when we can calculate the total size by multiplying the number - * of elements by the bytes per element (for repeated fixed-size types like float, fixed32, etc.) - * - * @param size The pre-calculated total size to add - */ - inline void add_precalculated_size(uint32_t size) { total_size_ += size; } - - /** - * @brief Calculates and adds the size of a nested message field to the total message size - * - * This helper function directly updates the total_size reference if the nested size - * is greater than zero. - * - * @param nested_size The pre-calculated size of the nested message - */ - inline void add_message_field(uint32_t field_id_size, uint32_t nested_size) { - if (nested_size != 0) { - add_message_field_force(field_id_size, nested_size); - } + static constexpr uint32_t calc_fixed64(uint32_t field_id_size, uint64_t value) { + return value ? field_id_size + 8 : 0; } - - /** - * @brief Calculates and adds the size of a nested message field to the total message size (force version) - * - * @param nested_size The pre-calculated size of the nested message - */ - inline void add_message_field_force(uint32_t field_id_size, uint32_t nested_size) { - // Always calculate size when force is true - // Field ID + length varint + nested message content - total_size_ += field_id_size + varint(nested_size) + nested_size; + static constexpr uint32_t calc_sfixed64(uint32_t field_id_size, int64_t value) { + return value ? field_id_size + 8 : 0; } - - /** - * @brief Calculates and adds the size of a nested message field to the total message size - * - * This version takes a ProtoMessage object, calculates its size internally, - * and updates the total_size reference. This eliminates the need for a temporary variable - * at the call site. - * - * @param message The nested message object - */ - inline void add_message_object(uint32_t field_id_size, const ProtoMessage &message) { - // Calculate nested message size by creating a temporary ProtoSize - ProtoSize nested_calc; - message.calculate_size(nested_calc); - uint32_t nested_size = nested_calc.get_size(); - - // Use the base implementation with the calculated nested_size - add_message_field(field_id_size, nested_size); + static constexpr uint32_t calc_message(uint32_t field_id_size, uint32_t nested_size) { + return nested_size ? field_id_size + varint(nested_size) + nested_size : 0; } - - /** - * @brief Calculates and adds the size of a nested message field to the total message size (force version) - * - * @param message The nested message object - */ - inline void add_message_object_force(uint32_t field_id_size, const ProtoMessage &message) { - // Calculate nested message size by creating a temporary ProtoSize - ProtoSize nested_calc; - message.calculate_size(nested_calc); - uint32_t nested_size = nested_calc.get_size(); - - // Use the base implementation with the calculated nested_size - add_message_field_force(field_id_size, nested_size); - } - - /** - * @brief Calculates and adds the sizes of all messages in a repeated field to the total message size - * - * This helper processes a vector of message objects, calculating the size for each message - * and adding it to the total size. - * - * @tparam MessageType The type of the nested messages in the vector - * @param messages Vector of message objects - */ - template<typename MessageType> - inline void add_repeated_message(uint32_t field_id_size, const std::vector<MessageType> &messages) { - // Skip if the vector is empty - if (!messages.empty()) { - // Use the force version for all messages in the repeated field - for (const auto &message : messages) { - add_message_object_force(field_id_size, message); - } - } - } - - /** - * @brief Calculates and adds the sizes of all messages in a repeated field to the total message size (FixedVector - * version) - * - * @tparam MessageType The type of the nested messages in the FixedVector - * @param messages FixedVector of message objects - */ - template<typename MessageType> - inline void add_repeated_message(uint32_t field_id_size, const FixedVector<MessageType> &messages) { - // Skip if the fixed vector is empty - if (!messages.empty()) { - // Use the force version for all messages in the repeated field - for (const auto &message : messages) { - add_message_object_force(field_id_size, message); - } - } - } - - /** - * @brief Calculate size of a packed repeated sint32 field - */ - inline void add_packed_sint32(uint32_t field_id_size, const std::vector<int32_t> &values) { - if (values.empty()) - return; - - size_t packed_size = 0; - for (int value : values) { - packed_size += varint(encode_zigzag32(value)); - } - - // field_id + length varint + packed data - total_size_ += field_id_size + varint(static_cast<uint32_t>(packed_size)) + static_cast<uint32_t>(packed_size); + static constexpr uint32_t calc_message_force(uint32_t field_id_size, uint32_t nested_size) { + return field_id_size + varint(nested_size) + nested_size; } }; // Implementation of methods that depend on ProtoSize being fully defined -inline uint32_t ProtoMessage::calculated_size() const { - ProtoSize size; - this->calculate_size(size); - return size.get_size(); -} - // Implementation of encode_packed_sint32 - must be after ProtoSize is defined inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values) { if (values.empty()) @@ -949,31 +685,30 @@ inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std: } } -// Implementation of encode_message - must be after ProtoMessage is defined -inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) { - // Calculate the message size first - ProtoSize msg_size; - value.calculate_size(msg_size); - uint32_t msg_length_bytes = msg_size.get_size(); +// Encode thunk — converts void* back to concrete type for direct encode() call +template<typename T> void proto_encode_msg(const void *msg, ProtoWriteBuffer &buf) { + static_cast<const T *>(msg)->encode(buf); +} - // Skip empty singular messages (matches add_message_field which skips when nested_size == 0) - // Repeated messages (force=true) are always encoded since an empty item is meaningful +// Implementation of encode_message - must be after ProtoMessage is defined +template<typename T> inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const T &value, bool force) { + this->encode_message(field_id, value.calculate_size(), &value, &proto_encode_msg<T>, force); +} + +// Non-template core for encode_message +inline void ProtoWriteBuffer::encode_message(uint32_t field_id, uint32_t msg_length_bytes, const void *value, + void (*encode_fn)(const void *, ProtoWriteBuffer &), bool force) { if (msg_length_bytes == 0 && !force) return; - - this->encode_field_raw(field_id, 2); // type 2: Length-delimited message - - // Write the length varint directly through pos_ + this->encode_field_raw(field_id, 2); this->encode_varint_raw(msg_length_bytes); - - // Encode nested message - pos_ advances directly through the reference #ifdef ESPHOME_DEBUG_API uint8_t *start = this->pos_; - value.encode(*this); + encode_fn(value, *this); if (static_cast<uint32_t>(this->pos_ - start) != msg_length_bytes) this->debug_check_encode_size_(field_id, msg_length_bytes, this->pos_ - start); #else - value.encode(*this); + encode_fn(value, *this); #endif } @@ -993,14 +728,6 @@ class ProtoService { virtual void on_no_setup_connection() = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0; virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0; - /** - * Send a protobuf message by calculating its size, allocating a buffer, encoding, and sending. - * This is the implementation method - callers should use send_message() which adds logging. - * @param msg The protobuf message to send. - * @param message_type The message type identifier. - * @return True if the message was sent successfully, false otherwise. - */ - virtual bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) = 0; // Authentication helper methods inline bool check_connection_setup_() { diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index b2000fbd94..21573f0184 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -183,10 +183,7 @@ void BluetoothConnection::send_service_for_discovery_() { static constexpr size_t MAX_PACKET_SIZE = 1360; // Keep running total of actual message size - size_t current_size = 0; - api::ProtoSize size; - resp.calculate_size(size); - current_size = size.get_size(); + size_t current_size = resp.calculate_size(); while (this->send_service_ < this->service_count_) { esp_gattc_service_elem_t service_result; @@ -302,9 +299,7 @@ void BluetoothConnection::send_service_for_discovery_() { } // end if (total_char_count > 0) // Calculate the actual size of just this service - api::ProtoSize service_sizer; - service_resp.calculate_size(service_sizer); - size_t service_size = service_sizer.get_size() + 1; // +1 for field tag + size_t service_size = service_resp.calculate_size() + 1; // +1 for field tag // Check if adding this service would exceed the limit if (current_size + service_size > MAX_PACKET_SIZE) { @@ -333,7 +328,7 @@ void BluetoothConnection::send_service_for_discovery_() { } // Send the message with dynamically batched services - api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE); + api_conn->send_message(resp); } void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) { @@ -422,7 +417,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga resp.address = this->address_; resp.handle = param->read.handle; resp.set_data(param->read.value, param->read.value_len); - api_connection->send_message(resp, api::BluetoothGATTReadResponse::MESSAGE_TYPE); + api_connection->send_message(resp); break; } case ESP_GATTC_WRITE_CHAR_EVT: @@ -438,7 +433,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga api::BluetoothGATTWriteResponse resp; resp.address = this->address_; resp.handle = param->write.handle; - api_connection->send_message(resp, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); + api_connection->send_message(resp); break; } case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { @@ -454,7 +449,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->unreg_for_notify.handle; - api_connection->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); + api_connection->send_message(resp); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { @@ -470,7 +465,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->reg_for_notify.handle; - api_connection->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); + api_connection->send_message(resp); break; } case ESP_GATTC_NOTIFY_EVT: { @@ -483,7 +478,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga resp.address = this->address_; resp.handle = param->notify.handle; resp.set_data(param->notify.value, param->notify.value_len); - api_connection->send_message(resp, api::BluetoothGATTNotifyDataResponse::MESSAGE_TYPE); + api_connection->send_message(resp); break; } default: diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index cab328e2f5..21da4ead14 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -44,7 +44,7 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta resp.configured_mode = this->configured_scan_active_ ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE : api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE; - this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE); + this->api_connection_->send_message(resp); } void BluetoothProxy::log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state) { @@ -112,7 +112,7 @@ void BluetoothProxy::flush_pending_advertisements() { return; // Send the message - this->api_connection_->send_message(this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE); + this->api_connection_->send_message(this->response_); ESP_LOGV(TAG, "Sent batch of %u BLE advertisements", this->response_.advertisements_len); @@ -269,7 +269,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest call.success = ret == ESP_OK; call.error = ret; - this->api_connection_->send_message(call, api::BluetoothDeviceClearCacheResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); break; } @@ -389,7 +389,7 @@ void BluetoothProxy::send_device_connection(uint64_t address, bool connected, ui call.connected = connected; call.mtu = mtu; call.error = error; - this->api_connection_->send_message(call, api::BluetoothDeviceConnectionResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); } void BluetoothProxy::send_connections_free() { if (this->api_connection_ != nullptr) { @@ -398,7 +398,7 @@ void BluetoothProxy::send_connections_free() { } void BluetoothProxy::send_connections_free(api::APIConnection *api_connection) { - api_connection->send_message(this->connections_free_response_, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE); + api_connection->send_message(this->connections_free_response_); } void BluetoothProxy::send_gatt_services_done(uint64_t address) { @@ -406,7 +406,7 @@ void BluetoothProxy::send_gatt_services_done(uint64_t address) { return; api::BluetoothGATTGetServicesDoneResponse call; call.address = address; - this->api_connection_->send_message(call, api::BluetoothGATTGetServicesDoneResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); } void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) { @@ -416,7 +416,7 @@ void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_ call.address = address; call.handle = handle; call.error = error; - this->api_connection_->send_message(call, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); } void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) { @@ -427,7 +427,7 @@ void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_ call.paired = paired; call.error = error; - this->api_connection_->send_message(call, api::BluetoothDevicePairingResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); } void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) { @@ -438,7 +438,7 @@ void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_e call.success = success; call.error = error; - this->api_connection_->send_message(call, api::BluetoothDeviceUnpairingResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); } void BluetoothProxy::bluetooth_scanner_set_mode(bool active) { diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index d6cbfd4b21..51d52a8af8 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -251,8 +251,7 @@ void VoiceAssistant::loop() { } #endif - if (this->api_client_ == nullptr || - !this->api_client_->send_message(msg, api::VoiceAssistantRequest::MESSAGE_TYPE)) { + if (this->api_client_ == nullptr || !this->api_client_->send_message(msg)) { ESP_LOGW(TAG, "Could not request start"); this->error_trigger_.trigger("not-connected", "Could not request start"); this->continuous_ = false; @@ -275,7 +274,7 @@ void VoiceAssistant::loop() { api::VoiceAssistantAudio msg; msg.data = this->send_buffer_; msg.data_len = read_bytes; - this->api_client_->send_message(msg, api::VoiceAssistantAudio::MESSAGE_TYPE); + this->api_client_->send_message(msg); } else { if (!this->udp_socket_running_) { if (!this->start_udp_socket_()) { @@ -354,7 +353,7 @@ void VoiceAssistant::loop() { api::VoiceAssistantAnnounceFinished msg; msg.success = true; - this->api_client_->send_message(msg, api::VoiceAssistantAnnounceFinished::MESSAGE_TYPE); + this->api_client_->send_message(msg); break; } } @@ -612,7 +611,7 @@ void VoiceAssistant::signal_stop_() { ESP_LOGD(TAG, "Signaling stop"); api::VoiceAssistantRequest msg; msg.start = false; - this->api_client_->send_message(msg, api::VoiceAssistantRequest::MESSAGE_TYPE); + this->api_client_->send_message(msg); } void VoiceAssistant::start_playback_timeout_() { @@ -622,7 +621,7 @@ void VoiceAssistant::start_playback_timeout_() { api::VoiceAssistantAnnounceFinished msg; msg.success = true; - this->api_client_->send_message(msg, api::VoiceAssistantAnnounceFinished::MESSAGE_TYPE); + this->api_client_->send_message(msg); }); } diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index b0836ac072..9e5c57814d 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -119,7 +119,7 @@ void ZWaveProxy::process_uart_() { // If this is a data frame, use frame length indicator + 2 (for SoF + checksum), else assume 1 for ACK/NAK/CAN this->outgoing_proto_msg_.data_len = this->buffer_[0] == ZWAVE_FRAME_TYPE_START ? this->buffer_[1] + 2 : 1; } - this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE); + this->api_connection_->send_message(this->outgoing_proto_msg_); } } } @@ -209,7 +209,7 @@ void ZWaveProxy::send_homeid_changed_msg_(api::APIConnection *conn) { msg.data_len = this->home_id_.size(); if (conn != nullptr) { // Send to specific connection - conn->send_message(msg, api::ZWaveProxyRequest::MESSAGE_TYPE); + conn->send_message(msg); } else if (api::global_api_server != nullptr) { // We could add code to manage a second subscription type, but, since this message is // very infrequent and small, we simply send it to all clients @@ -346,7 +346,7 @@ void ZWaveProxy::parse_start_(uint8_t byte) { this->buffer_[0] = byte; this->outgoing_proto_msg_.data = this->buffer_.data(); this->outgoing_proto_msg_.data_len = 1; - this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE); + this->api_connection_->send_message(this->outgoing_proto_msg_); } } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 9c9cda4d36..85352689e6 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -270,18 +270,21 @@ class TypeInfo(ABC): def _get_simple_size_calculation( self, name: str, force: bool, base_method: str, value_expr: str = None ) -> str: - """Helper for simple size calculations. + """Helper for simple size calculations using static ProtoSize methods. Args: name: Field name force: Whether this is for a repeated field - base_method: Base method name (e.g., "add_int32") + base_method: Base method name (e.g., "int32") value_expr: Optional value expression (defaults to name) """ field_id_size = self.calculate_field_id_size() - method = f"{base_method}_force" if force else base_method + method = f"calc_{base_method}_force" if force else f"calc_{base_method}" + # calc_bool_force only takes field_id_size (no value needed - bool is always 1 byte) + if base_method == "bool" and force: + return f"size += ProtoSize::{method}({field_id_size});" value = value_expr or name - return f"size.{method}({field_id_size}, {value});" + return f"size += ProtoSize::{method}({field_id_size}, {value});" @abstractmethod def get_size_calculation(self, name: str, force: bool = False) -> str: @@ -410,7 +413,7 @@ class DoubleType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - return f"size.add_double({field_id_size}, {name});" + return f"size += ProtoSize::calc_fixed64({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 8 @@ -434,7 +437,7 @@ class FloatType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - return f"size.add_float({field_id_size}, {name});" + return f"size += ProtoSize::calc_float({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 4 @@ -457,7 +460,7 @@ class Int64Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_int64") + return self._get_simple_size_calculation(name, force, "int64") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -477,7 +480,7 @@ class UInt64Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_uint64") + return self._get_simple_size_calculation(name, force, "uint64") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -497,7 +500,7 @@ class Int32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_int32") + return self._get_simple_size_calculation(name, force, "int32") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -518,7 +521,7 @@ class Fixed64Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - return f"size.add_fixed64({field_id_size}, {name});" + return f"size += ProtoSize::calc_fixed64({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 8 @@ -542,7 +545,7 @@ class Fixed32Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - return f"size.add_fixed32({field_id_size}, {name});" + return f"size += ProtoSize::calc_fixed32({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 4 @@ -563,7 +566,7 @@ class BoolType(TypeInfo): return f"out.append(YESNO({name}));" def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_bool") + return self._get_simple_size_calculation(name, force, "bool") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 1 # field ID + 1 byte @@ -647,18 +650,18 @@ class StringType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: # For SOURCE_CLIENT only messages, use the string field directly if not self._needs_encode: - return self._get_simple_size_calculation(name, force, "add_length") + return self._get_simple_size_calculation(name, force, "length") # Check if this is being called from a repeated field context # In that case, 'name' will be 'it' and we need to use the repeated version if name == "it": - # For repeated fields, we need to use add_length_force which includes field ID + # For repeated fields, we need to use length_force which includes field ID field_id_size = self.calculate_field_id_size() - return f"size.add_length_force({field_id_size}, it.size());" + return f"size += ProtoSize::calc_length_force({field_id_size}, it.size());" # For messages that need encoding, use the StringRef size field_id_size = self.calculate_field_id_size() - return f"size.add_length({field_id_size}, this->{self.field_name}_ref_.size());" + return f"size += ProtoSize::calc_length({field_id_size}, this->{self.field_name}_ref_.size());" def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string @@ -721,7 +724,9 @@ class MessageType(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_message_object") + field_id_size = self.calculate_field_id_size() + method = "calc_message_force" if force else "calc_message" + return f"size += ProtoSize::{method}({field_id_size}, {name}.calculate_size());" def get_estimated_size(self) -> int: # For message types, we can't easily estimate the submessage size without @@ -822,7 +827,7 @@ class BytesType(TypeInfo): ) def get_size_calculation(self, name: str, force: bool = False) -> str: - return f"size.add_length({self.calculate_field_id_size()}, this->{self.field_name}_len_);" + return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}_len_);" def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes @@ -897,7 +902,7 @@ class PointerToBytesBufferType(PointerToBufferTypeBase): ) def get_size_calculation(self, name: str, force: bool = False) -> str: - return f"size.add_length({self.calculate_field_id_size()}, this->{self.field_name}_len);" + return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}_len);" class PointerToStringBufferType(PointerToBufferTypeBase): @@ -939,7 +944,7 @@ class PointerToStringBufferType(PointerToBufferTypeBase): return f'dump_field(out, "{self.name}", this->{self.field_name});' def get_size_calculation(self, name: str, force: bool = False) -> str: - return f"size.add_length({self.calculate_field_id_size()}, this->{self.field_name}.size());" + return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}.size());" def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string @@ -1103,9 +1108,9 @@ class FixedArrayBytesType(TypeInfo): if force: # For repeated fields, always calculate size (no zero check) - return f"size.add_length_force({field_id_size}, {length_field});" - # For non-repeated fields, add_length already checks for zero - return f"size.add_length({field_id_size}, {length_field});" + return f"size += ProtoSize::calc_length_force({field_id_size}, {length_field});" + # For non-repeated fields, length already checks for zero + return f"size += ProtoSize::calc_length({field_id_size}, {length_field});" def get_estimated_size(self) -> int: # Estimate based on typical BLE advertisement size @@ -1132,7 +1137,7 @@ class UInt32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_uint32") + return self._get_simple_size_calculation(name, force, "uint32") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -1168,7 +1173,7 @@ class EnumType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: return self._get_simple_size_calculation( - name, force, "add_uint32", f"static_cast<uint32_t>({name})" + name, force, "uint32", f"static_cast<uint32_t>({name})" ) def get_estimated_size(self) -> int: @@ -1190,7 +1195,7 @@ class SFixed32Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - return f"size.add_sfixed32({field_id_size}, {name});" + return f"size += ProtoSize::calc_sfixed32({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 4 @@ -1214,7 +1219,7 @@ class SFixed64Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - return f"size.add_sfixed64({field_id_size}, {name});" + return f"size += ProtoSize::calc_sfixed64({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 8 @@ -1237,7 +1242,7 @@ class SInt32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_sint32") + return self._get_simple_size_calculation(name, force, "sint32") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -1257,7 +1262,7 @@ class SInt64Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_sint64") + return self._get_simple_size_calculation(name, force, "sint64") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -1694,11 +1699,17 @@ class RepeatedTypeInfo(TypeInfo): # For repeated fields, we always need to pass force=True to the underlying type's calculation # This is because the encode method always sets force=true for repeated fields - # Handle message types separately as they use a dedicated helper + # Handle message types separately - generate inline loop if isinstance(self._ti, MessageType): field_id_size = self._ti.calculate_field_id_size() - container = f"*{name}" if self._use_pointer else name - return f"size.add_repeated_message({field_id_size}, {container});" + container_ref = f"*{name}" if self._use_pointer else name + empty_check = f"{name}->empty()" if self._use_pointer else f"{name}.empty()" + o = f"if (!{empty_check}) {{\n" + o += f" for (const auto &it : {container_ref}) {{\n" + o += f" size += ProtoSize::calc_message_force({field_id_size}, it.calculate_size());\n" + o += " }\n" + o += "}" + return o # For non-message types, generate size calculation with iteration container_ref = f"*{name}" if self._use_pointer else name @@ -1713,14 +1724,14 @@ class RepeatedTypeInfo(TypeInfo): field_id_size = self._ti.calculate_field_id_size() bytes_per_element = field_id_size + num_bytes size_expr = f"{name}->size()" if self._use_pointer else f"{name}.size()" - o += f" size.add_precalculated_size({size_expr} * {bytes_per_element});\n" + o += f" size += {size_expr} * {bytes_per_element};\n" else: # Other types need the actual value # Special handling for const char* elements if self._use_pointer and "const char" in self._container_no_template: field_id_size = self.calculate_field_id_size() o += f" for (const char *it : {container_ref}) {{\n" - o += f" size.add_length_force({field_id_size}, strlen(it));\n" + o += f" size += ProtoSize::calc_length_force({field_id_size}, strlen(it));\n" else: auto_ref = "" if self._ti_is_bool else "&" o += f" for (const auto {auto_ref}it : {container_ref}) {{\n" @@ -2233,23 +2244,19 @@ def build_message_type( o += indent("\n".join(encode)) + "\n" o += "}\n" cpp += o - prot = "void encode(ProtoWriteBuffer &buffer) const override;" + prot = "void encode(ProtoWriteBuffer &buffer) const;" public_content.append(prot) # If no fields to encode or message doesn't need encoding, the default implementation in ProtoMessage will be used # Add calculate_size method only if this message needs encoding and has fields if needs_encode and size_calc: - o = f"void {desc.name}::calculate_size(ProtoSize &size) const {{" - # For a single field, just inline it for simplicity - if len(size_calc) == 1 and len(size_calc[0]) + len(o) + 3 < 120: - o += f" {size_calc[0]} }}\n" - else: - # For multiple fields - o += "\n" - o += indent("\n".join(size_calc)) + "\n" - o += "}\n" + o = f"uint32_t {desc.name}::calculate_size() const {{\n" + o += " uint32_t size = 0;\n" + o += indent("\n".join(size_calc)) + "\n" + o += " return size;\n" + o += "}\n" cpp += o - prot = "void calculate_size(ProtoSize &size) const override;" + prot = "uint32_t calculate_size() const;" public_content.append(prot) # If no fields to calculate size for or message doesn't need encoding, the default implementation in ProtoMessage will be used @@ -2933,14 +2940,8 @@ static const char *const TAG = "api.service"; hpp += " public:\n" hpp += "#endif\n\n" - # Add non-template send_message method - hpp += " bool send_message(const ProtoMessage &msg, uint8_t message_type) {\n" - hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" - hpp += " DumpBuffer dump_buf;\n" - hpp += " this->log_send_message_(msg.message_name(), msg.dump_to(dump_buf));\n" - hpp += "#endif\n" - hpp += " return this->send_message_impl(msg, message_type);\n" - hpp += " }\n\n" + # send_message is now a template on APIConnection directly + # No non-template send_message method needed here # Add logging helper method implementations to cpp cpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" From 3c7956e72d34f3cc3fb9f5100afc09e974982f26 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:04:00 -0500 Subject: [PATCH 411/609] [multiple] Add default initializers to uninitialized member variables (#14556) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/bedjet/bedjet_hub.h | 8 ++++---- .../components/current_based/current_based_cover.h | 12 ++++++------ esphome/components/deep_sleep/deep_sleep_component.h | 2 +- esphome/components/max6956/max6956.h | 4 ++-- esphome/components/ms8607/ms8607.h | 8 ++++---- .../remote_transmitter/remote_transmitter.h | 2 +- esphome/components/sen5x/sen5x.h | 8 ++++---- esphome/components/sim800l/sim800l.h | 10 +++++----- 8 files changed, 27 insertions(+), 27 deletions(-) diff --git a/esphome/components/bedjet/bedjet_hub.h b/esphome/components/bedjet/bedjet_hub.h index 6258795b02..59b0af93ad 100644 --- a/esphome/components/bedjet/bedjet_hub.h +++ b/esphome/components/bedjet/bedjet_hub.h @@ -164,10 +164,10 @@ class BedJetHub : public esphome::ble_client::BLEClientNode, public PollingCompo std::unique_ptr<BedjetCodec> codec_; bool discover_characteristics_(); - uint16_t char_handle_cmd_; - uint16_t char_handle_name_; - uint16_t char_handle_status_; - uint16_t config_descr_status_; + uint16_t char_handle_cmd_{0}; + uint16_t char_handle_name_{0}; + uint16_t char_handle_status_{0}; + uint16_t config_descr_status_{0}; uint8_t write_notify_config_descriptor_(bool enable); }; diff --git a/esphome/components/current_based/current_based_cover.h b/esphome/components/current_based/current_based_cover.h index 76bd85cdf7..40b39517e4 100644 --- a/esphome/components/current_based/current_based_cover.h +++ b/esphome/components/current_based/current_based_cover.h @@ -67,21 +67,21 @@ class CurrentBasedCover : public cover::Cover, public Component { sensor::Sensor *open_sensor_{nullptr}; Trigger<> open_trigger_; - float open_moving_current_threshold_; + float open_moving_current_threshold_{0.0f}; float open_obstacle_current_threshold_{FLT_MAX}; - uint32_t open_duration_; + uint32_t open_duration_{0}; sensor::Sensor *close_sensor_{nullptr}; Trigger<> close_trigger_; - float close_moving_current_threshold_; + float close_moving_current_threshold_{0.0f}; float close_obstacle_current_threshold_{FLT_MAX}; - uint32_t close_duration_; + uint32_t close_duration_{0}; uint32_t max_duration_{UINT32_MAX}; bool malfunction_detection_{true}; Trigger<> malfunction_trigger_; - uint32_t start_sensing_delay_; - float obstacle_rollback_; + uint32_t start_sensing_delay_{0}; + float obstacle_rollback_{0.0f}; Trigger<> *prev_command_trigger_{nullptr}; uint32_t last_recompute_time_{0}; diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 1998b815f3..14713d51a1 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -145,7 +145,7 @@ class DeepSleepComponent : public Component { #endif // USE_BK72XX #ifdef USE_ESP32 - InternalGPIOPin *wakeup_pin_; + InternalGPIOPin *wakeup_pin_{nullptr}; WakeupPinMode wakeup_pin_mode_{WAKEUP_PIN_MODE_IGNORE}; #if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) diff --git a/esphome/components/max6956/max6956.h b/esphome/components/max6956/max6956.h index 0c609b0b43..31f97c11f8 100644 --- a/esphome/components/max6956/max6956.h +++ b/esphome/components/max6956/max6956.h @@ -63,8 +63,8 @@ class MAX6956 : public Component, public i2c::I2CDevice { bool read_reg_(uint8_t reg, uint8_t *value); // write a value to a given register bool write_reg_(uint8_t reg, uint8_t value); - max6956::MAX6956CURRENTMODE brightness_mode_; - uint8_t global_brightness_; + max6956::MAX6956CURRENTMODE brightness_mode_{}; + uint8_t global_brightness_{0}; private: int8_t prev_bright_[28] = {0}; diff --git a/esphome/components/ms8607/ms8607.h b/esphome/components/ms8607/ms8607.h index ceb3dd22c8..2888b6cdd2 100644 --- a/esphome/components/ms8607/ms8607.h +++ b/esphome/components/ms8607/ms8607.h @@ -67,9 +67,9 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice { /// use raw temperature & pressure to calculate & publish values void calculate_values_(uint32_t raw_temperature, uint32_t raw_pressure); - sensor::Sensor *temperature_sensor_; - sensor::Sensor *pressure_sensor_; - sensor::Sensor *humidity_sensor_; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; /** I2CDevice object to communicate with secondary I2C address for the humidity sensor * @@ -77,7 +77,7 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice { * * Default address for humidity is 0x40 */ - MS8607HumidityDevice *humidity_device_; + MS8607HumidityDevice *humidity_device_{nullptr}; /// This device's pressure & temperature calibration values, read from PROM struct CalibrationValues { diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index aee52ea170..6b4ebfe24b 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -96,7 +96,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, bool inverted_{false}; bool non_blocking_{false}; #endif - uint8_t carrier_duty_percent_; + uint8_t carrier_duty_percent_{50}; Trigger<> transmit_trigger_; Trigger<> complete_trigger_; diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h index e3bf931b41..a9d4da86b8 100644 --- a/esphome/components/sen5x/sen5x.h +++ b/esphome/components/sen5x/sen5x.h @@ -104,12 +104,12 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri char serial_number_[17] = "UNKNOWN"; uint16_t voc_baseline_state_[4]{0}; - uint32_t voc_baseline_time_; - uint16_t firmware_version_; + uint32_t voc_baseline_time_{0}; + uint16_t firmware_version_{0}; Sen5xType type_{Sen5xType::UNKNOWN}; - ERRORCODE error_code_; + ERRORCODE error_code_{ERRORCODE::UNKNOWN}; bool initialized_{false}; - bool store_baseline_; + bool store_baseline_{false}; sensor::Sensor *pm_1_0_sensor_{nullptr}; sensor::Sensor *pm_2_5_sensor_{nullptr}; diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h index a2da686ce1..e9e2f66d78 100644 --- a/esphome/components/sim800l/sim800l.h +++ b/esphome/components/sim800l/sim800l.h @@ -107,11 +107,11 @@ class Sim800LComponent : public uart::UARTDevice, public PollingComponent { std::string recipient_; std::string outgoing_message_; std::string ussd_; - bool send_pending_; - bool dial_pending_; - bool connect_pending_; - bool disconnect_pending_; - bool send_ussd_pending_; + bool send_pending_{false}; + bool dial_pending_{false}; + bool connect_pending_{false}; + bool disconnect_pending_{false}; + bool send_ussd_pending_{false}; uint8_t call_state_{6}; CallbackManager<void(std::string, std::string)> sms_received_callback_; From 3db436e48e0cafd48711bd782600cbea7a5f9adc Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:05:34 -0500 Subject: [PATCH 412/609] [esp32_ble_server][espnow][time] Fix logic bugs (#14553) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .../components/esp32_ble_server/ble_server.cpp | 18 +++++++----------- esphome/components/espnow/automation.h | 4 ++-- esphome/components/time/real_time_clock.cpp | 2 +- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index f292cf8722..ecc53e197f 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -7,6 +7,7 @@ #ifdef USE_ESP32 +#include <algorithm> #include <nvs_flash.h> #include <freertos/FreeRTOSConfig.h> #include <esp_bt_main.h> @@ -38,21 +39,16 @@ void BLEServer::loop() { case RUNNING: { // Start all services that are pending to start if (!this->services_to_start_.empty()) { - uint16_t index_to_remove = 0; - // Iterate over the services to start - for (unsigned i = 0; i < this->services_to_start_.size(); i++) { - BLEService *service = this->services_to_start_[i]; + for (auto &service : this->services_to_start_) { if (service->is_created()) { service->start(); // Needs to be called once per characteristic in the service - } else { - index_to_remove = i + 1; } } - // Remove the services that have been started - if (index_to_remove > 0) { - this->services_to_start_.erase(this->services_to_start_.begin(), - this->services_to_start_.begin() + index_to_remove - 1); - } + // Remove services that have been started + this->services_to_start_.erase( + std::remove_if(this->services_to_start_.begin(), this->services_to_start_.end(), + [](BLEService *service) { return service->is_starting() || service->is_running(); }), + this->services_to_start_.end()); } break; } diff --git a/esphome/components/espnow/automation.h b/esphome/components/espnow/automation.h index 0b26681400..0fbb14e388 100644 --- a/esphome/components/espnow/automation.h +++ b/esphome/components/espnow/automation.h @@ -138,7 +138,7 @@ class OnReceiveTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, protected: bool has_address_{false}; - const uint8_t *address_[ESP_NOW_ETH_ALEN]; + uint8_t address_[ESP_NOW_ETH_ALEN]; }; class OnUnknownPeerTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>, public ESPNowUnknownPeerHandler { @@ -167,7 +167,7 @@ class OnBroadcastedTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_ protected: bool has_address_{false}; - const uint8_t *address_[ESP_NOW_ETH_ALEN]; + uint8_t address_[ESP_NOW_ETH_ALEN]; }; } // namespace esphome::espnow diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 2e758ad8e7..566344fa88 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -90,7 +90,7 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { }; struct timezone tz = {0, 0}; int ret = settimeofday(&timev, &tz); - if (ret == EINVAL) { + if (ret != 0 && errno == EINVAL) { // Some ESP8266 frameworks abort when timezone parameter is not NULL // while ESP32 expects it not to be NULL ret = settimeofday(&timev, nullptr); From 7b8ba9bf206f319f614ce52d4d80c5e649eb73ee Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:14:12 -0500 Subject: [PATCH 413/609] [multiple] Fix cast/operator precedence bugs (#14560) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/datetime/date_entity.h | 2 +- esphome/components/es7210/es7210.cpp | 2 +- esphome/components/sgp4x/sgp4x.cpp | 2 +- esphome/components/tsl2591/tsl2591.cpp | 4 +++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/esphome/components/datetime/date_entity.h b/esphome/components/datetime/date_entity.h index cbf2b85506..8233e809a1 100644 --- a/esphome/components/datetime/date_entity.h +++ b/esphome/components/datetime/date_entity.h @@ -91,7 +91,7 @@ class DateCall { DateEntity *parent_; - optional<int16_t> year_; + optional<uint16_t> year_; optional<uint8_t> month_; optional<uint8_t> day_; }; diff --git a/esphome/components/es7210/es7210.cpp b/esphome/components/es7210/es7210.cpp index 1358121c1b..4371075fa9 100644 --- a/esphome/components/es7210/es7210.cpp +++ b/esphome/components/es7210/es7210.cpp @@ -172,7 +172,7 @@ uint8_t ES7210::es7210_gain_reg_value_(float mic_gain) { // reg: 12 - 34.5dB, 13 - 36dB, 14 - 37.5dB mic_gain += 0.5; if (mic_gain <= 33.0) { - return (uint8_t) mic_gain / 3; + return (uint8_t) (mic_gain / 3); } if (mic_gain < 36.0) { return 12; diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index 44d0a54080..cb41e374f8 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -199,7 +199,7 @@ void SGP4xComponent::measure_raw_() { response_words = 2; } } - uint16_t rhticks = llround((uint16_t) ((humidity * 65535) / 100)); + uint16_t rhticks = (uint16_t) llround((humidity * 65535) / 100); uint16_t tempticks = (uint16_t) (((temperature + 45) * 65535) / 175); // first parameter are the relative humidity ticks data[0] = rhticks; diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp index 42c524a074..4ce673a91a 100644 --- a/esphome/components/tsl2591/tsl2591.cpp +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -327,7 +327,9 @@ uint16_t TSL2591Component::get_illuminance(TSL2591SensorChannel channel, uint32_ return (combined_illuminance >> 16); } else if (channel == TSL2591_SENSOR_CHANNEL_VISIBLE) { // Reads all and subtracts out the infrared - return ((combined_illuminance & 0xFFFF) - (combined_illuminance >> 16)); + uint16_t full = combined_illuminance & 0xFFFF; + uint16_t ir = combined_illuminance >> 16; + return (ir > full) ? 0 : (full - ir); } // unknown channel! ESP_LOGE(TAG, "get_illuminance() caller requested an unknown channel: %d", channel); From 219d5170e006e7297004b1cfeb7e5323307980b0 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:15:54 -0500 Subject: [PATCH 414/609] [noblex] Fix IR receive losing decoded bytes between calls (#14533) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/noblex/noblex.cpp | 130 +++++++++++++-------------- esphome/components/noblex/noblex.h | 3 +- 2 files changed, 64 insertions(+), 69 deletions(-) diff --git a/esphome/components/noblex/noblex.cpp b/esphome/components/noblex/noblex.cpp index f1e76eabf2..e7e421d177 100644 --- a/esphome/components/noblex/noblex.cpp +++ b/esphome/components/noblex/noblex.cpp @@ -118,15 +118,15 @@ void NoblexClimate::transmit_state() { data->mark(NOBLEX_HEADER_MARK); data->space(NOBLEX_HEADER_SPACE); // Data (sent remote_state from the MSB to the LSB) - for (uint8_t i : remote_state) { - for (int8_t j = 7; j >= 0; j--) { - if ((i == 4) & (j == 4)) { + for (int byte_idx = 0; byte_idx < 8; byte_idx++) { + for (int8_t bit_idx = 7; bit_idx >= 0; bit_idx--) { + if ((byte_idx == 4) && (bit_idx == 4)) { // Header intermediate data->mark(NOBLEX_BIT_MARK); data->space(NOBLEX_GAP); // gap en bit 36 } else { data->mark(NOBLEX_BIT_MARK); - bool bit = i & (1 << j); + bool bit = remote_state[byte_idx] & (1 << bit_idx); data->space(bit ? NOBLEX_ONE_SPACE : NOBLEX_ZERO_SPACE); } } @@ -145,76 +145,71 @@ void NoblexClimate::transmit_state() { // Handle received IR Buffer bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { - uint8_t remote_state[8] = {0}; - uint8_t crc = 0, crc_calculated = 0; - - if (!receiving_) { - // Validate header - if (data.expect_item(NOBLEX_HEADER_MARK, NOBLEX_HEADER_SPACE)) { - ESP_LOGV(TAG, "Header"); - receiving_ = true; - // Read first 36 bits - for (int i = 0; i < 5; i++) { - // Read bit - for (int j = 7; j >= 0; j--) { - if ((i == 4) & (j == 4)) { - remote_state[i] |= 1 << j; - // Header intermediate - ESP_LOGVV(TAG, "GAP"); - return false; - } else if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) { - remote_state[i] |= 1 << j; - } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) { - ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j); - return false; - } - } - ESP_LOGV(TAG, "Byte %d %02X", i, remote_state[i]); - } - - } else { - ESP_LOGV(TAG, "Header fail"); - receiving_ = false; - return false; - } - - } else { - // Read the remaining 28 bits - for (int i = 4; i < 8; i++) { - // Read bit + if (data.peek_item(NOBLEX_HEADER_MARK, NOBLEX_HEADER_SPACE)) { + // First part: header + first 36 bits, followed by 20ms gap + data.expect_item(NOBLEX_HEADER_MARK, NOBLEX_HEADER_SPACE); + ESP_LOGV(TAG, "Header"); + this->receiving_ = false; + memset(this->remote_state_, 0, sizeof(this->remote_state_)); + for (int i = 0; i < 5; i++) { for (int j = 7; j >= 0; j--) { - if ((i == 4) & (j >= 4)) { - // nothing + if ((i == 4) && (j == 4)) { + this->remote_state_[i] |= 1 << j; + ESP_LOGVV(TAG, "GAP"); + this->receiving_ = true; + return false; } else if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) { - remote_state[i] |= 1 << j; + this->remote_state_[i] |= 1 << j; } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) { ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j); return false; } } - ESP_LOGV(TAG, "Byte %d %02X", i, remote_state[i]); + ESP_LOGV(TAG, "Byte %d %02X", i, this->remote_state_[i]); } + return false; + } - // Read crc - for (int i = 3; i >= 0; i--) { - if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) { - crc |= 1 << i; + // Second part: remaining 28 bits + 4-bit CRC + footer + if (!this->receiving_) { + return false; + } + this->receiving_ = false; + for (int i = 4; i < 8; i++) { + for (int j = 7; j >= 0; j--) { + if ((i == 4) && (j >= 4)) { + // already decoded in first part + } else if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) { + this->remote_state_[i] |= 1 << j; } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) { - ESP_LOGVV(TAG, "Bit %d CRC fail", i); + ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j); return false; } } - ESP_LOGV(TAG, "CRC %02X", crc); - - // Validate footer - if (!data.expect_mark(NOBLEX_BIT_MARK)) { - ESP_LOGV(TAG, "Footer fail"); - return false; - } - receiving_ = false; + ESP_LOGV(TAG, "Byte %d %02X", i, this->remote_state_[i]); } - for (uint8_t i : remote_state) + // Read CRC + uint8_t crc = 0; + for (int i = 3; i >= 0; i--) { + if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) { + crc |= 1 << i; + } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) { + ESP_LOGVV(TAG, "Bit %d CRC fail", i); + return false; + } + } + ESP_LOGV(TAG, "CRC %02X", crc); + + // Validate footer + if (!data.expect_mark(NOBLEX_BIT_MARK)) { + ESP_LOGV(TAG, "Footer fail"); + return false; + } + + // Validate CRC + uint8_t crc_calculated = 0; + for (uint8_t i : this->remote_state_) crc_calculated += reverse_bits(i); crc_calculated = reverse_bits(uint8_t(crc_calculated & 0x0F)) >> 4; ESP_LOGVV(TAG, "CRC calc %02X", crc_calculated); @@ -224,11 +219,12 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { return false; } - ESP_LOGD(TAG, "Received noblex code: %02X%02X %02X%02X %02X%02X %02X%02X", remote_state[0], remote_state[1], - remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6], remote_state[7]); + ESP_LOGD(TAG, "Received noblex code: %02X%02X %02X%02X %02X%02X %02X%02X", this->remote_state_[0], + this->remote_state_[1], this->remote_state_[2], this->remote_state_[3], this->remote_state_[4], + this->remote_state_[5], this->remote_state_[6], this->remote_state_[7]); auto powered_on = false; - if ((remote_state[0] & NOBLEX_POWER) == NOBLEX_POWER) { + if ((this->remote_state_[0] & NOBLEX_POWER) == NOBLEX_POWER) { powered_on = true; this->powered_on_assumed = powered_on; } else { @@ -241,7 +237,7 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { // Set received mode if (powered_on_assumed) { - auto mode = (remote_state[0] & 0xE0) >> 5; + auto mode = (this->remote_state_[0] & 0xE0) >> 5; ESP_LOGV(TAG, "Mode: %02X", mode); switch (mode) { case IRNoblexMode::IR_NOBLEX_MODE_AUTO: @@ -263,7 +259,7 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { } // Set received temp - uint8_t temp = remote_state[1]; + uint8_t temp = this->remote_state_[1]; ESP_LOGVV(TAG, "Temperature Raw: %02X", temp); temp = 0x0F & reverse_bits(temp); @@ -272,7 +268,7 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { this->target_temperature = temp; // Set received fan speed - auto fan = (remote_state[0] & 0x0C) >> 2; + auto fan = (this->remote_state_[0] & 0x0C) >> 2; ESP_LOGV(TAG, "Fan: %02X", fan); switch (fan) { case IRNoblexFan::IR_NOBLEX_FAN_HIGH: @@ -291,7 +287,7 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { } // Set received swing status - if (remote_state[0] & 0x02) { + if (this->remote_state_[0] & 0x02) { ESP_LOGV(TAG, "Swing vertical"); this->swing_mode = climate::CLIMATE_SWING_VERTICAL; } else { @@ -299,8 +295,6 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { this->swing_mode = climate::CLIMATE_SWING_OFF; } - for (uint8_t &i : remote_state) - i = 0; this->publish_state(); return true; } // end on_receive() diff --git a/esphome/components/noblex/noblex.h b/esphome/components/noblex/noblex.h index 57990db005..3d52a1a538 100644 --- a/esphome/components/noblex/noblex.h +++ b/esphome/components/noblex/noblex.h @@ -41,7 +41,8 @@ class NoblexClimate : public climate_ir::ClimateIR { /// Handle received IR Buffer. bool on_receive(remote_base::RemoteReceiveData data) override; bool send_swing_cmd_{false}; - bool receiving_ = false; + bool receiving_{false}; + uint8_t remote_state_[8]{}; }; } // namespace noblex From 9ab5f5d451a4d1aaf030fbff72bff0011478e5fb Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:42:05 -0500 Subject: [PATCH 415/609] [light] Fix unsigned underflow in addressable scan effect (#14546) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .../light/addressable_light_effect.h | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index a85ea4661d..461ddbc085 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -171,12 +171,27 @@ class AddressableScanEffect : public AddressableLightEffect { if (now - this->last_move_ < this->move_interval_) return; - if (direction_) { + const auto num_leds = static_cast<uint32_t>(it.size()); + if (this->scan_width_ >= num_leds) { + it.all() = current_color; + it.schedule_show(); + this->last_move_ = now; + return; + } + + const uint32_t max_pos = num_leds - this->scan_width_; + if (this->at_led_ >= max_pos) { + this->at_led_ = max_pos; + this->direction_ = false; + } + + if (this->direction_) { this->at_led_++; - if (this->at_led_ == it.size() - this->scan_width_) + if (this->at_led_ >= max_pos) this->direction_ = false; } else { - this->at_led_--; + if (this->at_led_ > 0) + this->at_led_--; if (this->at_led_ == 0) this->direction_ = true; } From a9cceebb33612054866465254456336fd4d76ba5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:48:50 -0500 Subject: [PATCH 416/609] [pid][nextion][pn532_i2c][pipsolar] Fix copy-paste and logic bugs (#14551) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/nextion/nextion_component.cpp | 2 +- esphome/components/pid/pid_autotuner.cpp | 4 ++-- esphome/components/pipsolar/pipsolar.cpp | 1 + esphome/components/pn532_i2c/pn532_i2c.cpp | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/esphome/components/nextion/nextion_component.cpp b/esphome/components/nextion/nextion_component.cpp index 324ad87372..30c8b80524 100644 --- a/esphome/components/nextion/nextion_component.cpp +++ b/esphome/components/nextion/nextion_component.cpp @@ -88,7 +88,7 @@ void NextionComponent::update_component_settings(bool force_update) { this->send_state_to_nextion(); } - if (this->component_flags_.bco_needs_update || (force_update && this->component_flags_.bco2_is_set)) { + if (this->component_flags_.bco_needs_update || (force_update && this->component_flags_.bco_is_set)) { this->nextion_->set_component_background_color(this->variable_name_.c_str(), this->bco_); this->component_flags_.bco_needs_update = false; } diff --git a/esphome/components/pid/pid_autotuner.cpp b/esphome/components/pid/pid_autotuner.cpp index d1d9c200cf..e1ddd1d7c6 100644 --- a/esphome/components/pid/pid_autotuner.cpp +++ b/esphome/components/pid/pid_autotuner.cpp @@ -97,7 +97,7 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce } bool zc_symmetrical = this->frequency_detector_.is_increase_decrease_symmetrical(); - bool amplitude_convergent = this->frequency_detector_.is_increase_decrease_symmetrical(); + bool amplitude_convergent = this->amplitude_detector_.is_amplitude_convergent(); if (!zc_symmetrical || !amplitude_convergent) { // The frequency/amplitude is not fully accurate yet, try to wait // until the fault clears, or terminate after a while anyway @@ -362,7 +362,7 @@ bool PIDAutotuner::OscillationAmplitudeDetector::is_amplitude_convergent() const for (auto v : this->phase_mins) global_min = std::min(global_min, v); for (auto v : this->phase_maxs) - global_max = std::min(global_max, v); + global_max = std::max(global_max, v); float global_amplitude = (global_max - global_min) / 2.0f; float mean_amplitude = this->get_mean_oscillation_amplitude(); return (mean_amplitude - global_amplitude) / (global_amplitude) < 0.05f; diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index 9c5caec775..eb6d3931e0 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -647,6 +647,7 @@ void Pipsolar::handle_qpiws_(const char *message) { case 34: this->publish_binary_sensor_(enabled, this->warning_high_ac_input_during_bus_soft_start_); value_warnings_present |= enabled.value_or(false); + break; case 35: this->publish_binary_sensor_(enabled, this->warning_battery_equalization_); value_warnings_present |= enabled.value_or(false); diff --git a/esphome/components/pn532_i2c/pn532_i2c.cpp b/esphome/components/pn532_i2c/pn532_i2c.cpp index b306222a21..41f0f079aa 100644 --- a/esphome/components/pn532_i2c/pn532_i2c.cpp +++ b/esphome/components/pn532_i2c/pn532_i2c.cpp @@ -49,7 +49,7 @@ bool PN532I2C::read_response(uint8_t command, std::vector<uint8_t> &data) { return false; } - if (data[1] != 0x00 && data[2] != 0x00 && data[3] != 0xFF) { + if (data[1] != 0x00 || data[2] != 0x00 || data[3] != 0xFF) { // invalid packet ESP_LOGV(TAG, "read data invalid preamble!"); return false; @@ -95,7 +95,7 @@ uint8_t PN532I2C::read_response_length_() { return 0; } - if (data[1] != 0x00 && data[2] != 0x00 && data[3] != 0xFF) { + if (data[1] != 0x00 || data[2] != 0x00 || data[3] != 0xFF) { // invalid packet ESP_LOGV(TAG, "read data invalid preamble!"); return 0; From 8f3db96291c06708a23f5171515cd26ec84e33a1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:50:26 -0500 Subject: [PATCH 417/609] [esp32_ble_server][weikai][ade7880] Fix copy-paste bugs (#14552) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/ade7880/ade7880.cpp | 6 +++--- esphome/components/ade7880/ade7880_registers.h | 3 +++ esphome/components/esp32_ble_server/ble_characteristic.cpp | 4 ++-- esphome/components/weikai/weikai.cpp | 4 +++- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/esphome/components/ade7880/ade7880.cpp b/esphome/components/ade7880/ade7880.cpp index f6a15190cd..8fb3e55b91 100644 --- a/esphome/components/ade7880/ade7880.cpp +++ b/esphome/components/ade7880/ade7880.cpp @@ -121,7 +121,7 @@ void ADE7880::update() { this->update_sensor_from_s32_register16_(chan->forward_active_energy, AFWATTHR, [&chan](float val) { return chan->forward_active_energy_total += val / 14400.0f; }); - this->update_sensor_from_s32_register16_(chan->reverse_active_energy, AFWATTHR, [&chan](float val) { + this->update_sensor_from_s32_register16_(chan->reverse_active_energy, ARWATTHR, [&chan](float val) { return chan->reverse_active_energy_total += val / 14400.0f; }); } @@ -137,7 +137,7 @@ void ADE7880::update() { this->update_sensor_from_s32_register16_(chan->forward_active_energy, BFWATTHR, [&chan](float val) { return chan->forward_active_energy_total += val / 14400.0f; }); - this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BFWATTHR, [&chan](float val) { + this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BRWATTHR, [&chan](float val) { return chan->reverse_active_energy_total += val / 14400.0f; }); } @@ -153,7 +153,7 @@ void ADE7880::update() { this->update_sensor_from_s32_register16_(chan->forward_active_energy, CFWATTHR, [&chan](float val) { return chan->forward_active_energy_total += val / 14400.0f; }); - this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CFWATTHR, [&chan](float val) { + this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CRWATTHR, [&chan](float val) { return chan->reverse_active_energy_total += val / 14400.0f; }); } diff --git a/esphome/components/ade7880/ade7880_registers.h b/esphome/components/ade7880/ade7880_registers.h index 8b5b68abb0..9fd8ca3bf5 100644 --- a/esphome/components/ade7880/ade7880_registers.h +++ b/esphome/components/ade7880/ade7880_registers.h @@ -85,6 +85,9 @@ constexpr uint16_t CWATTHR = 0xE402; constexpr uint16_t AFWATTHR = 0xE403; constexpr uint16_t BFWATTHR = 0xE404; constexpr uint16_t CFWATTHR = 0xE405; +constexpr uint16_t ARWATTHR = 0xE406; +constexpr uint16_t BRWATTHR = 0xE407; +constexpr uint16_t CRWATTHR = 0xE408; constexpr uint16_t AFVARHR = 0xE409; constexpr uint16_t BFVARHR = 0xE40A; constexpr uint16_t CFVARHR = 0xE40B; diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index d4ccefd9b2..1806354712 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -310,8 +310,8 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt (*this->on_write_callback_)(this->value_, param->exec_write.conn_id); } } - esp_err_t err = - esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr); + esp_err_t err = esp_ble_gatts_send_response(gatts_if, param->exec_write.conn_id, param->exec_write.trans_id, + ESP_GATT_OK, nullptr); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err); } diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp index 1d835daf1e..3f5d6c787c 100644 --- a/esphome/components/weikai/weikai.cpp +++ b/esphome/components/weikai/weikai.cpp @@ -445,6 +445,7 @@ void WeikaiChannel::flush() { } size_t WeikaiChannel::xfer_fifo_to_buffer_() { + size_t total = 0; size_t to_transfer; size_t free; while ((to_transfer = this->rx_in_fifo_()) && (free = this->receive_buffer_.free())) { @@ -458,9 +459,10 @@ size_t WeikaiChannel::xfer_fifo_to_buffer_() { this->reg(0).read_fifo(data, to_transfer); for (size_t i = 0; i < to_transfer; i++) this->receive_buffer_.push(data[i]); + total += to_transfer; } } // while work to do - return to_transfer; + return total; } /// From 0469612d0774ef26acef975446e4c187dde876f8 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:02:17 -0500 Subject: [PATCH 418/609] [multiple] Fix assorted medium-severity bugs (#14555) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/bytebuffer/bytebuffer.h | 2 +- esphome/components/cap1188/cap1188.cpp | 2 +- esphome/components/hte501/hte501.cpp | 2 +- .../components/ina2xx_base/ina2xx_base.cpp | 6 +---- esphome/components/inkplate/inkplate.cpp | 10 ++++----- esphome/components/msa3xx/msa3xx.cpp | 6 +---- esphome/components/nfc/ndef_record_text.cpp | 5 +++++ esphome/components/nfc/nfc.cpp | 22 +++++++++++++------ esphome/components/nfc/nfc.h | 2 +- .../components/template/text/template_text.h | 6 ++++- 10 files changed, 36 insertions(+), 27 deletions(-) diff --git a/esphome/components/bytebuffer/bytebuffer.h b/esphome/components/bytebuffer/bytebuffer.h index 030484ce32..3c68094dbc 100644 --- a/esphome/components/bytebuffer/bytebuffer.h +++ b/esphome/components/bytebuffer/bytebuffer.h @@ -263,7 +263,7 @@ class ByteBuffer { void put_uint8(uint8_t value, size_t offset) { this->data_[offset] = value; } void put_uint16(uint16_t value, size_t offset) { this->put(value, offset); } - void put_uint24(uint32_t value, size_t offset) { this->put(value, offset); } + void put_uint24(uint32_t value, size_t offset) { this->put_uint32_(value, offset, 3); } void put_uint32(uint32_t value, size_t offset) { this->put(value, offset); } void put_uint64(uint64_t value, size_t offset) { this->put(value, offset); } // Signed versions of the put functions diff --git a/esphome/components/cap1188/cap1188.cpp b/esphome/components/cap1188/cap1188.cpp index 9e8c87d147..64bdc620cd 100644 --- a/esphome/components/cap1188/cap1188.cpp +++ b/esphome/components/cap1188/cap1188.cpp @@ -92,7 +92,7 @@ void CAP1188Component::loop() { this->read_register(CAP1188_MAIN, &data, 1); data = data & ~CAP1188_MAIN_INT; - this->write_register(CAP1188_MAIN, &data, 2); + this->write_register(CAP1188_MAIN, &data, 1); } for (auto *channel : this->channels_) { diff --git a/esphome/components/hte501/hte501.cpp b/esphome/components/hte501/hte501.cpp index 972e72c170..ef9ef1fabf 100644 --- a/esphome/components/hte501/hte501.cpp +++ b/esphome/components/hte501/hte501.cpp @@ -49,7 +49,7 @@ void HTE501Component::update() { this->set_timeout(50, [this]() { uint8_t i2c_response[6]; this->read(i2c_response, 6); - if (i2c_response[2] != crc8(i2c_response, 2, 0xFF, 0x31, true) && + if (i2c_response[2] != crc8(i2c_response, 2, 0xFF, 0x31, true) || i2c_response[5] != crc8(i2c_response + 3, 2, 0xFF, 0x31, true)) { this->error_code_ = CRC_CHECK_FAILED; this->status_set_warning(); diff --git a/esphome/components/ina2xx_base/ina2xx_base.cpp b/esphome/components/ina2xx_base/ina2xx_base.cpp index 8a20192c1e..9f510eef74 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.cpp +++ b/esphome/components/ina2xx_base/ina2xx_base.cpp @@ -599,11 +599,7 @@ bool INA2XX::read_unsigned_16_(uint8_t reg, uint16_t &out) { } int64_t INA2XX::two_complement_(uint64_t value, uint8_t bits) { - if (value > (1ULL << (bits - 1))) { - return (int64_t) (value - (1ULL << bits)); - } else { - return (int64_t) value; - } + return (int64_t) (value << (64 - bits)) >> (64 - bits); } } // namespace ina2xx_base } // namespace esphome diff --git a/esphome/components/inkplate/inkplate.cpp b/esphome/components/inkplate/inkplate.cpp index df9c2b29c7..7551c6fc77 100644 --- a/esphome/components/inkplate/inkplate.cpp +++ b/esphome/components/inkplate/inkplate.cpp @@ -407,7 +407,7 @@ void Inkplate::display1b_() { break; } - uint32_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t clock = (1UL << this->cl_pin_->get_pin()); uint32_t data_mask = this->get_data_pin_mask_(); ESP_LOGV(TAG, "Display1b start loops (%ums)", millis() - start_time); @@ -575,7 +575,7 @@ void Inkplate::display3b_() { break; } - uint32_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t clock = (1UL << this->cl_pin_->get_pin()); uint32_t data_mask = this->get_data_pin_mask_(); uint32_t pos; uint32_t data; @@ -646,7 +646,7 @@ bool Inkplate::partial_update_() { int rep = (this->model_ == INKPLATE_6_V2) ? 6 : 5; eink_on_(); - uint32_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t clock = (1UL << this->cl_pin_->get_pin()); uint32_t data_mask = this->get_data_pin_mask_(); for (int k = 0; k < rep; k++) { vscan_start_(); @@ -704,7 +704,7 @@ void Inkplate::vscan_start_() { } void Inkplate::hscan_start_(uint32_t d) { - uint8_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t clock = (1UL << this->cl_pin_->get_pin()); this->sph_pin_->digital_write(false); GPIO.out_w1ts = d | clock; GPIO.out_w1tc = this->get_data_pin_mask_() | clock; @@ -751,7 +751,7 @@ void Inkplate::clean_fast_(uint8_t c, uint8_t rep) { uint32_t send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | (((data & 0b11100000) >> 5) << 25); - uint32_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t clock = (1UL << this->cl_pin_->get_pin()); for (int k = 0; k < rep; k++) { vscan_start_(); diff --git a/esphome/components/msa3xx/msa3xx.cpp b/esphome/components/msa3xx/msa3xx.cpp index e46bfed193..6d6b21e6af 100644 --- a/esphome/components/msa3xx/msa3xx.cpp +++ b/esphome/components/msa3xx/msa3xx.cpp @@ -364,11 +364,7 @@ void MSA3xxComponent::setup_offset_(float offset_x, float offset_y, float offset } int64_t MSA3xxComponent::twos_complement_(uint64_t value, uint8_t bits) { - if (value > (1ULL << (bits - 1))) { - return (int64_t) (value - (1ULL << bits)); - } else { - return (int64_t) value; - } + return (int64_t) (value << (64 - bits)) >> (64 - bits); } void binary_event_debounce(bool state, bool old_state, uint32_t now, uint32_t &last_ms, Trigger<> &trigger, diff --git a/esphome/components/nfc/ndef_record_text.cpp b/esphome/components/nfc/ndef_record_text.cpp index 80b0108b46..8a9a2cb014 100644 --- a/esphome/components/nfc/ndef_record_text.cpp +++ b/esphome/components/nfc/ndef_record_text.cpp @@ -14,6 +14,11 @@ NdefRecordText::NdefRecordText(const std::vector<uint8_t> &payload) { uint8_t language_code_length = payload[0] & 0b00111111; // Todo, make use of encoding bit? + if (1 + language_code_length > payload.size()) { + ESP_LOGE(TAG, "Record payload too short for language code"); + return; + } + this->language_code_ = std::string(payload.begin() + 1, payload.begin() + 1 + language_code_length); this->text_ = std::string(payload.begin() + 1 + language_code_length, payload.end()); diff --git a/esphome/components/nfc/nfc.cpp b/esphome/components/nfc/nfc.cpp index 8567b0969a..55543cd292 100644 --- a/esphome/components/nfc/nfc.cpp +++ b/esphome/components/nfc/nfc.cpp @@ -35,7 +35,7 @@ uint8_t guess_tag_type(uint8_t uid_length) { } } -uint8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data) { +int8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data) { for (uint8_t i = 0; i < MIFARE_CLASSIC_BLOCK_SIZE; i++) { if (data[i] == 0x00) { // Do nothing, skip @@ -49,17 +49,25 @@ uint8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data) { } bool decode_mifare_classic_tlv(std::vector<uint8_t> &data, uint32_t &message_length, uint8_t &message_start_index) { + if (data.size() < MIFARE_CLASSIC_BLOCK_SIZE) { + ESP_LOGE(TAG, "Error, data too short for NDEF detection."); + return false; + } auto i = get_mifare_classic_ndef_start_index(data); - if (data[i] != 0x03) { + if (i < 0 || data[i] != 0x03) { ESP_LOGE(TAG, "Error, Can't decode message length."); return false; } - if (data[i + 1] == 0xFF) { - message_length = ((0xFF & data[i + 2]) << 8) | (0xFF & data[i + 3]); - message_start_index = i + MIFARE_CLASSIC_LONG_TLV_SIZE; + uint8_t idx = static_cast<uint8_t>(i); + if (idx + 4 <= data.size() && data[idx + 1] == 0xFF) { + message_length = ((0xFF & data[idx + 2]) << 8) | (0xFF & data[idx + 3]); + message_start_index = idx + MIFARE_CLASSIC_LONG_TLV_SIZE; + } else if (idx + 2 <= data.size()) { + message_length = data[idx + 1]; + message_start_index = idx + MIFARE_CLASSIC_SHORT_TLV_SIZE; } else { - message_length = data[i + 1]; - message_start_index = i + MIFARE_CLASSIC_SHORT_TLV_SIZE; + ESP_LOGE(TAG, "Error, TLV data too short."); + return false; } return true; } diff --git a/esphome/components/nfc/nfc.h b/esphome/components/nfc/nfc.h index cdaea82af6..8ca5cb7ea4 100644 --- a/esphome/components/nfc/nfc.h +++ b/esphome/components/nfc/nfc.h @@ -72,7 +72,7 @@ ESPDEPRECATED("Use format_bytes_to() with stack buffer instead. Removed in 2026. std::string format_bytes(std::span<const uint8_t> bytes); uint8_t guess_tag_type(uint8_t uid_length); -uint8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data); +int8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data); bool decode_mifare_classic_tlv(std::vector<uint8_t> &data, uint32_t &message_length, uint8_t &message_start_index); uint32_t get_mifare_classic_buffer_size(uint32_t message_length); diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index 88c6afdf2c..7f176db09e 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -52,7 +52,11 @@ template<uint8_t SZ> class TextSaver : public TemplateTextSaverBase { bool hasdata = this->pref_.load(&temp); if (hasdata) { - value.assign(temp + 1, (size_t) temp[0]); + size_t len = static_cast<uint8_t>(temp[0]); + if (len > SZ) { + len = SZ; + } + value.assign(temp + 1, len); } this->prev_.assign(value); From 4f4b2bfdecc1ccc06f344d4e0c6dbad88537a3a6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:14:35 -0500 Subject: [PATCH 419/609] [bmp581_base][bl0906] Fix 24-bit sign extension bugs (#14558) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/bl0906/bl0906.cpp | 4 +++- esphome/components/bmp581_base/bmp581_base.cpp | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/bl0906/bl0906.cpp b/esphome/components/bl0906/bl0906.cpp index c1cd48a1ac..7b643bba98 100644 --- a/esphome/components/bl0906/bl0906.cpp +++ b/esphome/components/bl0906/bl0906.cpp @@ -10,7 +10,9 @@ static const char *const TAG = "bl0906"; constexpr uint32_t to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; } -constexpr int32_t to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; } +constexpr int32_t to_int32_t(sbe24_t input) { + return static_cast<int32_t>(encode_uint32((uint8_t) input.h, input.m, input.l, 0)) >> 8; +} // The SUM byte is (Addr+Data_L+Data_M+Data_H)&0xFF negated; constexpr uint8_t bl0906_checksum(const uint8_t address, const DataPacket *data) { diff --git a/esphome/components/bmp581_base/bmp581_base.cpp b/esphome/components/bmp581_base/bmp581_base.cpp index c4a96ebc39..89a92de31d 100644 --- a/esphome/components/bmp581_base/bmp581_base.cpp +++ b/esphome/components/bmp581_base/bmp581_base.cpp @@ -429,7 +429,7 @@ bool BMP581Component::read_temperature_(float &temperature) { } // temperature MSB is in data[2], LSB is in data[1], XLSB in data[0] - int32_t raw_temp = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; + int32_t raw_temp = static_cast<int32_t>(encode_uint32(data[2], data[1], data[0], 0)) >> 8; temperature = (float) (raw_temp / 65536.0); // convert measurement to degrees Celsius (page 22 of datasheet) return true; @@ -458,7 +458,7 @@ bool BMP581Component::read_temperature_and_pressure_(float &temperature, float & } // temperature MSB is in data[2], LSB is in data[1], XLSB in data[0] - int32_t raw_temp = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; + int32_t raw_temp = static_cast<int32_t>(encode_uint32(data[2], data[1], data[0], 0)) >> 8; temperature = (float) (raw_temp / 65536.0); // convert measurement to degrees Celsius (page 22 of datasheet) // pressure MSB is in data[5], LSB is in data[4], XLSB in data[3] From 2c83c6a79f356d3f0f6910fc573e47c53e695c5e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:47:56 -0500 Subject: [PATCH 420/609] [shelly_dimmer][lvgl][seeed_mr60fda2][packet_transport] Fix buffer bounds checks (#14534) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/lvgl/lvgl_esphome.cpp | 3 ++ .../packet_transport/packet_transport.cpp | 2 + .../seeed_mr60fda2/seeed_mr60fda2.cpp | 44 ++++++++----------- .../shelly_dimmer/shelly_dimmer.cpp | 4 +- 4 files changed, 25 insertions(+), 28 deletions(-) diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 3e447e9169..66cb25b864 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -422,6 +422,9 @@ void LvglComponent::write_random_() { auto row = random_uint32() % this->disp_drv_.ver_res; row = row / this->draw_rounding * this->draw_rounding; auto size = ((random_uint32() % 32) / this->draw_rounding + 2) * this->draw_rounding - 1; + // clamp size so the square fits within the draw buffer + if ((size + 1) * (size + 1) > this->draw_buf_.size) + size = static_cast<decltype(size)>(sqrtf(this->draw_buf_.size)) - 1; lv_area_t area; area.x1 = col; area.y1 = row; diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 6f1286b469..964037a02c 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -137,6 +137,8 @@ class PacketDecoder { return DECODE_EMPTY; if (this->buffer_[this->position_] != key) return DECODE_UNMATCHED; + if (this->position_ + 1 + sizeof(T) > this->len_) + return DECODE_ERROR; this->position_++; T value = 0; for (size_t i = 0; i != sizeof(T); ++i) { diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp index 5d571618d3..c6527a948e 100644 --- a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp @@ -149,28 +149,25 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { switch (this->current_frame_locate_) { case LOCATE_FRAME_HEADER: // starting buffer if (buffer == FRAME_HEADER_BUFFER) { - this->current_frame_len_ = 1; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_len_ = 0; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; } break; case LOCATE_ID_FRAME1: this->current_frame_id_ = buffer << 8; - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; break; case LOCATE_ID_FRAME2: this->current_frame_id_ += buffer; - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; break; case LOCATE_LENGTH_FRAME_H: this->current_data_frame_len_ = buffer << 8; - if (this->current_data_frame_len_ == 0x00) { - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + if (this->current_data_frame_len_ == 0) { + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; } else { this->current_frame_locate_ = LOCATE_FRAME_HEADER; @@ -181,15 +178,13 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { if (this->current_data_frame_len_ > DATA_BUF_MAX_SIZE) { this->current_frame_locate_ = LOCATE_FRAME_HEADER; } else { - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; } break; case LOCATE_TYPE_FRAME1: this->current_frame_type_ = buffer << 8; - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; break; case LOCATE_TYPE_FRAME2: @@ -198,8 +193,7 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { (this->current_frame_type_ == PEOPLE_EXIST_TYPE_BUFFER) || (this->current_frame_type_ == RESULT_INSTALL_HEIGHT) || (this->current_frame_type_ == RESULT_PARAMETERS) || (this->current_frame_type_ == RESULT_HEIGHT_THRESHOLD) || (this->current_frame_type_ == RESULT_SENSITIVITY)) { - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; } else { this->current_frame_locate_ = LOCATE_FRAME_HEADER; @@ -207,8 +201,7 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { break; case LOCATE_HEAD_CKSUM_FRAME: if (validate_checksum(this->current_frame_buf_, this->current_frame_len_, buffer)) { - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; } else { ESP_LOGD(TAG, "HEAD_CKSUM_FRAME ERROR: 0x%02x", buffer); @@ -223,21 +216,20 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { } break; case LOCATE_DATA_FRAME: - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; - this->current_data_buf_[this->current_frame_len_ - LEN_TO_DATA_FRAME] = buffer; - if (this->current_frame_len_ - LEN_TO_HEAD_CKSUM == this->current_data_frame_len_) { - this->current_frame_locate_++; - } - if (this->current_frame_len_ > FRAME_BUF_MAX_SIZE) { + if (this->current_frame_len_ >= FRAME_BUF_MAX_SIZE) { ESP_LOGD(TAG, "PRACTICE_DATA_FRAME_LEN ERROR: %d", this->current_frame_len_ - LEN_TO_HEAD_CKSUM); this->current_frame_locate_ = LOCATE_FRAME_HEADER; + break; + } + this->current_data_buf_[this->current_frame_len_ - LEN_TO_DATA_FRAME + 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; + if (this->current_frame_len_ - LEN_TO_HEAD_CKSUM == this->current_data_frame_len_) { + this->current_frame_locate_++; } break; case LOCATE_DATA_CKSUM_FRAME: if (validate_checksum(this->current_data_buf_, this->current_data_frame_len_, buffer)) { - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; this->process_frame_(); } else { diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.cpp b/esphome/components/shelly_dimmer/shelly_dimmer.cpp index bdb33d31af..88fcbcbfe1 100644 --- a/esphome/components/shelly_dimmer/shelly_dimmer.cpp +++ b/esphome/components/shelly_dimmer/shelly_dimmer.cpp @@ -188,8 +188,8 @@ bool ShellyDimmer::upgrade_firmware_() { break; } - std::memcpy(buffer, p, BUFFER_SIZE); - p += BUFFER_SIZE; + std::memcpy(buffer, p, len); + p += len; if (stm32_write_memory(stm32, addr, buffer, len) != STM32_ERR_OK) { ESP_LOGW(TAG, "Failed to write to STM32 flash memory"); From 587bf68091caffac14f4d7ed2714fac28848354c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:03:30 -0500 Subject: [PATCH 421/609] [ltr501][pvvx_mithermometer][smt100] Convert static locals to instance members (#14569) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/ltr501/ltr501.cpp | 21 +++++++++---------- esphome/components/ltr501/ltr501.h | 3 +++ .../pvvx_mithermometer/pvvx_mithermometer.cpp | 7 +++---- .../pvvx_mithermometer/pvvx_mithermometer.h | 2 ++ esphome/components/smt100/smt100.cpp | 16 +++++++------- esphome/components/smt100/smt100.h | 3 +++ 6 files changed, 28 insertions(+), 24 deletions(-) diff --git a/esphome/components/ltr501/ltr501.cpp b/esphome/components/ltr501/ltr501.cpp index 04de91e362..4c9006be1d 100644 --- a/esphome/components/ltr501/ltr501.cpp +++ b/esphome/components/ltr501/ltr501.cpp @@ -146,7 +146,6 @@ void LTRAlsPs501Component::update() { void LTRAlsPs501Component::loop() { ErrorCode err = i2c::ERROR_OK; - static uint8_t tries{0}; switch (this->state_) { case State::DELAYED_SETUP: @@ -175,20 +174,20 @@ void LTRAlsPs501Component::loop() { case State::WAITING_FOR_DATA: if (this->is_als_data_ready_(this->als_readings_) == LtrDataAvail::LTR_DATA_OK) { - tries = 0; + this->tries_ = 0; ESP_LOGV(TAG, "Reading sensor data assuming gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain), get_itime_ms(this->als_readings_.integration_time)); this->read_sensor_data_(this->als_readings_); this->apply_lux_calculation_(this->als_readings_); this->state_ = State::DATA_COLLECTED; - } else if (tries >= MAX_TRIES) { + } else if (this->tries_ >= MAX_TRIES) { ESP_LOGW(TAG, "Can't get data after several tries. Aborting."); - tries = 0; + this->tries_ = 0; this->status_set_warning(); this->state_ = State::IDLE; return; } else { - tries++; + this->tries_++; } break; @@ -230,21 +229,21 @@ void LTRAlsPs501Component::loop() { } void LTRAlsPs501Component::check_and_trigger_ps_() { - static uint32_t last_high_trigger_time{0}; - static uint32_t last_low_trigger_time{0}; uint16_t ps_data = this->read_ps_data_(); uint32_t now = millis(); if (ps_data != this->ps_readings_) { this->ps_readings_ = ps_data; // Higher values - object is closer to sensor - if (ps_data > this->ps_threshold_high_ && now - last_high_trigger_time >= this->ps_cooldown_time_s_ * 1000) { - last_high_trigger_time = now; + if (ps_data > this->ps_threshold_high_ && + now - this->last_ps_high_trigger_time_ >= this->ps_cooldown_time_s_ * 1000) { + this->last_ps_high_trigger_time_ = now; ESP_LOGD(TAG, "Proximity high threshold triggered. Value = %d, Trigger level = %d", ps_data, this->ps_threshold_high_); this->on_ps_high_trigger_callback_.call(); - } else if (ps_data < this->ps_threshold_low_ && now - last_low_trigger_time >= this->ps_cooldown_time_s_ * 1000) { - last_low_trigger_time = now; + } else if (ps_data < this->ps_threshold_low_ && + now - this->last_ps_low_trigger_time_ >= this->ps_cooldown_time_s_ * 1000) { + this->last_ps_low_trigger_time_ = now; ESP_LOGD(TAG, "Proximity low threshold triggered. Value = %d, Trigger level = %d", ps_data, this->ps_threshold_low_); this->on_ps_low_trigger_callback_.call(); diff --git a/esphome/components/ltr501/ltr501.h b/esphome/components/ltr501/ltr501.h index 02c025da30..d9a53c9bd4 100644 --- a/esphome/components/ltr501/ltr501.h +++ b/esphome/components/ltr501/ltr501.h @@ -74,6 +74,7 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { READY_TO_PUBLISH, KEEP_PUBLISHING } state_{State::NOT_INITIALIZED}; + uint8_t tries_{0}; LtrType ltr_type_{LtrType::LTR_TYPE_ALS_ONLY}; @@ -130,6 +131,8 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { PsGain501 ps_gain_{PsGain501::PS_GAIN_1}; uint16_t ps_threshold_high_{0xffff}; uint16_t ps_threshold_low_{0x0000}; + uint32_t last_ps_high_trigger_time_{0}; + uint32_t last_ps_low_trigger_time_{0}; // // Sensors for publishing data diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp index 239a1e74fe..35badf48bb 100644 --- a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp @@ -66,12 +66,11 @@ optional<ParseResult> PVVXMiThermometer::parse_header_(const esp32_ble_tracker:: return {}; } - static uint8_t last_frame_count = 0; - if (last_frame_count == raw[13]) { - ESP_LOGVV(TAG, "parse_header(): duplicate data packet received (%hhu).", last_frame_count); + if (this->last_frame_count_ == raw[13]) { + ESP_LOGVV(TAG, "parse_header(): duplicate data packet received (%hhu).", this->last_frame_count_); return {}; } - last_frame_count = raw[13]; + this->last_frame_count_ = raw[13]; return result; } diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h index c15e1e7e22..09b5e91a16 100644 --- a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h @@ -39,6 +39,8 @@ class PVVXMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevic sensor::Sensor *battery_voltage_{nullptr}; sensor::Sensor *signal_strength_{nullptr}; + uint8_t last_frame_count_{0}; + optional<ParseResult> parse_header_(const esp32_ble_tracker::ServiceData &service_data); bool parse_message_(const std::vector<uint8_t> &message, ParseResult &result); bool report_results_(const optional<ParseResult> &result, const char *address); diff --git a/esphome/components/smt100/smt100.cpp b/esphome/components/smt100/smt100.cpp index 1bcb964264..105cc06edb 100644 --- a/esphome/components/smt100/smt100.cpp +++ b/esphome/components/smt100/smt100.cpp @@ -12,10 +12,9 @@ void SMT100Component::update() { } void SMT100Component::loop() { - static char buffer[MAX_LINE_LENGTH]; while (this->available() != 0) { - if (readline_(read(), buffer, MAX_LINE_LENGTH) > 0) { - int counts = (int) strtol((strtok(buffer, ",")), nullptr, 10); + if (this->readline_(this->read(), this->readline_buffer_, MAX_LINE_LENGTH) > 0) { + int counts = (int) strtol((strtok(this->readline_buffer_, ",")), nullptr, 10); float permittivity = (float) strtod((strtok(nullptr, ",")), nullptr); float moisture = (float) strtod((strtok(nullptr, ",")), nullptr); float temperature = (float) strtod((strtok(nullptr, ",")), nullptr); @@ -56,7 +55,6 @@ void SMT100Component::dump_config() { } int SMT100Component::readline_(int readch, char *buffer, int len) { - static int pos = 0; int rpos; if (readch > 0) { @@ -64,13 +62,13 @@ int SMT100Component::readline_(int readch, char *buffer, int len) { case '\n': // Ignore new-lines break; case '\r': // Return on CR - rpos = pos; - pos = 0; // Reset position index ready for next time + rpos = this->readline_pos_; + this->readline_pos_ = 0; // Reset position index ready for next time return rpos; default: - if (pos < len - 1) { - buffer[pos++] = readch; - buffer[pos] = 0; + if (this->readline_pos_ < len - 1) { + buffer[this->readline_pos_++] = readch; + buffer[this->readline_pos_] = 0; } } } diff --git a/esphome/components/smt100/smt100.h b/esphome/components/smt100/smt100.h index df8803e1c6..cb01b1ed55 100644 --- a/esphome/components/smt100/smt100.h +++ b/esphome/components/smt100/smt100.h @@ -28,6 +28,9 @@ class SMT100Component : public PollingComponent, public uart::UARTDevice { protected: int readline_(int readch, char *buffer, int len); + char readline_buffer_[MAX_LINE_LENGTH]{}; + int readline_pos_{0}; + sensor::Sensor *counts_sensor_{nullptr}; sensor::Sensor *permittivity_sensor_{nullptr}; sensor::Sensor *moisture_sensor_{nullptr}; From 5777908da712d64dde2f760d68aaa707b606dc1f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:03:53 -0500 Subject: [PATCH 422/609] [iaqcore][scd30][sen21231][beken_spi_led_strip] Fix uninitialized variables and missing error checks (#14568) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/beken_spi_led_strip/led_strip.cpp | 2 +- esphome/components/iaqcore/iaqcore.cpp | 10 ++++++---- esphome/components/scd30/scd30.cpp | 2 +- esphome/components/sen21231/sen21231.cpp | 7 ++++++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/esphome/components/beken_spi_led_strip/led_strip.cpp b/esphome/components/beken_spi_led_strip/led_strip.cpp index 67b8472257..f425f3ca5c 100644 --- a/esphome/components/beken_spi_led_strip/led_strip.cpp +++ b/esphome/components/beken_spi_led_strip/led_strip.cpp @@ -78,7 +78,7 @@ static void spi_set_clock(uint32_t max_hz) { int source_clk = 0; int spi_clk = 0; int div = 0; - uint32_t param; + uint32_t param = PWD_SPI_CLK_BIT; if (max_hz > 4333000) { if (max_hz > 30000000) { spi_clk = 30000000; diff --git a/esphome/components/iaqcore/iaqcore.cpp b/esphome/components/iaqcore/iaqcore.cpp index 274f9086b6..c414eb8f60 100644 --- a/esphome/components/iaqcore/iaqcore.cpp +++ b/esphome/components/iaqcore/iaqcore.cpp @@ -10,11 +10,13 @@ static const char *const TAG = "iaqcore"; enum IAQCoreErrorCode : uint8_t { ERROR_OK = 0, ERROR_RUNIN = 0x10, ERROR_BUSY = 0x01, ERROR_ERROR = 0x80 }; +static constexpr size_t SENSOR_DATA_LENGTH = 9; + struct SensorData { - uint16_t co2; - IAQCoreErrorCode status; int32_t resistance; + uint16_t co2; uint16_t tvoc; + IAQCoreErrorCode status; SensorData(const uint8_t *buffer) { this->co2 = encode_uint16(buffer[0], buffer[1]); @@ -33,9 +35,9 @@ void IAQCore::setup() { } void IAQCore::update() { - uint8_t buffer[sizeof(SensorData)]; + uint8_t buffer[SENSOR_DATA_LENGTH]; - if (this->read_register(0xB5, buffer, sizeof(buffer)) != i2c::ERROR_OK) { + if (this->read_register(0xB5, buffer, SENSOR_DATA_LENGTH) != i2c::ERROR_OK) { ESP_LOGD(TAG, "Read failed"); this->status_set_warning(); this->publish_nans_(); diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index 3c2c06fd68..e61d0142be 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -222,7 +222,7 @@ bool SCD30Component::force_recalibration_with_reference(uint16_t co2_reference) } uint16_t SCD30Component::get_forced_calibration_reference() { - uint16_t forced_calibration_reference; + uint16_t forced_calibration_reference = 0; // Get current CO2 calibration if (!this->get_register(SCD30_CMD_FORCED_CALIBRATION, forced_calibration_reference)) { ESP_LOGE(TAG, "Unable to read forced calibration reference."); diff --git a/esphome/components/sen21231/sen21231.cpp b/esphome/components/sen21231/sen21231.cpp index 67001c3f14..8c9f3d7134 100644 --- a/esphome/components/sen21231/sen21231.cpp +++ b/esphome/components/sen21231/sen21231.cpp @@ -20,7 +20,12 @@ void Sen21231Sensor::dump_config() { void Sen21231Sensor::read_data_() { person_sensor_results_t results; - this->read_bytes(PERSON_SENSOR_I2C_ADDRESS, (uint8_t *) &results, sizeof(results)); + if (!this->read_bytes(PERSON_SENSOR_I2C_ADDRESS, (uint8_t *) &results, sizeof(results))) { + ESP_LOGW(TAG, "Failed to read data from SEN21231"); + this->status_set_warning(); + return; + } + this->status_clear_warning(); ESP_LOGD(TAG, "SEN21231: %d faces detected", results.num_faces); this->publish_state(results.num_faces); if (results.num_faces == 1) { From de7572bd3e7ebb9308744efb6f7faf88c480629d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:04:12 -0500 Subject: [PATCH 423/609] [lightwaverf] Fix ISR safety issues (#14563) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/lightwaverf/LwRx.cpp | 14 +++++++++++--- esphome/components/lightwaverf/LwRx.h | 4 ++-- esphome/components/lightwaverf/LwTx.cpp | 3 ++- esphome/components/lightwaverf/LwTx.h | 4 ++-- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/esphome/components/lightwaverf/LwRx.cpp b/esphome/components/lightwaverf/LwRx.cpp index 2b1ad5e870..9710457850 100644 --- a/esphome/components/lightwaverf/LwRx.cpp +++ b/esphome/components/lightwaverf/LwRx.cpp @@ -8,6 +8,7 @@ #include "LwRx.h" #include <cstring> +#include "esphome/core/helpers.h" namespace esphome { namespace lightwaverf { @@ -185,13 +186,20 @@ bool LwRx::lwrx_getmessage(uint8_t *buf, uint8_t len) { bool ret = true; int16_t j = 0; // int if (this->rx_msgcomplete && len <= RX_MSGLEN) { + // Copy message under interrupt lock to prevent ISR overwriting rx_msg mid-read + uint8_t msg_copy[RX_MSGLEN]; + { + InterruptLock lock; + memcpy(msg_copy, this->rx_msg, RX_MSGLEN); + this->rx_msgcomplete = false; + } for (uint8_t i = 0; ret && i < RX_MSGLEN; i++) { if (this->rx_translate || (len != RX_MSGLEN)) { - j = this->rx_find_nibble_(this->rx_msg[i]); + j = this->rx_find_nibble_(msg_copy[i]); if (j < 0) ret = false; } else { - j = this->rx_msg[i]; + j = msg_copy[i]; } switch (len) { case 4: @@ -199,6 +207,7 @@ bool LwRx::lwrx_getmessage(uint8_t *buf, uint8_t len) { buf[2] = j; if (i == 2) buf[3] = j; + [[fallthrough]]; case 2: if (i == 3) buf[0] = j; @@ -212,7 +221,6 @@ bool LwRx::lwrx_getmessage(uint8_t *buf, uint8_t len) { break; } } - this->rx_msgcomplete = false; } else { ret = false; } diff --git a/esphome/components/lightwaverf/LwRx.h b/esphome/components/lightwaverf/LwRx.h index 7200f9a51c..8b34de9fbb 100644 --- a/esphome/components/lightwaverf/LwRx.h +++ b/esphome/components/lightwaverf/LwRx.h @@ -105,8 +105,8 @@ class LwRx { uint32_t rx_prev; // time of previous interrupt in microseconds - bool rx_msgcomplete = false; // set high when message available - bool rx_translate = true; // Set false to get raw data + volatile bool rx_msgcomplete = false; // set high when message available + bool rx_translate = true; // Set false to get raw data uint8_t rx_state = 0; diff --git a/esphome/components/lightwaverf/LwTx.cpp b/esphome/components/lightwaverf/LwTx.cpp index b69b93b978..8852935bfd 100644 --- a/esphome/components/lightwaverf/LwTx.cpp +++ b/esphome/components/lightwaverf/LwTx.cpp @@ -192,7 +192,8 @@ void LwTx::lwtx_set_gap_multiplier(uint8_t gap_multiplier) { this->tx_gap_multip void LwTx::lw_timer_start() { { InterruptLock lock; - static LwTx *arg = this; // NOLINT + static LwTx *arg; + arg = this; timer1_attachInterrupt([] { isr_t_xtimer(arg); }); timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP); timer1_write(this->espPeriod); diff --git a/esphome/components/lightwaverf/LwTx.h b/esphome/components/lightwaverf/LwTx.h index fe7b942a3a..9192426440 100644 --- a/esphome/components/lightwaverf/LwTx.h +++ b/esphome/components/lightwaverf/LwTx.h @@ -62,8 +62,8 @@ class LwTx { uint8_t tx_repeats = 12; // Number of repeats of message sent uint8_t txon = 1; uint8_t txoff = 0; - bool tx_msg_active = false; // set true to activate message sending - bool tx_translate = true; // Set false to send raw data + volatile bool tx_msg_active = false; // set true to activate message sending + bool tx_translate = true; // Set false to send raw data uint8_t tx_buf[TX_MSGLEN]; // the message buffer during reception uint8_t tx_repeat = 0; // counter for repeats From c26c5935b6c9bb1b7a521efa7188a0c3dad74e66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:12:19 -1000 Subject: [PATCH 424/609] Bump github/codeql-action from 4.32.5 to 4.32.6 (#14566) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 4bd018b5c9..1a0c54da6d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5 + uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5 + uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 with: category: "/language:${{matrix.language}}" From 086c1bb505dc718d35e9c91b6fbea5bb54705735 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:12:44 -1000 Subject: [PATCH 425/609] Bump docker/build-push-action from 6.19.2 to 7.0.0 in /.github/actions/build-image (#14567) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-image/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index 38e93c4f17..a895226030 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -47,7 +47,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false @@ -73,7 +73,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false From 035f98569326bafd7da350e4a15c2739c424e987 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 22:16:36 +0000 Subject: [PATCH 426/609] Bump ruff from 0.15.4 to 0.15.5 (#14565) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d70dd9d0e1..b036da6ef1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.3 + rev: v0.15.5 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 6b2617b656..93a20896aa 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.4 # also change in .pre-commit-config.yaml when updating +ruff==0.15.5 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From d8deb2255d3a982bb3a755efa9291a2a59014c69 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:18:09 -0500 Subject: [PATCH 427/609] [mipi_rgb] Fix byte order and dirty bounds in fill() (#14537) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/mipi_rgb/mipi_rgb.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index 7ff6868c15..ae7c795846 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -288,9 +288,7 @@ void MipiRgb::draw_pixel_at(int x, int y, Color color) { if (!this->check_buffer_()) return; size_t pos = (y * this->width_) + x; - uint8_t hi_byte = static_cast<uint8_t>(color.r & 0xF8) | (color.g >> 5); - uint8_t lo_byte = static_cast<uint8_t>((color.g & 0x1C) << 3) | (color.b >> 3); - uint16_t new_color = hi_byte | (lo_byte << 8); // big endian + uint16_t new_color = convert_big_endian(display::ColorUtil::color_to_565(color)); if (this->buffer_[pos] == new_color) return; this->buffer_[pos] = new_color; @@ -315,10 +313,12 @@ void MipiRgb::fill(Color color) { } auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_); - uint8_t hi_byte = static_cast<uint8_t>(color.r & 0xF8) | (color.g >> 5); - uint8_t lo_byte = static_cast<uint8_t>((color.g & 0x1C) << 3) | (color.b >> 3); - uint16_t new_color = lo_byte | (hi_byte << 8); // little endian + uint16_t new_color = convert_big_endian(display::ColorUtil::color_to_565(color)); std::fill_n(ptr_16, this->width_ * this->height_, new_color); + this->x_low_ = 0; + this->y_low_ = 0; + this->x_high_ = this->width_ - 1; + this->y_high_ = this->height_ - 1; } int MipiRgb::get_width() { From f53ee70caadb75ef3ffe20f2a471d15ba06e34a7 Mon Sep 17 00:00:00 2001 From: AndreKR <haensel@creations.de> Date: Sat, 7 Mar 2026 01:29:20 +0100 Subject: [PATCH 428/609] [http_request] Make TLS buffer configurable on ESP8266 (#14009) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/http_request/__init__.py | 10 ++++++++++ .../components/http_request/http_request_arduino.cpp | 8 +++----- esphome/components/http_request/http_request_arduino.h | 10 ++++++++++ tests/components/http_request/test.esp8266-ard.yaml | 8 +++++--- 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 2d6ecae0bc..81337ebdf6 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -50,6 +50,8 @@ CONF_FOLLOW_REDIRECTS = "follow_redirects" CONF_REDIRECT_LIMIT = "redirect_limit" CONF_BUFFER_SIZE_RX = "buffer_size_rx" CONF_BUFFER_SIZE_TX = "buffer_size_tx" +CONF_TLS_BUFFER_SIZE_RX = "tls_buffer_size_rx" +CONF_TLS_BUFFER_SIZE_TX = "tls_buffer_size_tx" CONF_CA_CERTIFICATE_PATH = "ca_certificate_path" CONF_MAX_RESPONSE_BUFFER_SIZE = "max_response_buffer_size" @@ -124,6 +126,12 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault(CONF_BUFFER_SIZE_TX, esp32=512): cv.All( cv.uint16_t, cv.only_on_esp32 ), + cv.SplitDefault(CONF_TLS_BUFFER_SIZE_RX, esp8266=512): cv.All( + cv.uint16_t, cv.only_on_esp8266 + ), + cv.SplitDefault(CONF_TLS_BUFFER_SIZE_TX, esp8266=512): cv.All( + cv.uint16_t, cv.only_on_esp8266 + ), cv.Optional(CONF_CA_CERTIFICATE_PATH): cv.All( cv.file_, cv.Any(cv.only_on(PLATFORM_HOST), cv.only_on_esp32), @@ -150,6 +158,8 @@ async def to_code(config): if CORE.is_esp8266 and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]: cg.add_define("USE_HTTP_REQUEST_ESP8266_HTTPS") + cg.add(var.set_tls_buffer_size_rx(config[CONF_TLS_BUFFER_SIZE_RX])) + cg.add(var.set_tls_buffer_size_tx(config[CONF_TLS_BUFFER_SIZE_TX])) if timeout_ms := config.get(CONF_WATCHDOG_TIMEOUT): cg.add(var.set_watchdog_timeout(timeout_ms)) diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 56b51e8b5a..f0dd649285 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -18,8 +18,6 @@ namespace esphome::http_request { static const char *const TAG = "http_request.arduino"; #ifdef USE_ESP8266 -static constexpr int RX_BUFFER_SIZE = 512; -static constexpr int TX_BUFFER_SIZE = 512; // ESP8266 Arduino core (WiFiClientSecureBearSSL.cpp) returns -1000 on OOM static constexpr int ESP8266_SSL_ERR_OOM = -1000; #endif @@ -58,7 +56,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur ESP_LOGV(TAG, "ESP8266 HTTPS connection with WiFiClientSecure"); stream_ptr = std::make_unique<WiFiClientSecure>(); WiFiClientSecure *secure_client = static_cast<WiFiClientSecure *>(stream_ptr.get()); - secure_client->setBufferSizes(RX_BUFFER_SIZE, TX_BUFFER_SIZE); + secure_client->setBufferSizes(this->tls_buffer_size_rx_, this->tls_buffer_size_tx_); secure_client->setInsecure(); } else { stream_ptr = std::make_unique<WiFiClient>(); @@ -138,8 +136,8 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur } ESP_LOGW(TAG, "SSL failure: %s (Code: %d)", LOG_STR_ARG(error_msg), last_error); if (last_error == ESP8266_SSL_ERR_OOM) { - ESP_LOGW(TAG, "Heap free: %u bytes, configured buffer sizes: %u bytes", ESP.getFreeHeap(), - static_cast<unsigned int>(RX_BUFFER_SIZE + TX_BUFFER_SIZE)); + ESP_LOGW(TAG, "Configured TLS buffer sizes: %u/%u bytes, check max free heap block using the debug component", + (unsigned int) this->tls_buffer_size_rx_, (unsigned int) this->tls_buffer_size_tx_); } } else { ESP_LOGW(TAG, "Connection failure with no error code"); diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index d5ce5c0ff3..b009d45b1c 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -47,10 +47,20 @@ class HttpContainerArduino : public HttpContainer { }; class HttpRequestArduino : public HttpRequestComponent { + public: +#ifdef USE_ESP8266 + void set_tls_buffer_size_rx(uint16_t size) { this->tls_buffer_size_rx_ = size; } + void set_tls_buffer_size_tx(uint16_t size) { this->tls_buffer_size_tx_ = size; } +#endif + protected: std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body, const std::vector<Header> &request_headers, const std::vector<std::string> &lower_case_collect_headers) override; +#ifdef USE_ESP8266 + uint16_t tls_buffer_size_rx_{512}; + uint16_t tls_buffer_size_tx_{512}; +#endif }; } // namespace esphome::http_request diff --git a/tests/components/http_request/test.esp8266-ard.yaml b/tests/components/http_request/test.esp8266-ard.yaml index c1937b5a10..dd2d0df62b 100644 --- a/tests/components/http_request/test.esp8266-ard.yaml +++ b/tests/components/http_request/test.esp8266-ard.yaml @@ -1,4 +1,6 @@ -substitutions: - verify_ssl: "false" - <<: !include common.yaml + +http_request: + verify_ssl: false + tls_buffer_size_rx: 16384 + tls_buffer_size_tx: 512 From df11e2765ed016dabcdc211b7321289fa7cbeccf Mon Sep 17 00:00:00 2001 From: Ricardo Sanz <me@ricardosa.nz> Date: Sat, 7 Mar 2026 02:00:52 +0100 Subject: [PATCH 429/609] [climate][haier][template][core] Relocate CONF_CURRENT_TEMPERATURE to general const file (#14503) --- esphome/components/climate/__init__.py | 2 +- esphome/components/haier/climate.py | 8 ++------ esphome/components/template/water_heater/__init__.py | 2 +- esphome/const.py | 1 + 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 1f449ad2a4..f5b91c502c 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_AWAY_COMMAND_TOPIC, CONF_AWAY_STATE_TOPIC, CONF_CURRENT_HUMIDITY_STATE_TOPIC, + CONF_CURRENT_TEMPERATURE, CONF_CURRENT_TEMPERATURE_STATE_TOPIC, CONF_CUSTOM_FAN_MODE, CONF_CUSTOM_PRESET, @@ -112,7 +113,6 @@ CLIMATE_SWING_MODES = { validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) -CONF_CURRENT_TEMPERATURE = "current_temperature" CONF_MIN_HUMIDITY = "min_humidity" CONF_MAX_HUMIDITY = "max_humidity" CONF_TARGET_HUMIDITY = "target_humidity" diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index 8c3649058f..6c208f6caa 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -3,15 +3,11 @@ import logging from esphome import automation import esphome.codegen as cg from esphome.components import climate, logger, uart -from esphome.components.climate import ( - CONF_CURRENT_TEMPERATURE, - ClimateMode, - ClimatePreset, - ClimateSwingMode, -) +from esphome.components.climate import ClimateMode, ClimatePreset, ClimateSwingMode import esphome.config_validation as cv from esphome.const import ( CONF_BEEPER, + CONF_CURRENT_TEMPERATURE, CONF_DISPLAY, CONF_ID, CONF_LEVEL, diff --git a/esphome/components/template/water_heater/__init__.py b/esphome/components/template/water_heater/__init__.py index 71f98c826a..cb5f2dbe56 100644 --- a/esphome/components/template/water_heater/__init__.py +++ b/esphome/components/template/water_heater/__init__.py @@ -4,6 +4,7 @@ from esphome.components import water_heater import esphome.config_validation as cv from esphome.const import ( CONF_AWAY, + CONF_CURRENT_TEMPERATURE, CONF_ID, CONF_MODE, CONF_OPTIMISTIC, @@ -18,7 +19,6 @@ from esphome.types import ConfigType from .. import template_ns -CONF_CURRENT_TEMPERATURE = "current_temperature" CONF_IS_ON = "is_on" TemplateWaterHeater = template_ns.class_( diff --git a/esphome/const.py b/esphome/const.py index 060e962573..88e3c33fbc 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -274,6 +274,7 @@ CONF_CURRENT = "current" CONF_CURRENT_HUMIDITY_STATE_TOPIC = "current_humidity_state_topic" CONF_CURRENT_OPERATION = "current_operation" CONF_CURRENT_RESISTOR = "current_resistor" +CONF_CURRENT_TEMPERATURE = "current_temperature" CONF_CURRENT_TEMPERATURE_STATE_TOPIC = "current_temperature_state_topic" CONF_CUSTOM = "custom" CONF_CUSTOM_FAN_MODE = "custom_fan_mode" From 9b489c9eba639b8dea29dec7815c32c22cda28a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 03:52:51 +0000 Subject: [PATCH 430/609] Bump aioesphomeapi from 44.2.0 to 44.3.1 (#14580) 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 f111e05a9d..8e875eba62 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.2.0 +aioesphomeapi==44.3.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 05ae69b766e9745892e97c8ac40d7017d5fecb3b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Mar 2026 19:00:37 -1000 Subject: [PATCH 431/609] [api] Sync api.proto from aioesphomeapi (#14579) --- esphome/components/api/api.proto | 132 ++++++++++++++ esphome/components/api/api_pb2.cpp | 161 +++++++++++++++++ esphome/components/api/api_pb2.h | 200 ++++++++++++++++++++- esphome/components/api/api_pb2_dump.cpp | 117 ++++++++++++ esphome/components/api/api_pb2_service.cpp | 66 +++++++ esphome/components/api/api_pb2_service.h | 21 +++ 6 files changed, 696 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 802e3e3ae2..618fd1b83c 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -58,6 +58,7 @@ service APIConnection { rpc subscribe_bluetooth_connections_free(SubscribeBluetoothConnectionsFreeRequest) returns (BluetoothConnectionsFreeResponse) {} rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc bluetooth_scanner_set_mode(BluetoothScannerSetModeRequest) returns (void) {} + rpc bluetooth_set_connection_params(BluetoothSetConnectionParamsRequest) returns (BluetoothSetConnectionParamsResponse) {} rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {} rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {} @@ -69,6 +70,12 @@ service APIConnection { rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {} rpc infrared_rf_transmit_raw_timings(InfraredRFTransmitRawTimingsRequest) returns (void) {} + + rpc serial_proxy_configure(SerialProxyConfigureRequest) returns (void) {} + rpc serial_proxy_write(SerialProxyWriteRequest) returns (void) {} + rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {} + rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {} + rpc serial_proxy_request(SerialProxyRequest) returns (void) {} } @@ -198,6 +205,17 @@ message DeviceInfo { uint32 area_id = 3; } +enum SerialProxyPortType { + SERIAL_PROXY_PORT_TYPE_TTL = 0; + SERIAL_PROXY_PORT_TYPE_RS232 = 1; + SERIAL_PROXY_PORT_TYPE_RS485 = 2; +} + +message SerialProxyInfo { + string name = 1; // Human-readable port name + SerialProxyPortType port_type = 2; // Port type (RS232, RS485) +} + message DeviceInfoResponse { option (id) = 10; option (source) = SOURCE_SERVER; @@ -260,6 +278,9 @@ message DeviceInfoResponse { // Indicates if Z-Wave proxy support is available and features supported uint32 zwave_proxy_feature_flags = 23 [(field_ifdef) = "USE_ZWAVE_PROXY"]; uint32 zwave_home_id = 24 [(field_ifdef) = "USE_ZWAVE_PROXY"]; + + // Serial proxy instance metadata + repeated SerialProxyInfo serial_proxies = 25 [(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"]; } message ListEntitiesRequest { @@ -2517,3 +2538,114 @@ message InfraredRFReceiveEvent { fixed32 key = 2; // Key identifying the receiver instance repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods } + +// ==================== SERIAL PROXY ==================== + +enum SerialProxyParity { + SERIAL_PROXY_PARITY_NONE = 0; + SERIAL_PROXY_PARITY_EVEN = 1; + SERIAL_PROXY_PARITY_ODD = 2; +} + +// Configure UART parameters for a serial proxy instance +message SerialProxyConfigureRequest { + option (id) = 138; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + uint32 baudrate = 2; // Baud rate in bits per second + bool flow_control = 3; // Enable hardware flow control + SerialProxyParity parity = 4; // Parity setting + uint32 stop_bits = 5; // Number of stop bits (1 or 2) + uint32 data_size = 6; // Number of data bits (5-8) +} + +// Data received from a serial device, forwarded to clients +message SerialProxyDataReceived { + option (id) = 139; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SERIAL_PROXY"; + option (no_delay) = true; + + uint32 instance = 1; // Instance index (0-based) + bytes data = 2; // Raw data received from the serial device +} + +// Write data to a serial device +message SerialProxyWriteRequest { + option (id) = 140; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + option (no_delay) = true; + + uint32 instance = 1; // Instance index (0-based) + bytes data = 2; // Raw data to write to the serial device +} + +// Set modem control pin states (RTS and DTR) +message SerialProxySetModemPinsRequest { + option (id) = 141; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags +} + +// Request current modem control pin states +message SerialProxyGetModemPinsRequest { + option (id) = 142; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) +} + +// Response with current modem control pin states +message SerialProxyGetModemPinsResponse { + option (id) = 143; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags +} + +enum SerialProxyRequestType { + SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0; // Subscribe to receive data from this serial proxy instance + SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1; // Unsubscribe from this serial proxy instance + SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2; // Flush the serial port (block until all TX data is sent) +} + +// Generic request message for simple serial proxy operations +message SerialProxyRequest { + option (id) = 144; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + SerialProxyRequestType type = 2; // Request type +} + +// ==================== BLUETOOTH CONNECTION PARAMS ==================== +message BluetoothSetConnectionParamsRequest { + option (id) = 145; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 min_interval = 2; // units of 1.25ms + uint32 max_interval = 3; // units of 1.25ms + uint32 latency = 4; + uint32 timeout = 5; // units of 10ms +} + +message BluetoothSetConnectionParamsResponse { + option (id) = 146; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + int32 error = 2; +} diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index d8703aa416..b1176de539 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -71,6 +71,18 @@ uint32_t DeviceInfo::calculate_size() const { return size; } #endif +#ifdef USE_SERIAL_PROXY +void SerialProxyInfo::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_string(1, this->name); + buffer.encode_uint32(2, static_cast<uint32_t>(this->port_type)); +} +uint32_t SerialProxyInfo::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->port_type)); + return size; +} +#endif void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(2, this->name); buffer.encode_string(3, this->mac_address); @@ -125,6 +137,11 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { #ifdef USE_ZWAVE_PROXY buffer.encode_uint32(24, this->zwave_home_id); #endif +#ifdef USE_SERIAL_PROXY + for (const auto &it : this->serial_proxies) { + buffer.encode_message(25, it); + } +#endif } uint32_t DeviceInfoResponse::calculate_size() const { uint32_t size = 0; @@ -180,6 +197,11 @@ uint32_t DeviceInfoResponse::calculate_size() const { #endif #ifdef USE_ZWAVE_PROXY size += ProtoSize::calc_uint32(2, this->zwave_home_id); +#endif +#ifdef USE_SERIAL_PROXY + for (const auto &it : this->serial_proxies) { + size += ProtoSize::calc_message_force(2, it.calculate_size()); + } #endif return size; } @@ -3714,5 +3736,144 @@ uint32_t InfraredRFReceiveEvent::calculate_size() const { return size; } #endif +#ifdef USE_SERIAL_PROXY +bool SerialProxyConfigureRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->instance = value.as_uint32(); + break; + case 2: + this->baudrate = value.as_uint32(); + break; + case 3: + this->flow_control = value.as_bool(); + break; + case 4: + this->parity = static_cast<enums::SerialProxyParity>(value.as_uint32()); + break; + case 5: + this->stop_bits = value.as_uint32(); + break; + case 6: + this->data_size = value.as_uint32(); + break; + default: + return false; + } + return true; +} +void SerialProxyDataReceived::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_uint32(1, this->instance); + buffer.encode_bytes(2, this->data_ptr_, this->data_len_); +} +uint32_t SerialProxyDataReceived::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->instance); + size += ProtoSize::calc_length(1, this->data_len_); + return size; +} +bool SerialProxyWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->instance = value.as_uint32(); + break; + default: + return false; + } + return true; +} +bool SerialProxyWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->data = value.data(); + this->data_len = value.size(); + break; + } + default: + return false; + } + return true; +} +bool SerialProxySetModemPinsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->instance = value.as_uint32(); + break; + case 2: + this->line_states = value.as_uint32(); + break; + default: + return false; + } + return true; +} +bool SerialProxyGetModemPinsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->instance = value.as_uint32(); + break; + default: + return false; + } + return true; +} +void SerialProxyGetModemPinsResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_uint32(1, this->instance); + buffer.encode_uint32(2, this->line_states); +} +uint32_t SerialProxyGetModemPinsResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->instance); + size += ProtoSize::calc_uint32(1, this->line_states); + return size; +} +bool SerialProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->instance = value.as_uint32(); + break; + case 2: + this->type = static_cast<enums::SerialProxyRequestType>(value.as_uint32()); + break; + default: + return false; + } + return true; +} +#endif +#ifdef USE_BLUETOOTH_PROXY +bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->address = value.as_uint64(); + break; + case 2: + this->min_interval = value.as_uint32(); + break; + case 3: + this->max_interval = value.as_uint32(); + break; + case 4: + this->latency = value.as_uint32(); + break; + case 5: + this->timeout = value.as_uint32(); + break; + default: + return false; + } + return true; +} +void BluetoothSetConnectionParamsResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_int32(2, this->error); +} +uint32_t BluetoothSetConnectionParamsResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_int32(1, this->error); + return size; +} +#endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 89cb1158f3..a6167dc810 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -11,6 +11,11 @@ namespace esphome::api { namespace enums { +enum SerialProxyPortType : uint32_t { + SERIAL_PROXY_PORT_TYPE_TTL = 0, + SERIAL_PROXY_PORT_TYPE_RS232 = 1, + SERIAL_PROXY_PORT_TYPE_RS485 = 2, +}; enum EntityCategory : uint32_t { ENTITY_CATEGORY_NONE = 0, ENTITY_CATEGORY_CONFIG = 1, @@ -317,6 +322,18 @@ enum ZWaveProxyRequestType : uint32_t { ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE = 2, }; #endif +#ifdef USE_SERIAL_PROXY +enum SerialProxyParity : uint32_t { + SERIAL_PROXY_PARITY_NONE = 0, + SERIAL_PROXY_PARITY_EVEN = 1, + SERIAL_PROXY_PARITY_ODD = 2, +}; +enum SerialProxyRequestType : uint32_t { + SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0, + SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1, + SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2, +}; +#endif } // namespace enums @@ -477,10 +494,24 @@ class DeviceInfo final : public ProtoMessage { protected: }; #endif +#ifdef USE_SERIAL_PROXY +class SerialProxyInfo final : public ProtoMessage { + public: + StringRef name{}; + enums::SerialProxyPortType port_type{}; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +#endif class DeviceInfoResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 10; - static constexpr uint8_t ESTIMATED_SIZE = 255; + static constexpr uint16_t ESTIMATED_SIZE = 309; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "device_info_response"; } #endif @@ -532,6 +563,9 @@ class DeviceInfoResponse final : public ProtoMessage { #endif #ifdef USE_ZWAVE_PROXY uint32_t zwave_home_id{0}; +#endif +#ifdef USE_SERIAL_PROXY + std::array<SerialProxyInfo, SERIAL_PROXY_COUNT> serial_proxies{}; #endif void encode(ProtoWriteBuffer &buffer) const; uint32_t calculate_size() const; @@ -3061,5 +3095,169 @@ class InfraredRFReceiveEvent final : public ProtoMessage { protected: }; #endif +#ifdef USE_SERIAL_PROXY +class SerialProxyConfigureRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 138; + static constexpr uint8_t ESTIMATED_SIZE = 20; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_configure_request"; } +#endif + uint32_t instance{0}; + uint32_t baudrate{0}; + bool flow_control{false}; + enums::SerialProxyParity parity{}; + uint32_t stop_bits{0}; + uint32_t data_size{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SerialProxyDataReceived final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 139; + static constexpr uint8_t ESTIMATED_SIZE = 23; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_data_received"; } +#endif + uint32_t instance{0}; + const uint8_t *data_ptr_{nullptr}; + size_t data_len_{0}; + void set_data(const uint8_t *data, size_t len) { + this->data_ptr_ = data; + this->data_len_ = len; + } + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +class SerialProxyWriteRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 140; + static constexpr uint8_t ESTIMATED_SIZE = 23; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_write_request"; } +#endif + uint32_t instance{0}; + const uint8_t *data{nullptr}; + uint16_t data_len{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SerialProxySetModemPinsRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 141; + static constexpr uint8_t ESTIMATED_SIZE = 8; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_set_modem_pins_request"; } +#endif + uint32_t instance{0}; + uint32_t line_states{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SerialProxyGetModemPinsRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 142; + static constexpr uint8_t ESTIMATED_SIZE = 4; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_get_modem_pins_request"; } +#endif + uint32_t instance{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SerialProxyGetModemPinsResponse final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 143; + static constexpr uint8_t ESTIMATED_SIZE = 8; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_get_modem_pins_response"; } +#endif + uint32_t instance{0}; + uint32_t line_states{0}; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +class SerialProxyRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 144; + static constexpr uint8_t ESTIMATED_SIZE = 6; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_request"; } +#endif + uint32_t instance{0}; + enums::SerialProxyRequestType type{}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +#endif +#ifdef USE_BLUETOOTH_PROXY +class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 145; + static constexpr uint8_t ESTIMATED_SIZE = 20; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "bluetooth_set_connection_params_request"; } +#endif + uint64_t address{0}; + uint32_t min_interval{0}; + uint32_t max_interval{0}; + uint32_t latency{0}; + uint32_t timeout{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothSetConnectionParamsResponse final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 146; + static constexpr uint8_t ESTIMATED_SIZE = 8; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "bluetooth_set_connection_params_response"; } +#endif + uint64_t address{0}; + int32_t error{0}; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +#endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 4eec42e936..086b1bdc2f 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -100,6 +100,18 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint out.append(hex_buf).append("\n"); } +template<> const char *proto_enum_to_string<enums::SerialProxyPortType>(enums::SerialProxyPortType value) { + switch (value) { + case enums::SERIAL_PROXY_PORT_TYPE_TTL: + return "SERIAL_PROXY_PORT_TYPE_TTL"; + case enums::SERIAL_PROXY_PORT_TYPE_RS232: + return "SERIAL_PROXY_PORT_TYPE_RS232"; + case enums::SERIAL_PROXY_PORT_TYPE_RS485: + return "SERIAL_PROXY_PORT_TYPE_RS485"; + default: + return "UNKNOWN"; + } +} template<> const char *proto_enum_to_string<enums::EntityCategory>(enums::EntityCategory value) { switch (value) { case enums::ENTITY_CATEGORY_NONE: @@ -752,6 +764,32 @@ template<> const char *proto_enum_to_string<enums::ZWaveProxyRequestType>(enums: } } #endif +#ifdef USE_SERIAL_PROXY +template<> const char *proto_enum_to_string<enums::SerialProxyParity>(enums::SerialProxyParity value) { + switch (value) { + case enums::SERIAL_PROXY_PARITY_NONE: + return "SERIAL_PROXY_PARITY_NONE"; + case enums::SERIAL_PROXY_PARITY_EVEN: + return "SERIAL_PROXY_PARITY_EVEN"; + case enums::SERIAL_PROXY_PARITY_ODD: + return "SERIAL_PROXY_PARITY_ODD"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string<enums::SerialProxyRequestType>(enums::SerialProxyRequestType value) { + switch (value) { + case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE: + return "SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE"; + case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: + return "SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE"; + case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: + return "SERIAL_PROXY_REQUEST_TYPE_FLUSH"; + default: + return "UNKNOWN"; + } +} +#endif const char *HelloRequest::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "HelloRequest"); @@ -801,6 +839,14 @@ const char *DeviceInfo::dump_to(DumpBuffer &out) const { return out.c_str(); } #endif +#ifdef USE_SERIAL_PROXY +const char *SerialProxyInfo::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyInfo"); + dump_field(out, "name", this->name); + dump_field(out, "port_type", static_cast<enums::SerialProxyPortType>(this->port_type)); + return out.c_str(); +} +#endif const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "DeviceInfoResponse"); dump_field(out, "name", this->name); @@ -861,6 +907,13 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const { #endif #ifdef USE_ZWAVE_PROXY dump_field(out, "zwave_home_id", this->zwave_home_id); +#endif +#ifdef USE_SERIAL_PROXY + for (const auto &it : this->serial_proxies) { + out.append(" serial_proxies: "); + it.dump_to(out); + out.append("\n"); + } #endif return out.c_str(); } @@ -2510,6 +2563,70 @@ const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const { return out.c_str(); } #endif +#ifdef USE_SERIAL_PROXY +const char *SerialProxyConfigureRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyConfigureRequest"); + dump_field(out, "instance", this->instance); + dump_field(out, "baudrate", this->baudrate); + dump_field(out, "flow_control", this->flow_control); + dump_field(out, "parity", static_cast<enums::SerialProxyParity>(this->parity)); + dump_field(out, "stop_bits", this->stop_bits); + dump_field(out, "data_size", this->data_size); + return out.c_str(); +} +const char *SerialProxyDataReceived::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyDataReceived"); + dump_field(out, "instance", this->instance); + dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); + return out.c_str(); +} +const char *SerialProxyWriteRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyWriteRequest"); + dump_field(out, "instance", this->instance); + dump_bytes_field(out, "data", this->data, this->data_len); + return out.c_str(); +} +const char *SerialProxySetModemPinsRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxySetModemPinsRequest"); + dump_field(out, "instance", this->instance); + dump_field(out, "line_states", this->line_states); + return out.c_str(); +} +const char *SerialProxyGetModemPinsRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyGetModemPinsRequest"); + dump_field(out, "instance", this->instance); + return out.c_str(); +} +const char *SerialProxyGetModemPinsResponse::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyGetModemPinsResponse"); + dump_field(out, "instance", this->instance); + dump_field(out, "line_states", this->line_states); + return out.c_str(); +} +const char *SerialProxyRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyRequest"); + dump_field(out, "instance", this->instance); + dump_field(out, "type", static_cast<enums::SerialProxyRequestType>(this->type)); + return out.c_str(); +} +#endif +#ifdef USE_BLUETOOTH_PROXY +const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "BluetoothSetConnectionParamsRequest"); + dump_field(out, "address", this->address); + dump_field(out, "min_interval", this->min_interval); + dump_field(out, "max_interval", this->max_interval); + dump_field(out, "latency", this->latency); + dump_field(out, "timeout", this->timeout); + return out.c_str(); +} +const char *BluetoothSetConnectionParamsResponse::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "BluetoothSetConnectionParamsResponse"); + dump_field(out, "address", this->address); + dump_field(out, "error", this->error); + return out.c_str(); +} +#endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index f9151ae3b4..f2f7fa5238 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -634,6 +634,72 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_infrared_rf_transmit_raw_timings_request(msg); break; } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxyConfigureRequest::MESSAGE_TYPE: { + SerialProxyConfigureRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_configure_request"), msg); +#endif + this->on_serial_proxy_configure_request(msg); + break; + } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxyWriteRequest::MESSAGE_TYPE: { + SerialProxyWriteRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_write_request"), msg); +#endif + this->on_serial_proxy_write_request(msg); + break; + } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxySetModemPinsRequest::MESSAGE_TYPE: { + SerialProxySetModemPinsRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_set_modem_pins_request"), msg); +#endif + this->on_serial_proxy_set_modem_pins_request(msg); + break; + } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxyGetModemPinsRequest::MESSAGE_TYPE: { + SerialProxyGetModemPinsRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_get_modem_pins_request"), msg); +#endif + this->on_serial_proxy_get_modem_pins_request(msg); + break; + } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxyRequest::MESSAGE_TYPE: { + SerialProxyRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_request"), msg); +#endif + this->on_serial_proxy_request(msg); + break; + } +#endif +#ifdef USE_BLUETOOTH_PROXY + case BluetoothSetConnectionParamsRequest::MESSAGE_TYPE: { + BluetoothSetConnectionParamsRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_bluetooth_set_connection_params_request"), msg); +#endif + this->on_bluetooth_set_connection_params_request(msg); + break; + } #endif default: break; diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index e70b97196b..a031d2d969 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -216,6 +216,27 @@ class APIServerConnectionBase : public ProtoService { virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){}; #endif +#ifdef USE_SERIAL_PROXY + virtual void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &value){}; +#endif + +#ifdef USE_SERIAL_PROXY + virtual void on_serial_proxy_write_request(const SerialProxyWriteRequest &value){}; +#endif +#ifdef USE_SERIAL_PROXY + virtual void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &value){}; +#endif +#ifdef USE_SERIAL_PROXY + virtual void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &value){}; +#endif + +#ifdef USE_SERIAL_PROXY + virtual void on_serial_proxy_request(const SerialProxyRequest &value){}; +#endif +#ifdef USE_BLUETOOTH_PROXY + virtual void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){}; +#endif + protected: void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; }; From cbebb811965d63f115ea4fba5c8fd7f6da393b29 Mon Sep 17 00:00:00 2001 From: rwrozelle <rwrozelle@gmail.com> Date: Sat, 7 Mar 2026 08:12:27 -0800 Subject: [PATCH 432/609] [openthread] move esp functions into correct file (#14588) --- esphome/components/openthread/openthread.cpp | 9 ++------- esphome/components/openthread/openthread.h | 2 ++ esphome/components/openthread/openthread_esp.cpp | 5 +++++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index fb81481299..1596b6e990 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -1,7 +1,6 @@ #include "esphome/core/defines.h" #ifdef USE_OPENTHREAD #include "openthread.h" -#include "esp_openthread.h" #include <freertos/portmacro.h> @@ -51,7 +50,7 @@ void OpenThreadComponent::on_state_changed_(otChangedFlags flags, void *context) auto *self = static_cast<OpenThreadComponent *>(context); // This runs on the OpenThread task thread with the OT lock held, // so we can safely call otThreadGetDeviceRole directly. - otInstance *instance = esp_openthread_get_instance(); + otInstance *instance = self->get_openthread_instance_(); otDeviceRole role = otThreadGetDeviceRole(instance); self->connected_ = role >= OT_DEVICE_ROLE_CHILD; } @@ -233,16 +232,12 @@ bool OpenThreadComponent::teardown() { otSrpClientClearHostAndServices(instance); otSrpClientBuffersFreeAllServices(instance); global_openthread_component = nullptr; -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) ESP_LOGD(TAG, "Exit main loop "); - int error = esp_openthread_mainloop_exit(); + int error = this->openthread_stop_(); if (error != ESP_OK) { ESP_LOGW(TAG, "Failed attempt to stop main loop %d", error); this->teardown_complete_ = true; } -#else - this->teardown_complete_ = true; -#endif } return this->teardown_complete_; } diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index c87f4fa7c1..75d8fe11fd 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -44,6 +44,8 @@ class OpenThreadComponent : public Component { protected: std::optional<otIp6Address> get_omr_address_(InstanceLock &lock); static void on_state_changed_(otChangedFlags flags, void *context); + otInstance *get_openthread_instance_(); + int openthread_stop_(); std::function<void()> factory_reset_external_callback_; #if CONFIG_OPENTHREAD_MTD uint32_t poll_period_{0}; diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index cdc7a404b2..9cc9223b52 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -190,6 +190,8 @@ void OpenThreadComponent::ot_main() { vTaskDelete(NULL); } +int OpenThreadComponent::openthread_stop_() { return esp_openthread_mainloop_exit(); } + network::IPAddresses OpenThreadComponent::get_ip_addresses() { network::IPAddresses addresses; struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; @@ -204,6 +206,9 @@ network::IPAddresses OpenThreadComponent::get_ip_addresses() { return addresses; } +// not thread safe, only use in read-only use cases +otInstance *OpenThreadComponent::get_openthread_instance_() { return esp_openthread_get_instance(); } + std::optional<InstanceLock> InstanceLock::try_acquire(int delay) { if (esp_openthread_lock_acquire(delay)) { return InstanceLock(); From 0e106d843c730eb15e083ebd0147124dadc99a5a Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Sat, 7 Mar 2026 17:18:21 +0100 Subject: [PATCH 433/609] [nrf52][zephyr] support for multi on rate callbacks (#14557) --- esphome/components/nrf52/__init__.py | 3 +++ esphome/components/nrf52/dfu.cpp | 35 ++++++++------------------- esphome/components/nrf52/dfu.h | 1 - esphome/components/zephyr/__init__.py | 20 +++++++++++---- esphome/components/zephyr/cdc_acm.cpp | 30 +++++++++++++++++++++++ esphome/components/zephyr/cdc_acm.h | 27 +++++++++++++++++++++ esphome/components/zephyr/const.py | 2 ++ 7 files changed, 87 insertions(+), 31 deletions(-) create mode 100644 esphome/components/zephyr/cdc_acm.cpp create mode 100644 esphome/components/zephyr/cdc_acm.h diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index a12d1db1ab..0a9fb5939a 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -20,8 +20,10 @@ from esphome.components.zephyr import ( ) from esphome.components.zephyr.const import ( BOOTLOADER_MCUBOOT, + CONF_CDC_ACM, KEY_BOOTLOADER, KEY_ZEPHYR, + CdcAcm, ) import esphome.config_validation as cv from esphome.const import ( @@ -159,6 +161,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_VERSION): cv.string_strict, } ), + cv.GenerateID(CONF_CDC_ACM): cv.declare_id(CdcAcm), } ), set_framework, diff --git a/esphome/components/nrf52/dfu.cpp b/esphome/components/nrf52/dfu.cpp index 9e49373467..c2017248d2 100644 --- a/esphome/components/nrf52/dfu.cpp +++ b/esphome/components/nrf52/dfu.cpp @@ -2,42 +2,27 @@ #ifdef USE_NRF52_DFU -#include <zephyr/device.h> -#include <zephyr/drivers/uart.h> -#include <zephyr/drivers/uart/cdc_acm.h> #include "esphome/core/log.h" +#include "esphome/components/zephyr/cdc_acm.h" namespace esphome { namespace nrf52 { static const char *const TAG = "dfu"; -volatile bool goto_dfu = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - static const uint32_t DFU_DBL_RESET_MAGIC = 0x5A1AD5; // SALADS -#define DEVICE_AND_COMMA(node_id) DEVICE_DT_GET(node_id), - -static void cdc_dte_rate_callback(const struct device * /*unused*/, uint32_t rate) { - if (rate == 1200) { - goto_dfu = true; - } -} void DeviceFirmwareUpdate::setup() { this->reset_pin_->setup(); - const struct device *cdc_dev[] = {DT_FOREACH_STATUS_OKAY(zephyr_cdc_acm_uart, DEVICE_AND_COMMA)}; - for (auto &idx : cdc_dev) { - cdc_acm_dte_rate_callback_set(idx, cdc_dte_rate_callback); - } -} - -void DeviceFirmwareUpdate::loop() { - if (goto_dfu) { - goto_dfu = false; - volatile uint32_t *dbl_reset_mem = (volatile uint32_t *) 0x20007F7C; - (*dbl_reset_mem) = DFU_DBL_RESET_MAGIC; - this->reset_pin_->digital_write(true); - } +#if defined(CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT) + zephyr::global_cdc_acm->add_on_rate_callback([this](const device *, uint32_t rate) { + if (rate == 1200) { + volatile uint32_t *dbl_reset_mem = (volatile uint32_t *) 0x20007F7C; + (*dbl_reset_mem) = DFU_DBL_RESET_MAGIC; + this->reset_pin_->digital_write(true); + } + }); +#endif } void DeviceFirmwareUpdate::dump_config() { diff --git a/esphome/components/nrf52/dfu.h b/esphome/components/nrf52/dfu.h index 979a4567cf..71060e43c1 100644 --- a/esphome/components/nrf52/dfu.h +++ b/esphome/components/nrf52/dfu.h @@ -10,7 +10,6 @@ namespace nrf52 { class DeviceFirmwareUpdate : public Component { public: void setup() override; - void loop() override; void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void dump_config() override; diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index 4cc71bddca..b8a091feb9 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -5,11 +5,13 @@ from typing import TypedDict import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_BOARD, KEY_CORE, KEY_FRAMEWORK_VERSION -from esphome.core import CORE +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.helpers import copy_file_if_changed, write_file_if_changed +from esphome.types import ConfigType from .const import ( BOOTLOADER_MCUBOOT, + CONF_CDC_ACM, KEY_BOARD, KEY_BOOTLOADER, KEY_EXTRA_BUILD_FILES, @@ -54,7 +56,7 @@ class ZephyrData(TypedDict): user: dict[str, list[str]] -def zephyr_set_core_data(config): +def zephyr_set_core_data(config: ConfigType) -> None: CORE.data[KEY_ZEPHYR] = ZephyrData( board=config[CONF_BOARD], bootloader=config[KEY_BOOTLOADER], @@ -64,7 +66,6 @@ def zephyr_set_core_data(config): pm_static=[], user={}, ) - return config def zephyr_data() -> ZephyrData: @@ -110,7 +111,7 @@ def add_extra_script(stage: str, filename: str, path: Path) -> None: cg.add_platformio_option("extra_scripts", [key]) -def zephyr_to_code(config): +def zephyr_to_code(config: ConfigType) -> None: cg.add_build_flag("-DUSE_ZEPHYR") cg.add_define("USE_NATIVE_64BIT_TIME") cg.set_cpp_standard("gnu++20") @@ -132,6 +133,15 @@ def zephyr_to_code(config): Path(__file__).parent / "pre_build.py.script", ) + CORE.add_job(_cdc_acm_to_code, config) + + +@coroutine_with_priority(CoroPriority.FINAL) +async def _cdc_acm_to_code(config: ConfigType) -> None: + if "CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT" in zephyr_data()[KEY_PRJ_CONF]: + var = cg.new_Pvariable(config[CONF_CDC_ACM]) + await cg.register_component(var, {}) + def zephyr_setup_preferences(): cg.add(zephyr_ns.setup_preferences()) @@ -151,7 +161,7 @@ def _format_prj_conf_val(value: PrjConfValueType) -> str: raise ValueError -def zephyr_add_cdc_acm(config, id): +def zephyr_add_cdc_acm(config: ConfigType, id: int) -> None: framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] if CORE.is_nrf52 and framework_ver >= cv.Version(3, 2, 0): zephyr_add_prj_conf("CONFIG_USB_DEVICE_STACK_NEXT", False) diff --git a/esphome/components/zephyr/cdc_acm.cpp b/esphome/components/zephyr/cdc_acm.cpp new file mode 100644 index 0000000000..04ee9a0bef --- /dev/null +++ b/esphome/components/zephyr/cdc_acm.cpp @@ -0,0 +1,30 @@ +#if defined(CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT) +#include "cdc_acm.h" +#include <zephyr/drivers/uart.h> +#include <zephyr/drivers/uart/cdc_acm.h> + +#define DEVICE_AND_COMMA(node_id) DEVICE_DT_GET(node_id), + +namespace esphome::zephyr { + +CdcAcm::CdcAcm() { global_cdc_acm = this; } + +void CdcAcm::setup() { +#if DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) + const struct device *cdc_dev[] = {DT_FOREACH_STATUS_OKAY(zephyr_cdc_acm_uart, DEVICE_AND_COMMA)}; + for (auto &idx : cdc_dev) { + // only one global callback can be registered + cdc_acm_dte_rate_callback_set(idx, CdcAcm::cdc_dte_rate_callback_); + } +#endif // DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) +} + +void CdcAcm::cdc_dte_rate_callback_(const struct device *device, uint32_t rate) { + global_cdc_acm->defer([device, rate]() { global_cdc_acm->rate_callbacks_.call(device, rate); }); +} + +CdcAcm *global_cdc_acm; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome::zephyr + +#endif diff --git a/esphome/components/zephyr/cdc_acm.h b/esphome/components/zephyr/cdc_acm.h new file mode 100644 index 0000000000..2e9da85a11 --- /dev/null +++ b/esphome/components/zephyr/cdc_acm.h @@ -0,0 +1,27 @@ +#pragma once +#if defined(CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT) + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include <zephyr/device.h> + +namespace esphome::zephyr { + +class CdcAcm : public Component { + public: + CdcAcm(); + void setup() override; + void add_on_rate_callback(std::function<void(const device *, uint32_t)> &&callback) { + this->rate_callbacks_.add(std::move(callback)); + } + + protected: + static void cdc_dte_rate_callback_(const device *device, uint32_t rate); + CallbackManager<void(const device *, uint32_t)> rate_callbacks_; +}; + +extern CdcAcm *global_cdc_acm; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome::zephyr + +#endif diff --git a/esphome/components/zephyr/const.py b/esphome/components/zephyr/const.py index 06a4fc42bc..f67b058ed7 100644 --- a/esphome/components/zephyr/const.py +++ b/esphome/components/zephyr/const.py @@ -14,3 +14,5 @@ KEY_BOARD: Final = "board" KEY_USER: Final = "user" zephyr_ns = cg.esphome_ns.namespace("zephyr") +CdcAcm = zephyr_ns.class_("CdcAcm", cg.Component) +CONF_CDC_ACM = "cdc_acm" From 8b62c35ea7831d2c0e24f836e096b7b4c1cb1ca0 Mon Sep 17 00:00:00 2001 From: Simon Redman <simon@ergotech.com> Date: Sat, 7 Mar 2026 11:41:37 -0500 Subject: [PATCH 434/609] [uart] Add error message when initializing UART with unsupported configuration (#13229) --- esphome/components/uart/uart_component_libretiny.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index cb4465068d..83d2acb332 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -110,7 +110,7 @@ void LibreTinyUARTComponent::setup() { #if LT_HW_UART2 ESP_LOGE(TAG, " TX=%u, RX=%u", PIN_SERIAL2_TX, PIN_SERIAL2_RX); #endif - this->mark_failed(); + this->mark_failed(LOG_STR("SoftwareSerial is not implemented for this chip.")); return; #endif } From 15ffbb0b05da71f6ef008c0a38ffd185d0490edc Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 7 Mar 2026 11:51:02 -0500 Subject: [PATCH 435/609] [uart] Fully enable raw mode with host serial (#14573) --- esphome/components/uart/uart_component_host.cpp | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/esphome/components/uart/uart_component_host.cpp b/esphome/components/uart/uart_component_host.cpp index 0e5ef3c6bd..9dce25c500 100644 --- a/esphome/components/uart/uart_component_host.cpp +++ b/esphome/components/uart/uart_component_host.cpp @@ -124,17 +124,10 @@ void HostUartComponent::setup() { fcntl(this->file_descriptor_, F_SETFL, 0); struct termios options; tcgetattr(this->file_descriptor_, &options); + cfmakeraw(&options); options.c_cflag &= ~CRTSCTS; options.c_cflag |= CREAD | CLOCAL; - options.c_lflag &= ~ICANON; - options.c_lflag &= ~ECHO; - options.c_lflag &= ~ECHOE; - options.c_lflag &= ~ECHONL; - options.c_lflag &= ~ISIG; - options.c_iflag &= ~(IXON | IXOFF | IXANY); - options.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL); - options.c_oflag &= ~OPOST; - options.c_oflag &= ~ONLCR; + options.c_iflag &= ~(IXOFF | IXANY); // Set data bits options.c_cflag &= ~CSIZE; // Mask the character size bits switch (this->data_bits_) { From abc870006cce42296a2f581478f79fbe69df2391 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 07:25:13 -1000 Subject: [PATCH 436/609] [captive_portal] Enable support for RP2040 (#14505) --- esphome/components/captive_portal/__init__.py | 9 ++++----- tests/components/captive_portal/test.rp2040-ard.yaml | 1 + 2 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 tests/components/captive_portal/test.rp2040-ard.yaml diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 6c190814c0..cd877fc879 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -13,6 +13,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_LN882X, + PLATFORM_RP2040, PLATFORM_RTL87XX, PlatformFramework, ) @@ -53,6 +54,7 @@ CONFIG_SCHEMA = cv.All( PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_LN882X, + PLATFORM_RP2040, PLATFORM_RTL87XX, ] ), @@ -103,11 +105,8 @@ async def to_code(config): if config[CONF_COMPRESSION] == "gzip": cg.add_define("USE_CAPTIVE_PORTAL_GZIP") - if CORE.using_arduino: - if CORE.is_esp8266: - cg.add_library("DNSServer", None) - if CORE.is_libretiny: - cg.add_library("DNSServer", None) + if CORE.using_arduino and (CORE.is_esp8266 or CORE.is_libretiny or CORE.is_rp2040): + cg.add_library("DNSServer", None) # Only compile the ESP-IDF DNS server when using ESP-IDF framework diff --git a/tests/components/captive_portal/test.rp2040-ard.yaml b/tests/components/captive_portal/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/captive_portal/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From f57fa4cc8d66a99f1e6bef885e2f0c8f96280c04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 07:25:33 -1000 Subject: [PATCH 437/609] [bluetooth_proxy] Add BLE connection parameters API (#14577) --- esphome/components/api/api_connection.cpp | 3 ++ esphome/components/api/api_connection.h | 1 + .../bluetooth_proxy/bluetooth_connection.h | 4 +++ .../bluetooth_proxy/bluetooth_proxy.cpp | 29 +++++++++++++++++++ .../bluetooth_proxy/bluetooth_proxy.h | 3 ++ .../esp32_ble_client/ble_client_base.cpp | 5 ++-- .../esp32_ble_client/ble_client_base.h | 4 +-- 7 files changed, 45 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8721072e49..bd3de02895 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1188,6 +1188,9 @@ void APIConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScanner bluetooth_proxy::global_bluetooth_proxy->bluetooth_scanner_set_mode( msg.mode == enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE); } +void APIConnection::on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg) { + bluetooth_proxy::global_bluetooth_proxy->bluetooth_set_connection_params(msg); +} #endif #ifdef USE_VOICE_ASSISTANT diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 54b6db6800..b075bc83ab 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -148,6 +148,7 @@ class APIConnection final : public APIServerConnectionBase { void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override; void on_subscribe_bluetooth_connections_free_request() override; void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override; + void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg) override; #endif #ifdef USE_HOMEASSISTANT_TIME diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.h b/esphome/components/bluetooth_proxy/bluetooth_connection.h index 60bbc93e8b..b50ea2d6a2 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.h @@ -24,6 +24,10 @@ class BluetoothConnection final : public esp32_ble_client::BLEClientBase { esp_err_t notify_characteristic(uint16_t handle, bool enable); + esp_err_t update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout) { + return this->update_conn_params_(min_interval, max_interval, latency, timeout, "custom"); + } + void set_address(uint64_t address) override; protected: diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 21da4ead14..87206996b2 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -3,7 +3,9 @@ #include "esphome/core/log.h" #include "esphome/core/macros.h" #include "esphome/core/application.h" +#include <algorithm> #include <cstring> +#include <limits> #ifdef USE_ESP32 @@ -361,6 +363,33 @@ void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest } } +void BluetoothProxy::bluetooth_set_connection_params(const api::BluetoothSetConnectionParamsRequest &msg) { + if (this->api_connection_ == nullptr) + return; + + auto *connection = this->get_connection_(msg.address, false); + api::BluetoothSetConnectionParamsResponse resp; + resp.address = msg.address; + + if (connection == nullptr || !connection->connected()) { + ESP_LOGW(TAG, "[%d] [%s] Cannot set connection params, not connected", + connection ? static_cast<int>(connection->connection_index_) : -1, + connection ? connection->address_str() : "unknown"); + resp.error = ESP_GATT_NOT_CONNECTED; + this->api_connection_->send_message(resp); + return; + } + + // Protobuf fields are uint32_t to future-proof the API if BLE ever supports wider values; + // clamp to uint16_t since the current BLE spec defines these as 16-bit. + constexpr uint32_t max_val = std::numeric_limits<uint16_t>::max(); + resp.error = connection->update_connection_params(static_cast<uint16_t>(std::min(msg.min_interval, max_val)), + static_cast<uint16_t>(std::min(msg.max_interval, max_val)), + static_cast<uint16_t>(std::min(msg.latency, max_val)), + static_cast<uint16_t>(std::min(msg.timeout, max_val))); + this->api_connection_->send_message(resp); +} + void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags) { if (this->api_connection_ != nullptr) { ESP_LOGE(TAG, "Only one API subscription is allowed at a time"); diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 85461755aa..f1b723e719 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -46,6 +46,7 @@ enum BluetoothProxyFeature : uint32_t { FEATURE_CACHE_CLEARING = 1 << 4, FEATURE_RAW_ADVERTISEMENTS = 1 << 5, FEATURE_STATE_AND_MODE = 1 << 6, + FEATURE_CONNECTION_PARAMS_SETTING = 1 << 7, }; enum BluetoothProxySubscriptionFlag : uint32_t { @@ -82,6 +83,7 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, void bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg); void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg); void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg); + void bluetooth_set_connection_params(const api::BluetoothSetConnectionParamsRequest &msg); void subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags); void unsubscribe_api_connection(api::APIConnection *api_connection); @@ -130,6 +132,7 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, flags |= BluetoothProxyFeature::FEATURE_REMOTE_CACHING; flags |= BluetoothProxyFeature::FEATURE_PAIRING; flags |= BluetoothProxyFeature::FEATURE_CACHE_CLEARING; + flags |= BluetoothProxyFeature::FEATURE_CONNECTION_PARAMS_SETTING; } return flags; diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index e6a85c784a..2f17334c77 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -236,8 +236,8 @@ void BLEClientBase::log_warning_(const char *message) { ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, message); } -void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, - uint16_t timeout, const char *param_type) { +esp_err_t BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, + uint16_t timeout, const char *param_type) { esp_ble_conn_update_params_t conn_params = {{0}}; memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t)); conn_params.min_int = min_interval; @@ -249,6 +249,7 @@ void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_inte if (err != ESP_OK) { this->log_gattc_warning_("esp_ble_gap_update_conn_params", err); } + return err; } void BLEClientBase::set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index c2336b2349..af4f1b3029 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -129,8 +129,8 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void log_event_(const char *name); void log_gattc_lifecycle_event_(const char *name); void log_gattc_data_event_(const char *name); - void update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, - const char *param_type); + esp_err_t update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, + const char *param_type); void set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, const char *param_type); void log_gattc_warning_(const char *operation, esp_gatt_status_t status); From 45f20d9c06119e3c02aa8e8be131297c032da4e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 07:26:01 -1000 Subject: [PATCH 438/609] [core] Merge set_name + set_entity_strings into configure_entity_ (#14444) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/core/entity_base.cpp | 15 ++- esphome/core/entity_base.h | 30 ++--- esphome/core/entity_helpers.py | 17 ++- .../binary_sensor/test_binary_sensor.py | 2 +- tests/component_tests/button/test_button.py | 2 +- tests/component_tests/sensor/test_sensor.py | 15 ++- tests/component_tests/text/test_text.py | 2 +- .../text_sensor/test_text_sensor.py | 25 ++++- tests/unit_tests/core/test_entity_helpers.py | 103 +++++++----------- 9 files changed, 112 insertions(+), 99 deletions(-) diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 5c4e1c4445..3274640eb3 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -10,8 +10,8 @@ static const char *const TAG = "entity_base"; // Entity Name const StringRef &EntityBase::get_name() const { return this->name_; } -void EntityBase::set_name(const char *name) { this->set_name(name, 0); } -void EntityBase::set_name(const char *name, uint32_t object_id_hash) { + +void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_strings_packed) { this->name_ = StringRef(name); if (this->name_.empty()) { #ifdef USE_DEVICES @@ -44,6 +44,17 @@ void EntityBase::set_name(const char *name, uint32_t object_id_hash) { this->calc_object_id_(); } } + // Unpack entity string table indices. + // Packed: [23..16] icon | [15..8] UoM | [7..0] device_class (each 8 bits) +#ifdef USE_ENTITY_DEVICE_CLASS + this->device_class_idx_ = entity_strings_packed & 0xFF; +#endif +#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT + this->uom_idx_ = (entity_strings_packed >> 8) & 0xFF; +#endif +#ifdef USE_ENTITY_ICON + this->icon_idx_ = (entity_strings_packed >> 16) & 0xFF; +#endif } // Weak default lookup functions — overridden by generated code in main.cpp diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 20eb68b67a..accd532b0d 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -12,6 +12,10 @@ #include "device.h" #endif +// Forward declarations for friend access from codegen-generated setup() +void setup(); // NOLINT(readability-redundant-declaration) - may be declared in Arduino.h +void original_setup(); // NOLINT(readability-redundant-declaration) - used by cpp unit tests + namespace esphome { // Extern lookup functions for entity string tables. @@ -54,12 +58,8 @@ enum EntityCategory : uint8_t { // The generic Entity base class that provides an interface common to all Entities. class EntityBase { public: - // Get/set the name of this Entity + // Get the name of this Entity const StringRef &get_name() const; - void set_name(const char *name); - /// Set name with pre-computed object_id hash (avoids runtime hash calculation) - /// Use hash=0 for dynamic names that need runtime calculation - void set_name(const char *name, uint32_t object_id_hash); // Get whether this Entity has its own name or it should use the device friendly_name. bool has_own_name() const { return this->flags_.has_own_name; } @@ -104,20 +104,6 @@ class EntityBase { this->flags_.entity_category = static_cast<uint8_t>(entity_category); } - // Set entity string table indices — one call per entity from codegen. - // Packed: [23..16] icon | [15..8] UoM | [7..0] device_class (each 8 bits) - void set_entity_strings([[maybe_unused]] uint32_t packed) { -#ifdef USE_ENTITY_DEVICE_CLASS - this->device_class_idx_ = packed & 0xFF; -#endif -#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT - this->uom_idx_ = (packed >> 8) & 0xFF; -#endif -#ifdef USE_ENTITY_ICON - this->icon_idx_ = (packed >> 16) & 0xFF; -#endif - } - // Get this entity's device class into a stack buffer. // On non-ESP8266: returns pointer to PROGMEM string directly (buffer unused). // On ESP8266: copies from PROGMEM to buffer, returns buffer pointer. @@ -239,6 +225,12 @@ class EntityBase { } protected: + friend void ::setup(); + friend void ::original_setup(); + + /// Combined entity setup from codegen: set name, object_id hash, and entity string indices. + void configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_strings_packed); + /// Non-template helper for make_entity_preference() to avoid code bloat. /// When preference hash algorithm changes, migration logic goes here. ESPPreferenceObject make_entity_preference_(size_t size, uint32_t version); diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index a46d2466fd..4fa109fb0e 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -31,8 +31,10 @@ DOMAIN = "entity_string_pool" _KEY_DC_IDX = "_entity_dc_idx" _KEY_UOM_IDX = "_entity_uom_idx" _KEY_ICON_IDX = "_entity_icon_idx" +_KEY_ENTITY_NAME = "_entity_name" +_KEY_OBJECT_ID_HASH = "_entity_object_id_hash" -# Bit layout for set_entity_strings(packed) — must match C++ setter in entity_base.h: +# Bit layout for entity_strings_packed in configure_entity_() — must match C++ in entity_base.h: # [23..16] icon (8 bits) | [15..8] UoM (8 bits) | [7..0] device_class (8 bits) _DC_SHIFT = 0 _UOM_SHIFT = 8 @@ -219,17 +221,18 @@ def setup_unit_of_measurement(config: ConfigType) -> None: def finalize_entity_strings(var: MockObj, config: ConfigType) -> None: - """Emit a single set_entity_strings() call with all packed indices. + """Emit a single configure_entity_() call with name, hash, and packed string indices. Call this at the end of each component's setup function, after setup_entity() and any register_device_class/register_unit_of_measurement calls. """ + entity_name = config[_KEY_ENTITY_NAME] + object_id_hash = config[_KEY_OBJECT_ID_HASH] dc_idx = config.get(_KEY_DC_IDX, 0) uom_idx = config.get(_KEY_UOM_IDX, 0) icon_idx = config.get(_KEY_ICON_IDX, 0) packed = (dc_idx << _DC_SHIFT) | (uom_idx << _UOM_SHIFT) | (icon_idx << _ICON_SHIFT) - if packed != 0: - add(var.set_entity_strings(packed)) + add(var.configure_entity_(entity_name, object_id_hash, packed)) def get_base_entity_object_id( @@ -331,13 +334,15 @@ async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) -> device: MockObj = await get_variable(device_id_obj) add(var.set_device(device)) - # Set the entity name with pre-computed object_id hash + # Pre-compute entity name and object_id hash for configure_entity_() + # which is emitted later by finalize_entity_strings(). # For named entities: pre-compute hash from entity name # For empty-name entities: pass 0, C++ calculates hash at runtime from # device name, friendly_name, or app name (bug-for-bug compatibility) entity_name = config[CONF_NAME] object_id_hash = fnv1_hash_object_id(entity_name) if entity_name else 0 - add(var.set_name(entity_name, object_id_hash)) + config[_KEY_ENTITY_NAME] = entity_name + config[_KEY_OBJECT_ID_HASH] = object_id_hash # Only set disabled_by_default if True (default is False) if config[CONF_DISABLED_BY_DEFAULT]: add(var.set_disabled_by_default(True)) diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py index ce4e64681f..fbc2f37d9a 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.py +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -29,7 +29,7 @@ def test_binary_sensor_sets_mandatory_fields(generate_main): ) # Then - assert 'bs_1->set_name("test bs1",' in main_cpp + assert 'bs_1->configure_entity_("test bs1",' in main_cpp assert "bs_1->set_pin(" in main_cpp diff --git a/tests/component_tests/button/test_button.py b/tests/component_tests/button/test_button.py index 797b6fb1a4..9f94d61c8c 100644 --- a/tests/component_tests/button/test_button.py +++ b/tests/component_tests/button/test_button.py @@ -26,7 +26,7 @@ def test_button_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/button/test_button.yaml") # Then - assert 'wol_1->set_name("wol_test_1",' in main_cpp + assert 'wol_1->configure_entity_("wol_test_1",' in main_cpp assert "wol_2->set_macaddr(18, 52, 86, 120, 144, 171);" in main_cpp diff --git a/tests/component_tests/sensor/test_sensor.py b/tests/component_tests/sensor/test_sensor.py index 221e7edf2c..d9ab3a022c 100644 --- a/tests/component_tests/sensor/test_sensor.py +++ b/tests/component_tests/sensor/test_sensor.py @@ -1,5 +1,15 @@ """Tests for the sensor component.""" +import re + + +def _extract_packed_value(main_cpp, var_name): + """Extract the third (packed) argument from a configure_entity_ call.""" + pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)" + match = re.search(pattern, main_cpp) + assert match, f"configure_entity_ call not found for {var_name}" + return int(match.group(1)) + def test_sensor_device_class_set(generate_main): """ @@ -10,5 +20,6 @@ def test_sensor_device_class_set(generate_main): # When main_cpp = generate_main("tests/component_tests/sensor/test_sensor.yaml") - # Then - assert "s_1->set_entity_strings(" in main_cpp + # Then: device_class: voltage means packed value must be non-zero + packed = _extract_packed_value(main_cpp, "s_1") + assert packed != 0 diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 16f5f980a5..3ceaa9b8f8 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -25,7 +25,7 @@ def test_text_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert 'it_1->set_name("test 1 text",' in main_cpp + assert 'it_1->configure_entity_("test 1 text",' in main_cpp def test_text_config_value_internal_set(generate_main): diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py index 4aaebe04d1..f30b820e94 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.py +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -1,5 +1,15 @@ """Tests for the text sensor component.""" +import re + + +def _extract_packed_value(main_cpp, var_name): + """Extract the third (packed) argument from a configure_entity_ call.""" + pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)" + match = re.search(pattern, main_cpp) + assert match, f"configure_entity_ call not found for {var_name}" + return int(match.group(1)) + def test_text_sensor_is_setup(generate_main): """ @@ -25,9 +35,9 @@ def test_text_sensor_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") # Then - assert 'ts_1->set_name("Template Text Sensor 1",' in main_cpp - assert 'ts_2->set_name("Template Text Sensor 2",' in main_cpp - assert 'ts_3->set_name("Template Text Sensor 3",' in main_cpp + assert 'ts_1->configure_entity_("Template Text Sensor 1",' in main_cpp + assert 'ts_2->configure_entity_("Template Text Sensor 2",' in main_cpp + assert 'ts_3->configure_entity_("Template Text Sensor 3",' in main_cpp def test_text_sensor_config_value_internal_set(generate_main): @@ -53,6 +63,9 @@ def test_text_sensor_device_class_set(generate_main): # When main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") - # Then - assert "ts_2->set_entity_strings(" in main_cpp - assert "ts_3->set_entity_strings(" in main_cpp + # Then: ts_2 has device_class: timestamp, ts_3 has device_class: date + # so their packed values must be non-zero + packed_ts_2 = _extract_packed_value(main_cpp, "ts_2") + assert packed_ts_2 != 0 + packed_ts_3 = _extract_packed_value(main_cpp, "ts_3") + assert packed_ts_3 != 0 diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index 1392a1d043..3f6faaee54 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -32,9 +32,11 @@ from esphome.helpers import sanitize, snake_case from .common import load_config_from_fixture -# Pre-compiled regex pattern for extracting names from set_name calls -# Matches: .set_name("name", hash) or .set_name("name") -SET_NAME_PATTERN = re.compile(r'\.set_name\(["\']([^"\']*)["\']') +# Pre-compiled regex pattern for extracting names from configure_entity_/set_name calls +# Matches: .configure_entity_("name", ...) or .set_name("name", ...) +ENTITY_NAME_PATTERN = re.compile( + r'\.(?:configure_entity_|set_name)\(["\']([^"\']*)["\']' +) FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "entity_helpers" @@ -276,15 +278,23 @@ def setup_test_environment() -> Generator[list[str], None, None]: entity_helpers.add = original_add -def extract_object_id_from_expressions(expressions: list[str]) -> str | None: - """Extract the object ID that would be computed from set_name calls. +def extract_object_id_from_config(config: dict[str, Any]) -> str | None: + """Extract the object ID from config keys set by _setup_entity_impl.""" + name = config.get("_entity_name") + if name is None: + return None + if name: + return sanitize(snake_case(name)) + # Empty name - fall back to friendly_name or device name + if CORE.friendly_name: + return sanitize(snake_case(CORE.friendly_name)) + return sanitize(snake_case(CORE.name)) if CORE.name else None - Since object_id is now computed from the name (via snake_case + sanitize), - we extract the name from set_name() calls and compute the expected object_id. - For empty names, we fall back to CORE.friendly_name or CORE.name. - """ + +def extract_object_id_from_expressions(expressions: list[str]) -> str | None: + """Extract the object ID from configure_entity_() calls in generated expressions.""" for expr in expressions: - if match := SET_NAME_PATTERN.search(expr): + if match := ENTITY_NAME_PATTERN.search(expr): name = match.group(1) if name: return sanitize(snake_case(name)) @@ -299,8 +309,6 @@ def extract_object_id_from_expressions(expressions: list[str]) -> str | None: async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> None: """Test setup_entity with unique names.""" - added_expressions = setup_test_environment - # Create mock entities var1 = MockObj("sensor1") var2 = MockObj("sensor2") @@ -312,13 +320,10 @@ async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> } await _setup_entity_impl(var1, config1, "sensor") - # Get object ID from first entity - object_id1 = extract_object_id_from_expressions(added_expressions) + # Get object ID from first entity (stored in config, emitted later by finalize) + object_id1 = extract_object_id_from_config(config1) assert object_id1 == "temperature" - # Clear for next entity - added_expressions.clear() - # Set up second entity with different name config2 = { CONF_NAME: "Humidity", @@ -327,7 +332,7 @@ async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> await _setup_entity_impl(var2, config2, "sensor") # Get object ID from second entity - object_id2 = extract_object_id_from_expressions(added_expressions) + object_id2 = extract_object_id_from_config(config2) assert object_id2 == "humidity" @@ -337,8 +342,6 @@ async def test_setup_entity_different_platforms( ) -> None: """Test that same name on different platforms doesn't conflict.""" - added_expressions = setup_test_environment - # Create mock entities sensor = MockObj("sensor1") binary_sensor = MockObj("binary_sensor1") @@ -356,15 +359,11 @@ async def test_setup_entity_different_platforms( (text_sensor, "text_sensor"), ] - object_ids: list[str] = [] for var, platform in platforms: - added_expressions.clear() await _setup_entity_impl(var, config, platform) - object_id = extract_object_id_from_expressions(added_expressions) - object_ids.append(object_id) - # All should get base object ID without suffix - assert all(obj_id == "status" for obj_id in object_ids) + # All should get the same object ID (name stored in config, not platform-specific) + assert extract_object_id_from_config(config) == "status" @pytest.fixture @@ -389,7 +388,6 @@ async def test_setup_entity_with_devices( setup_test_environment: list[str], mock_get_variable: dict[ID, MockObj] ) -> None: """Test that same name on different devices doesn't conflict.""" - added_expressions = setup_test_environment # Create mock devices device1_id = ID("device1", type="Device") @@ -418,24 +416,18 @@ async def test_setup_entity_with_devices( } # Get object IDs - object_ids: list[str] = [] for var, config in [(sensor1, config1), (sensor2, config2)]: - added_expressions.clear() await _setup_entity_impl(var, config, "sensor") - object_id = extract_object_id_from_expressions(added_expressions) - object_ids.append(object_id) # Both should get base object ID without suffix (different devices) - assert object_ids[0] == "temperature" - assert object_ids[1] == "temperature" + assert extract_object_id_from_config(config1) == "temperature" + assert extract_object_id_from_config(config2) == "temperature" @pytest.mark.asyncio async def test_setup_entity_empty_name(setup_test_environment: list[str]) -> None: """Test setup_entity with empty entity name.""" - added_expressions = setup_test_environment - var = MockObj("sensor1") config = { @@ -445,7 +437,7 @@ async def test_setup_entity_empty_name(setup_test_environment: list[str]) -> Non await _setup_entity_impl(var, config, "sensor") - object_id = extract_object_id_from_expressions(added_expressions) + object_id = extract_object_id_from_config(config) # Should use friendly name assert object_id == "test_device" @@ -456,8 +448,6 @@ async def test_setup_entity_special_characters( ) -> None: """Test setup_entity with names containing special characters.""" - added_expressions = setup_test_environment - var = MockObj("sensor1") config = { @@ -466,7 +456,7 @@ async def test_setup_entity_special_characters( } await _setup_entity_impl(var, config, "sensor") - object_id = extract_object_id_from_expressions(added_expressions) + object_id = extract_object_id_from_config(config) # Special characters should be sanitized assert object_id == "temperature_sensor_" @@ -476,8 +466,6 @@ async def test_setup_entity_special_characters( async def test_setup_entity_with_icon(setup_test_environment: list[str]) -> None: """Test setup_entity sets icon correctly.""" - setup_test_environment # noqa: F841 - fixture initializes CORE state - var = MockObj("sensor1") config = { @@ -800,10 +788,9 @@ async def test_setup_entity_empty_name_with_device( # Check that set_device was called assert any("sensor1.set_device" in expr for expr in added_expressions) - # For empty-name entities, Python passes 0 - C++ calculates hash at runtime - assert any('set_name("", 0)' in expr for expr in added_expressions), ( - f"Expected set_name with hash 0, got {added_expressions}" - ) + # For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime + assert config.get("_entity_name") == "" + assert config.get("_entity_object_id_hash") == 0 @pytest.mark.asyncio @@ -815,7 +802,6 @@ async def test_setup_entity_empty_name_with_mac_suffix( For empty-name entities, Python passes 0 and C++ calculates the hash at runtime from friendly_name (bug-for-bug compatibility). """ - added_expressions = setup_test_environment # Set up CORE.config with name_add_mac_suffix enabled CORE.config = {"name_add_mac_suffix": True} @@ -831,10 +817,9 @@ async def test_setup_entity_empty_name_with_mac_suffix( await _setup_entity_impl(var, config, "sensor") - # For empty-name entities, Python passes 0 - C++ calculates hash at runtime - assert any('set_name("", 0)' in expr for expr in added_expressions), ( - f"Expected set_name with hash 0, got {added_expressions}" - ) + # For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime + assert config.get("_entity_name") == "" + assert config.get("_entity_object_id_hash") == 0 @pytest.mark.asyncio @@ -847,7 +832,6 @@ async def test_setup_entity_empty_name_with_mac_suffix_no_friendly_name( at runtime. In this case C++ will hash the empty friendly_name (bug-for-bug compatibility). """ - added_expressions = setup_test_environment # Set up CORE.config with name_add_mac_suffix enabled CORE.config = {"name_add_mac_suffix": True} @@ -863,10 +847,9 @@ async def test_setup_entity_empty_name_with_mac_suffix_no_friendly_name( await _setup_entity_impl(var, config, "sensor") - # For empty-name entities, Python passes 0 - C++ calculates hash at runtime - assert any('set_name("", 0)' in expr for expr in added_expressions), ( - f"Expected set_name with hash 0, got {added_expressions}" - ) + # For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime + assert config.get("_entity_name") == "" + assert config.get("_entity_object_id_hash") == 0 @pytest.mark.asyncio @@ -878,7 +861,6 @@ async def test_setup_entity_empty_name_no_mac_suffix_no_friendly_name( For empty-name entities, Python passes 0 and C++ calculates the hash at runtime from the device name. """ - added_expressions = setup_test_environment # No MAC suffix (either not set or False) CORE.config = {} @@ -896,10 +878,9 @@ async def test_setup_entity_empty_name_no_mac_suffix_no_friendly_name( await _setup_entity_impl(var, config, "sensor") - # For empty-name entities, Python passes 0 - C++ calculates hash at runtime - assert any('set_name("", 0)' in expr for expr in added_expressions), ( - f"Expected set_name with hash 0, got {added_expressions}" - ) + # For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime + assert config.get("_entity_name") == "" + assert config.get("_entity_object_id_hash") == 0 def test_register_string_overflow() -> None: @@ -976,7 +957,7 @@ async def test_setup_entity_direct_call(setup_test_environment: list[str]) -> No # Direct call mode: await setup_entity(var, config, "camera") await setup_entity(var, config, "camera") - # Should have called set_name + # Should have emitted configure_entity_ object_id = extract_object_id_from_expressions(added_expressions) assert object_id == "my_camera" From 77f2c371b2b20b00d6e064bd5cbc618b2f1e22c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 07:26:34 -1000 Subject: [PATCH 439/609] [api] Single-pass protobuf encode for BLE proxy advertisements (#14575) --- esphome/components/api/api_pb2.cpp | 28 ++++++------ esphome/components/api/proto.cpp | 71 +++++++++++++++++++++++++++++ esphome/components/api/proto.h | 44 ++++++++---------- script/api_protobuf/api_protobuf.py | 17 +++---- 4 files changed, 111 insertions(+), 49 deletions(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index b1176de539..1944aac6e8 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -120,16 +120,16 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { #endif #ifdef USE_DEVICES for (const auto &it : this->devices) { - buffer.encode_message(20, it); + buffer.encode_sub_message(20, it); } #endif #ifdef USE_AREAS for (const auto &it : this->areas) { - buffer.encode_message(21, it); + buffer.encode_sub_message(21, it); } #endif #ifdef USE_AREAS - buffer.encode_message(22, this->area, false); + buffer.encode_optional_sub_message(22, this->area); #endif #ifdef USE_ZWAVE_PROXY buffer.encode_uint32(23, this->zwave_proxy_feature_flags); @@ -920,13 +920,13 @@ uint32_t HomeassistantServiceMap::calculate_size() const { void HomeassistantActionRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->service); for (auto &it : this->data) { - buffer.encode_message(2, it); + buffer.encode_sub_message(2, it); } for (auto &it : this->data_template) { - buffer.encode_message(3, it); + buffer.encode_sub_message(3, it); } for (auto &it : this->variables) { - buffer.encode_message(4, it); + buffer.encode_sub_message(4, it); } buffer.encode_bool(5, this->is_event); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES @@ -1126,7 +1126,7 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); buffer.encode_fixed32(2, this->key); for (auto &it : this->args) { - buffer.encode_message(3, it); + buffer.encode_sub_message(3, it); } buffer.encode_uint32(4, static_cast<uint32_t>(this->supports_response)); } @@ -2133,7 +2133,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category)); buffer.encode_bool(8, this->supports_pause); for (auto &it : this->supported_formats) { - buffer.encode_message(9, it); + buffer.encode_sub_message(9, it); } #ifdef USE_DEVICES buffer.encode_uint32(10, this->device_id); @@ -2264,7 +2264,7 @@ uint32_t BluetoothLERawAdvertisement::calculate_size() const { } void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const { for (uint16_t i = 0; i < this->advertisements_len; i++) { - buffer.encode_message(1, this->advertisements[i]); + buffer.encode_sub_message(1, this->advertisements[i]); } } uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const { @@ -2343,7 +2343,7 @@ void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_uint32(3, this->properties); for (auto &it : this->descriptors) { - buffer.encode_message(4, it); + buffer.encode_sub_message(4, it); } buffer.encode_uint32(5, this->short_uuid); } @@ -2370,7 +2370,7 @@ void BluetoothGATTService::encode(ProtoWriteBuffer &buffer) const { } buffer.encode_uint32(2, this->handle); for (auto &it : this->characteristics) { - buffer.encode_message(3, it); + buffer.encode_sub_message(3, it); } buffer.encode_uint32(4, this->short_uuid); } @@ -2392,7 +2392,7 @@ uint32_t BluetoothGATTService::calculate_size() const { void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); for (auto &it : this->services) { - buffer.encode_message(2, it); + buffer.encode_sub_message(2, it); } } uint32_t BluetoothGATTGetServicesResponse::calculate_size() const { @@ -2673,7 +2673,7 @@ void VoiceAssistantRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->start); buffer.encode_string(2, this->conversation_id); buffer.encode_uint32(3, this->flags); - buffer.encode_message(4, this->audio_settings, false); + buffer.encode_optional_sub_message(4, this->audio_settings); buffer.encode_string(5, this->wake_word_phrase); } uint32_t VoiceAssistantRequest::calculate_size() const { @@ -2906,7 +2906,7 @@ bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoL } void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer &buffer) const { for (auto &it : this->available_wake_words) { - buffer.encode_message(1, it); + buffer.encode_sub_message(1, it); } for (const auto &it : *this->active_wake_words) { buffer.encode_string(2, it, true); diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index a252907fd7..1ca6b702ad 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -1,5 +1,6 @@ #include "proto.h" #include <cinttypes> +#include <cstring> #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -87,6 +88,76 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size return count; } +// Single-pass encode for repeated submessage elements (non-template core). +// Writes field tag, reserves 1 byte for length varint, encodes the submessage body, +// then backpatches the actual length. For the common case (body < 128 bytes), this is +// just a single byte write with no memmove — all current repeated submessage types +// (BLE advertisements at ~47B, GATT descriptors at ~24B, service args, etc.) take +// this fast path. +// +// The memmove fallback for body >= 128 bytes exists only for correctness (e.g., a GATT +// characteristic with many descriptors). It is safe because calculate_size() already +// reserved space for the full multi-byte varint — the shift fills that reserved space: +// +// calculate_size() allocates per element: tag + varint_size(body) + body_size +// +// After encode, before memmove (1 byte reserved, body written): +// [tag][__][body ..... body][??] +// ^ ^-- unused byte (v2 space from calculate_size) +// len_pos +// +// After memmove(body_start+1, body_start, body_size): +// [tag][__][__][body ..... body] +// ^ ^-- body shifted forward, fills v2 space exactly +// len_pos +// +// After writing 2-byte varint at len_pos: +// [tag][v1][v2][body ..... body] +// ^-- pos_ = element end, within buffer +void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const void *value, + void (*encode_fn)(const void *, ProtoWriteBuffer &)) { + this->encode_field_raw(field_id, 2); + // Reserve 1 byte for length varint (optimistic: submessage < 128 bytes) + uint8_t *len_pos = this->pos_; + this->debug_check_bounds_(1); + this->pos_++; + uint8_t *body_start = this->pos_; + encode_fn(value, *this); + uint32_t body_size = static_cast<uint32_t>(this->pos_ - body_start); + if (body_size < 128) [[likely]] { + // Common case: 1-byte varint, just backpatch + *len_pos = static_cast<uint8_t>(body_size); + return; + } + // Compute extra bytes needed for varint beyond the 1 already reserved + uint8_t extra = ProtoSize::varint(body_size) - 1; + // Shift body forward to make room for the extra varint bytes + this->debug_check_bounds_(extra); + std::memmove(body_start + extra, body_start, body_size); + uint8_t *end = this->pos_ + extra; + // Write the full varint at len_pos + this->pos_ = len_pos; + this->encode_varint_raw(body_size); + this->pos_ = end; +} + +// Non-template core for encode_optional_sub_message. +void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value, + void (*encode_fn)(const void *, ProtoWriteBuffer &)) { + if (nested_size == 0) + return; + this->encode_field_raw(field_id, 2); + this->encode_varint_raw(nested_size); +#ifdef ESPHOME_DEBUG_API + uint8_t *start = this->pos_; + encode_fn(value, *this); + if (static_cast<uint32_t>(this->pos_ - start) != nested_size) + this->debug_check_encode_size_(field_id, nested_size, this->pos_ - start); +#else + encode_fn(value, *this); +#endif +} + #ifdef ESPHOME_DEBUG_API void ProtoWriteBuffer::debug_check_bounds_(size_t bytes, const char *caller) { if (this->pos_ + bytes > this->buffer_->data() + this->buffer_->size()) { diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 702208d9de..bbdd11b29d 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -185,7 +185,7 @@ class ProtoVarInt { #endif }; -// Forward declarations for decode_to_message, encode_message and encode_packed_sint32 +// Forward declarations for decode_to_message and related encoding helpers class ProtoDecodableMessage; class ProtoMessage; class ProtoSize; @@ -363,12 +363,18 @@ class ProtoWriteBuffer { } /// Encode a packed repeated sint32 field (zero-copy from vector) void encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values); - /// Encode a nested message field (force=true for repeated, false for singular) - /// Templated so concrete message type is preserved for direct encode/calculate_size calls. - template<typename T> void encode_message(uint32_t field_id, const T &value, bool force = true); - // Non-template core for encode_message — all buffer work happens here - void encode_message(uint32_t field_id, uint32_t msg_length_bytes, const void *value, - void (*encode_fn)(const void *, ProtoWriteBuffer &), bool force); + /// Single-pass encode for repeated submessage elements. + /// Thin template wrapper; all buffer work is in the non-template core. + template<typename T> void encode_sub_message(uint32_t field_id, const T &value); + /// Encode an optional singular submessage field — skips if empty. + /// Thin template wrapper; all buffer work is in the non-template core. + template<typename T> void encode_optional_sub_message(uint32_t field_id, const T &value); + + // Non-template core for encode_sub_message — backpatch approach. + void encode_sub_message(uint32_t field_id, const void *value, void (*encode_fn)(const void *, ProtoWriteBuffer &)); + // Non-template core for encode_optional_sub_message. + void encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value, + void (*encode_fn)(const void *, ProtoWriteBuffer &)); std::vector<uint8_t> *get_buffer() const { return buffer_; } protected: @@ -690,26 +696,14 @@ template<typename T> void proto_encode_msg(const void *msg, ProtoWriteBuffer &bu static_cast<const T *>(msg)->encode(buf); } -// Implementation of encode_message - must be after ProtoMessage is defined -template<typename T> inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const T &value, bool force) { - this->encode_message(field_id, value.calculate_size(), &value, &proto_encode_msg<T>, force); +// Thin template wrapper; delegates to non-template core in proto.cpp. +template<typename T> inline void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const T &value) { + this->encode_sub_message(field_id, &value, &proto_encode_msg<T>); } -// Non-template core for encode_message -inline void ProtoWriteBuffer::encode_message(uint32_t field_id, uint32_t msg_length_bytes, const void *value, - void (*encode_fn)(const void *, ProtoWriteBuffer &), bool force) { - if (msg_length_bytes == 0 && !force) - return; - this->encode_field_raw(field_id, 2); - this->encode_varint_raw(msg_length_bytes); -#ifdef ESPHOME_DEBUG_API - uint8_t *start = this->pos_; - encode_fn(value, *this); - if (static_cast<uint32_t>(this->pos_ - start) != msg_length_bytes) - this->debug_check_encode_size_(field_id, msg_length_bytes, this->pos_ - start); -#else - encode_fn(value, *this); -#endif +// Thin template wrapper; delegates to non-template core. +template<typename T> inline void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, const T &value) { + this->encode_optional_sub_message(field_id, value.calculate_size(), &value, &proto_encode_msg<T>); } // Implementation of decode_to_message - must be after ProtoDecodableMessage is defined diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 85352689e6..7ae7063a41 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -690,15 +690,12 @@ class MessageType(TypeInfo): @property def encode_func(self) -> str: - return "encode_message" + return "encode_optional_sub_message" @property def encode_content(self) -> str: - # Singular message fields pass force=false (skip empty messages) - # The default for encode_nested_message is force=true (for repeated fields) - return ( - f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, false);" - ) + # Singular message fields skip encoding when empty + return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" @property def decode_length(self) -> str: @@ -1322,9 +1319,9 @@ class FixedArrayRepeatedType(TypeInfo): """Helper to generate encode statement for a single element.""" if isinstance(self._ti, EnumType): return f"buffer.{self._ti.encode_func}({self.number}, static_cast<uint32_t>({element}), true);" - # MessageType.encode_message doesn't have a force parameter + # Repeated message elements use encode_sub_message (force=true is default) if isinstance(self._ti, MessageType): - return f"buffer.{self._ti.encode_func}({self.number}, {element});" + return f"buffer.encode_sub_message({self.number}, {element});" return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" @property @@ -1650,9 +1647,9 @@ class RepeatedTypeInfo(TypeInfo): """Helper to generate encode call for a single element.""" if isinstance(self._ti, EnumType): return f"buffer.{self._ti.encode_func}({self.number}, static_cast<uint32_t>({element}), true);" - # MessageType.encode_message doesn't have a force parameter + # Repeated message elements use encode_sub_message (force=true is default) if isinstance(self._ti, MessageType): - return f"buffer.{self._ti.encode_func}({self.number}, {element});" + return f"buffer.encode_sub_message({self.number}, {element});" return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" @property From e7b8ec18f17b23709719c63e1d05a6b9ecfb0054 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 07:26:50 -1000 Subject: [PATCH 440/609] [api] Inline APIServer::is_connected() for common no-arg path (#14574) --- esphome/components/api/api_server.cpp | 6 +----- esphome/components/api/api_server.h | 8 ++++++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 06816fe3e0..17d69405ad 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -582,11 +582,7 @@ void APIServer::request_time() { } #endif -bool APIServer::is_connected(bool state_subscription_only) const { - if (!state_subscription_only) { - return !this->clients_.empty(); - } - +bool APIServer::is_connected_with_state_subscription() const { for (const auto &client : this->clients_) { if (client->flags_.state_subscription) { return true; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index e6c10d1595..e5f371d8a1 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -185,7 +185,8 @@ class APIServer : public Component, void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings); #endif - bool is_connected(bool state_subscription_only = false) const; + bool is_connected() const { return !this->clients_.empty(); } + bool is_connected_with_state_subscription() const; #ifdef USE_API_HOMEASSISTANT_STATES struct HomeAssistantStateSubscription { @@ -323,7 +324,10 @@ template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> { TEMPLATABLE_VALUE(bool, state_subscription_only) public: bool check(const Ts &...x) override { - return global_api_server->is_connected(this->state_subscription_only_.value(x...)); + if (this->state_subscription_only_.value(x...)) { + return global_api_server->is_connected_with_state_subscription(); + } + return global_api_server->is_connected(); } }; From a0cd35c5fc2b8b1e9ab0db69fd70b90c95e8dbad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 07:27:08 -1000 Subject: [PATCH 441/609] [core] Inline status_clear_warning/error fast path (#14571) --- esphome/core/component.cpp | 8 ++------ esphome/core/component.h | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index a9ff3ec1eb..2879d4b5ab 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -422,15 +422,11 @@ void Component::status_set_error(const LogString *message) { store_component_error_message(this, LOG_STR_ARG(message), true); } } -void Component::status_clear_warning() { - if ((this->component_state_ & STATUS_LED_WARNING) == 0) - return; +void Component::status_clear_warning_slow_path_() { this->component_state_ &= ~STATUS_LED_WARNING; ESP_LOGW(TAG, "%s cleared Warning flag", LOG_STR_ARG(this->get_component_log_str())); } -void Component::status_clear_error() { - if ((this->component_state_ & STATUS_LED_ERROR) == 0) - return; +void Component::status_clear_error_slow_path_() { this->component_state_ &= ~STATUS_LED_ERROR; ESP_LOGE(TAG, "%s cleared Error flag", LOG_STR_ARG(this->get_component_log_str())); } diff --git a/esphome/core/component.h b/esphome/core/component.h index 59222dc4f4..7266f57e15 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -251,9 +251,17 @@ class Component { void status_set_error(const char *message); void status_set_error(const LogString *message); - void status_clear_warning(); + void status_clear_warning() { + if ((this->component_state_ & STATUS_LED_WARNING) == 0) + return; + this->status_clear_warning_slow_path_(); + } - void status_clear_error(); + void status_clear_error() { + if ((this->component_state_ & STATUS_LED_ERROR) == 0) + return; + this->status_clear_error_slow_path_(); + } /** Set warning status flag and automatically clear it after a timeout. * @@ -505,6 +513,9 @@ class Component { bool cancel_defer(const char *name); // NOLINT bool cancel_defer(uint32_t id); // NOLINT + void status_clear_warning_slow_path_(); + void status_clear_error_slow_path_(); + // Ordered for optimal packing on 32-bit systems const LogString *component_source_{nullptr}; uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s) From 3f700bac1cebf7eb6ff3b20a87d8c8af5cb9fc41 Mon Sep 17 00:00:00 2001 From: Diorcet Yann <diorcety@users.noreply.github.com> Date: Sat, 7 Mar 2026 19:50:44 +0100 Subject: [PATCH 442/609] [component] Fix components for compatibility with stricter compilers (#14545) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/esp32_ble/ble.cpp | 2 +- esphome/components/ethernet/esp_eth_phy_jl1101.c | 2 +- esphome/components/ethernet/ethernet_component.cpp | 4 ++-- esphome/components/heatpumpir/climate.py | 1 + .../http_request/update/http_request_update.cpp | 3 ++- esphome/components/mlx90393/sensor_mlx90393.cpp | 2 +- esphome/components/whirlpool/whirlpool.cpp | 7 +++++++ esphome/components/whirlpool/whirlpool.h | 12 ++++++------ 8 files changed, 21 insertions(+), 12 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index bbe972b9f3..7fa5370072 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -451,7 +451,7 @@ void ESP32BLE::loop() { ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT { - esp_ble_gap_cb_param_t *param; + esp_ble_gap_cb_param_t *param = NULL; // clang-format off switch (gap_event) { // All three scan complete events have the same structure with just status diff --git a/esphome/components/ethernet/esp_eth_phy_jl1101.c b/esphome/components/ethernet/esp_eth_phy_jl1101.c index 5e73e99101..a19f7aa6b0 100644 --- a/esphome/components/ethernet/esp_eth_phy_jl1101.c +++ b/esphome/components/ethernet/esp_eth_phy_jl1101.c @@ -27,7 +27,7 @@ #include "esp_rom_sys.h" #include "esp_idf_version.h" -#if defined(USE_ARDUINO) || ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) +#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) static const char *TAG = "jl1101"; #define PHY_CHECK(a, str, goto_tag, ...) \ diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 0bc67c8b03..d6b0d40cd9 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -209,7 +209,7 @@ void EthernetComponent::setup() { break; } #endif -#ifdef USE_ETHERNET_JL1101 +#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) case ETHERNET_TYPE_JL1101: { this->phy_ = esp_eth_phy_new_jl1101(&phy_config); break; @@ -374,7 +374,7 @@ void EthernetComponent::dump_config() { eth_type = "IP101"; break; #endif -#ifdef USE_ETHERNET_JL1101 +#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) case ETHERNET_TYPE_JL1101: eth_type = "JL1101"; break; diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index 0d760938d0..7743da77ab 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -125,6 +125,7 @@ async def to_code(config): cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT])) cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) + cg.add_build_flag("-Wno-error=overloaded-virtual") cg.add_library("tonia/HeatpumpIR", "1.0.40") if CORE.is_libretiny or CORE.is_esp32: diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 1900f69a69..c40590af95 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -128,7 +128,8 @@ void HttpRequestUpdate::update_task(void *params) { this_update->update_info_.title = root[ESPHOME_F("name")].as<std::string>(); this_update->update_info_.latest_version = root[ESPHOME_F("version")].as<std::string>(); - for (auto build : root[ESPHOME_F("builds")].as<JsonArray>()) { + auto builds_array = root[ESPHOME_F("builds")].as<JsonArray>(); + for (auto build : builds_array) { if (!build[ESPHOME_F("chipFamily")].is<const char *>()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; diff --git a/esphome/components/mlx90393/sensor_mlx90393.cpp b/esphome/components/mlx90393/sensor_mlx90393.cpp index ee52f9b9ab..01084e50df 100644 --- a/esphome/components/mlx90393/sensor_mlx90393.cpp +++ b/esphome/components/mlx90393/sensor_mlx90393.cpp @@ -160,7 +160,7 @@ bool MLX90393Cls::verify_setting_(MLX90393Setting which) { uint8_t read_value = 0xFF; uint8_t expected_value = 0xFF; uint8_t read_status = -1; - char read_back_str[25] = {0}; + char read_back_str[33] = {0}; switch (which) { case MLX90393_GAIN_SEL: { diff --git a/esphome/components/whirlpool/whirlpool.cpp b/esphome/components/whirlpool/whirlpool.cpp index e9f602e97f..86209cb7a6 100644 --- a/esphome/components/whirlpool/whirlpool.cpp +++ b/esphome/components/whirlpool/whirlpool.cpp @@ -32,6 +32,13 @@ const uint8_t WHIRLPOOL_SWING_MASK = 128; const uint8_t WHIRLPOOL_POWER = 0x04; +WhirlpoolClimate::WhirlpoolClimate() + : climate_ir::ClimateIR( + WHIRLPOOL_DG11J1_3A_TEMP_MIN, WHIRLPOOL_DG11J1_3A_TEMP_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}), + model_(MODEL_DG11J1_3A) {} + void WhirlpoolClimate::transmit_state() { this->last_transmit_time_ = millis(); // setting the time of the last transmission. uint8_t remote_state[WHIRLPOOL_STATE_LENGTH] = {0}; diff --git a/esphome/components/whirlpool/whirlpool.h b/esphome/components/whirlpool/whirlpool.h index 992b2a7adf..ada5a36de9 100644 --- a/esphome/components/whirlpool/whirlpool.h +++ b/esphome/components/whirlpool/whirlpool.h @@ -19,11 +19,7 @@ const float WHIRLPOOL_DG11J1_91_TEMP_MIN = 16.0; class WhirlpoolClimate : public climate_ir::ClimateIR { public: - WhirlpoolClimate() - : climate_ir::ClimateIR(temperature_min_(), temperature_max_(), 1.0f, true, true, - {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, - climate::CLIMATE_FAN_HIGH}, - {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {} + WhirlpoolClimate(); void setup() override { climate_ir::ClimateIR::setup(); @@ -37,7 +33,11 @@ class WhirlpoolClimate : public climate_ir::ClimateIR { climate_ir::ClimateIR::control(call); } - void set_model(Model model) { this->model_ = model; } + void set_model(Model model) { + this->model_ = model; + this->minimum_temperature_ = temperature_min_(); + this->maximum_temperature_ = temperature_max_(); + } // used to track when to send the power toggle command bool powered_on_assumed; From f2dfb5e1dcfd7bd47677f8d0c75b41bbd0e25bb7 Mon Sep 17 00:00:00 2001 From: Oliver Kleinecke <okleinecke@web.de> Date: Sun, 8 Mar 2026 00:16:12 +0100 Subject: [PATCH 443/609] [uart][usb_uart] Add debug_prefix option to distinguish multiple defined uarts in log (#14525) --- esphome/components/uart/__init__.py | 13 ++++- esphome/components/uart/uart_debugger.cpp | 18 +++--- esphome/components/uart/uart_debugger.h | 17 ++++-- esphome/components/usb_uart/__init__.py | 5 +- esphome/components/usb_uart/usb_uart.cpp | 4 +- esphome/components/usb_uart/usb_uart.h | 3 + tests/components/uart/test.esp32-c3-idf.yaml | 16 +++++- tests/components/uart/test.esp32-idf.yaml | 60 +++++++++++++++----- tests/components/uart/test.esp8266-ard.yaml | 24 +++++--- tests/components/uart/test.host.yaml | 2 +- tests/components/uart/test.rp2040-ard.yaml | 16 +++++- tests/components/usb_uart/common.yaml | 8 +++ 12 files changed, 138 insertions(+), 48 deletions(-) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 3bc4263b31..1b976b73a9 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -203,8 +203,10 @@ UART_DIRECTIONS = { # round numbers. AFTER_DEFAULTS = {CONF_BYTES: 150, CONF_TIMEOUT: "100ms"} +CONF_DEBUG_PREFIX = "debug_prefix" + # By default, log in hex format when no specific sequence is provided. -DEFAULT_DEBUG_OUTPUT = "UARTDebug::log_hex(direction, bytes, ':');" +DEFAULT_DEBUG_OUTPUT = "UARTDebug::log_hex(direction, bytes, ':', debug_prefix);" DEFAULT_SEQUENCE = [{CONF_LAMBDA: make_data_base(DEFAULT_DEBUG_OUTPUT)}] @@ -242,6 +244,7 @@ DEBUG_SCHEMA = cv.Schema( ): automation.validate_automation(), cv.Optional(CONF_DUMMY_RECEIVER, default=False): cv.boolean, cv.GenerateID(CONF_DUMMY_RECEIVER_ID): cv.declare_id(UARTDummyReceiver), + cv.Optional(CONF_DEBUG_PREFIX, default=""): cv.string, } ) @@ -283,7 +286,11 @@ async def debug_to_code(config, parent): for action in config[CONF_SEQUENCE]: await automation.build_automation( trigger, - [(UARTDirection, "direction"), (cg.std_vector.template(cg.uint8), "bytes")], + [ + (UARTDirection, "direction"), + (cg.std_vector.template(cg.uint8), "bytes"), + (cg.StringRef, "debug_prefix"), + ], action, ) cg.add(trigger.set_direction(config[CONF_DIRECTION])) @@ -299,6 +306,8 @@ async def debug_to_code(config, parent): if config[CONF_DUMMY_RECEIVER]: dummy = cg.new_Pvariable(config[CONF_DUMMY_RECEIVER_ID], parent) await cg.register_component(dummy, {}) + if debug_prefix := config[CONF_DEBUG_PREFIX]: + cg.add(trigger.set_debug_prefix(debug_prefix)) cg.add_define("USE_UART_DEBUGGER") diff --git a/esphome/components/uart/uart_debugger.cpp b/esphome/components/uart/uart_debugger.cpp index 5490154d01..e0be599d8d 100644 --- a/esphome/components/uart/uart_debugger.cpp +++ b/esphome/components/uart/uart_debugger.cpp @@ -74,7 +74,7 @@ bool UARTDebugger::has_buffered_bytes_() { return !this->bytes_.empty(); } void UARTDebugger::fire_trigger_() { this->is_triggering_ = true; - trigger(this->last_direction_, this->bytes_); + trigger(this->last_direction_, this->bytes_, this->debug_prefix_); this->bytes_.clear(); this->is_triggering_ = false; } @@ -94,7 +94,7 @@ void UARTDummyReceiver::loop() { // TCP connection(s). Without these delays, debug log lines could go // missing when UART devices block the main loop for too long. -void UARTDebug::log_hex(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator) { +void UARTDebug::log_hex(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator, StringRef prefix) { std::string res; if (direction == UART_DIRECTION_RX) { res += "<<< "; @@ -110,11 +110,11 @@ void UARTDebug::log_hex(UARTDirection direction, std::vector<uint8_t> bytes, uin buf_append_printf(buf, sizeof(buf), 0, "%02X", bytes[i]); res += buf; } - ESP_LOGD(TAG, "%s", res.c_str()); + ESP_LOGD(TAG, "%s%s", prefix.c_str(), res.c_str()); delay(10); } -void UARTDebug::log_string(UARTDirection direction, std::vector<uint8_t> bytes) { +void UARTDebug::log_string(UARTDirection direction, std::vector<uint8_t> bytes, StringRef prefix) { std::string res; if (direction == UART_DIRECTION_RX) { res += "<<< \""; @@ -154,11 +154,11 @@ void UARTDebug::log_string(UARTDirection direction, std::vector<uint8_t> bytes) } } res += '"'; - ESP_LOGD(TAG, "%s", res.c_str()); + ESP_LOGD(TAG, "%s%s", prefix.c_str(), res.c_str()); delay(10); } -void UARTDebug::log_int(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator) { +void UARTDebug::log_int(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator, StringRef prefix) { std::string res; size_t len = bytes.size(); if (direction == UART_DIRECTION_RX) { @@ -174,11 +174,11 @@ void UARTDebug::log_int(UARTDirection direction, std::vector<uint8_t> bytes, uin buf_append_printf(buf, sizeof(buf), 0, "%u", bytes[i]); res += buf; } - ESP_LOGD(TAG, "%s", res.c_str()); + ESP_LOGD(TAG, "%s%s", prefix.c_str(), res.c_str()); delay(10); } -void UARTDebug::log_binary(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator) { +void UARTDebug::log_binary(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator, StringRef prefix) { std::string res; size_t len = bytes.size(); if (direction == UART_DIRECTION_RX) { @@ -194,7 +194,7 @@ void UARTDebug::log_binary(UARTDirection direction, std::vector<uint8_t> bytes, buf_append_printf(buf, sizeof(buf), 0, "0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(bytes[i]), bytes[i]); res += buf; } - ESP_LOGD(TAG, "%s", res.c_str()); + ESP_LOGD(TAG, "%s%s", prefix.c_str(), res.c_str()); delay(10); } diff --git a/esphome/components/uart/uart_debugger.h b/esphome/components/uart/uart_debugger.h index df87655962..da33bea70c 100644 --- a/esphome/components/uart/uart_debugger.h +++ b/esphome/components/uart/uart_debugger.h @@ -5,6 +5,7 @@ #include <vector> #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/string_ref.h" #include "uart.h" #include "uart_component.h" @@ -17,7 +18,7 @@ namespace esphome::uart { /// 'appropriate time' means exactly, is determined by a number of /// configurable constraints. E.g. when a given number of bytes is gathered /// and/or when no more data has been seen for a given time interval. -class UARTDebugger : public Component, public Trigger<UARTDirection, std::vector<uint8_t>> { +class UARTDebugger : public Component, public Trigger<UARTDirection, std::vector<uint8_t>, StringRef> { public: explicit UARTDebugger(UARTComponent *parent); void loop() override; @@ -41,6 +42,8 @@ class UARTDebugger : public Component, public Trigger<UARTDirection, std::vector /// logging will be triggered. void add_delimiter_byte(uint8_t byte) { this->after_delimiter_.push_back(byte); } + void set_debug_prefix(const char *prefix) { this->debug_prefix_ = StringRef(prefix); } + protected: UARTDirection for_direction_; UARTDirection last_direction_{}; @@ -51,6 +54,7 @@ class UARTDebugger : public Component, public Trigger<UARTDirection, std::vector std::vector<uint8_t> after_delimiter_{}; size_t after_delimiter_pos_{}; bool is_triggering_{false}; + StringRef debug_prefix_{}; bool is_my_direction_(UARTDirection direction); bool is_recursive_(); @@ -81,18 +85,21 @@ class UARTDebug { public: /// Log the bytes as hex values, separated by the provided separator /// character. - static void log_hex(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator); + static void log_hex(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator, + StringRef prefix = StringRef()); /// Log the bytes as string values, escaping unprintable characters. - static void log_string(UARTDirection direction, std::vector<uint8_t> bytes); + static void log_string(UARTDirection direction, std::vector<uint8_t> bytes, StringRef prefix = StringRef()); /// Log the bytes as integer values, separated by the provided separator /// character. - static void log_int(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator); + static void log_int(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator, + StringRef prefix = StringRef()); /// Log the bytes as '<binary> (<hex>)' values, separated by the provided /// separator. - static void log_binary(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator); + static void log_binary(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator, + StringRef prefix = StringRef()); }; } // namespace esphome::uart diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index f0ee53d028..eedf590eca 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import socket from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS -from esphome.components.uart import UARTComponent +from esphome.components.uart import CONF_DEBUG_PREFIX, UARTComponent from esphome.components.usb_host import register_usb_client, usb_device_schema import esphome.config_validation as cv from esphome.const import ( @@ -90,6 +90,7 @@ def channel_schema(channels, baud_rate_required): ), cv.Optional(CONF_DUMMY_RECEIVER, default=False): cv.boolean, cv.Optional(CONF_DEBUG, default=False): cv.boolean, + cv.Optional(CONF_DEBUG_PREFIX, default=""): cv.string, } ) ), @@ -129,6 +130,8 @@ async def to_code(config): cg.add(chvar.set_baud_rate(channel[CONF_BAUD_RATE])) cg.add(chvar.set_dummy_receiver(channel[CONF_DUMMY_RECEIVER])) cg.add(chvar.set_debug(channel[CONF_DEBUG])) + if channel[CONF_DEBUG_PREFIX]: + cg.add(chvar.set_debug_prefix(channel[CONF_DEBUG_PREFIX])) cg.add(var.add_channel(chvar)) if channel[CONF_DEBUG]: cg.add_define("USE_UART_DEBUGGER") diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index e20bbd02db..a1f8738491 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -142,7 +142,7 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) { size_t n = std::min(len - off, BATCH); memcpy(buf, ">>> ", 4); format_hex_pretty_to(buf + 4, sizeof(buf) - 4, data + off, n, ','); - ESP_LOGD(TAG, "%s", buf); + ESP_LOGD(TAG, "%s%s", this->debug_prefix_.c_str(), buf); } } #endif @@ -219,7 +219,7 @@ void USBUartComponent::loop() { char buf[4 + format_hex_pretty_size(UsbDataChunk::MAX_CHUNK_SIZE)]; // "<<< " + hex memcpy(buf, "<<< ", 4); format_hex_pretty_to(buf + 4, sizeof(buf) - 4, chunk->data, chunk->length, ','); - ESP_LOGD(TAG, "%s", buf); + ESP_LOGD(TAG, "%s%s", channel->debug_prefix_.c_str(), buf); } #endif diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 0d471e46f6..7290f8a958 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -3,6 +3,7 @@ #if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" #include "esphome/components/uart/uart_component.h" #include "esphome/components/usb_host/usb_host.h" #include "esphome/core/lock_free_queue.h" @@ -114,6 +115,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon void set_parity(UARTParityOptions parity) { this->parity_ = parity; } void set_debug(bool debug) { this->debug_ = debug; } void set_dummy_receiver(bool dummy_receiver) { this->dummy_receiver_ = dummy_receiver; } + void set_debug_prefix(const char *prefix) { this->debug_prefix_ = StringRef(prefix); } /// Register a callback invoked immediately after data is pushed to the input ring buffer. /// Called from USBUartComponent::loop() in the main loop context. @@ -138,6 +140,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon const uint8_t index_; bool debug_{}; bool dummy_receiver_{}; + StringRef debug_prefix_{}; }; class USBUartComponent : public usb_host::USBClient { diff --git a/tests/components/uart/test.esp32-c3-idf.yaml b/tests/components/uart/test.esp32-c3-idf.yaml index b053290a8b..2eae37e824 100644 --- a/tests/components/uart/test.esp32-c3-idf.yaml +++ b/tests/components/uart/test.esp32-c3-idf.yaml @@ -1,11 +1,15 @@ esphome: on_boot: then: - - uart.write: 'Hello World' - - uart.write: [0x00, 0x20, 0x42] + - uart.write: + id: uart_id + data: 'Hello World' + - uart.write: + id: uart_id + data: [0x00, 0x20, 0x42] uart: - - id: uart_uart + - id: uart_id tx_pin: 4 rx_pin: 5 flow_control_pin: 6 @@ -16,3 +20,9 @@ uart: rx_timeout: 1 parity: EVEN stop_bits: 2 + - id: uart_debug + tx_pin: 18 + rx_pin: 19 + baud_rate: 115200 + debug: + debug_prefix: "[UART1] " diff --git a/tests/components/uart/test.esp32-idf.yaml b/tests/components/uart/test.esp32-idf.yaml index 2a97f9a5de..fa76316b9c 100644 --- a/tests/components/uart/test.esp32-idf.yaml +++ b/tests/components/uart/test.esp32-idf.yaml @@ -1,13 +1,19 @@ esphome: on_boot: then: - - uart.write: 'Hello World' - - uart.write: [0x00, 0x20, 0x42] - - uart.write: !lambda |- - return {0xAA, 0xBB, 0xCC}; + - uart.write: + id: uart_id + data: 'Hello World' + - uart.write: + id: uart_id + data: [0x00, 0x20, 0x42] + - uart.write: + id: uart_id + data: !lambda |- + return {0xAA, 0xBB, 0xCC}; uart: - - id: uart_uart + - id: uart_id tx_pin: 17 rx_pin: 16 flow_control_pin: 4 @@ -18,32 +24,54 @@ uart: rx_timeout: 1 parity: EVEN stop_bits: 2 + - id: uart_debug + tx_pin: 21 + rx_pin: 22 + baud_rate: 115200 + debug: + debug_prefix: "[UART1] " + - id: uart_debug_custom + tx_pin: 25 + rx_pin: 26 + baud_rate: 9600 + debug: + debug_prefix: "[UART2] " + after: + delimiter: "\n" + sequence: + - lambda: UARTDebug::log_string(direction, bytes, debug_prefix); + - id: uart_debug_no_prefix + tx_pin: 32 + rx_pin: 33 + baud_rate: 9600 + debug: packet_transport: - platform: uart + uart_id: uart_id switch: # Test uart switch with single state (array) - platform: uart name: "UART Switch Single Array" - uart_id: uart_uart + uart_id: uart_id data: [0x01, 0x02, 0x03] # Test uart switch with single state (string) - platform: uart name: "UART Switch Single String" - uart_id: uart_uart + uart_id: uart_id data: "ON" # Test uart switch with turn_on/turn_off (arrays) - platform: uart name: "UART Switch Dual Array" - uart_id: uart_uart + uart_id: uart_id data: turn_on: [0xA0, 0xA1, 0xA2] turn_off: [0xB0, 0xB1, 0xB2] # Test uart switch with turn_on/turn_off (strings) - platform: uart name: "UART Switch Dual String" - uart_id: uart_uart + uart_id: uart_id data: turn_on: "TURN_ON" turn_off: "TURN_OFF" @@ -61,24 +89,26 @@ button: # Test uart button with array data - platform: uart name: "UART Button Array" - uart_id: uart_uart + uart_id: uart_id data: [0xFF, 0xEE, 0xDD] # Test uart button with string data - platform: uart name: "UART Button String" - uart_id: uart_uart + uart_id: uart_id data: "BUTTON_PRESS" # Test uart button with lambda (function pointer) - platform: template name: "UART Lambda Test" on_press: - - uart.write: !lambda |- - std::string cmd = "VALUE=" + str_sprintf("%.0f", id(test_number).state) + "\r\n"; - return std::vector<uint8_t>(cmd.begin(), cmd.end()); + - uart.write: + id: uart_id + data: !lambda |- + std::string cmd = "VALUE=" + str_sprintf("%.0f", id(test_number).state) + "\r\n"; + return std::vector<uint8_t>(cmd.begin(), cmd.end()); event: - platform: uart - uart_id: uart_uart + uart_id: uart_id name: "UART Event" event_types: - "string_event_A": "*A#" diff --git a/tests/components/uart/test.esp8266-ard.yaml b/tests/components/uart/test.esp8266-ard.yaml index c2670b289a..4c9d6fd736 100644 --- a/tests/components/uart/test.esp8266-ard.yaml +++ b/tests/components/uart/test.esp8266-ard.yaml @@ -1,11 +1,15 @@ esphome: on_boot: then: - - uart.write: 'Hello World' - - uart.write: [0x00, 0x20, 0x42] + - uart.write: + id: uart_id + data: 'Hello World' + - uart.write: + id: uart_id + data: [0x00, 0x20, 0x42] uart: - - id: uart_uart + - id: uart_id tx_pin: 4 rx_pin: 5 baud_rate: 9600 @@ -13,15 +17,21 @@ uart: rx_buffer_size: 512 parity: EVEN stop_bits: 2 + - id: uart_debug + tx_pin: 14 + rx_pin: 12 + baud_rate: 115200 + debug: + debug_prefix: "[UART1] " switch: - platform: uart name: "UART Switch Array" - uart_id: uart_uart + uart_id: uart_id data: [0x01, 0x02, 0x03] - platform: uart name: "UART Switch Dual" - uart_id: uart_uart + uart_id: uart_id data: turn_on: [0xA0, 0xA1] turn_off: [0xB0, 0xB1] @@ -29,12 +39,12 @@ switch: button: - platform: uart name: "UART Button" - uart_id: uart_uart + uart_id: uart_id data: [0xFF, 0xEE] event: - platform: uart - uart_id: uart_uart + uart_id: uart_id name: "UART Event" event_types: - "string_event_A": "*A#" diff --git a/tests/components/uart/test.host.yaml b/tests/components/uart/test.host.yaml index 63f0ade084..bc64245c7b 100644 --- a/tests/components/uart/test.host.yaml +++ b/tests/components/uart/test.host.yaml @@ -5,7 +5,7 @@ esphome: - uart.write: [0x00, 0x20, 0x42] uart: - - id: uart_uart + - id: uart_id port: "/dev/ttyS0" baud_rate: 9600 data_bits: 8 diff --git a/tests/components/uart/test.rp2040-ard.yaml b/tests/components/uart/test.rp2040-ard.yaml index 09178f1663..5eb2b533ea 100644 --- a/tests/components/uart/test.rp2040-ard.yaml +++ b/tests/components/uart/test.rp2040-ard.yaml @@ -1,11 +1,15 @@ esphome: on_boot: then: - - uart.write: 'Hello World' - - uart.write: [0x00, 0x20, 0x42] + - uart.write: + id: uart_id + data: 'Hello World' + - uart.write: + id: uart_id + data: [0x00, 0x20, 0x42] uart: - - id: uart_uart + - id: uart_id tx_pin: 4 rx_pin: 5 baud_rate: 9600 @@ -13,3 +17,9 @@ uart: rx_buffer_size: 512 parity: EVEN stop_bits: 2 + - id: uart_debug + tx_pin: 8 + rx_pin: 9 + baud_rate: 115200 + debug: + debug_prefix: "[UART1] " diff --git a/tests/components/usb_uart/common.yaml b/tests/components/usb_uart/common.yaml index 474c3f5c8d..5869b9468b 100644 --- a/tests/components/usb_uart/common.yaml +++ b/tests/components/usb_uart/common.yaml @@ -34,3 +34,11 @@ usb_uart: - id: channel_4_1 debug: true dummy_receiver: true + debug_prefix: "[ESP_JTAG] " + - id: uart_5 + type: cp210x + channels: + - id: channel_5_1 + baud_rate: 9600 + debug: true + debug_prefix: "[CP210X] " From 545395a6f0d2180c648998f0ded5302f01df89cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 13:16:19 -1000 Subject: [PATCH 444/609] [ci] Add RP2350 to PR template test environment (#14599) --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 965b186c31..72013e411e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -25,7 +25,7 @@ - [ ] ESP32 - [ ] ESP32 IDF - [ ] ESP8266 -- [ ] RP2040 +- [ ] RP2040/RP2350 - [ ] BK72xx - [ ] RTL87xx - [ ] LN882x From 888f3d804b9550036c20153218e4286d38c88775 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 13:22:50 -1000 Subject: [PATCH 445/609] [ld2420] Add integration tests with mock UART (#14471) --- .../uart_mock/uart_mock.cpp | 8 +- .../fixtures/uart_mock_ld2420.yaml | 187 ++++++++++++ .../fixtures/uart_mock_ld2420_simple.yaml | 141 +++++++++ tests/integration/test_uart_mock_ld2420.py | 273 ++++++++++++++++++ 4 files changed, 605 insertions(+), 4 deletions(-) create mode 100644 tests/integration/fixtures/uart_mock_ld2420.yaml create mode 100644 tests/integration/fixtures/uart_mock_ld2420_simple.yaml create mode 100644 tests/integration/test_uart_mock_ld2420.py diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp index affcc8d908..e8c07d632d 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -104,12 +104,12 @@ void MockUartComponent::write_array(const uint8_t *data, size_t len) { } #endif - if (this->scenario_active_) { - this->try_match_response_(); - } + // Responses are always active - they are request-response pairs triggered by + // component TX, not timed injections. No race condition with test subscription. + this->try_match_response_(); // This directly calls a tx_hook (lambda) as an alternative to the simpler match_response mechanism. - if (this->tx_hook_ && this->scenario_active_) { + if (this->tx_hook_) { std::vector<uint8_t> buf(data, data + len); this->tx_hook_(buf); } diff --git a/tests/integration/fixtures/uart_mock_ld2420.yaml b/tests/integration/fixtures/uart_mock_ld2420.yaml new file mode 100644 index 0000000000..5380b81071 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_ld2420.yaml @@ -0,0 +1,187 @@ +esphome: + name: uart-mock-ld2420-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy ld2420's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + id: mock_uart + baud_rate: 115200 + auto_start: false + responses: + # Version-specific response: match the complete firmware version command TX. + # CMD_READ_VERSION (0x0000) TX = FD FC FB FA 02 00 00 00 04 03 02 01 + # Returns "v2.0.0" → get_firmware_int = 200 >= 154 → energy mode + # + # Response layout: + # [0-3] FD FC FB FA = header + # [4-5] 0C 00 = length 12 + # [6] 00 = cmd (CMD_READ_VERSION) + # [7] 01 = status (ACK) + # [8-9] 00 00 = error = 0 + # [10] 06 = ver_len = 6 + # [11] 00 = padding + # [12-17] "v2.0.0" = version string + # [18-21] 04 03 02 01 = footer + - expect_tx: + [0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01] + inject_rx: + [ + 0xFD, 0xFC, 0xFB, 0xFA, + 0x0C, 0x00, + 0x00, 0x01, + 0x00, 0x00, + 0x06, 0x00, + 0x76, 0x32, 0x2E, 0x30, 0x2E, 0x30, + 0x04, 0x03, 0x02, 0x01, + ] + + # Catch-all response: match any command footer (04 03 02 01). + # Returns a generic ACK with cmd=0xFF (CMD_ENABLE_CONF case in switch). + # All commands get unblocked via cmd_reply_.ack = true. + # Data fields stay zeroed (min_gate=0, max_gate=0, timeout=0, thresholds=0). + # + # Response layout: + # [0-3] FD FC FB FA = header + # [4-5] 04 00 = length 4 + # [6] FF = cmd (handled as CMD_ENABLE_CONF) + # [7] 01 = status (ACK) + # [8-9] 00 00 = error = 0 + # [10-13] 04 03 02 01 = footer + - expect_tx: [0x04, 0x03, 0x02, 0x01] + inject_rx: + [ + 0xFD, 0xFC, 0xFB, 0xFA, + 0x04, 0x00, + 0xFF, 0x01, + 0x00, 0x00, + 0x04, 0x03, 0x02, 0x01, + ] + + injections: + # Phase 1 (t=100ms): Valid LD2420 energy mode data frame - happy path + # Buffer is clean (buffer_pos_=0). This frame should parse correctly. + # Presence: 1 (target detected), Distance: 100cm, Gate energies: all 0 + # + # Energy frame layout (45 bytes): + # [0-3] F4 F3 F2 F1 = energy frame header + # [4-5] 23 00 = length 35 (1+2+32) + # [6] 01 = presence (1 = target) + # [7-8] 64 00 = distance 100 (uint16_t LE) + # [9-40] 00 00 x16 = 16 gate energies (uint16_t LE each) + # [41-44] F8 F7 F6 F5 = energy frame footer + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x23, 0x00, + 0x01, + 0x64, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 2 (t=300ms): Garbage bytes + # LD2420's readline_ does NOT check frame headers at position 0 (unlike LD2412), + # so these bytes accumulate in the buffer. buffer_pos_ goes from 0 to 7. + - delay: 200ms + inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22] + + # Phase 3 (t=400ms): Truncated energy frame WITH footer (13 bytes) + # This tests PR #14458 bug #3: missing length validation in handle_energy_mode_. + # The 7 garbage bytes from Phase 2 are still in the buffer (buffer_pos_=7). + # These 13 bytes are appended at positions 7-19 (buffer_pos_=20). + # Energy footer at positions 16-19 triggers handle_energy_mode_(buffer, 20). + # + # Pre-fix: handle_energy_mode_ reads 32 bytes of gate energy from buffer[9:40], + # which is past the 20 actual bytes. Reads uninitialized data. + # No "Energy frame too short" warning exists. + # Post-fix: len=20 < 41 → logs "Energy frame too short: 20 bytes", returns early. + # + # Frame: header + length + presence + distance + footer (no gate data) + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x23, 0x00, + 0x01, + 0x64, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 4 (t=600ms): Overflow - inject 50 bytes of 0xFF (MAX_LINE_LENGTH=50) + # After Phase 3, buffer_pos_=0 (reset after energy footer detection). + # 49 bytes fill positions 0-48 (buffer_pos_=49), 50th byte triggers overflow. + # Logs "Max command length exceeded; ignoring", buffer_pos_=0. + - delay: 200ms + inject_rx: + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ] + + # Phase 5 (t=1500ms): Valid frame after overflow - recovery test + # Buffer was reset by overflow. This valid frame should parse correctly. + # Presence: 1 (target), Distance: 50cm + # Delay=900ms ensures >1000ms gap from Phase 1 for REFRESH_RATE_MS throttle. + - delay: 900ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x23, 0x00, + 0x01, + 0x32, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + +button: + - platform: template + name: "Start Scenario" + on_press: + - lambda: 'id(mock_uart).start_scenario();' + +ld2420: + id: ld2420_dev + uart_id: mock_uart + +sensor: + - platform: ld2420 + ld2420_id: ld2420_dev + moving_distance: + name: "Moving Distance" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + +binary_sensor: + - platform: ld2420 + ld2420_id: ld2420_dev + has_target: + name: "Has Target" + filters: + - settle: 50ms diff --git a/tests/integration/fixtures/uart_mock_ld2420_simple.yaml b/tests/integration/fixtures/uart_mock_ld2420_simple.yaml new file mode 100644 index 0000000000..2ceca5d35d --- /dev/null +++ b/tests/integration/fixtures/uart_mock_ld2420_simple.yaml @@ -0,0 +1,141 @@ +esphome: + name: uart-mock-ld2420-simple-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy ld2420's DEPENDENCIES = ["uart"] +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + id: mock_uart + baud_rate: 115200 + auto_start: false + responses: + # Catch-all response only (no version-specific response). + # Without a version response, firmware_ver_ stays at default "v0.0.0". + # get_firmware_int("v0.0.0") = 0 < 154 → simple mode (CMD_SYSTEM_MODE_SIMPLE). + - expect_tx: [0x04, 0x03, 0x02, 0x01] + inject_rx: + [ + 0xFD, 0xFC, 0xFB, 0xFA, + 0x04, 0x00, + 0xFF, 0x01, + 0x00, 0x00, + 0x04, 0x03, 0x02, 0x01, + ] + + injections: + # Phase 1 (t=100ms): Valid simple mode text frame - happy path + # "ON Range 0100\r\n" → presence=true, distance=100 + # Simple mode frames end with \r\n (0x0D 0x0A), triggering handle_simple_mode_. + - delay: 100ms + inject_rx: + [ + 0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20, + 0x30, 0x31, 0x30, 0x30, + 0x0D, 0x0A, + ] + + # Phase 2 (t=300ms): Garbage bytes + # LD2420's readline_ stores all bytes regardless of header. buffer_pos_ = 7. + - delay: 200ms + inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22] + + # Phase 3 (t=500ms): Overflow - inject 50 bytes of 0xFF (MAX_LINE_LENGTH=50) + # buffer_pos_ starts at 7 (from Phase 2 garbage). + # Positions 7-48 fill (42 bytes), byte 43 triggers overflow (buffer_pos_=49). + # After overflow: buffer_pos_=0, remaining 7 bytes fill positions 0-6. + # Final buffer_pos_ = 7. + - delay: 200ms + inject_rx: + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ] + + # Phase 4 (t=1400ms): Recovery after overflow + # buffer_pos_ = 7 (from overflow remainder). These 15 bytes fill positions 7-21. + # At position 21 (0x0A), \r\n detected → handle_simple_mode_(buffer, 22). + # Parser skips 0xFF bytes at positions 0-6, finds "ON" at positions 7-8, + # parses digits "0050" → distance=50. + # Delay=900ms ensures >1000ms gap from Phase 1 for REFRESH_RATE_MS throttle. + - delay: 900ms + inject_rx: + [ + 0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20, + 0x30, 0x30, 0x35, 0x30, + 0x0D, 0x0A, + ] + + # Phase 5 (t=2500ms): 16-digit distance - tests PR #14458 bug #1 + # "ON Range 0000000000000000\r\n" has 16 digit characters. + # handle_simple_mode_ outbuf is 16 bytes, can hold 15 digits (index 0-14). + # + # Pre-fix: At the 16th digit, index=15, (index < bufsize-1) is false. + # The digit branch doesn't increment pos. The else branch is skipped. + # pos stays at the 16th digit FOREVER → INFINITE LOOP. + # The binary hangs, no more state updates, test times out. + # Post-fix: pos always increments (moved outside digit branch). + # 16th digit skipped, loop continues to \r\n. distance=0. + - delay: 1100ms + inject_rx: + [ + 0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x0D, 0x0A, + ] + + # Phase 6 (t=3700ms): Post-bug-trigger recovery + # If Phase 5 didn't hang, this frame should parse correctly. + # "ON Range 0025\r\n" → distance=25 + # Delay=1200ms ensures >1000ms gap from Phase 5 for throttle. + - delay: 1200ms + inject_rx: + [ + 0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20, + 0x30, 0x30, 0x32, 0x35, + 0x0D, 0x0A, + ] + +button: + - platform: template + name: "Start Scenario" + on_press: + - lambda: 'id(mock_uart).start_scenario();' + +ld2420: + id: ld2420_dev + uart_id: mock_uart + +sensor: + - platform: ld2420 + ld2420_id: ld2420_dev + moving_distance: + name: "Moving Distance" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + +binary_sensor: + - platform: ld2420 + ld2420_id: ld2420_dev + has_target: + name: "Has Target" + filters: + - settle: 50ms diff --git a/tests/integration/test_uart_mock_ld2420.py b/tests/integration/test_uart_mock_ld2420.py new file mode 100644 index 0000000000..ae28da4d3e --- /dev/null +++ b/tests/integration/test_uart_mock_ld2420.py @@ -0,0 +1,273 @@ +"""Integration test for LD2420 component with mock UART. + +Tests: +test_uart_mock_ld2420 (energy mode): + 1. Happy path - valid energy frame publishes correct sensor values + 2. Garbage resilience - random bytes don't crash the component + 3. Truncated energy frame - triggers "Energy frame too short" warning (PR #14458 bug #3) + 4. Buffer overflow recovery - overflow resets the parser + 5. Post-overflow parsing - next valid frame after overflow is parsed correctly + 6. TX logging - verifies LD2420 sends expected setup commands + +test_uart_mock_ld2420_simple (simple mode): + 1. Happy path - valid simple mode text frame publishes correct values + 2. Garbage resilience + 3. Buffer overflow recovery + 4. 16-digit distance triggers infinite loop pre-fix (PR #14458 bug #1) + 5. Post-bug-trigger recovery proves the parser survived +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path + +from aioesphomeapi import ButtonInfo +import pytest + +from .state_utils import InitialStateHelper, SensorStateCollector, find_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_uart_mock_ld2420( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test LD2420 energy mode: happy path, truncated frame, overflow, and recovery.""" + # Replace external component path placeholder + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track overflow warning in logs + overflow_seen = loop.create_future() + + # Track "Energy frame too short" warning (PR #14458 bug #3 fix) + # This message ONLY exists after the fix. Pre-fix, handle_energy_mode_ + # silently reads past the buffer without any warning. + truncated_frame_warning_seen = loop.create_future() + + # Track TX data logged by the mock for assertions + tx_log_lines: list[str] = [] + + def line_callback(line: str) -> None: + if "Max command length exceeded" in line and not overflow_seen.done(): + overflow_seen.set_result(True) + if "Energy frame too short" in line and not truncated_frame_warning_seen.done(): + truncated_frame_warning_seen.set_result(True) + # Capture all TX log lines from uart_mock + if "uart_mock" in line and "TX " in line: + tx_log_lines.append(line) + + collector = SensorStateCollector( + sensor_names=["moving_distance"], + binary_sensor_names=["has_target"], + ) + + # Signal when we see recovery frame values + recovery_received = collector.add_waiter( + lambda: pytest.approx(50.0) in collector.sensor_states["moving_distance"] + ) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + collector.build_key_mapping(entities) + + # Set up initial state helper + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states( + initial_state_helper.on_state_wrapper(collector.on_state) + ) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + + # Wait for Phase 1 - all sensors and binary sensors have at least one value + try: + await collector.wait_for_all(timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 1 frame. Received:\n" + f" sensor_states: {collector.sensor_states}\n" + f" binary_states: {collector.binary_states}" + ) + + # Phase 1 values: moving=100, has_target=true + assert collector.sensor_states["moving_distance"][0] == pytest.approx(100.0) + assert collector.binary_states["has_target"][0] is True + + # Wait for the recovery frame (Phase 5) to be parsed + # This proves the component survived garbage + truncated + overflow + try: + await asyncio.wait_for(recovery_received, timeout=5.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for recovery frame. Received:\n" + f" sensor_states: {collector.sensor_states}" + ) + + # Verify overflow warning was logged + assert overflow_seen.done(), ( + "Expected 'Max command length exceeded' warning in logs" + ) + + # Verify truncated frame warning was logged (PR #14458 bug #3) + # This assertion FAILS before PR #14458 because the length check + # and warning message did not exist. + assert truncated_frame_warning_seen.done(), ( + "Expected 'Energy frame too short' warning in logs. " + "This indicates PR #14458 fix for handle_energy_mode_ length " + "validation is missing." + ) + + # Verify LD2420 sent setup commands (TX logging) + assert len(tx_log_lines) > 0, "Expected TX log lines from uart_mock" + tx_data = " ".join(tx_log_lines) + # Verify command frame header appears (FD:FC:FB:FA) + assert "FD.FC.FB.FA" in tx_data or "FD:FC:FB:FA" in tx_data, ( + "Expected LD2420 command frame header FD:FC:FB:FA in TX log" + ) + # Verify command frame footer appears (04:03:02:01) + assert "04.03.02.01" in tx_data or "04:03:02:01" in tx_data, ( + "Expected LD2420 command frame footer 04:03:02:01 in TX log" + ) + + # Recovery frame values (Phase 5, after overflow) + recovery_values = [ + v + for v in collector.sensor_states["moving_distance"] + if v == pytest.approx(50.0) + ] + assert len(recovery_values) >= 1, ( + f"Expected moving_distance=50 in recovery, got: {collector.sensor_states['moving_distance']}" + ) + + +@pytest.mark.asyncio +async def test_uart_mock_ld2420_simple( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test LD2420 simple mode: happy path, overflow, and 16-digit bug trigger.""" + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track overflow warning in logs + overflow_seen = loop.create_future() + + def line_callback(line: str) -> None: + if "Max command length exceeded" in line and not overflow_seen.done(): + overflow_seen.set_result(True) + + collector = SensorStateCollector( + sensor_names=["moving_distance"], + binary_sensor_names=["has_target"], + ) + + # Signal for recovery frames + recovery_received = collector.add_waiter( + lambda: pytest.approx(50.0) in collector.sensor_states["moving_distance"] + ) + post_bug_received = collector.add_waiter( + lambda: pytest.approx(25.0) in collector.sensor_states["moving_distance"] + ) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + collector.build_key_mapping(entities) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states( + initial_state_helper.on_state_wrapper(collector.on_state) + ) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + + # Wait for Phase 1 - all sensors and binary sensors have at least one value + try: + await collector.wait_for_all(timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 1 frame. Received:\n" + f" sensor_states: {collector.sensor_states}\n" + f" binary_states: {collector.binary_states}" + ) + + # Phase 1: simple mode "ON Range 0100\r\n" → distance=100, presence=true + assert collector.sensor_states["moving_distance"][0] == pytest.approx(100.0) + assert collector.binary_states["has_target"][0] is True + + # Wait for Phase 4 recovery (distance=50) after overflow + try: + await asyncio.wait_for(recovery_received, timeout=5.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for recovery frame. Received:\n" + f" moving_distance: {collector.sensor_states['moving_distance']}" + ) + + # Verify overflow warning was logged + assert overflow_seen.done(), ( + "Expected 'Max command length exceeded' warning in logs" + ) + + # Wait for Phase 6: distance=25 (post-16-digit-bug recovery) + # This assertion FAILS before PR #14458 because the 16-digit frame + # in Phase 5 causes an infinite loop in handle_simple_mode_ pre-fix. + # The binary hangs, Phase 6 never fires, and this wait times out. + try: + await asyncio.wait_for(post_bug_received, timeout=8.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for post-bug recovery (distance=25). " + f"This likely means Phase 5 (16-digit frame) caused an infinite " + f"loop in handle_simple_mode_, indicating PR #14458 bug #1 fix " + f"is missing.\n" + f" moving_distance values: {collector.sensor_states['moving_distance']}" + ) + + # Verify post-bug value + post_bug_values = [ + v + for v in collector.sensor_states["moving_distance"] + if v == pytest.approx(25.0) + ] + assert len(post_bug_values) >= 1, ( + f"Expected moving_distance=25 after 16-digit test, " + f"got: {collector.sensor_states['moving_distance']}" + ) From ea7cfffddaf60c5da30bf9dfa976fcf044477c06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 02:40:13 +0000 Subject: [PATCH 446/609] Bump aioesphomeapi from 44.3.1 to 44.4.0 (#14609) 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 8e875eba62..a1300a67f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.3.1 +aioesphomeapi==44.4.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 66919ef969574f8e6553d2146089c41ee9cecd27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 18:33:54 -1000 Subject: [PATCH 447/609] [i2s_audio] Include legacy driver IDF component when use_legacy is set (#14613) --- esphome/components/i2s_audio/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index 1cd2e97a5e..65b09b93f6 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -281,6 +281,8 @@ async def to_code(config): if use_legacy(): cg.add_define("USE_I2S_LEGACY") + # Legacy I2S API lives in the "driver" shim component (driver/i2s.h) + include_builtin_idf_component("driver") # Helps avoid callbacks being skipped due to processor load add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True) From d55fe9a34b50400347a7d57f6679dbbb514ea8ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 18:34:35 -1000 Subject: [PATCH 448/609] [api] Fix value-initialization of DeviceInfoResponse (#14615) Co-authored-by: Keith Burzinski <kbx81x@gmail.com> --- esphome/components/api/api_connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index bd3de02895..b9b33ddcc2 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1571,7 +1571,7 @@ bool APIConnection::send_ping_response_() { } bool APIConnection::send_device_info_response_() { - DeviceInfoResponse resp{}; + DeviceInfoResponse resp; resp.name = StringRef(App.get_name()); resp.friendly_name = StringRef(App.get_friendly_name()); #ifdef USE_AREAS From 9fea8fe01b4c976caced70881cb986729768c305 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 7 Mar 2026 23:50:36 -0500 Subject: [PATCH 449/609] [vbus][rf_bridge][sensirion_common] Add buffer size guards (#14597) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/rf_bridge/rf_bridge.cpp | 3 +++ esphome/components/rf_bridge/rf_bridge.h | 1 + esphome/components/sensirion_common/i2c_sensirion.cpp | 2 +- esphome/components/vbus/vbus.cpp | 3 +++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index 700e2ba162..5ca629c12b 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -145,6 +145,9 @@ void RFBridgeComponent::loop() { } avail -= to_read; for (size_t i = 0; i < to_read; i++) { + if (this->rx_buffer_.size() > MAX_RX_BUFFER_SIZE) { + this->rx_buffer_.clear(); + } if (this->parse_bridge_byte_(buf[i])) { ESP_LOGVV(TAG, "Parsed: 0x%02X", buf[i]); this->last_bridge_byte_ = now; diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h index d2f75c819d..c93b636c38 100644 --- a/esphome/components/rf_bridge/rf_bridge.h +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -30,6 +30,7 @@ static const uint8_t RF_CODE_RFIN_BUCKET = 0xB1; static const uint8_t RF_CODE_BEEP = 0xC0; static const uint8_t RF_CODE_STOP = 0x55; static const uint8_t RF_DEBOUNCE = 200; +static const size_t MAX_RX_BUFFER_SIZE = 512; struct RFBridgeData { uint16_t sync; diff --git a/esphome/components/sensirion_common/i2c_sensirion.cpp b/esphome/components/sensirion_common/i2c_sensirion.cpp index 26702c148c..0279e08b9f 100644 --- a/esphome/components/sensirion_common/i2c_sensirion.cpp +++ b/esphome/components/sensirion_common/i2c_sensirion.cpp @@ -12,7 +12,7 @@ static const char *const TAG = "sensirion_i2c"; static const size_t BUFFER_STACK_SIZE = 16; bool SensirionI2CDevice::read_data(uint16_t *data, const uint8_t len) { - const uint8_t num_bytes = len * 3; + const size_t num_bytes = len * 3; uint8_t buf[num_bytes]; this->last_error_ = this->read(buf, num_bytes); diff --git a/esphome/components/vbus/vbus.cpp b/esphome/components/vbus/vbus.cpp index 8616da010d..c6786ee31e 100644 --- a/esphome/components/vbus/vbus.cpp +++ b/esphome/components/vbus/vbus.cpp @@ -87,6 +87,9 @@ void VBus::loop() { this->state_ = 0; ESP_LOGD(TAG, "P1 empty message"); } + } else if (this->buffer_.size() > 15) { + ESP_LOGW(TAG, "Unknown protocol 0x%02x, discarding", this->protocol_); + this->state_ = 0; } continue; } From be6c3c52ac89534f00e54dfa26974402d813a4bf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 18:59:13 -1000 Subject: [PATCH 450/609] [api] Add force proto field option to skip zero checks on hot path (#14610) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/api/api.proto | 6 ++-- esphome/components/api/api_options.proto | 6 ++++ esphome/components/api/api_pb2.cpp | 14 ++++----- script/api_protobuf/api_protobuf.py | 38 ++++++++++++++++++++++-- 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 618fd1b83c..257a7aaf82 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1604,11 +1604,11 @@ message BluetoothLEAdvertisementResponse { } message BluetoothLERawAdvertisement { - uint64 address = 1; - sint32 rssi = 2; + uint64 address = 1 [(force) = true]; + sint32 rssi = 2 [(force) = true]; uint32 address_type = 3; - bytes data = 4 [(fixed_array_size) = 62]; + bytes data = 4 [(fixed_array_size) = 62, (force) = true]; } message BluetoothLERawAdvertisementsResponse { diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index a863f2c7a8..02600f0977 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -90,4 +90,10 @@ extend google.protobuf.FieldOptions { // - uint16_t <field>_length_{0}; // - uint16_t <field>_count_{0}; optional bool packed_buffer = 50015 [default=false]; + + // force: Always encode this field, even when its value equals the proto3 default. + // Skips the zero/empty check in calculate_size() and encode(), using the _force + // variant of the calc_ method. Use on fields that are almost always non-default + // to eliminate dead branches on hot paths. + optional bool force = 50016 [default=false]; } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 1944aac6e8..38ebfb9464 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -139,7 +139,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { #endif #ifdef USE_SERIAL_PROXY for (const auto &it : this->serial_proxies) { - buffer.encode_message(25, it); + buffer.encode_sub_message(25, it); } #endif } @@ -2249,17 +2249,17 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, return true; } void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_sint32(2, this->rssi); + buffer.encode_uint64(1, this->address, true); + buffer.encode_sint32(2, this->rssi, true); buffer.encode_uint32(3, this->address_type); - buffer.encode_bytes(4, this->data, this->data_len); + buffer.encode_bytes(4, this->data, this->data_len, true); } uint32_t BluetoothLERawAdvertisement::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_uint64(1, this->address); - size += ProtoSize::calc_sint32(1, this->rssi); + size += ProtoSize::calc_uint64_force(1, this->address); + size += ProtoSize::calc_sint32_force(1, this->rssi); size += ProtoSize::calc_uint32(1, this->address_type); - size += ProtoSize::calc_length(1, this->data_len); + size += ProtoSize::calc_length_force(1, this->data_len); return size; } void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 7ae7063a41..206f8f558b 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -151,6 +151,11 @@ class TypeInfo(ABC): """Check if the field is repeated.""" return self._field.label == FieldDescriptorProto.LABEL_REPEATED + @property + def force(self) -> bool: + """Check if this field should always be encoded (skip zero/empty check).""" + return get_field_opt(self._field, pb.force, False) + @property def wire_type(self) -> WireType: """Get the wire type for the field.""" @@ -218,6 +223,8 @@ class TypeInfo(ABC): @property def encode_content(self) -> str: + if self.force: + return f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, true);" return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" encode_func = None @@ -413,6 +420,8 @@ class DoubleType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() + if force: + return f"size += {field_id_size + self.get_fixed_size_bytes()};" return f"size += ProtoSize::calc_fixed64({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: @@ -437,6 +446,8 @@ class FloatType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() + if force: + return f"size += {field_id_size + self.get_fixed_size_bytes()};" return f"size += ProtoSize::calc_float({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: @@ -521,6 +532,8 @@ class Fixed64Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() + if force: + return f"size += {field_id_size + self.get_fixed_size_bytes()};" return f"size += ProtoSize::calc_fixed64({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: @@ -545,6 +558,8 @@ class Fixed32Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() + if force: + return f"size += {field_id_size + self.get_fixed_size_bytes()};" return f"size += ProtoSize::calc_fixed32({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: @@ -607,6 +622,8 @@ class StringType(TypeInfo): @property def encode_content(self) -> str: # Use the StringRef + if self.force: + return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_, true);" return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_);" def dump(self, name): @@ -694,7 +711,7 @@ class MessageType(TypeInfo): @property def encode_content(self) -> str: - # Singular message fields skip encoding when empty + # encode_sub_message always encodes (uses backpatch), no force needed return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" @property @@ -771,6 +788,8 @@ class BytesType(TypeInfo): @property def encode_content(self) -> str: + if self.force: + return f"buffer.encode_bytes({self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_, true);" return f"buffer.encode_bytes({self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_);" def dump(self, name: str) -> str: @@ -876,6 +895,8 @@ class PointerToBytesBufferType(PointerToBufferTypeBase): @property def encode_content(self) -> str: + if self.force: + return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);" @property @@ -923,6 +944,10 @@ class PointerToStringBufferType(PointerToBufferTypeBase): @property def encode_content(self) -> str: + if self.force: + return ( + f"buffer.encode_string({self.number}, this->{self.field_name}, true);" + ) return f"buffer.encode_string({self.number}, this->{self.field_name});" @property @@ -1086,6 +1111,8 @@ class FixedArrayBytesType(TypeInfo): @property def encode_content(self) -> str: + if self.force: + return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);" def dump(self, name: str) -> str: @@ -1159,6 +1186,8 @@ class EnumType(TypeInfo): @property def encode_content(self) -> str: + if self.force: + return f"buffer.{self.encode_func}({self.number}, static_cast<uint32_t>(this->{self.field_name}), true);" return f"buffer.{self.encode_func}({self.number}, static_cast<uint32_t>(this->{self.field_name}));" def dump(self, name: str) -> str: @@ -1192,6 +1221,8 @@ class SFixed32Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() + if force: + return f"size += {field_id_size + self.get_fixed_size_bytes()};" return f"size += ProtoSize::calc_sfixed32({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: @@ -1216,6 +1247,8 @@ class SFixed64Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() + if force: + return f"size += {field_id_size + self.get_fixed_size_bytes()};" return f"size += ProtoSize::calc_sfixed64({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: @@ -2134,7 +2167,8 @@ def build_message_type( encode.extend(wrap_with_ifdef(ti.encode_content, field_ifdef)) size_calc.extend( wrap_with_ifdef( - ti.get_size_calculation(f"this->{ti.field_name}"), field_ifdef + ti.get_size_calculation(f"this->{ti.field_name}", ti.force), + field_ifdef, ) ) From 5e842a8b207230be2fd8db09baf5098f62767a21 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Sat, 7 Mar 2026 23:23:13 -0600 Subject: [PATCH 451/609] [uart] Return `flush` result, expose timeout via config (#14608) Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/ble_nus/ble_nus.cpp | 5 +++-- esphome/components/ble_nus/ble_nus.h | 2 +- esphome/components/uart/__init__.py | 6 ++++++ esphome/components/uart/uart.h | 2 +- esphome/components/uart/uart_component.h | 16 +++++++++++++++- .../components/uart/uart_component_esp8266.cpp | 3 ++- esphome/components/uart/uart_component_esp8266.h | 2 +- .../components/uart/uart_component_esp_idf.cpp | 13 +++++++++++-- esphome/components/uart/uart_component_esp_idf.h | 5 ++++- esphome/components/uart/uart_component_host.cpp | 5 +++-- esphome/components/uart/uart_component_host.h | 2 +- .../components/uart/uart_component_libretiny.cpp | 3 ++- .../components/uart/uart_component_libretiny.h | 2 +- .../components/uart/uart_component_rp2040.cpp | 3 ++- esphome/components/uart/uart_component_rp2040.h | 2 +- esphome/components/usb_cdc_acm/usb_cdc_acm.h | 2 +- .../components/usb_cdc_acm/usb_cdc_acm_esp32.cpp | 11 ++++++++--- esphome/components/usb_uart/usb_uart.cpp | 5 ++++- esphome/components/usb_uart/usb_uart.h | 2 +- esphome/components/weikai/weikai.cpp | 5 +++-- esphome/components/weikai/weikai.h | 2 +- tests/components/ld2450/common.h | 2 +- tests/components/uart/common.h | 2 +- .../external_components/uart_mock/uart_mock.cpp | 3 ++- .../external_components/uart_mock/uart_mock.h | 2 +- 25 files changed, 77 insertions(+), 30 deletions(-) diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index d1710100a0..e38dc99802 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -100,16 +100,17 @@ size_t BLENUS::available() { #endif } -void BLENUS::flush() { +uart::FlushResult BLENUS::flush() { constexpr uint32_t timeout_5sec = 5000; uint32_t start = millis(); while (atomic_get(&this->tx_status_) != TX_DISABLED && !ring_buf_is_empty(&global_ble_tx_ring_buf)) { if (millis() - start > timeout_5sec) { ESP_LOGW(TAG, "Flush timeout"); - return; + return uart::FlushResult::TIMEOUT; } delay(1); } + return uart::FlushResult::SUCCESS; } void BLENUS::connected(bt_conn *conn, uint8_t err) { diff --git a/esphome/components/ble_nus/ble_nus.h b/esphome/components/ble_nus/ble_nus.h index 67e9ae9f97..b482c240e5 100644 --- a/esphome/components/ble_nus/ble_nus.h +++ b/esphome/components/ble_nus/ble_nus.h @@ -26,7 +26,7 @@ class BLENUS : public uart::UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + uart::FlushResult flush() override; void check_logger_conflict() override {} void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } #ifdef USE_LOGGER diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 1b976b73a9..2cb6eac050 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -183,6 +183,7 @@ UART_PARITY_OPTIONS = { "ODD": UARTParityOptions.UART_CONFIG_PARITY_ODD, } +CONF_FLUSH_TIMEOUT = "flush_timeout" CONF_RX_FULL_THRESHOLD = "rx_full_threshold" CONF_RX_TIMEOUT = "rx_timeout" @@ -266,6 +267,9 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault(CONF_RX_TIMEOUT, esp32=2): cv.All( cv.only_on_esp32, cv.validate_bytes, cv.int_range(min=0, max=92) ), + cv.Optional(CONF_FLUSH_TIMEOUT): cv.All( + cv.only_on_esp32, cv.positive_time_period_milliseconds + ), cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8), cv.Optional(CONF_PARITY, default="NONE"): cv.enum( @@ -345,6 +349,8 @@ async def to_code(config): ) cg.add(var.set_rx_full_threshold(config[CONF_RX_FULL_THRESHOLD])) cg.add(var.set_rx_timeout(config[CONF_RX_TIMEOUT])) + if CONF_FLUSH_TIMEOUT in config: + cg.add(var.set_flush_timeout(config[CONF_FLUSH_TIMEOUT])) cg.add(var.set_stop_bits(config[CONF_STOP_BITS])) cg.add(var.set_data_bits(config[CONF_DATA_BITS])) cg.add(var.set_parity(config[CONF_PARITY])) diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index bb91e5cd7c..2c4fb34c9a 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -45,7 +45,7 @@ class UARTDevice { size_t available() { return this->parent_->available(); } - void flush() { this->parent_->flush(); } + FlushResult flush() { return this->parent_->flush(); } // Compat APIs int read() { diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 078ce64b30..3d7e71b89f 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -29,6 +29,14 @@ enum UARTDirection { const LogString *parity_to_str(UARTParityOptions parity); +/// Result of a flush() call. +enum class FlushResult { + SUCCESS, ///< Confirmed: all bytes left the TX FIFO. + TIMEOUT, ///< Confirmed: timed out before TX completed. + FAILED, ///< Confirmed: driver or hardware error. + ASSUMED_SUCCESS, ///< Platform cannot report result; success is assumed. +}; + class UARTComponent { public: // Writes an array of bytes to the UART bus. @@ -72,7 +80,13 @@ class UARTComponent { virtual size_t available() = 0; // Pure virtual method to block until all bytes have been written to the UART bus. - virtual void flush() = 0; + // @return FlushResult indicating whether the flush was confirmed, timed out, failed, or assumed successful. + virtual FlushResult flush() = 0; + + // Sets the maximum time to wait for TX to drain during flush(). + // Only meaningful on ESP32 (IDF). Other platforms ignore this value. + // @param flush_timeout_ms Timeout in milliseconds; 0 means wait indefinitely. + virtual void set_flush_timeout(uint32_t flush_timeout_ms) {} // Sets the TX (transmit) pin for the UART bus. // @param tx_pin Pointer to the internal GPIO pin used for transmission. diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 3ebf381c84..91218c4300 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -213,13 +213,14 @@ size_t ESP8266UartComponent::available() { return this->sw_serial_->available(); } } -void ESP8266UartComponent::flush() { +FlushResult ESP8266UartComponent::flush() { ESP_LOGVV(TAG, " Flushing"); if (this->hw_serial_ != nullptr) { this->hw_serial_->flush(); } else { this->sw_serial_->flush(); } + return FlushResult::ASSUMED_SUCCESS; } void ESP8266SoftwareSerial::setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_pin, uint32_t baud_rate, uint8_t stop_bits, uint32_t data_bits, UARTParityOptions parity, diff --git a/esphome/components/uart/uart_component_esp8266.h b/esphome/components/uart/uart_component_esp8266.h index e84cbe386d..ca90dc5964 100644 --- a/esphome/components/uart/uart_component_esp8266.h +++ b/esphome/components/uart/uart_component_esp8266.h @@ -58,7 +58,7 @@ class ESP8266UartComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + FlushResult flush() override; uint32_t get_config(); diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 544404448b..47ddf1a38d 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -230,6 +230,9 @@ void IDFUARTComponent::dump_config() { " RX Timeout: %u", this->rx_buffer_size_, this->rx_full_threshold_, this->rx_timeout_); } + if (this->flush_timeout_ms_ > 0) { + ESP_LOGCONFIG(TAG, " Flush Timeout: %" PRIu32 " ms", this->flush_timeout_ms_); + } ESP_LOGCONFIG(TAG, " Baud Rate: %" PRIu32 " baud\n" " Data Bits: %u\n" @@ -332,9 +335,15 @@ size_t IDFUARTComponent::available() { return available; } -void IDFUARTComponent::flush() { +FlushResult IDFUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing"); - uart_wait_tx_done(this->uart_num_, portMAX_DELAY); + TickType_t ticks = this->flush_timeout_ms_ == 0 ? portMAX_DELAY : pdMS_TO_TICKS(this->flush_timeout_ms_); + esp_err_t err = uart_wait_tx_done(this->uart_num_, ticks); + if (err == ESP_OK) + return FlushResult::SUCCESS; + if (err == ESP_ERR_TIMEOUT) + return FlushResult::TIMEOUT; + return FlushResult::FAILED; } void IDFUARTComponent::check_logger_conflict() {} diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 631bf54cd5..9fa2013cfd 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -31,7 +31,9 @@ class IDFUARTComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + FlushResult flush() override; + + void set_flush_timeout(uint32_t flush_timeout_ms) override { this->flush_timeout_ms_ = flush_timeout_ms; } uint8_t get_hw_serial_number() { return this->uart_num_; } @@ -57,6 +59,7 @@ class IDFUARTComponent : public UARTComponent, public Component { bool has_peek_{false}; uint8_t peek_byte_; + uint32_t flush_timeout_ms_{0}; ///< 0 means wait indefinitely (portMAX_DELAY). #ifdef USE_UART_WAKE_LOOP_ON_RX // ISR callback for UART RX data notification — wakes the main loop directly. diff --git a/esphome/components/uart/uart_component_host.cpp b/esphome/components/uart/uart_component_host.cpp index 9dce25c500..66026f3ccd 100644 --- a/esphome/components/uart/uart_component_host.cpp +++ b/esphome/components/uart/uart_component_host.cpp @@ -274,12 +274,13 @@ size_t HostUartComponent::available() { return result; }; -void HostUartComponent::flush() { +FlushResult HostUartComponent::flush() { if (this->file_descriptor_ == -1) { - return; + return FlushResult::ASSUMED_SUCCESS; } tcflush(this->file_descriptor_, TCIOFLUSH); ESP_LOGV(TAG, " Flushing"); + return FlushResult::ASSUMED_SUCCESS; } void HostUartComponent::update_error_(const std::string &error) { diff --git a/esphome/components/uart/uart_component_host.h b/esphome/components/uart/uart_component_host.h index 89b951093b..c22efdcb92 100644 --- a/esphome/components/uart/uart_component_host.h +++ b/esphome/components/uart/uart_component_host.h @@ -18,7 +18,7 @@ class HostUartComponent : public UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + FlushResult flush() override; void set_name(std::string port_name) { port_name_ = port_name; }; protected: diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index 83d2acb332..6a550f296a 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -170,9 +170,10 @@ bool LibreTinyUARTComponent::read_array(uint8_t *data, size_t len) { } size_t LibreTinyUARTComponent::available() { return this->serial_->available(); } -void LibreTinyUARTComponent::flush() { +FlushResult LibreTinyUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing"); this->serial_->flush(); + return FlushResult::ASSUMED_SUCCESS; } void LibreTinyUARTComponent::check_logger_conflict() { diff --git a/esphome/components/uart/uart_component_libretiny.h b/esphome/components/uart/uart_component_libretiny.h index 31f082d31e..77df808067 100644 --- a/esphome/components/uart/uart_component_libretiny.h +++ b/esphome/components/uart/uart_component_libretiny.h @@ -22,7 +22,7 @@ class LibreTinyUARTComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + FlushResult flush() override; uint16_t get_config(); diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index faf8f4d90f..858f1a02dd 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -187,9 +187,10 @@ bool RP2040UartComponent::read_array(uint8_t *data, size_t len) { return true; } size_t RP2040UartComponent::available() { return this->serial_->available(); } -void RP2040UartComponent::flush() { +FlushResult RP2040UartComponent::flush() { ESP_LOGVV(TAG, " Flushing"); this->serial_->flush(); + return FlushResult::ASSUMED_SUCCESS; } } // namespace esphome::uart diff --git a/esphome/components/uart/uart_component_rp2040.h b/esphome/components/uart/uart_component_rp2040.h index 4ca58e8dc6..891212ca74 100644 --- a/esphome/components/uart/uart_component_rp2040.h +++ b/esphome/components/uart/uart_component_rp2040.h @@ -25,7 +25,7 @@ class RP2040UartComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + FlushResult flush() override; uint16_t get_config(); diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.h b/esphome/components/usb_cdc_acm/usb_cdc_acm.h index ddcc65232d..624f41cf8c 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.h +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.h @@ -82,7 +82,7 @@ class USBCDCACMInstance : public uart::UARTComponent, public Parented<USBCDCACMC bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + uart::FlushResult flush() override; protected: void check_logger_conflict() override; diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp b/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp index 44de986f9a..1a36ef9f31 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp @@ -326,10 +326,10 @@ size_t USBCDCACMInstance::available() { return waiting + (this->has_peek_ ? 1 : 0); } -void USBCDCACMInstance::flush() { +uart::FlushResult USBCDCACMInstance::flush() { // Wait for TX ring buffer to be empty if (this->usb_tx_ringbuf_ == nullptr) { - return; + return uart::FlushResult::ASSUMED_SUCCESS; } UBaseType_t waiting = 1; @@ -341,7 +341,12 @@ void USBCDCACMInstance::flush() { } // Also wait for USB to finish transmitting - tinyusb_cdcacm_write_flush(static_cast<tinyusb_cdcacm_itf_t>(this->itf_), pdMS_TO_TICKS(100)); + esp_err_t err = tinyusb_cdcacm_write_flush(static_cast<tinyusb_cdcacm_itf_t>(this->itf_), pdMS_TO_TICKS(100)); + if (err == ESP_OK) + return uart::FlushResult::SUCCESS; + if (err == ESP_ERR_TIMEOUT) + return uart::FlushResult::TIMEOUT; + return uart::FlushResult::FAILED; } void USBCDCACMInstance::check_logger_conflict() {} diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index a1f8738491..a0101a5546 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -166,7 +166,7 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) { this->parent_->start_output(this); } -void USBUartChannel::flush() { +uart::FlushResult USBUartChannel::flush() { // Spin until the output queue is drained and the last USB transfer completes. // Safe to call from the main loop only. // The 100 ms timeout guards against a device that stops responding mid-flush; @@ -177,6 +177,9 @@ void USBUartChannel::flush() { this->parent_->start_output(this); yield(); } + if (!this->output_queue_.empty() || this->output_started_.load()) + return uart::FlushResult::TIMEOUT; + return uart::FlushResult::SUCCESS; } bool USBUartChannel::peek_byte(uint8_t *data) { diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 7290f8a958..671f0cab8c 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -110,7 +110,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override { return this->input_buffer_.get_available(); } - void flush() override; + uart::FlushResult flush() override; void check_logger_conflict() override {} void set_parity(UARTParityOptions parity) { this->parity_ = parity; } void set_debug(bool debug) { this->debug_ = debug; } diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp index 3f5d6c787c..f01d164e9f 100644 --- a/esphome/components/weikai/weikai.cpp +++ b/esphome/components/weikai/weikai.cpp @@ -433,15 +433,16 @@ void WeikaiChannel::write_array(const uint8_t *buffer, size_t length) { this->reg(0).write_fifo(const_cast<uint8_t *>(buffer), length); } -void WeikaiChannel::flush() { +uart::FlushResult WeikaiChannel::flush() { uint32_t const start_time = millis(); while (this->tx_fifo_is_not_empty_()) { // wait until buffer empty if (millis() - start_time > 200) { ESP_LOGW(TAG, "WARNING flush timeout - still %d bytes not sent after 200 ms", this->tx_in_fifo_()); - return; + return uart::FlushResult::TIMEOUT; } yield(); // reschedule our thread to avoid blocking } + return uart::FlushResult::SUCCESS; } size_t WeikaiChannel::xfer_fifo_to_buffer_() { diff --git a/esphome/components/weikai/weikai.h b/esphome/components/weikai/weikai.h index 43c3a1e4f4..715b82bfc7 100644 --- a/esphome/components/weikai/weikai.h +++ b/esphome/components/weikai/weikai.h @@ -380,7 +380,7 @@ class WeikaiChannel : public uart::UARTComponent { /// @details If we refer to Serial.flush() in Arduino it says: ** Waits for the transmission of outgoing serial data /// to complete. (Prior to Arduino 1.0, this the method was removing any buffered incoming serial data.). ** Therefore /// we wait until all bytes are gone with a timeout of 100 ms - void flush() override; + uart::FlushResult flush() override; protected: friend class WeikaiComponent; diff --git a/tests/components/ld2450/common.h b/tests/components/ld2450/common.h index d5ffbe1295..9f9e7b3e9f 100644 --- a/tests/components/ld2450/common.h +++ b/tests/components/ld2450/common.h @@ -16,7 +16,7 @@ class MockUARTComponent : public uart::UARTComponent { MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); MOCK_METHOD(size_t, available, (), (override)); - MOCK_METHOD(void, flush, (), (override)); + MOCK_METHOD(uart::FlushResult, flush, (), (override)); MOCK_METHOD(void, check_logger_conflict, (), (override)); }; diff --git a/tests/components/uart/common.h b/tests/components/uart/common.h index 1f9bfa15e7..f7e2d8a3f7 100644 --- a/tests/components/uart/common.h +++ b/tests/components/uart/common.h @@ -30,7 +30,7 @@ class MockUARTComponent : public UARTComponent { MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); MOCK_METHOD(size_t, available, (), (override)); - MOCK_METHOD(void, flush, (), (override)); + MOCK_METHOD(FlushResult, flush, (), (override)); MOCK_METHOD(void, check_logger_conflict, (), (override)); }; diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp index e8c07d632d..1edeb97cf1 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -144,8 +144,9 @@ bool MockUartComponent::read_array(uint8_t *data, size_t len) { size_t MockUartComponent::available() { return this->rx_buffer_.size(); } -void MockUartComponent::flush() { +uart::FlushResult MockUartComponent::flush() { // Nothing to flush in mock + return uart::FlushResult::ASSUMED_SUCCESS; } void MockUartComponent::set_rx_full_threshold(size_t rx_full_threshold) { diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h index 901e371dec..c9afb96357 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h @@ -28,7 +28,7 @@ class MockUartComponent : public uart::UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + uart::FlushResult flush() override; void set_rx_full_threshold(size_t rx_full_threshold) override; void set_rx_timeout(size_t rx_timeout) override; From 04cff1c916ece7a8abe16a17f0c8bc4e07628d56 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Sun, 8 Mar 2026 00:04:14 -0600 Subject: [PATCH 452/609] [usb_uart] Return `flush` result, expose timeout via config (#14616) --- esphome/components/usb_uart/__init__.py | 6 +++++- esphome/components/usb_uart/usb_uart.cpp | 10 ++++++---- esphome/components/usb_uart/usb_uart.h | 11 ++++++----- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index eedf590eca..2d85723d72 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import socket from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS -from esphome.components.uart import CONF_DEBUG_PREFIX, UARTComponent +from esphome.components.uart import CONF_DEBUG_PREFIX, CONF_FLUSH_TIMEOUT, UARTComponent from esphome.components.usb_host import register_usb_client, usb_device_schema import esphome.config_validation as cv from esphome.const import ( @@ -91,6 +91,9 @@ def channel_schema(channels, baud_rate_required): cv.Optional(CONF_DUMMY_RECEIVER, default=False): cv.boolean, cv.Optional(CONF_DEBUG, default=False): cv.boolean, cv.Optional(CONF_DEBUG_PREFIX, default=""): cv.string, + cv.Optional( + CONF_FLUSH_TIMEOUT, default="100ms" + ): cv.positive_time_period_milliseconds, } ) ), @@ -129,6 +132,7 @@ async def to_code(config): cg.add(chvar.set_parity(channel[CONF_PARITY])) cg.add(chvar.set_baud_rate(channel[CONF_BAUD_RATE])) cg.add(chvar.set_dummy_receiver(channel[CONF_DUMMY_RECEIVER])) + cg.add(chvar.set_flush_timeout(channel[CONF_FLUSH_TIMEOUT])) cg.add(chvar.set_debug(channel[CONF_DEBUG])) if channel[CONF_DEBUG_PREFIX]: cg.add(chvar.set_debug_prefix(channel[CONF_DEBUG_PREFIX])) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index a0101a5546..83de0b39fc 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -169,10 +169,10 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) { uart::FlushResult USBUartChannel::flush() { // Spin until the output queue is drained and the last USB transfer completes. // Safe to call from the main loop only. - // The 100 ms timeout guards against a device that stops responding mid-flush; + // The flush_timeout_ms_ timeout guards against a device that stops responding mid-flush; // in that case the main loop is blocked for the full duration. - uint32_t start = millis(); // 100 ms safety timeout - while ((!this->output_queue_.empty() || this->output_started_.load()) && millis() - start < 100) { + uint32_t start = millis(); + while ((!this->output_queue_.empty() || this->output_started_.load()) && millis() - start < this->flush_timeout_ms_) { // Kick start_output() in case data arrived but no transfer is in flight yet. this->parent_->start_output(this); yield(); @@ -260,10 +260,12 @@ void USBUartComponent::dump_config() { " Data Bits: %u\n" " Parity: %s\n" " Stop bits: %s\n" + " Flush Timeout: %" PRIu32 " ms\n" " Debug: %s\n" " Dummy receiver: %s", channel->index_, channel->baud_rate_, channel->data_bits_, PARITY_NAMES[channel->parity_], - STOP_BITS_NAMES[channel->stop_bits_], YESNO(channel->debug_), YESNO(channel->dummy_receiver_)); + STOP_BITS_NAMES[channel->stop_bits_], channel->flush_timeout_ms_, YESNO(channel->debug_), + YESNO(channel->dummy_receiver_)); } } void USBUartComponent::start_input(USBUartChannel *channel) { diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 671f0cab8c..b1748aebf2 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -116,6 +116,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon void set_debug(bool debug) { this->debug_ = debug; } void set_dummy_receiver(bool dummy_receiver) { this->dummy_receiver_ = dummy_receiver; } void set_debug_prefix(const char *prefix) { this->debug_prefix_ = StringRef(prefix); } + void set_flush_timeout(uint32_t flush_timeout_ms) override { this->flush_timeout_ms_ = flush_timeout_ms; } /// Register a callback invoked immediately after data is pushed to the input ring buffer. /// Called from USBUartComponent::loop() in the main loop context. @@ -124,23 +125,23 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon void set_rx_callback(std::function<void()> cb) { this->rx_callback_ = std::move(cb); } protected: - // Larger structures first for better alignment + // Larger structures first (8+ bytes) RingBuffer input_buffer_; LockFreeQueue<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT> output_queue_; EventPool<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT> output_pool_; std::function<void()> rx_callback_{}; CdcEps cdc_dev_{}; - // Enum (likely 4 bytes) + StringRef debug_prefix_{}; + // 4-byte fields UARTParityOptions parity_{UART_CONFIG_PARITY_NONE}; - // Group atomics together (each 1 byte) + uint32_t flush_timeout_ms_{100}; + // 1-byte fields (no padding between groups) std::atomic<bool> input_started_{true}; std::atomic<bool> output_started_{true}; std::atomic<bool> initialised_{false}; - // Group regular bytes together to minimize padding const uint8_t index_; bool debug_{}; bool dummy_receiver_{}; - StringRef debug_prefix_{}; }; class USBUartComponent : public usb_host::USBClient { From e4b89a69d4aba18e05b0a965e98e5f242637daf2 Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Sun, 8 Mar 2026 07:32:20 +0100 Subject: [PATCH 453/609] [nrf52, ota] ble and serial OTA based on mcumgr (#11932) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick+github@koston.org> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/nrf52/__init__.py | 46 +++++- esphome/components/ota/__init__.py | 9 +- esphome/components/safe_mode/safe_mode.cpp | 15 +- esphome/components/zephyr_mcumgr/__init__.py | 0 .../components/zephyr_mcumgr/ota/__init__.py | 141 +++++++++++++++++ .../zephyr_mcumgr/ota/ota_zephyr_mcumgr.cpp | 143 ++++++++++++++++++ .../zephyr_mcumgr/ota/ota_zephyr_mcumgr.h | 29 ++++ esphome/core/defines.h | 1 + script/helpers_zephyr.py | 16 ++ tests/components/ota/test.nrf52-mcumgr.yaml | 27 ++++ 11 files changed, 422 insertions(+), 6 deletions(-) create mode 100644 esphome/components/zephyr_mcumgr/__init__.py create mode 100644 esphome/components/zephyr_mcumgr/ota/__init__.py create mode 100644 esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.cpp create mode 100644 esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.h create mode 100644 tests/components/ota/test.nrf52-mcumgr.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 8bf896d159..d60dbc729d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -587,6 +587,7 @@ esphome/components/xl9535/* @mreditor97 esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 esphome/components/xxtea/* @clydebarrow esphome/components/zephyr/* @tomaszduda23 +esphome/components/zephyr_mcumgr/ota/* @tomaszduda23 esphome/components/zhlt01/* @cfeenstra1024 esphome/components/zigbee/* @tomaszduda23 esphome/components/zio_ultrasonic/* @kahrendt diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 0a9fb5939a..c3a10a9944 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -27,10 +27,15 @@ from esphome.components.zephyr.const import ( ) import esphome.config_validation as cv from esphome.const import ( + CONF_ADVANCED, CONF_BOARD, + CONF_DISABLED, + CONF_ENABLE_OTA_ROLLBACK, CONF_FRAMEWORK, CONF_ID, + CONF_OTA, CONF_RESET_PIN, + CONF_SAFE_MODE, CONF_VERSION, CONF_VOLTAGE, KEY_CORE, @@ -41,6 +46,7 @@ from esphome.const import ( ThreadModel, ) from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority +import esphome.final_validate as fv from esphome.storage_json import StorageJSON from esphome.types import ConfigType @@ -133,6 +139,7 @@ CONF_UICR_ERASE = "uicr_erase" VOLTAGE_LEVELS = [1.8, 2.1, 2.4, 2.7, 3.0, 3.3] + CONFIG_SCHEMA = cv.All( _detect_bootloader, set_core_data, @@ -156,9 +163,19 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_UICR_ERASE, default=False): cv.boolean, } ), - cv.Optional(CONF_FRAMEWORK, default={CONF_VERSION: "2.6.1-a"}): cv.Schema( + cv.Optional( + CONF_FRAMEWORK, + default={}, + ): cv.Schema( { - cv.Required(CONF_VERSION): cv.string_strict, + cv.Optional(CONF_VERSION, default="2.6.1-a"): cv.string_strict, + cv.Optional(CONF_ADVANCED, default={}): cv.Schema( + { + cv.Optional( + CONF_ENABLE_OTA_ROLLBACK, default=True + ): cv.boolean, + } + ), } ), cv.GenerateID(CONF_CDC_ACM): cv.declare_id(CdcAcm), @@ -181,6 +198,24 @@ def _final_validate(config): _LOGGER.warning( "Selected generic Adafruit bootloader. The board might crash. Consider settings `bootloader:`" ) + full_config = fv.full_config.get() + conf = config[CONF_FRAMEWORK] + advanced = conf[CONF_ADVANCED] + + if advanced[CONF_ENABLE_OTA_ROLLBACK]: + # "disabled: false" means safe mode *is* enabled. + safe_mode_config = full_config.get(CONF_SAFE_MODE, {CONF_DISABLED: True}) + safe_mode_enabled = not safe_mode_config[CONF_DISABLED] + ota_enabled = CONF_OTA in full_config + # Both need to be enabled for rollback to work + if not (ota_enabled and safe_mode_enabled): + # But only warn if ota is even possible + if ota_enabled: + _LOGGER.warning( + "OTA rollback requires safe_mode, disabling rollback support" + ) + # disable the rollback feature anyway since it can't be used. + advanced[CONF_ENABLE_OTA_ROLLBACK] = False FINAL_VALIDATE_SCHEMA = _final_validate @@ -247,6 +282,11 @@ async def to_code(config: ConfigType) -> None: if reg0_config[CONF_UICR_ERASE]: cg.add_define("USE_NRF52_UICR_ERASE") + conf = config[CONF_FRAMEWORK] + advanced = conf[CONF_ADVANCED] + # Enable OTA rollback support + if advanced[CONF_ENABLE_OTA_ROLLBACK]: + cg.add_define("USE_OTA_ROLLBACK") # c++ support if framework_ver < cv.Version(2, 9, 2): zephyr_add_prj_conf("CPLUSPLUS", True) @@ -259,7 +299,7 @@ async def to_code(config: ConfigType) -> None: zephyr_add_prj_conf("WDT_DISABLE_AT_BOOT", False) # disable console zephyr_add_prj_conf("UART_CONSOLE", False) - zephyr_add_prj_conf("CONSOLE", False) + zephyr_add_prj_conf("CONSOLE", False, False) # use NFC pins as GPIO if framework_ver < cv.Version(2, 9, 2): zephyr_add_prj_conf("NFCT_PINS_AS_GPIOS", True) diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index ee54d5f8d3..8f31eb5cdd 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -18,7 +18,14 @@ from esphome.coroutine import CoroPriority OTA_STATE_LISTENER_KEY = "ota_state_listener" CODEOWNERS = ["@esphome/core"] -AUTO_LOAD = ["md5", "safe_mode"] + + +def AUTO_LOAD() -> list[str]: + components = ["safe_mode"] + if not CORE.using_zephyr: + components.extend(["md5"]) + return components + IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index fe2acd9612..40fa03392b 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -9,10 +9,14 @@ #include <cinttypes> #include <cstdio> -#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) +#ifdef USE_OTA_ROLLBACK +#ifdef USE_ZEPHYR +#include <zephyr/dfu/mcuboot.h> +#elif defined(USE_ESP32) #include <esp_ota_ops.h> #include <esp_system.h> #endif +#endif namespace esphome::safe_mode { @@ -66,9 +70,16 @@ float SafeModeComponent::get_setup_priority() const { return setup_priority::AFT void SafeModeComponent::mark_successful() { this->clean_rtc(); this->boot_successful_ = true; -#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) +#if defined(USE_OTA_ROLLBACK) +// Mark OTA partition as valid to prevent rollback +#if defined(USE_ZEPHYR) + if (!boot_is_img_confirmed()) { + boot_write_img_confirmed(); + } +#elif defined(USE_ESP32) // Mark OTA partition as valid to prevent rollback esp_ota_mark_app_valid_cancel_rollback(); +#endif #endif // Disable loop since we no longer need to check this->disable_loop(); diff --git a/esphome/components/zephyr_mcumgr/__init__.py b/esphome/components/zephyr_mcumgr/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/zephyr_mcumgr/ota/__init__.py b/esphome/components/zephyr_mcumgr/ota/__init__.py new file mode 100644 index 0000000000..b0d86190b8 --- /dev/null +++ b/esphome/components/zephyr_mcumgr/ota/__init__.py @@ -0,0 +1,141 @@ +import esphome.codegen as cg +from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code +from esphome.components.zephyr import ( + zephyr_add_cdc_acm, + zephyr_add_overlay, + zephyr_add_prj_conf, + zephyr_data, +) +from esphome.components.zephyr.const import BOOTLOADER_MCUBOOT, KEY_BOOTLOADER +import esphome.config_validation as cv +from esphome.const import CONF_HARDWARE_UART, CONF_ID, Framework +from esphome.core import CORE, coroutine_with_priority +from esphome.coroutine import CoroPriority +from esphome.types import ConfigType + +CODEOWNERS = ["@tomaszduda23"] +DEPENDENCIES = ["zephyr"] + +ZephyrMcumgrOTAComponent = cg.esphome_ns.namespace("zephyr_mcumgr").class_( + "OTAComponent", OTAComponent +) + +CONF_BLE = "ble" +CONF_TRANSPORT = "transport" + + +def _validate_transport(conf: ConfigType) -> ConfigType: + transport = conf[CONF_TRANSPORT] + if transport[CONF_BLE] or CONF_HARDWARE_UART in transport: + return conf + raise cv.Invalid( + f"At least one transport protocol has to be enabled. Set '{CONF_BLE}: true' or '{CONF_HARDWARE_UART}'" + ) + + +UARTS = { + "CDC": ("cdc_acm_uart0", 0), + "CDC1": ("cdc_acm_uart1", 1), + "UART0": ("uart0", -1), + "UART1": ("uart1", -1), +} + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ZephyrMcumgrOTAComponent), + cv.Optional(CONF_TRANSPORT, default={CONF_BLE: True}): cv.Schema( + { + cv.Optional(CONF_BLE, default=False): cv.boolean, + cv.Optional( + CONF_HARDWARE_UART, + ): cv.one_of(*UARTS, upper=True), + } + ), + } + ) + .extend(BASE_OTA_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), + _validate_transport, + cv.only_with_framework(Framework.ZEPHYR), +) + + +def _validate_mcumgr_bootloader(config: ConfigType) -> None: + bootloader = zephyr_data()[KEY_BOOTLOADER] + if bootloader != BOOTLOADER_MCUBOOT: + raise cv.Invalid(f"'{bootloader}' bootloader does not support OTA") + + +KEY_ZEPHYR_BLE_SERVER = "zephyr_ble_server" + + +def _validate_ble_server(config: ConfigType) -> None: + if ( + config[CONF_TRANSPORT][CONF_BLE] + and KEY_ZEPHYR_BLE_SERVER not in CORE.loaded_integrations + ): + raise cv.Invalid(f"'{KEY_ZEPHYR_BLE_SERVER}' component is required for BLE OTA") + + +def _final_validate(config: ConfigType) -> None: + _validate_mcumgr_bootloader(config) + _validate_ble_server(config) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +@coroutine_with_priority(CoroPriority.OTA_UPDATES) +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await ota_to_code(var, config) + + await cg.register_component(var, config) + + zephyr_add_prj_conf("NET_BUF", True) + zephyr_add_prj_conf("ZCBOR", True) + zephyr_add_prj_conf("MCUMGR", True) + + zephyr_add_prj_conf("MCUMGR_GRP_IMG", True) + + zephyr_add_prj_conf("IMG_MANAGER", True) + zephyr_add_prj_conf("STREAM_FLASH", True) + zephyr_add_prj_conf("FLASH_MAP", True) + zephyr_add_prj_conf("FLASH", True) + + zephyr_add_prj_conf("IMG_ERASE_PROGRESSIVELY", True) + + zephyr_add_prj_conf("BOOTLOADER_MCUBOOT", True) + + zephyr_add_prj_conf("MCUMGR_MGMT_NOTIFICATION_HOOKS", True) + zephyr_add_prj_conf("MCUMGR_GRP_IMG_STATUS_HOOKS", True) + zephyr_add_prj_conf("MCUMGR_GRP_IMG_UPLOAD_CHECK_HOOK", True) + transport = config[CONF_TRANSPORT] + if transport[CONF_BLE]: + zephyr_add_prj_conf("MCUMGR_TRANSPORT_BT", True) + zephyr_add_prj_conf("MCUMGR_TRANSPORT_BT_REASSEMBLY", True) + + zephyr_add_prj_conf("MCUMGR_GRP_OS", True) + zephyr_add_prj_conf("MCUMGR_GRP_OS_MCUMGR_PARAMS", True) + + zephyr_add_prj_conf("NCS_SAMPLE_MCUMGR_BT_OTA_DFU_SPEEDUP", True) + if CONF_HARDWARE_UART in transport: + uart = UARTS[transport[CONF_HARDWARE_UART]] + uart_name = uart[0] + cdc_id = uart[1] + if cdc_id >= 0: + zephyr_add_cdc_acm(config, cdc_id) + zephyr_add_prj_conf("MCUMGR_TRANSPORT_UART", True) + zephyr_add_prj_conf("BASE64", True) + zephyr_add_prj_conf("CONSOLE", True) + zephyr_add_overlay( + f""" + / {{ + chosen {{ + zephyr,uart-mcumgr = &{uart_name}; + }}; + }}; + """ + ) diff --git a/esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.cpp b/esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.cpp new file mode 100644 index 0000000000..f1eac462bc --- /dev/null +++ b/esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.cpp @@ -0,0 +1,143 @@ +#ifdef USE_ZEPHYR +#include "ota_zephyr_mcumgr.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include <zephyr/sys/math_extras.h> +#include <zephyr/usb/usb_device.h> +#include <zephyr/dfu/mcuboot.h> + +// It should be from below header but there is problem with internal includes. +// #include <zephyr/mgmt/mcumgr/grp/img_mgmt/img_mgmt.h> +// NOLINTBEGIN(readability-identifier-naming,google-runtime-int) +struct img_mgmt_upload_action { + /** The total size of the image. */ + unsigned long long size; +}; + +struct img_mgmt_upload_req { + uint32_t image; /* 0 by default */ + size_t off; /* SIZE_MAX if unspecified */ +}; +// NOLINTEND(readability-identifier-naming,google-runtime-int) + +namespace esphome::zephyr_mcumgr { + +static_assert(sizeof(struct img_mgmt_upload_action) == 8, "ABI mismatch"); +static_assert(sizeof(struct img_mgmt_upload_req) == 8, "ABI mismatch"); +static_assert(offsetof(struct img_mgmt_upload_req, image) == 0, "ABI mismatch"); +static_assert(offsetof(struct img_mgmt_upload_req, off) == 4, "ABI mismatch"); + +static const char *const TAG = "zephyr_mcumgr"; +static OTAComponent *global_ota_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +static enum mgmt_cb_return mcumgr_img_mgmt_cb(uint32_t event, enum mgmt_cb_return prev_status, int32_t *rc, + uint16_t *group, bool *abort_more, void *data, size_t data_size) { + if (MGMT_EVT_OP_IMG_MGMT_DFU_CHUNK == event) { + const img_mgmt_upload_check &upload = *static_cast<img_mgmt_upload_check *>(data); + global_ota_component->update_chunk(upload); + } else if (MGMT_EVT_OP_IMG_MGMT_DFU_STARTED == event) { + global_ota_component->update_started(); + } else if (MGMT_EVT_OP_IMG_MGMT_DFU_CHUNK_WRITE_COMPLETE == event) { + global_ota_component->update_chunk_wrote(); + } else if (MGMT_EVT_OP_IMG_MGMT_DFU_PENDING == event) { + global_ota_component->update_pending(); + } else if (MGMT_EVT_OP_IMG_MGMT_DFU_STOPPED == event) { + global_ota_component->update_stopped(); + } else { + ESP_LOGD(TAG, "MCUmgr Image Management Event with the %d ID", u32_count_trailing_zeros(MGMT_EVT_GET_ID(event))); + } + return MGMT_CB_OK; +} + +OTAComponent::OTAComponent() { global_ota_component = this; } + +void OTAComponent::setup() { + this->img_mgmt_callback_.callback = mcumgr_img_mgmt_cb; + this->img_mgmt_callback_.event_id = MGMT_EVT_OP_IMG_MGMT_ALL; + mgmt_callback_register(&this->img_mgmt_callback_); +#ifdef CONFIG_USB_DEVICE_STACK + usb_enable(nullptr); +#endif +// Handle OTA rollback: mark partition valid immediately unless USE_OTA_ROLLBACK is enabled, +// in which case safe_mode will mark it valid after confirming successful boot. +#ifndef USE_OTA_ROLLBACK + if (!boot_is_img_confirmed()) { + boot_write_img_confirmed(); + } +#endif +} + +#ifdef ESPHOME_LOG_HAS_CONFIG +static const char *swap_type_str(uint8_t type) { + switch (type) { + case BOOT_SWAP_TYPE_NONE: + return "none"; + case BOOT_SWAP_TYPE_TEST: + return "test"; + case BOOT_SWAP_TYPE_PERM: + return "perm"; + case BOOT_SWAP_TYPE_REVERT: + return "revert"; + case BOOT_SWAP_TYPE_FAIL: + return "fail"; + } + + return "unknown"; +} +#endif + +void OTAComponent::dump_config() { + ESP_LOGCONFIG(TAG, + "Over-The-Air Updates:\n" + " swap type after reboot: %s\n" + " image confirmed: %s", + swap_type_str(mcuboot_swap_type()), YESNO(boot_is_img_confirmed())); +} + +void OTAComponent::update_chunk(const img_mgmt_upload_check &upload) { + float percentage = (upload.req->off * 100.0f) / upload.action->size; + this->defer([this, percentage]() { this->percentage_ = percentage; }); +} + +void OTAComponent::update_started() { + this->defer([this]() { + ESP_LOGD(TAG, "Starting update"); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_STARTED, 0.0f, 0); +#endif + }); +} + +void OTAComponent::update_chunk_wrote() { + uint32_t now = millis(); + if (now - this->last_progress_ > 1000) { + this->last_progress_ = now; + this->defer([this]() { + ESP_LOGD(TAG, "OTA in progress: %0.1f%%", this->percentage_); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_IN_PROGRESS, this->percentage_, 0); +#endif + }); + } +} + +void OTAComponent::update_pending() { + this->defer([this]() { + ESP_LOGD(TAG, "OTA pending"); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_COMPLETED, 100.0f, 0); +#endif + }); +} + +void OTAComponent::update_stopped() { + this->defer([this]() { + ESP_LOGD(TAG, "OTA stopped"); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(ota::OTA_RESPONSE_ERROR_UNKNOWN)); +#endif + }); +} + +} // namespace esphome::zephyr_mcumgr +#endif diff --git a/esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.h b/esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.h new file mode 100644 index 0000000000..ab98f93598 --- /dev/null +++ b/esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.h @@ -0,0 +1,29 @@ +#pragma once +#include "esphome/core/defines.h" +#ifdef USE_ZEPHYR +#include "esphome/components/ota/ota_backend.h" +#include <zephyr/mgmt/mcumgr/mgmt/callbacks.h> + +struct img_mgmt_upload_check; + +namespace esphome::zephyr_mcumgr { + +class OTAComponent : public ota::OTAComponent { + public: + OTAComponent(); + void setup() override; + void dump_config() override; + void update_chunk(const img_mgmt_upload_check &upload); + void update_started(); + void update_chunk_wrote(); + void update_pending(); + void update_stopped(); + + protected: + uint32_t last_progress_ = 0; + float percentage_ = 0; + mgmt_callback img_mgmt_callback_{}; +}; + +} // namespace esphome::zephyr_mcumgr +#endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index c5f38ab9aa..48c467f69f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -367,6 +367,7 @@ #define USE_NRF52_DFU #define USE_NRF52_REG0_VOUT 5 #define USE_NRF52_UICR_ERASE +#define USE_OTA_ROLLBACK #define USE_SOFTDEVICE_ID 7 #define USE_SOFTDEVICE_VERSION 1 #define USE_ZIGBEE diff --git a/script/helpers_zephyr.py b/script/helpers_zephyr.py index 1242a60cf4..66ef6ffc98 100644 --- a/script/helpers_zephyr.py +++ b/script/helpers_zephyr.py @@ -28,6 +28,22 @@ extern "C" void zboss_signal_handler() {}; CONFIG_NEWLIB_LIBC=y CONFIG_BT=y CONFIG_ADC=y +#mcumgr begin +CONFIG_NET_BUF=y +CONFIG_ZCBOR=y +CONFIG_MCUMGR=y +CONFIG_MCUMGR_GRP_IMG=y +CONFIG_IMG_MANAGER=y +CONFIG_STREAM_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_FLASH=y +CONFIG_IMG_ERASE_PROGRESSIVELY=y +CONFIG_BOOTLOADER_MCUBOOT=y +CONFIG_MCUMGR_MGMT_NOTIFICATION_HOOKS=y +CONFIG_MCUMGR_GRP_IMG_STATUS_HOOKS=y +CONFIG_MCUMGR_GRP_IMG_UPLOAD_CHECK_HOOK=y +CONFIG_MCUMGR_TRANSPORT_UART=y +#mcumgr end #zigbee begin CONFIG_ZIGBEE=y CONFIG_CRYPTO=y diff --git a/tests/components/ota/test.nrf52-mcumgr.yaml b/tests/components/ota/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..1e7986f61a --- /dev/null +++ b/tests/components/ota/test.nrf52-mcumgr.yaml @@ -0,0 +1,27 @@ +zephyr_ble_server: + +ota: + - platform: zephyr_mcumgr + transport: + ble: true + hardware_uart: CDC + on_begin: + then: + - logger.log: "OTA start" + on_progress: + then: + - logger.log: + format: "OTA progress %0.1f%%" + args: ["x"] + on_end: + then: + - logger.log: "OTA end" + on_error: + then: + - logger.log: + format: "OTA update error %d" + args: ["x"] + on_state_change: + then: + lambda: >- + ESP_LOGD("ota", "State %d", state); From d9e76da8064ba955c2406d13e07e1377ca77ea9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 07:59:25 +0000 Subject: [PATCH 454/609] Bump aioesphomeapi from 44.4.0 to 44.5.0 (#14617) 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 a1300a67f7..4124bf8791 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.4.0 +aioesphomeapi==44.5.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From a530aeec22174300264d58f791518d993658e9ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 22:09:12 -1000 Subject: [PATCH 455/609] [api] Inline varint and encode_varint_raw fast paths for hot loop performance (#14607) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/api/proto.cpp | 12 ++++++++ esphome/components/api/proto.h | 51 ++++++++++++++++++-------------- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 1ca6b702ad..fb229928e5 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -8,6 +8,18 @@ namespace esphome::api { static const char *const TAG = "api.proto"; +uint32_t ProtoSize::varint_slow(uint32_t value) { return varint_wide(value); } + +void ProtoWriteBuffer::encode_varint_raw_slow_(uint32_t value) { + do { + this->debug_check_bounds_(1); + *this->pos_++ = static_cast<uint8_t>(value | 0x80); + value >>= 7; + } while (value > 0x7F); + this->debug_check_bounds_(1); + *this->pos_++ = static_cast<uint8_t>(value); +} + #ifdef USE_API_VARINT64 optional<ProtoVarInt> ProtoVarInt::parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed, uint32_t result32) { diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index bbdd11b29d..adde0a8a85 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -240,14 +240,13 @@ class ProtoWriteBuffer { ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {} ProtoWriteBuffer(std::vector<uint8_t> *buffer, size_t write_pos) : buffer_(buffer), pos_(buffer->data() + write_pos) {} - void encode_varint_raw(uint32_t value) { - while (value > 0x7F) { + inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint32_t value) { + if (value < 128) [[likely]] { this->debug_check_bounds_(1); - *this->pos_++ = static_cast<uint8_t>(value | 0x80); - value >>= 7; + *this->pos_++ = static_cast<uint8_t>(value); + return; } - this->debug_check_bounds_(1); - *this->pos_++ = static_cast<uint8_t>(value); + this->encode_varint_raw_slow_(value); } void encode_varint_raw_64(uint64_t value) { while (value > 0x7F) { @@ -378,6 +377,9 @@ class ProtoWriteBuffer { std::vector<uint8_t> *get_buffer() const { return buffer_; } protected: + // Slow path for encode_varint_raw values >= 128, outlined to keep fast path small + void encode_varint_raw_slow_(uint32_t value) __attribute__((noinline)); + #ifdef ESPHOME_DEBUG_API void debug_check_bounds_(size_t bytes, const char *caller = __builtin_FUNCTION()); void debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual); @@ -511,24 +513,29 @@ class ProtoSize { * @param value The uint32_t value to calculate size for * @return The number of bytes needed to encode the value */ - static constexpr uint32_t varint(uint32_t value) { - // Optimized varint size calculation using leading zeros - // Each 7 bits requires one byte in the varint encoding - if (value < 128) - return 1; // 7 bits, common case for small values - - // For larger values, count bytes needed based on the position of the highest bit set - if (value < 16384) { - return 2; // 14 bits - } else if (value < 2097152) { - return 3; // 21 bits - } else if (value < 268435456) { - return 4; // 28 bits - } else { - return 5; // 32 bits (maximum for uint32_t) - } + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint(uint32_t value) { + if (value < 128) [[likely]] + return 1; // Fast path: 7 bits, most common case + if (__builtin_is_constant_evaluated()) + return varint_wide(value); + return varint_slow(value); } + private: + // Slow path for varint >= 128, outlined to keep fast path small + static uint32_t varint_slow(uint32_t value) __attribute__((noinline)); + // Shared cascade for values >= 128 (used by both constexpr and noinline paths) + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint_wide(uint32_t value) { + if (value < 16384) + return 2; + if (value < 2097152) + return 3; + if (value < 268435456) + return 4; + return 5; + } + + public: /** * @brief Calculates the size in bytes needed to encode a uint64_t value as a varint * From 2c705810cdaff126c8fa5270e012c8037c62311b Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Sun, 8 Mar 2026 09:40:52 +0100 Subject: [PATCH 456/609] [nrf52] allow to update OTA via cmd (#12344) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick+github@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/nrf52/__init__.py | 38 +++++-- esphome/components/nrf52/ota.py | 164 +++++++++++++++++++++++++++ requirements.txt | 1 + 3 files changed, 194 insertions(+), 9 deletions(-) create mode 100644 esphome/components/nrf52/ota.py diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index c3a10a9944..5054e5e0df 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -392,22 +392,42 @@ def _upload_using_platformio( def upload_program(config: ConfigType, args, host: str) -> bool: from esphome.__main__ import check_permissions, get_port_type - result = 0 - handled = False + mcumgr_device: str | None = None if get_port_type(host) == "SERIAL": check_permissions(host) - result = _upload_using_platformio(config, host, ["-t", "upload"]) - handled = True + if zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT: + mcumgr_device = host + else: + result = _upload_using_platformio(config, host, ["-t", "upload"]) + if result != 0: + raise EsphomeError(f"Upload failed with result: {result}") + return True # Handled: platformio serial upload if host == "PYOCD": result = _upload_using_platformio(config, host, ["-t", "flash_pyocd"]) - handled = True + if result != 0: + raise EsphomeError(f"Upload failed with result: {result}") + return True # Handled: platformio PYOCD upload - if result != 0: - raise EsphomeError(f"Upload failed with result: {result}") + # Deferred imports: bleak/smpclient are heavy, only load for BLE/mcumgr paths + from .ble_logger import is_mac_address + from .ota import smpmgr_scan, smpmgr_upload - return handled + if host == "BLE": + mcumgr_device = asyncio.run(smpmgr_scan(CORE.name)) + + if is_mac_address(host): + mcumgr_device = host + + if mcumgr_device: + firmware = Path( + CORE.relative_pioenvs_path(CORE.name, "zephyr", "app_update.bin") + ).resolve() + asyncio.run(smpmgr_upload(mcumgr_device, firmware)) + return True # Handled: mcumgr OTA upload + + return False # Not handled: let caller try default upload methods def show_logs(config: ConfigType, args, devices: list[str]) -> bool: @@ -415,7 +435,7 @@ def show_logs(config: ConfigType, args, devices: list[str]) -> bool: from .ble_logger import is_mac_address, logger_connect, logger_scan if devices[0] == "BLE": - ble_device = asyncio.run(logger_scan(CORE.config["esphome"]["name"])) + ble_device = asyncio.run(logger_scan(CORE.name)) if ble_device: address = ble_device.address else: diff --git a/esphome/components/nrf52/ota.py b/esphome/components/nrf52/ota.py new file mode 100644 index 0000000000..e4b26b45eb --- /dev/null +++ b/esphome/components/nrf52/ota.py @@ -0,0 +1,164 @@ +import asyncio +from dataclasses import asdict +import json +import logging +from pathlib import Path + +from bleak import BleakScanner +from bleak.exc import BleakDeviceNotFoundError +from smp.exceptions import SMPBadStartDelimiter +from smpclient import SMPClient +from smpclient.generics import error, success +from smpclient.mcuboot import IMAGE_TLV, ImageInfo, MCUBootImageError, TLVNotFound +from smpclient.requests.image_management import ImageStatesRead, ImageStatesWrite +from smpclient.requests.os_management import ResetWrite +from smpclient.transport import SMPTransportDisconnected +from smpclient.transport.ble import ( + SMPBLETransport, + SMPBLETransportDeviceNotFound, + SMPBLETransportException, +) +from smpclient.transport.serial import SMPSerialTransport + +from esphome.core import EsphomeError +from esphome.espota2 import ProgressBar + +from .ble_logger import is_mac_address + +SMP_SERVICE_UUID = "8D53DC1D-1DB7-4CD3-868B-8A527460AA84" +BLE_SCAN_TIMEOUT = 10.0 # seconds +RESET_DELAY = 2.0 # seconds to wait before reset, allows on_end action to execute + +_LOGGER = logging.getLogger(__name__) + + +def _json_state(o: object) -> object: + """JSON serializer for SMP image state objects.""" + if isinstance(o, (bytes, bytearray)): + return o.hex() + if hasattr(o, "hex"): + return o.hex() + if hasattr(o, "__dict__"): + return vars(o) + return str(o) + + +async def smpmgr_scan(name: str) -> str: + _LOGGER.info("Scanning bluetooth for %s...", name) + for device in await BleakScanner.discover( + timeout=BLE_SCAN_TIMEOUT, service_uuids=[SMP_SERVICE_UUID] + ): + if device.name == name: + return device.address + raise EsphomeError(f"BLE device {name} with OTA service not found") + + +async def smpmgr_upload(device: str, firmware: Path) -> None: + try: + await _smpmgr_upload(device, firmware) + except SMPTransportDisconnected as exc: + raise EsphomeError(f"{device} was disconnected.") from exc + except SMPBLETransportDeviceNotFound as exc: + raise EsphomeError(f"{device} was not found.") from exc + + +def _get_image_tlv_sha256(file: Path) -> bytes: + _LOGGER.info("Checking image: %s", str(file)) + try: + image_info = ImageInfo.load_file(str(file)) + _LOGGER.info( + "Image header:\n%s", json.dumps(asdict(image_info.header), indent=2) + ) + _LOGGER.debug(str(image_info)) + except MCUBootImageError as exc: + raise EsphomeError("Inspection of FW image failed") from exc + except FileNotFoundError as exc: + raise EsphomeError( + f"Firmware image file not found: {file}. Build with zephyr_mcumgr enabled" + ) from exc + + try: + image_tlv_sha256 = image_info.get_tlv(IMAGE_TLV.SHA256) + _LOGGER.info("Image tlv sha256: %s", image_tlv_sha256) + except TLVNotFound as exc: + raise EsphomeError("Could not find IMAGE_TLV_SHA256 in image.") from exc + return image_tlv_sha256.value + + +async def _smpmgr_upload(device: str, firmware: Path) -> None: + image_tlv_sha256 = _get_image_tlv_sha256(firmware) + + if is_mac_address(device): + smp_client = SMPClient(SMPBLETransport(), device) + else: + smp_client = SMPClient(SMPSerialTransport(), device) + + _LOGGER.info("Connecting %s...", device) + try: + await smp_client.connect() + except BleakDeviceNotFoundError as exc: + raise EsphomeError(f"Device {device} not found") from exc + except SMPBLETransportException as exc: + raise EsphomeError(f"Connection error with {device}") from exc + + _LOGGER.info("Connected %s...", device) + try: + await _smpmgr_upload_connected(smp_client, device, firmware, image_tlv_sha256) + finally: + await smp_client.disconnect() + + +async def _smpmgr_upload_connected( + smp_client: SMPClient, device: str, firmware: Path, image_tlv_sha256: bytes +) -> None: + try: + image_state = await smp_client.request(ImageStatesRead(), 2.5) + except (SMPBadStartDelimiter, TimeoutError) as exc: + raise EsphomeError(f"mcumgr is not supported by device ({device})") from exc + + already_uploaded = False + + if error(image_state): + raise EsphomeError(f"Failed to read image state from {device}: {image_state}") + if success(image_state): + if len(image_state.images) == 0: + _LOGGER.warning("No images on device!") + for image in image_state.images: + _LOGGER.info( + "Image state:\n%s", + json.dumps(image, indent=2, default=_json_state), + ) + if image.active and not image.confirmed: + raise EsphomeError("No free slot. Testing mode but not confirmed yet.") + if image.hash == image_tlv_sha256: + if already_uploaded: + raise EsphomeError("Both slots have the same image already") + if image.confirmed: + raise EsphomeError("The same image already confirmed") + _LOGGER.warning("The same image already uploaded") + already_uploaded = True + + if not already_uploaded: + with open(firmware, "rb") as file: + image = file.read() + upload_size = len(image) + progress = ProgressBar() + progress.update(0) + try: + async for offset in smp_client.upload(image): + progress.update(offset / upload_size) + finally: + progress.done() + + _LOGGER.info("Mark image for testing") + r = await smp_client.request(ImageStatesWrite(hash=image_tlv_sha256), 1.0) + + if error(r): + raise EsphomeError(f"Failed to mark image for testing on {device}: {r}") + + await asyncio.sleep(RESET_DELAY) + _LOGGER.info("Reset") + r = await smp_client.request(ResetWrite(), 1.0) + + if error(r): + raise EsphomeError(f"Failed to reset {device}: {r}") diff --git a/requirements.txt b/requirements.txt index 4124bf8791..03a7cac5c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,6 +23,7 @@ resvg-py==0.2.6 freetype-py==2.5.1 jinja2==3.1.6 bleak==2.1.1 +smpclient==6.0.0 requests==2.32.5 # esp-idf >= 5.0 requires this From 0c4a44566f9ba56d5f01bb6af2b436a1c859128b Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Sun, 8 Mar 2026 03:55:49 -0500 Subject: [PATCH 457/609] [serial_proxy] New component (#13944) Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- CODEOWNERS | 1 + esphome/components/api/api.proto | 20 ++ esphome/components/api/api_connection.cpp | 93 +++++++++ esphome/components/api/api_connection.h | 10 + esphome/components/api/api_pb2.cpp | 14 ++ esphome/components/api/api_pb2.h | 26 +++ esphome/components/api/api_pb2_dump.cpp | 24 +++ esphome/components/api/api_pb2_service.h | 1 + esphome/components/serial_proxy/__init__.py | 104 ++++++++++ .../components/serial_proxy/serial_proxy.cpp | 188 ++++++++++++++++++ .../components/serial_proxy/serial_proxy.h | 129 ++++++++++++ esphome/core/application.h | 17 ++ esphome/core/defines.h | 2 + tests/components/serial_proxy/common.yaml | 10 + .../serial_proxy/test.esp32-idf.yaml | 8 + .../serial_proxy/test.esp8266-ard.yaml | 8 + .../serial_proxy/test.rp2040-ard.yaml | 8 + 17 files changed, 663 insertions(+) create mode 100644 esphome/components/serial_proxy/__init__.py create mode 100644 esphome/components/serial_proxy/serial_proxy.cpp create mode 100644 esphome/components/serial_proxy/serial_proxy.h create mode 100644 tests/components/serial_proxy/common.yaml create mode 100644 tests/components/serial_proxy/test.esp32-idf.yaml create mode 100644 tests/components/serial_proxy/test.esp8266-ard.yaml create mode 100644 tests/components/serial_proxy/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index d60dbc729d..cb415bb625 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -435,6 +435,7 @@ esphome/components/sen5x/* @martgras esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core +esphome/components/serial_proxy/* @kbx81 esphome/components/sfa30/* @ghsensdev esphome/components/sgp40/* @SenexCrenshaw esphome/components/sgp4x/* @martgras @SenexCrenshaw diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 257a7aaf82..28332d67a5 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -2618,6 +2618,14 @@ enum SerialProxyRequestType { SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2; // Flush the serial port (block until all TX data is sent) } +enum SerialProxyStatus { + SERIAL_PROXY_STATUS_OK = 0; // Completed successfully; TX drain confirmed + SERIAL_PROXY_STATUS_ASSUMED_SUCCESS = 1; // Platform cannot confirm TX drain; success assumed + SERIAL_PROXY_STATUS_ERROR = 2; // Driver or hardware error + SERIAL_PROXY_STATUS_TIMEOUT = 3; // Timed out before TX completed + SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4; // Request type not supported by this instance +} + // Generic request message for simple serial proxy operations message SerialProxyRequest { option (id) = 144; @@ -2628,6 +2636,18 @@ message SerialProxyRequest { SerialProxyRequestType type = 2; // Request type } +// Response to a SerialProxyRequest (e.g. flush completion or failure) +message SerialProxyRequestResponse { + option (id) = 147; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + SerialProxyRequestType type = 2; // Which request type this responds to + SerialProxyStatus status = 3; // Result status + string error_message = 4; // Additional detail on failure (optional) +} + // ==================== BLUETOOTH CONNECTION PARAMS ==================== message BluetoothSetConnectionParamsRequest { option (id) = 145; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index b9b33ddcc2..43f5070a40 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1445,6 +1445,89 @@ void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRF void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) { this->send_message(msg); } #endif +#ifdef USE_SERIAL_PROXY +void APIConnection::on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) { + auto &proxies = App.get_serial_proxies(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %u out of range (max %u)", msg.instance, + static_cast<uint32_t>(proxies.size())); + return; + } + proxies[msg.instance]->configure(msg.baudrate, msg.flow_control, static_cast<uint8_t>(msg.parity), msg.stop_bits, + msg.data_size); +} + +void APIConnection::on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) { + auto &proxies = App.get_serial_proxies(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance); + return; + } + proxies[msg.instance]->write_from_client(msg.data, msg.data_len); +} + +void APIConnection::on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) { + auto &proxies = App.get_serial_proxies(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance); + return; + } + proxies[msg.instance]->set_modem_pins(msg.line_states); +} + +void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) { + auto &proxies = App.get_serial_proxies(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance); + return; + } + SerialProxyGetModemPinsResponse resp{}; + resp.instance = msg.instance; + resp.line_states = proxies[msg.instance]->get_modem_pins(); + this->send_message(resp); +} + +void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) { + auto &proxies = App.get_serial_proxies(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance); + return; + } + switch (msg.type) { + case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE: + case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: + proxies[msg.instance]->serial_proxy_request(this, msg.type); + break; + case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: { + SerialProxyRequestResponse resp{}; + resp.instance = msg.instance; + resp.type = enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH; + switch (proxies[msg.instance]->flush_port()) { + case uart::FlushResult::SUCCESS: + resp.status = enums::SERIAL_PROXY_STATUS_OK; + break; + case uart::FlushResult::ASSUMED_SUCCESS: + resp.status = enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS; + break; + case uart::FlushResult::TIMEOUT: + resp.status = enums::SERIAL_PROXY_STATUS_TIMEOUT; + break; + case uart::FlushResult::FAILED: + resp.status = enums::SERIAL_PROXY_STATUS_ERROR; + break; + } + this->send_message(resp); + break; + } + default: + ESP_LOGW(TAG, "Unknown serial proxy request type: %u", static_cast<uint32_t>(msg.type)); + break; + } +} + +void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) { this->send_message(msg); } +#endif + #ifdef USE_INFRARED uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *infrared = static_cast<infrared::Infrared *>(entity); @@ -1666,6 +1749,16 @@ bool APIConnection::send_device_info_response_() { resp.zwave_proxy_feature_flags = zwave_proxy::global_zwave_proxy->get_feature_flags(); resp.zwave_home_id = zwave_proxy::global_zwave_proxy->get_home_id(); #endif +#ifdef USE_SERIAL_PROXY + size_t serial_proxy_index = 0; + for (auto const &proxy : App.get_serial_proxies()) { + if (serial_proxy_index >= SERIAL_PROXY_COUNT) + break; + auto &info = resp.serial_proxies[serial_proxy_index++]; + info.name = StringRef(proxy->get_name()); + info.port_type = proxy->get_port_type(); + } +#endif #ifdef USE_API_NOISE resp.api_encryption_supported = true; #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index b075bc83ab..5d1469e419 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -189,6 +189,15 @@ class APIConnection final : public APIServerConnectionBase { void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg); #endif +#ifdef USE_SERIAL_PROXY + void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) override; + void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) override; + void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) override; + void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) override; + void on_serial_proxy_request(const SerialProxyRequest &msg) override; + void send_serial_proxy_data(const SerialProxyDataReceived &msg); +#endif + #ifdef USE_EVENT void send_event(event::Event *event); #endif @@ -254,6 +263,7 @@ class APIConnection final : public APIServerConnectionBase { return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::CONNECTED || this->is_authenticated(); } + bool is_marked_for_removal() const { return this->flags_.remove; } uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; } // Get client API version for feature detection diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 38ebfb9464..6fce10ca0f 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3840,6 +3840,20 @@ bool SerialProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } return true; } +void SerialProxyRequestResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_uint32(1, this->instance); + buffer.encode_uint32(2, static_cast<uint32_t>(this->type)); + buffer.encode_uint32(3, static_cast<uint32_t>(this->status)); + buffer.encode_string(4, this->error_message); +} +uint32_t SerialProxyRequestResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->instance); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->type)); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->status)); + size += ProtoSize::calc_length(1, this->error_message.size()); + return size; +} #endif #ifdef USE_BLUETOOTH_PROXY bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index a6167dc810..5c712508b9 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -333,6 +333,13 @@ enum SerialProxyRequestType : uint32_t { SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1, SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2, }; +enum SerialProxyStatus : uint32_t { + SERIAL_PROXY_STATUS_OK = 0, + SERIAL_PROXY_STATUS_ASSUMED_SUCCESS = 1, + SERIAL_PROXY_STATUS_ERROR = 2, + SERIAL_PROXY_STATUS_TIMEOUT = 3, + SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4, +}; #endif } // namespace enums @@ -3220,6 +3227,25 @@ class SerialProxyRequest final : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +class SerialProxyRequestResponse final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 147; + static constexpr uint8_t ESTIMATED_SIZE = 17; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_request_response"; } +#endif + uint32_t instance{0}; + enums::SerialProxyRequestType type{}; + enums::SerialProxyStatus status{}; + StringRef error_message{}; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; #endif #ifdef USE_BLUETOOTH_PROXY class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage { diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 086b1bdc2f..740bf2e47f 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -789,6 +789,22 @@ template<> const char *proto_enum_to_string<enums::SerialProxyRequestType>(enums return "UNKNOWN"; } } +template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::SerialProxyStatus value) { + switch (value) { + case enums::SERIAL_PROXY_STATUS_OK: + return "SERIAL_PROXY_STATUS_OK"; + case enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS: + return "SERIAL_PROXY_STATUS_ASSUMED_SUCCESS"; + case enums::SERIAL_PROXY_STATUS_ERROR: + return "SERIAL_PROXY_STATUS_ERROR"; + case enums::SERIAL_PROXY_STATUS_TIMEOUT: + return "SERIAL_PROXY_STATUS_TIMEOUT"; + case enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED: + return "SERIAL_PROXY_STATUS_NOT_SUPPORTED"; + default: + return "UNKNOWN"; + } +} #endif const char *HelloRequest::dump_to(DumpBuffer &out) const { @@ -2609,6 +2625,14 @@ const char *SerialProxyRequest::dump_to(DumpBuffer &out) const { dump_field(out, "type", static_cast<enums::SerialProxyRequestType>(this->type)); return out.c_str(); } +const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyRequestResponse"); + dump_field(out, "instance", this->instance); + dump_field(out, "type", static_cast<enums::SerialProxyRequestType>(this->type)); + dump_field(out, "status", static_cast<enums::SerialProxyStatus>(this->status)); + dump_field(out, "error_message", this->error_message); + return out.c_str(); +} #endif #ifdef USE_BLUETOOTH_PROXY const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const { diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index a031d2d969..10fd88d8e1 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -233,6 +233,7 @@ class APIServerConnectionBase : public ProtoService { #ifdef USE_SERIAL_PROXY virtual void on_serial_proxy_request(const SerialProxyRequest &value){}; #endif + #ifdef USE_BLUETOOTH_PROXY virtual void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){}; #endif diff --git a/esphome/components/serial_proxy/__init__.py b/esphome/components/serial_proxy/__init__.py new file mode 100644 index 0000000000..f9b8c375d2 --- /dev/null +++ b/esphome/components/serial_proxy/__init__.py @@ -0,0 +1,104 @@ +""" +Serial Proxy component for ESPHome. + +WARNING: This component is EXPERIMENTAL. The API (both Python configuration +and C++ interfaces) may change at any time without following the normal +breaking changes policy. Use at your own risk. + +Once the API is considered stable, this warning will be removed. + +Provides a proxy to/from a serial interface on the ESPHome device, allowing +Home Assistant to connect to the serial port and send/receive data to/from +an arbitrary serial device. +""" + +from dataclasses import dataclass + +from esphome import pins +import esphome.codegen as cg +from esphome.components import uart +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_NAME +from esphome.core import CORE, coroutine_with_priority +from esphome.coroutine import CoroPriority + +CODEOWNERS = ["@kbx81"] +DEPENDENCIES = ["api", "uart"] + +MULTI_CONF = True + +serial_proxy_ns = cg.esphome_ns.namespace("serial_proxy") +SerialProxy = serial_proxy_ns.class_("SerialProxy", cg.Component, uart.UARTDevice) + +api_enums_ns = cg.esphome_ns.namespace("api").namespace("enums") +SerialProxyPortType = api_enums_ns.enum("SerialProxyPortType") +SERIAL_PROXY_PORT_TYPES = { + "TTL": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_TTL, + "RS232": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_RS232, + "RS485": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_RS485, +} + +CONF_DTR_PIN = "dtr_pin" +CONF_PORT_TYPE = "port_type" +CONF_RTS_PIN = "rts_pin" + +DOMAIN = "serial_proxy" + + +@dataclass +class SerialProxyData: + count: int = 0 + + +def _get_data() -> SerialProxyData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = SerialProxyData() + return CORE.data[DOMAIN] + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SerialProxy), + cv.Required(CONF_NAME): cv.string_strict, + cv.Required(CONF_PORT_TYPE): cv.enum(SERIAL_PROXY_PORT_TYPES, upper=True), + cv.Optional(CONF_RTS_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_DTR_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +@coroutine_with_priority(CoroPriority.FINAL) +async def _add_serial_proxy_count_define(): + """Emit the SERIAL_PROXY_COUNT define once with the final instance count.""" + count = _get_data().count + if count > 0: + cg.add_define("SERIAL_PROXY_COUNT", count) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + cg.add(cg.App.register_serial_proxy(var)) + cg.add(var.set_name(config[CONF_NAME])) + cg.add(var.set_port_type(config[CONF_PORT_TYPE])) + cg.add_define("USE_SERIAL_PROXY") + + # Track instance count for the FINAL priority define + data = _get_data() + if data.count == 0: + # Schedule the count define job only once (on the first instance) + CORE.add_job(_add_serial_proxy_count_define) + data.count += 1 + + if CONF_RTS_PIN in config: + rts_pin = await cg.gpio_pin_expression(config[CONF_RTS_PIN]) + cg.add(var.set_rts_pin(rts_pin)) + + if CONF_DTR_PIN in config: + dtr_pin = await cg.gpio_pin_expression(config[CONF_DTR_PIN]) + cg.add(var.set_dtr_pin(dtr_pin)) diff --git a/esphome/components/serial_proxy/serial_proxy.cpp b/esphome/components/serial_proxy/serial_proxy.cpp new file mode 100644 index 0000000000..340f9b0cb8 --- /dev/null +++ b/esphome/components/serial_proxy/serial_proxy.cpp @@ -0,0 +1,188 @@ +#include "serial_proxy.h" + +#ifdef USE_SERIAL_PROXY + +#include "esphome/core/log.h" +#include "esphome/core/util.h" + +#ifdef USE_API +#include "esphome/components/api/api_connection.h" +#include "esphome/components/api/api_server.h" +#endif + +namespace esphome::serial_proxy { + +static const char *const TAG = "serial_proxy"; + +void SerialProxy::setup() { + // Set up modem control pins if configured + if (this->rts_pin_ != nullptr) { + this->rts_pin_->setup(); + this->rts_pin_->digital_write(this->rts_state_); + } + if (this->dtr_pin_ != nullptr) { + this->dtr_pin_->setup(); + this->dtr_pin_->digital_write(this->dtr_state_); + } +#ifdef USE_API + // instance_index_ is fixed at registration time; pre-set it so loop() only needs to update data + this->outgoing_msg_.instance = this->instance_index_; +#endif +} + +void SerialProxy::loop() { +#ifdef USE_API + // Detect subscriber disconnect + if (this->api_connection_ != nullptr && (this->api_connection_->is_marked_for_removal() || + !this->api_connection_->is_connection_setup() || !api_is_connected())) { + ESP_LOGW(TAG, "Subscriber disconnected"); + this->api_connection_ = nullptr; + } + + if (this->api_connection_ == nullptr) + return; + + // Read available data from UART and forward to subscribed client + size_t available = this->available(); + if (available == 0) + return; + + // Read in chunks up to SERIAL_PROXY_MAX_READ_SIZE + uint8_t buffer[SERIAL_PROXY_MAX_READ_SIZE]; + size_t to_read = std::min(available, sizeof(buffer)); + + if (!this->read_array(buffer, to_read)) + return; + + this->outgoing_msg_.set_data(buffer, to_read); + this->api_connection_->send_serial_proxy_data(this->outgoing_msg_); +#endif +} + +void SerialProxy::dump_config() { + ESP_LOGCONFIG(TAG, + "Serial Proxy [%u]:\n" + " Name: %s\n" + " Port Type: %s\n" + " RTS Pin: %s\n" + " DTR Pin: %s", + this->instance_index_, this->name_ != nullptr ? this->name_ : "", + this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS485 ? "RS485" + : this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS232 ? "RS232" + : "TTL", + this->rts_pin_ != nullptr ? "configured" : "not configured", + this->dtr_pin_ != nullptr ? "configured" : "not configured"); +} + +void SerialProxy::configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint8_t stop_bits, + uint8_t data_size) { + ESP_LOGD(TAG, "Configuring serial proxy [%u]: baud=%u, flow_ctrl=%s, parity=%u, stop=%u, data=%u", + this->instance_index_, baudrate, YESNO(flow_control), parity, stop_bits, data_size); + + auto *uart_comp = this->parent_; + if (uart_comp == nullptr) { + ESP_LOGE(TAG, "UART component not available"); + return; + } + + // Validate all parameters before applying any (values come from a remote client) + if (baudrate == 0) { + ESP_LOGW(TAG, "Invalid baud rate: 0"); + return; + } + if (stop_bits < 1 || stop_bits > 2) { + ESP_LOGW(TAG, "Invalid stop bits: %u (must be 1 or 2)", stop_bits); + return; + } + if (data_size < 5 || data_size > 8) { + ESP_LOGW(TAG, "Invalid data bits: %u (must be 5-8)", data_size); + return; + } + if (parity > 2) { + ESP_LOGW(TAG, "Invalid parity: %u (must be 0-2)", parity); + return; + } + + // Apply validated parameters + uart_comp->set_baud_rate(baudrate); + uart_comp->set_stop_bits(stop_bits); + uart_comp->set_data_bits(data_size); + + // Map parity value to UARTParityOptions + static const uart::UARTParityOptions PARITY_MAP[] = { + uart::UART_CONFIG_PARITY_NONE, + uart::UART_CONFIG_PARITY_EVEN, + uart::UART_CONFIG_PARITY_ODD, + }; + uart_comp->set_parity(PARITY_MAP[parity]); + + // load_settings() is available on ESP8266 and ESP32 platforms +#if defined(USE_ESP8266) || defined(USE_ESP32) + uart_comp->load_settings(true); +#endif + + if (flow_control) { + ESP_LOGW(TAG, "Hardware flow control requested but is not yet supported"); + } +} + +void SerialProxy::write_from_client(const uint8_t *data, size_t len) { + if (data == nullptr || len == 0) + return; + this->write_array(data, len); +} + +void SerialProxy::set_modem_pins(uint32_t line_states) { + const bool rts = (line_states & SERIAL_PROXY_LINE_STATE_FLAG_RTS) != 0; + const bool dtr = (line_states & SERIAL_PROXY_LINE_STATE_FLAG_DTR) != 0; + ESP_LOGV(TAG, "Setting modem pins [%u]: RTS=%s, DTR=%s", this->instance_index_, ONOFF(rts), ONOFF(dtr)); + + if (this->rts_pin_ != nullptr) { + this->rts_state_ = rts; + this->rts_pin_->digital_write(rts); + } + if (this->dtr_pin_ != nullptr) { + this->dtr_state_ = dtr; + this->dtr_pin_->digital_write(dtr); + } +} + +uint32_t SerialProxy::get_modem_pins() const { + return (this->rts_state_ ? SERIAL_PROXY_LINE_STATE_FLAG_RTS : 0u) | + (this->dtr_state_ ? SERIAL_PROXY_LINE_STATE_FLAG_DTR : 0u); +} + +uart::FlushResult SerialProxy::flush_port() { + ESP_LOGV(TAG, "Flushing serial proxy [%u]", this->instance_index_); + return this->flush(); +} + +#ifdef USE_API +void SerialProxy::serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type) { + switch (type) { + case api::enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE: + if (this->api_connection_ != nullptr) { + ESP_LOGE(TAG, "Only one API subscription is allowed at a time"); + return; + } + this->api_connection_ = api_connection; + ESP_LOGV(TAG, "API connection subscribed to serial proxy [%u]", this->instance_index_); + break; + case api::enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: + if (this->api_connection_ != api_connection) { + ESP_LOGV(TAG, "API connection is not subscribed to serial proxy [%u]", this->instance_index_); + return; + } + this->api_connection_ = nullptr; + ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%u]", this->instance_index_); + break; + default: + ESP_LOGW(TAG, "Unknown serial proxy request type: %u", static_cast<uint32_t>(type)); + break; + } +} +#endif + +} // namespace esphome::serial_proxy + +#endif // USE_SERIAL_PROXY diff --git a/esphome/components/serial_proxy/serial_proxy.h b/esphome/components/serial_proxy/serial_proxy.h new file mode 100644 index 0000000000..52f0654ff0 --- /dev/null +++ b/esphome/components/serial_proxy/serial_proxy.h @@ -0,0 +1,129 @@ +#pragma once + +// WARNING: This component is EXPERIMENTAL. The API may change at any time +// without following the normal breaking changes policy. Use at your own risk. +// Once the API is considered stable, this warning will be removed. + +#include "esphome/core/defines.h" + +#ifdef USE_SERIAL_PROXY + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/uart/uart.h" + +// Include api_pb2.h only when the API is enabled. The full include is needed +// to hold SerialProxyDataReceived by value as a pre-allocated member. +// Guarding prevents pulling conflicting Zephyr logging macro names into +// translation units that include this header without USE_API defined. +#ifdef USE_API +#include "esphome/components/api/api_pb2.h" +#endif + +// Forward-declare types needed outside the USE_API guard. +namespace esphome::api { +class APIConnection; +namespace enums { +enum SerialProxyPortType : uint32_t; +enum SerialProxyRequestType : uint32_t; +} // namespace enums +} // namespace esphome::api + +namespace esphome::serial_proxy { + +/// Bit flags for the line_states field exchanged with API clients. +/// Bit positions are stable API — new signals must use the next available bit. +enum SerialProxyLineStateFlag : uint32_t { + SERIAL_PROXY_LINE_STATE_FLAG_RTS = 1 << 0, ///< RTS (Request To Send) + SERIAL_PROXY_LINE_STATE_FLAG_DTR = 1 << 1, ///< DTR (Data Terminal Ready) +}; + +/// Maximum bytes to read from UART in a single loop iteration +inline constexpr size_t SERIAL_PROXY_MAX_READ_SIZE = 256; + +class SerialProxy : public uart::UARTDevice, public Component { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } + + /// Get the instance index (position in Application's serial_proxies_ vector) + uint32_t get_instance_index() const { return this->instance_index_; } + + /// Set the instance index (called by Application::register_serial_proxy) + void set_instance_index(uint32_t index) { this->instance_index_ = index; } + + /// Set the human-readable port name (from YAML configuration) + void set_name(const char *name) { this->name_ = name; } + + /// Get the human-readable port name + const char *get_name() const { return this->name_; } + + /// Set the port type (from YAML configuration) + void set_port_type(api::enums::SerialProxyPortType port_type) { this->port_type_ = port_type; } + + /// Get the port type + api::enums::SerialProxyPortType get_port_type() const { return this->port_type_; } + + /// Configure UART parameters and apply them + /// @param baudrate Baud rate in bits per second + /// @param flow_control True to enable hardware flow control + /// @param parity Parity setting (0=none, 1=even, 2=odd) + /// @param stop_bits Number of stop bits (1 or 2) + /// @param data_size Number of data bits (5-8) + void configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint8_t stop_bits, uint8_t data_size); + + /// Handle a subscribe/unsubscribe request from an API client + void serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type); + + /// Write data received from an API client to the serial device + /// @param data Pointer to data buffer + /// @param len Number of bytes to write + void write_from_client(const uint8_t *data, size_t len); + + /// Set modem pin states from a bitmask of SerialProxyLineStateFlag values + void set_modem_pins(uint32_t line_states); + + /// Get current modem pin states as a bitmask of SerialProxyLineStateFlag values + uint32_t get_modem_pins() const; + + /// Flush the serial port (block until all TX data is sent) + uart::FlushResult flush_port(); + + /// Set the RTS GPIO pin (from YAML configuration) + void set_rts_pin(GPIOPin *pin) { this->rts_pin_ = pin; } + + /// Set the DTR GPIO pin (from YAML configuration) + void set_dtr_pin(GPIOPin *pin) { this->dtr_pin_ = pin; } + + protected: + /// Instance index for identifying this proxy in API messages + uint32_t instance_index_{0}; + + /// Subscribed API client (only one allowed at a time) + api::APIConnection *api_connection_{nullptr}; + +#ifdef USE_API + /// Pre-allocated outgoing message; instance field is set once in setup() + api::SerialProxyDataReceived outgoing_msg_; +#endif + + /// Human-readable port name (points to a string literal in flash) + const char *name_{nullptr}; + + /// Port type + api::enums::SerialProxyPortType port_type_{}; + + /// Optional GPIO pins for modem control + GPIOPin *rts_pin_{nullptr}; + GPIOPin *dtr_pin_{nullptr}; + + /// Current modem pin states + bool rts_state_{false}; + bool dtr_state_{false}; +}; + +} // namespace esphome::serial_proxy + +#endif // USE_SERIAL_PROXY diff --git a/esphome/core/application.h b/esphome/core/application.h index 87f9fdf59a..49253b6324 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -102,6 +102,9 @@ void socket_wake(); // NOLINT(readability-redundant-declaration) #ifdef USE_INFRARED #include "esphome/components/infrared/infrared.h" #endif +#ifdef USE_SERIAL_PROXY +#include "esphome/components/serial_proxy/serial_proxy.h" +#endif #ifdef USE_EVENT #include "esphome/components/event/event.h" #endif @@ -267,6 +270,13 @@ class Application { void register_infrared(infrared::Infrared *infrared) { this->infrareds_.push_back(infrared); } #endif +#ifdef USE_SERIAL_PROXY + void register_serial_proxy(serial_proxy::SerialProxy *proxy) { + proxy->set_instance_index(this->serial_proxies_.size()); + this->serial_proxies_.push_back(proxy); + } +#endif + #ifdef USE_EVENT void register_event(event::Event *event) { this->events_.push_back(event); } #endif @@ -498,6 +508,10 @@ class Application { GET_ENTITY_METHOD(infrared::Infrared, infrared, infrareds) #endif +#ifdef USE_SERIAL_PROXY + auto &get_serial_proxies() const { return this->serial_proxies_; } +#endif + #ifdef USE_EVENT auto &get_events() const { return this->events_; } GET_ENTITY_METHOD(event::Event, event, events) @@ -747,6 +761,9 @@ class Application { #ifdef USE_INFRARED StaticVector<infrared::Infrared *, ESPHOME_ENTITY_INFRARED_COUNT> infrareds_{}; #endif +#ifdef USE_SERIAL_PROXY + StaticVector<serial_proxy::SerialProxy *, SERIAL_PROXY_COUNT> serial_proxies_{}; +#endif #ifdef USE_UPDATE StaticVector<update::UpdateEntity *, ESPHOME_ENTITY_UPDATE_COUNT> updates_{}; #endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 48c467f69f..51f474d80e 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -107,6 +107,7 @@ #define MDNS_SERVICE_COUNT 3 #define USE_MDNS_DYNAMIC_TXT #define MDNS_DYNAMIC_TXT_COUNT 2 +#define SERIAL_PROXY_COUNT 2 #define SNTP_SERVER_COUNT 3 #define USE_MEDIA_PLAYER #define USE_MEDIA_SOURCE @@ -119,6 +120,7 @@ #define USE_SELECT #define USE_SENSOR #define USE_SENSOR_FILTER +#define USE_SERIAL_PROXY #define USE_SETUP_PRIORITY_OVERRIDE #define USE_STATUS_LED #define USE_STATUS_SENSOR diff --git a/tests/components/serial_proxy/common.yaml b/tests/components/serial_proxy/common.yaml new file mode 100644 index 0000000000..6f03cf95df --- /dev/null +++ b/tests/components/serial_proxy/common.yaml @@ -0,0 +1,10 @@ +wifi: + ssid: MySSID + password: password1 + +api: + +serial_proxy: + - id: serial_proxy_1 + name: Test Serial Port + port_type: RS232 diff --git a/tests/components/serial_proxy/test.esp32-idf.yaml b/tests/components/serial_proxy/test.esp32-idf.yaml new file mode 100644 index 0000000000..b415125e84 --- /dev/null +++ b/tests/components/serial_proxy/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/serial_proxy/test.esp8266-ard.yaml b/tests/components/serial_proxy/test.esp8266-ard.yaml new file mode 100644 index 0000000000..96ab4ef6ac --- /dev/null +++ b/tests/components/serial_proxy/test.esp8266-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/serial_proxy/test.rp2040-ard.yaml b/tests/components/serial_proxy/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b28f2b5e05 --- /dev/null +++ b/tests/components/serial_proxy/test.rp2040-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + +<<: !include common.yaml From a9b5f95c768c58aeb4e117a94effe0121fdb77c2 Mon Sep 17 00:00:00 2001 From: Oliver Kleinecke <okleinecke@web.de> Date: Sun, 8 Mar 2026 11:24:39 +0100 Subject: [PATCH 458/609] [usb_uart] ch34x chip-type & port-count enumeration (#14544) --- esphome/components/usb_uart/ch34x.cpp | 94 ++++++++++++++++++++++++-- esphome/components/usb_uart/usb_uart.h | 37 ++++++++++ 2 files changed, 126 insertions(+), 5 deletions(-) diff --git a/esphome/components/usb_uart/ch34x.cpp b/esphome/components/usb_uart/ch34x.cpp index e6e52a9e2a..d5428cc8d7 100644 --- a/esphome/components/usb_uart/ch34x.cpp +++ b/esphome/components/usb_uart/ch34x.cpp @@ -8,13 +8,97 @@ namespace esphome::usb_uart { using namespace bytebuffer; -/** - * CH34x - */ + +struct CH34xEntry { + uint16_t pid; + uint8_t byte_idx; // which status.data[] byte to inspect + uint8_t mask; // bitmask applied before comparison + uint8_t match; // 0xFF = wildcard (default/fallthrough for this PID) + CH34xChipType chiptype; + const char *name; + uint8_t num_ports; +}; + +static const CH34xEntry CH34X_TABLE[] = { + {0x55D2, 1, 0xFF, 0x41, CHIP_CH342K, "CH342K", 2}, + {0x55D2, 1, 0xFF, 0xFF, CHIP_CH342F, "CH342F", 2}, + {0x55D3, 1, 0xFF, 0x02, CHIP_CH343J, "CH343J", 1}, + {0x55D3, 1, 0xFF, 0x01, CHIP_CH343K, "CH343K", 1}, + {0x55D3, 1, 0xFF, 0x18, CHIP_CH343G_AUTOBAUD, "CH343G_AUTOBAUD", 1}, + {0x55D3, 1, 0xFF, 0xFF, CHIP_CH343GP, "CH343GP", 1}, + {0x55D4, 1, 0xFF, 0x09, CHIP_CH9102X, "CH9102X", 1}, + {0x55D4, 1, 0xFF, 0xFF, CHIP_CH9102F, "CH9102F", 1}, + {0x55D5, 1, 0xFF, 0xC0, CHIP_CH344L, "CH344L", 4}, // CH344L vs CH344L_V2 resolved below + {0x55D5, 1, 0xFF, 0xFF, CHIP_CH344Q, "CH344Q", 4}, + {0x55D7, 1, 0xFF, 0xFF, CHIP_CH9103M, "CH9103M", 2}, + {0x55D8, 1, 0xFF, 0x0A, CHIP_CH9101RY, "CH9101RY", 1}, + {0x55D8, 1, 0xFF, 0xFF, CHIP_CH9101UH, "CH9101UH", 1}, + {0x55DB, 1, 0xFF, 0xFF, CHIP_CH347TF, "CH347TF", 1}, + {0x55DD, 1, 0xFF, 0xFF, CHIP_CH347TF, "CH347TF", 1}, + {0x55DA, 1, 0xFF, 0xFF, CHIP_CH347TF, "CH347TF", 2}, + {0x55DE, 1, 0xFF, 0xFF, CHIP_CH347TF, "CH347TF", 2}, + {0x55E7, 1, 0xFF, 0xFF, CHIP_CH339W, "CH339W", 1}, + {0x55DF, 1, 0xFF, 0xFF, CHIP_CH9104L, "CH9104L", 4}, + {0x55E9, 1, 0xFF, 0xFF, CHIP_CH9111L_M0, "CH9111L_M0", 1}, + {0x55EA, 1, 0xFF, 0xFF, CHIP_CH9111L_M1, "CH9111L_M1", 1}, + {0x55E8, 2, 0xFF, 0x48, CHIP_CH9114L, "CH9114L", 4}, + {0x55E8, 2, 0xFF, 0x49, CHIP_CH9114W, "CH9114W", 4}, + {0x55E8, 2, 0xFF, 0x4A, CHIP_CH9114F, "CH9114F", 4}, + {0x55EB, 4, 0x01, 0x01, CHIP_CH346C_M1, "CH346C_M1", 1}, + {0x55EB, 4, 0x01, 0xFF, CHIP_CH346C_M0, "CH346C_M0", 1}, + {0x55EC, 1, 0xFF, 0xFF, CHIP_CH346C_M2, "CH346C_M2", 2}, +}; void USBUartTypeCH34X::enable_channels() { - // enable the channels - for (auto channel : this->channels_) { + usb_host::transfer_cb_t cb = [this](const usb_host::TransferStatus &status) { + if (!status.success) { + this->defer([this, error_code = status.error_code]() { + ESP_LOGE(TAG, "CH34x chip detection failed: %s", esp_err_to_name(error_code)); + this->apply_line_settings_(); + }); + return; + } + CH34xChipType chiptype = CHIP_UNKNOWN; + uint8_t num_ports = 1; + for (const auto &e : CH34X_TABLE) { + if (e.pid != this->pid_) + continue; + if (e.match != 0xFF && (status.data[e.byte_idx] & e.mask) != e.match) + continue; + chiptype = e.chiptype; + num_ports = e.num_ports; + break; + } + // CH344L vs CH344L_V2 requires chipver (data[0]) in addition to chiptype (data[1]) + if (chiptype == CHIP_CH344L && (status.data[0] & 0xF0) != 0x40) + chiptype = CHIP_CH344L_V2; + const char *name = "unknown"; + for (const auto &e : CH34X_TABLE) { + if (e.chiptype == chiptype) { + name = e.name; + break; + } + } + this->defer([this, chiptype, num_ports, name]() { + this->chiptype_ = chiptype; + this->chip_name_ = name; + this->num_ports_ = num_ports; + ESP_LOGD(TAG, "CH34x chip: %s, ports: %u", name, this->num_ports_); + this->apply_line_settings_(); + }); + }; + // Vendor-specific GET_CHIP_VERSION request (bRequest=0x5F): returns chip ID bytes + // used to distinguish CH34x variants sharing the same PID. + this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_IN, 0x5F, 0, 0, cb, {0, 0, 0, 0, 0, 0, 0, 0}); +} + +void USBUartTypeCH34X::dump_config() { + USBUartTypeCdcAcm::dump_config(); + ESP_LOGCONFIG(TAG, " CH34x chip: %s", this->chip_name_); +} + +void USBUartTypeCH34X::apply_line_settings_() { + for (auto *channel : this->channels_) { if (!channel->initialised_.load()) continue; usb_host::transfer_cb_t callback = [=](const usb_host::TransferStatus &status) { diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index b1748aebf2..16469df7f6 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -35,6 +35,36 @@ struct CdcEps { uint8_t interrupt_interface_number; }; +enum CH34xChipType : uint8_t { + CHIP_CH342F = 0, + CHIP_CH342K, + CHIP_CH343GP, + CHIP_CH343G_AUTOBAUD, + CHIP_CH343K, + CHIP_CH343J, + CHIP_CH344L, + CHIP_CH344L_V2, + CHIP_CH344Q, + CHIP_CH347TF, + CHIP_CH9101UH, + CHIP_CH9101RY, + CHIP_CH9102F, + CHIP_CH9102X, + CHIP_CH9103M, + CHIP_CH9104L, + CHIP_CH340B, + CHIP_CH339W, + CHIP_CH9111L_M0, + CHIP_CH9111L_M1, + CHIP_CH9114L, + CHIP_CH9114W, + CHIP_CH9114F, + CHIP_CH346C_M0, + CHIP_CH346C_M1, + CHIP_CH346C_M2, + CHIP_UNKNOWN = 0xFF, +}; + enum UARTParityOptions { UART_CONFIG_PARITY_NONE = 0, UART_CONFIG_PARITY_ODD, @@ -192,10 +222,17 @@ class USBUartTypeCP210X : public USBUartTypeCdcAcm { class USBUartTypeCH34X : public USBUartTypeCdcAcm { public: USBUartTypeCH34X(uint16_t vid, uint16_t pid) : USBUartTypeCdcAcm(vid, pid) {} + void dump_config() override; protected: void enable_channels() override; std::vector<CdcEps> parse_descriptors(usb_device_handle_t dev_hdl) override; + + private: + void apply_line_settings_(); + CH34xChipType chiptype_{CHIP_UNKNOWN}; + const char *chip_name_{"unknown"}; + uint8_t num_ports_{1}; }; } // namespace esphome::usb_uart From 3f143d9f19ae29a408f1024e202aa869e86a7787 Mon Sep 17 00:00:00 2001 From: Diorcet Yann <diorcety@users.noreply.github.com> Date: Sun, 8 Mar 2026 14:50:32 +0100 Subject: [PATCH 459/609] [ethernet] Fix commit 3f700bac1cebf7eb6ff3b20a87d8c8af5cb9fc41 (#14618) --- esphome/components/ethernet/esp_eth_phy_jl1101.c | 2 ++ esphome/components/ethernet/ethernet_component.h | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/ethernet/esp_eth_phy_jl1101.c b/esphome/components/ethernet/esp_eth_phy_jl1101.c index a19f7aa6b0..b81d8227d4 100644 --- a/esphome/components/ethernet/esp_eth_phy_jl1101.c +++ b/esphome/components/ethernet/esp_eth_phy_jl1101.c @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include "esphome/core/defines.h" + #ifdef USE_ESP32 #include <string.h> diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index e54e1543e3..d9f05be9de 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -214,7 +214,7 @@ class EthernetComponent : public Component { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern EthernetComponent *global_eth_component; -#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) +#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config); #endif From 1b3a7f0b6a8d927d50348b59979cae2aaa437f99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:18:14 +0000 Subject: [PATCH 460/609] Bump aioesphomeapi from 44.5.0 to 44.5.1 (#14624) 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 03a7cac5c7..3da2d52b44 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.5.0 +aioesphomeapi==44.5.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 9be1876fae30e97e763480eb4e73ecaf3a4fa445 Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Sun, 8 Mar 2026 21:52:16 +0100 Subject: [PATCH 461/609] [ble_nus] make ble_nus timeout shorter than watchdog (#14619) Co-authored-by: J. Nick Koston <nick+github@koston.org> --- esphome/components/ble_nus/ble_nus.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index e38dc99802..d0d37dbf1c 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -71,7 +71,10 @@ bool BLENUS::read_array(uint8_t *data, size_t len) { this->has_peek_ = false; data++; if (--len == 0) { // Decrement len first, then check it... - return true; // No more to read +#ifdef USE_UART_DEBUGGER + this->debug_callback_.call(uart::UART_DIRECTION_RX, this->peek_buffer_); +#endif + return true; // No more to read } } @@ -101,10 +104,10 @@ size_t BLENUS::available() { } uart::FlushResult BLENUS::flush() { - constexpr uint32_t timeout_5sec = 5000; + constexpr uint32_t timeout_500ms = 500; uint32_t start = millis(); while (atomic_get(&this->tx_status_) != TX_DISABLED && !ring_buf_is_empty(&global_ble_tx_ring_buf)) { - if (millis() - start > timeout_5sec) { + if (millis() - start > timeout_500ms) { ESP_LOGW(TAG, "Flush timeout"); return uart::FlushResult::TIMEOUT; } From ad5811280aaa9bce7ecfa49ea4c280522d5ac2c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 10:59:43 -1000 Subject: [PATCH 462/609] =?UTF-8?q?[ci]=20Add=20medium-pr=20label=20for=20?= =?UTF-8?q?PRs=20with=20=E2=89=A4100=20lines=20changed=20(#14628)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/scripts/auto-label-pr/constants.js | 1 + .github/scripts/auto-label-pr/detectors.js | 7 ++++++- .github/scripts/auto-label-pr/index.js | 3 ++- .github/workflows/auto-label-pr.yml | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/scripts/auto-label-pr/constants.js b/.github/scripts/auto-label-pr/constants.js index 8c3a62cf19..1c33772c4c 100644 --- a/.github/scripts/auto-label-pr/constants.js +++ b/.github/scripts/auto-label-pr/constants.js @@ -14,6 +14,7 @@ module.exports = { 'chained-pr', 'core', 'small-pr', + 'medium-pr', 'dashboard', 'github-actions', 'by-code-owner', diff --git a/.github/scripts/auto-label-pr/detectors.js b/.github/scripts/auto-label-pr/detectors.js index 832fcb41db..fc63198019 100644 --- a/.github/scripts/auto-label-pr/detectors.js +++ b/.github/scripts/auto-label-pr/detectors.js @@ -103,7 +103,7 @@ async function detectCoreChanges(changedFiles) { } // Strategy: PR size detection -async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD) { +async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD) { const labels = new Set(); if (totalChanges <= SMALL_PR_THRESHOLD) { @@ -111,6 +111,11 @@ async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChange return labels; } + if (totalChanges <= MEDIUM_PR_THRESHOLD) { + labels.add('medium-pr'); + return labels; + } + const testAdditions = prFiles .filter(file => file.filename.startsWith('tests/')) .reduce((sum, file) => sum + (file.additions || 0), 0); diff --git a/.github/scripts/auto-label-pr/index.js b/.github/scripts/auto-label-pr/index.js index 483d2cb626..42588c0bc8 100644 --- a/.github/scripts/auto-label-pr/index.js +++ b/.github/scripts/auto-label-pr/index.js @@ -35,6 +35,7 @@ async function fetchApiData() { module.exports = async ({ github, context }) => { // Environment variables const SMALL_PR_THRESHOLD = parseInt(process.env.SMALL_PR_THRESHOLD); + const MEDIUM_PR_THRESHOLD = parseInt(process.env.MEDIUM_PR_THRESHOLD); const MAX_LABELS = parseInt(process.env.MAX_LABELS); const TOO_BIG_THRESHOLD = parseInt(process.env.TOO_BIG_THRESHOLD); const COMPONENT_LABEL_THRESHOLD = parseInt(process.env.COMPONENT_LABEL_THRESHOLD); @@ -120,7 +121,7 @@ module.exports = async ({ github, context }) => { detectNewComponents(prFiles), detectNewPlatforms(prFiles, apiData), detectCoreChanges(changedFiles), - detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD), + detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD), detectDashboardChanges(changedFiles), detectGitHubActionsChanges(changedFiles), detectCodeOwner(github, context, changedFiles), diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 6fcb50b70a..6376cf877e 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -12,6 +12,7 @@ permissions: env: SMALL_PR_THRESHOLD: 30 + MEDIUM_PR_THRESHOLD: 100 MAX_LABELS: 15 TOO_BIG_THRESHOLD: 1000 COMPONENT_LABEL_THRESHOLD: 10 From 50b3f9d25cb91b31fb0adc6bad090f2f5bb3d778 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Sun, 8 Mar 2026 17:09:06 -0500 Subject: [PATCH 463/609] [mixer_speaker] Add task debounce (#14581) --- .../mixer/speaker/mixer_speaker.cpp | 25 ++++++++++++++----- .../components/mixer/speaker/mixer_speaker.h | 7 +++--- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index 8e1278206f..9d11abb327 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -11,14 +11,14 @@ #include <array> #include <cstring> -namespace esphome { -namespace mixer_speaker { +namespace esphome::mixer_speaker { static const UBaseType_t MIXER_TASK_PRIORITY = 10; static const uint32_t STOPPING_TIMEOUT_MS = 5000; static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50; static const uint32_t TASK_DELAY_MS = 25; +static const uint32_t MIXER_AUTO_STOP_DEBOUNCE_MS = 200; static const size_t TASK_STACK_SIZE = 4096; @@ -471,6 +471,7 @@ void MixerSpeaker::loop() { this->task_.deallocate(); ESP_LOGD(TAG, "Stopped"); xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS); + this->all_stopped_since_ms_ = 0; } if (this->task_.is_created()) { @@ -483,8 +484,18 @@ void MixerSpeaker::loop() { } if (all_stopped) { - // Send stop command signal to the mixer task since no source speakers are active - xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_STOP); + if (this->all_stopped_since_ms_ == 0) { + this->all_stopped_since_ms_ = millis(); + } else if ((millis() - this->all_stopped_since_ms_) >= MIXER_AUTO_STOP_DEBOUNCE_MS) { + // Send stop command only after a short debounce to avoid stop/start thrash during rapid seeks. + xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_STOP); + } + } else { + this->all_stopped_since_ms_ = 0; + // New activity detected; clear any stale auto-stop request before it can stop the running task. + if (event_group_bits & MIXER_TASK_COMMAND_STOP) { + xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_STOP); + } } } else { // Task is fully stopped and cleaned up, check if we can disable loop @@ -515,6 +526,9 @@ esp_err_t MixerSpeaker::start(audio::AudioStreamInfo &stream_info) { this->enable_loop_soon_any_context(); // ensure loop processes command + // Starting a new stream supersedes any previously queued stop request. + xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_STOP); + uint32_t event_bits = xEventGroupGetBits(this->event_group_); if (!(event_bits & MIXER_TASK_COMMAND_START)) { // Set MIXER_TASK_COMMAND_START bit if not already set, and then immediately wake for low latency @@ -755,7 +769,6 @@ void MixerSpeaker::audio_mixer_task(void *params) { vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it } -} // namespace mixer_speaker -} // namespace esphome +} // namespace esphome::mixer_speaker #endif diff --git a/esphome/components/mixer/speaker/mixer_speaker.h b/esphome/components/mixer/speaker/mixer_speaker.h index 0e0b33c39b..29876ea262 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.h +++ b/esphome/components/mixer/speaker/mixer_speaker.h @@ -14,8 +14,7 @@ #include <atomic> -namespace esphome { -namespace mixer_speaker { +namespace esphome::mixer_speaker { /* Classes for mixing several source speaker audio streams and writing it to another speaker component. * - Volume controls are passed through to the output speaker @@ -200,9 +199,9 @@ class MixerSpeaker : public Component { optional<audio::AudioStreamInfo> audio_stream_info_; std::atomic<uint32_t> frames_in_pipeline_{0}; // Frames written to output but not yet played + uint32_t all_stopped_since_ms_{0}; // Debounce transient all-stopped windows before stopping task }; -} // namespace mixer_speaker -} // namespace esphome +} // namespace esphome::mixer_speaker #endif From d5dc4a39cb6f10fef73d4950fd21db3beca0f83a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:10:43 -0400 Subject: [PATCH 464/609] [i2s_audio] Fix mono sample swap and block 8-bit mono on ESP32 (#14516) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@koston.org> --- .../components/i2s_audio/microphone/__init__.py | 6 ++++++ .../microphone/i2s_audio_microphone.cpp | 17 +++++++++-------- .../components/i2s_audio/speaker/__init__.py | 13 +++++++++---- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 16 ++++++++-------- .../speaker/media_player/audio_pipeline.cpp | 2 +- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index dd23673db5..bf583b9f81 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -46,6 +46,12 @@ def _validate_esp32_variant(config): if config[CONF_ADC_TYPE] == "external": if config[CONF_PDM] and variant not in PDM_VARIANTS: raise cv.Invalid(f"{variant} does not support PDM") + if ( + variant == esp32.VARIANT_ESP32 + and config.get(CONF_BITS_PER_SAMPLE) == 8 + and config.get(CONF_CHANNEL) in (CONF_LEFT, CONF_RIGHT) + ): + raise cv.Invalid("8-bit mono mode is not supported on ESP32") return config if config[CONF_ADC_TYPE] == "internal": if variant not in INTERNAL_ADC_VARIANTS: diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index cdebc214e2..eb4506071e 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -281,7 +281,7 @@ bool I2SAudioMicrophone::start_driver_() { } /* Before reading data, start the RX channel first */ - i2s_channel_enable(this->rx_handle_); + err = i2s_channel_enable(this->rx_handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "Enabling failed: %s", esp_err_to_name(err)); return false; @@ -454,13 +454,14 @@ size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_w } this->status_clear_warning(); #if defined(USE_ESP32_VARIANT_ESP32) and not defined(USE_I2S_LEGACY) - // For ESP32 8/16 bit standard mono mode samples need to be switched. - if (this->slot_mode_ == I2S_SLOT_MODE_MONO && this->slot_bit_width_ <= 16 && !this->pdm_) { - size_t samples_read = bytes_read / sizeof(int16_t); - for (int i = 0; i < samples_read; i += 2) { - int16_t tmp = buf[i]; - buf[i] = buf[i + 1]; - buf[i + 1] = tmp; + // For ESP32 16-bit standard mono mode, adjacent samples need to be swapped. + if (this->slot_mode_ == I2S_SLOT_MODE_MONO && this->slot_bit_width_ == I2S_SLOT_BIT_WIDTH_16BIT && !this->pdm_) { + int16_t *samples = reinterpret_cast<int16_t *>(buf); + size_t sample_count = bytes_read / sizeof(int16_t); + for (size_t i = 0; i + 1 < sample_count; i += 2) { + int16_t tmp = samples[i]; + samples[i] = samples[i + 1]; + samples[i + 1] = tmp; } } #endif diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py index 2e009a1de1..b84cf7de3b 100644 --- a/esphome/components/i2s_audio/speaker/__init__.py +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -100,11 +100,16 @@ def _set_stream_limits(config): def _validate_esp32_variant(config): - if config[CONF_DAC_TYPE] != "internal": - return config variant = esp32.get_esp32_variant() - if variant not in INTERNAL_DAC_VARIANTS: - raise cv.Invalid(f"{variant} does not have an internal DAC") + if config[CONF_DAC_TYPE] == "internal": + if variant not in INTERNAL_DAC_VARIANTS: + raise cv.Invalid(f"{variant} does not have an internal DAC") + elif ( + variant == esp32.VARIANT_ESP32 + and config.get(CONF_BITS_PER_SAMPLE) == 8 + and config.get(CONF_CHANNEL) in (CONF_MONO, CONF_LEFT, CONF_RIGHT) + ): + raise cv.Invalid("8-bit mono mode is not supported on ESP32") return config diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index c934d12d65..a996702f8b 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -372,15 +372,15 @@ void I2SAudioSpeaker::speaker_task(void *params) { } #ifdef USE_ESP32_VARIANT_ESP32 - // For ESP32 8/16 bit mono mode samples need to be switched. + // For ESP32 16-bit mono mode, adjacent samples need to be swapped. if (this_speaker->current_stream_info_.get_channels() == 1 && - this_speaker->current_stream_info_.get_bits_per_sample() <= 16) { - size_t len = bytes_read / sizeof(int16_t); - int16_t *tmp_buf = (int16_t *) new_data; - for (size_t i = 0; i < len; i += 2) { - int16_t tmp = tmp_buf[i]; - tmp_buf[i] = tmp_buf[i + 1]; - tmp_buf[i + 1] = tmp; + this_speaker->current_stream_info_.get_bits_per_sample() == 16) { + int16_t *samples = reinterpret_cast<int16_t *>(new_data); + size_t sample_count = bytes_read / sizeof(int16_t); + for (size_t i = 0; i + 1 < sample_count; i += 2) { + int16_t tmp = samples[i]; + samples[i] = samples[i + 1]; + samples[i + 1] = tmp; } } #endif diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index 8cea3abcfc..0822d80254 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -504,7 +504,7 @@ void AudioPipeline::decode_task(void *params) { if (!started_playback && has_stream_info) { // Verify enough data is available before starting playback std::shared_ptr<RingBuffer> temp_ring_buffer = this_pipeline->raw_file_ring_buffer_.lock(); - if (temp_ring_buffer->available() >= initial_bytes_to_buffer) { + if (temp_ring_buffer != nullptr && temp_ring_buffer->available() >= initial_bytes_to_buffer) { started_playback = true; } } From e7730cff0024a9d48680ef6f2a2771f7461b6705 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 13:59:40 -1000 Subject: [PATCH 465/609] [esp32_ble] Optimize BLE event hot path performance (#14627) --- esphome/components/esp32_ble/ble_event.h | 18 ++++++------------ esphome/core/lock_free_queue.h | 10 +++++++++- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index 299fd7705f..ba87fd8805 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -155,44 +155,38 @@ class BLEEvent { void release() { switch (this->type_) { case GAP: - // GAP events don't have heap allocations + // GAP events never have heap allocations break; case GATTC: - // Param is now stored inline, only delete heap data if it was heap-allocated if (!this->event_.gattc.is_inline && this->event_.gattc.data.heap_data != nullptr) { delete[] this->event_.gattc.data.heap_data; + this->event_.gattc.data.heap_data = nullptr; } - // Clear critical fields to prevent issues if type changes - this->event_.gattc.is_inline = false; - this->event_.gattc.data.heap_data = nullptr; break; case GATTS: - // Param is now stored inline, only delete heap data if it was heap-allocated if (!this->event_.gatts.is_inline && this->event_.gatts.data.heap_data != nullptr) { delete[] this->event_.gatts.data.heap_data; + this->event_.gatts.data.heap_data = nullptr; } - // Clear critical fields to prevent issues if type changes - this->event_.gatts.is_inline = false; - this->event_.gatts.data.heap_data = nullptr; break; } } // Load new event data for reuse (replaces previous event data) + // Note: release() is NOT called here because EventPool::release() already + // calls event->release() before returning to the free list. Every event + // from allocate() is already in a clean state. void load_gap_event(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { - this->release(); this->type_ = GAP; this->init_gap_data_(e, p); } void load_gattc_event(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { - this->release(); this->type_ = GATTC; this->init_gattc_data_(e, i, p); } void load_gatts_event(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { - this->release(); this->type_ = GATTS; this->init_gatts_data_(e, i, p); } diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h index 522fbd36e1..316186ea54 100644 --- a/esphome/core/lock_free_queue.h +++ b/esphome/core/lock_free_queue.h @@ -104,7 +104,15 @@ template<class T, uint8_t SIZE> class LockFreeQueue { } } - uint16_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); } + uint16_t get_and_reset_dropped_count() { + // Fast path: relaxed load is a single instruction on all platforms. + // The atomic exchange (especially for uint16_t on Xtensa) compiles to + // an expensive sub-word CAS retry loop (~25 instructions + memory barriers). + // Since drops are rare, avoid the exchange in the common case. + if (dropped_count_.load(std::memory_order_relaxed) == 0) + return 0; + return dropped_count_.exchange(0, std::memory_order_relaxed); + } void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); } From 76c567a71cec76ca820ee0b23107b4258276b72e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 14:00:04 -1000 Subject: [PATCH 466/609] [scheduler] Use std::atomic<uint8_t> instead of std::atomic<bool> for remove flag (#14626) --- esphome/core/scheduler.h | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index cefbdd1b22..eb6cea4f37 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -178,9 +178,11 @@ class Scheduler { uint16_t next_execution_high_; // Upper 16 bits (millis_major counter) #ifdef ESPHOME_THREAD_MULTI_ATOMICS - // Multi-threaded with atomics: use atomic for lock-free access - // Place atomic<bool> separately since it can't be packed with bit fields - std::atomic<bool> remove{false}; + // Multi-threaded with atomics: use atomic uint8_t for lock-free access. + // std::atomic<bool> is not used because GCC on Xtensa generates an indirect + // function call for std::atomic<bool>::load() instead of inlining it. + // std::atomic<uint8_t> inlines correctly on all platforms. + std::atomic<uint8_t> remove{0}; // Bit-packed fields (4 bits used, 4 bits padding in 1 byte) enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; @@ -204,7 +206,7 @@ class Scheduler { next_execution_low_(0), next_execution_high_(0), #ifdef ESPHOME_THREAD_MULTI_ATOMICS - // remove is initialized in the member declaration as std::atomic<bool>{false} + // remove is initialized in the member declaration type(TIMEOUT), name_type_(NameType::STATIC_STRING), is_retry(false) { @@ -508,7 +510,7 @@ class Scheduler { // Multi-threaded with atomics: use atomic store with appropriate ordering // Release ordering when setting to true ensures cancellation is visible to other threads // Relaxed ordering when setting to false is sufficient for initialization - item->remove.store(removed, removed ? std::memory_order_release : std::memory_order_relaxed); + item->remove.store(removed ? 1 : 0, removed ? std::memory_order_release : std::memory_order_relaxed); #else // Single-threaded (ESPHOME_THREAD_SINGLE) or // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write From 771404668d04418b377c660398f6ae1812d8bdd5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 14:01:01 -1000 Subject: [PATCH 467/609] [api] Inline fast path of try_to_clear_buffer (#14630) --- esphome/components/api/api_connection.cpp | 6 +----- esphome/components/api/api_connection.h | 10 +++++++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 43f5070a40..28a770a4fb 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1945,11 +1945,7 @@ void APIConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSet #ifdef USE_API_HOMEASSISTANT_STATES void APIConnection::on_subscribe_home_assistant_states_request() { state_subs_at_ = 0; } #endif -bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { - if (this->flags_.remove) - return false; - if (this->helper_->can_write_without_blocking()) - return true; +bool APIConnection::try_to_clear_buffer_slow_(bool log_out_of_space) { delay(0); APIError err = this->helper_->loop(); if (err != APIError::OK) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 5d1469e419..ccb51186d6 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -305,7 +305,13 @@ class APIConnection final : public APIServerConnectionBase { this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size); } - bool try_to_clear_buffer(bool log_out_of_space); + bool try_to_clear_buffer(bool log_out_of_space) { + if (this->flags_.remove) + return false; + if (this->helper_->can_write_without_blocking()) + return true; + return this->try_to_clear_buffer_slow_(log_out_of_space); + } bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; const char *get_name() const { return this->helper_->get_client_name(); } @@ -315,6 +321,8 @@ class APIConnection final : public APIServerConnectionBase { } protected: + bool try_to_clear_buffer_slow_(bool log_out_of_space); + // Helper function to handle authentication completion void complete_authentication_(); From 66a5ad0d75cf90ebc22ca85d139a698090be30ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 14:06:55 -1000 Subject: [PATCH 468/609] [core] Skip zero-initialization of StaticVector data array (#14592) --- esphome/core/helpers.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 6ce5de4975..11e0afe526 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -215,7 +215,7 @@ template<typename T, size_t N> class StaticVector { using const_reverse_iterator = std::reverse_iterator<const_iterator>; private: - std::array<T, N> data_{}; + std::array<T, N> data_; // intentionally not value-initialized to avoid memset size_t count_{0}; public: From 93d7ec4d72dc77df92b01b3ca84cb29264e46ae5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 14:07:59 -1000 Subject: [PATCH 469/609] [esp32_ble] Inline ble_addr_to_uint64 to eliminate call overhead (#14591) --- esphome/components/esp32_ble/ble.cpp | 11 ----------- esphome/components/esp32_ble/ble.h | 11 ++++++++++- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 7fa5370072..ff9d9bb15a 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -727,17 +727,6 @@ void ESP32BLE::dump_config() { } } -uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { - uint64_t u = 0; - u |= uint64_t(address[0] & 0xFF) << 40; - u |= uint64_t(address[1] & 0xFF) << 32; - u |= uint64_t(address[2] & 0xFF) << 24; - u |= uint64_t(address[3] & 0xFF) << 16; - u |= uint64_t(address[4] & 0xFF) << 8; - u |= uint64_t(address[5] & 0xFF) << 0; - return u; -} - ESP32BLE *global_ble = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esphome::esp32_ble diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 2ce17e97be..04bec3f785 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -35,7 +35,16 @@ static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 100; // 64 + 36 (ring buffer size static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 88; // 64 + 24 (ring buffer size without PSRAM) #endif -uint64_t ble_addr_to_uint64(const esp_bd_addr_t address); +inline uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { + uint64_t u = 0; + u |= uint64_t(address[0] & 0xFF) << 40; + u |= uint64_t(address[1] & 0xFF) << 32; + u |= uint64_t(address[2] & 0xFF) << 24; + u |= uint64_t(address[3] & 0xFF) << 16; + u |= uint64_t(address[4] & 0xFF) << 8; + u |= uint64_t(address[5] & 0xFF) << 0; + return u; +} // NOLINTNEXTLINE(modernize-use-using) typedef struct { From 88536ff72bce462b8290d48ca7bc2f3f40a5e9b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 14:31:42 -1000 Subject: [PATCH 470/609] [modbus] Fix timeout for non-hardware UARTs (e.g., USB UART) (#14614) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/modbus/modbus.cpp | 18 ++++- esphome/components/uart/uart_component.h | 6 +- .../external_components/uart_mock/__init__.py | 8 +- .../uart_mock/automation.h | 16 +++- .../uart_mock/uart_mock.cpp | 20 +++++ .../external_components/uart_mock/uart_mock.h | 10 +++ .../uart_mock_modbus_no_threshold.yaml | 64 +++++++++++++++ tests/integration/test_uart_mock_modbus.py | 79 +++++++++++++++++++ 8 files changed, 213 insertions(+), 8 deletions(-) create mode 100644 tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 28e26e307e..82672217c5 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -11,6 +11,11 @@ static const char *const TAG = "modbus"; // Maximum bytes to log for Modbus frames (truncated if larger) static constexpr size_t MODBUS_MAX_LOG_BYTES = 64; +// Approximate bits per character on the wire (depends on parity/stop bit config) +static constexpr uint32_t MODBUS_BITS_PER_CHAR = 11; +// Milliseconds per second +static constexpr uint32_t MS_PER_SEC = 1000; + void Modbus::setup() { if (this->flow_control_pin_ != nullptr) { this->flow_control_pin_->setup(); @@ -19,10 +24,17 @@ void Modbus::setup() { this->frame_delay_ms_ = std::max(2, // 1750us minimum per spec - rounded up to 2ms. // 3.5 characters * 11 bits per character * 1000ms/sec / (bits/sec) (Standard modbus frame delay) - (uint16_t) (3.5 * 11 * 1000 / this->parent_->get_baud_rate()) + 1); + (uint16_t) (3.5 * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate()) + 1); + // When rx_full_threshold is configured (non-zero), the UART has a hardware FIFO with a + // meaningful threshold (e.g., ESP32 native UART), so we can calculate a precise delay. + // Otherwise (e.g., USB UART), use 50ms to handle data arriving in chunks. + static constexpr uint16_t DEFAULT_LONG_RX_BUFFER_DELAY_MS = 50; + size_t rx_threshold = this->parent_->get_rx_full_threshold(); this->long_rx_buffer_delay_ms_ = - (this->parent_->get_rx_full_threshold() * 11 * 1000 / this->parent_->get_baud_rate()) + 1; + rx_threshold != uart::UARTComponent::RX_FULL_THRESHOLD_UNSET + ? (rx_threshold * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate()) + 1 + : DEFAULT_LONG_RX_BUFFER_DELAY_MS; } void Modbus::loop() { @@ -290,7 +302,7 @@ void Modbus::send_next_frame_() { this->last_send_tx_offset_ = 0; } else { this->write_array(frame.data.get(), frame.size); - this->last_send_tx_offset_ = frame.size * 11 * 1000 / this->parent_->get_baud_rate() + 1; + this->last_send_tx_offset_ = frame.size * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate() + 1; } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 3d7e71b89f..853de719fe 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -39,6 +39,8 @@ enum class FlushResult { class UARTComponent { public: + static constexpr size_t RX_FULL_THRESHOLD_UNSET = 0; + // Writes an array of bytes to the UART bus. // @param data A vector of bytes to be written. void write_array(const std::vector<uint8_t> &data) { this->write_array(&data[0], data.size()); } @@ -201,7 +203,9 @@ class UARTComponent { InternalGPIOPin *rx_pin_{}; InternalGPIOPin *flow_control_pin_{}; size_t rx_buffer_size_{}; - size_t rx_full_threshold_{1}; + // ESP32 (both Arduino and ESP-IDF) always sets this at codegen time via set_rx_full_threshold(). + // Other platforms (USB UART, Arduino, etc.) leave it unset. + size_t rx_full_threshold_{RX_FULL_THRESHOLD_UNSET}; size_t rx_timeout_{0}; uint32_t baud_rate_{0}; uint8_t stop_bits_{0}; diff --git a/tests/integration/fixtures/external_components/uart_mock/__init__.py b/tests/integration/fixtures/external_components/uart_mock/__init__.py index c10d73354e..fdd481b397 100644 --- a/tests/integration/fixtures/external_components/uart_mock/__init__.py +++ b/tests/integration/fixtures/external_components/uart_mock/__init__.py @@ -62,6 +62,7 @@ CONFIG_INJECT_RX_SCHEMA = cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(MockUartComponent), cv.Required("data"): cv.templatable(validate_raw_data), + cv.Optional(CONF_DELAY): cv.positive_time_period_milliseconds, }, key=CONF_DATA, ) @@ -87,7 +88,7 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(): cv.declare_id(MockUartComponent), cv.Required(CONF_BAUD_RATE): cv.int_range(min=1), cv.Optional(CONF_RX_BUFFER_SIZE, default=256): cv.validate_bytes, - cv.Optional(CONF_RX_FULL_THRESHOLD, default=10): cv.int_range(min=1, max=120), + cv.Optional(CONF_RX_FULL_THRESHOLD): cv.int_range(min=1, max=120), cv.Optional(CONF_RX_TIMEOUT, default=2): cv.int_range(min=0, max=92), cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8), @@ -126,6 +127,8 @@ async def inject_rx_to_code(config, action_id, template_arg, args): arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) cg.add(var.set_data_static(arr, len(data))) + if CONF_DELAY in config: + cg.add(var.set_delay(config[CONF_DELAY])) return var @@ -135,7 +138,8 @@ async def to_code(config): cg.add(var.set_baud_rate(config[CONF_BAUD_RATE])) cg.add(var.set_rx_buffer_size(config[CONF_RX_BUFFER_SIZE])) - cg.add(var.set_rx_full_threshold(config[CONF_RX_FULL_THRESHOLD])) + if CONF_RX_FULL_THRESHOLD in config: + cg.add(var.set_rx_full_threshold(config[CONF_RX_FULL_THRESHOLD])) cg.add(var.set_rx_timeout(config[CONF_RX_TIMEOUT])) cg.add(var.set_stop_bits(config[CONF_STOP_BITS])) cg.add(var.set_data_bits(config[CONF_DATA_BITS])) diff --git a/tests/integration/fixtures/external_components/uart_mock/automation.h b/tests/integration/fixtures/external_components/uart_mock/automation.h index 83a057d3a0..b2336ad065 100644 --- a/tests/integration/fixtures/external_components/uart_mock/automation.h +++ b/tests/integration/fixtures/external_components/uart_mock/automation.h @@ -22,18 +22,30 @@ template<typename... Ts> class MockUartInjectRXAction : public Action<Ts...>, pu this->len_ = len; // Length >= 0 indicates static mode } + void set_delay(uint32_t delay_ms) { this->delay_ms_ = delay_ms; } + void play(const Ts &...x) override { if (this->len_ >= 0) { // Static mode: use pointer and length - this->parent_->inject_to_rx_buffer(this->code_.data, static_cast<size_t>(this->len_)); + if (this->delay_ms_ > 0) { + std::vector<uint8_t> data(this->code_.data, this->code_.data + this->len_); + this->parent_->inject_to_rx_buffer_delayed(data, this->delay_ms_); + } else { + this->parent_->inject_to_rx_buffer(this->code_.data, static_cast<size_t>(this->len_)); + } } else { // Template mode: call function auto val = this->code_.func(x...); - this->parent_->inject_to_rx_buffer(val); + if (this->delay_ms_ > 0) { + this->parent_->inject_to_rx_buffer_delayed(val, this->delay_ms_); + } else { + this->parent_->inject_to_rx_buffer(val); + } } } protected: + uint32_t delay_ms_{0}; ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length union Code { std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas) diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp index 1edeb97cf1..1a15da76d1 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -53,6 +53,15 @@ void MockUartComponent::loop() { } } + // Process staged RX - deliver bytes whose delay has elapsed + uint32_t now_ms = millis(); + while (!this->staged_rx_.empty() && (static_cast<int32_t>(now_ms - this->staged_rx_.front().available_at_ms) >= 0)) { + auto &staged = this->staged_rx_.front(); + ESP_LOGD(TAG, "Delivering %zu staged RX bytes", staged.data.size()); + this->inject_to_rx_buffer(staged.data); + this->staged_rx_.pop_front(); + } + // Process delayed responses for (auto &response : this->responses_) { if (response.delay_ms > 0 && response.last_match_ms > 0 && now - response.last_match_ms >= response.delay_ms) { @@ -209,4 +218,15 @@ void MockUartComponent::inject_to_rx_buffer(const std::vector<uint8_t> &data) { } } +void MockUartComponent::inject_to_rx_buffer_delayed(const std::vector<uint8_t> &data, uint32_t delay_ms) { + if (!data.empty() && data.size() <= 64) { + char hex_buf[format_hex_pretty_size(64)]; + ESP_LOGD(TAG, "Staging %zu RX bytes with %ums delay: %s", data.size(), delay_ms, + format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size())); + } else if (data.size() > 64) { + ESP_LOGD(TAG, "Staging %zu RX bytes with %ums delay (too large to log inline)", data.size(), delay_ms); + } + this->staged_rx_.push_back({data, millis() + delay_ms}); +} + } // namespace esphome::uart_mock diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h index c9afb96357..82e3b3d563 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h @@ -43,6 +43,8 @@ class MockUartComponent : public uart::UARTComponent, public Component { void set_tx_hook(std::function<void(const std::vector<uint8_t> &)> &&cb) { this->tx_hook_ = std::move(cb); } void inject_to_rx_buffer(const std::vector<uint8_t> &data); void inject_to_rx_buffer(const uint8_t *data, size_t len); + // Stage bytes for delayed delivery - simulates transport-level latency (e.g., USB packets) + void inject_to_rx_buffer_delayed(const std::vector<uint8_t> &data, uint32_t delay_ms); protected: void check_logger_conflict() override {} @@ -82,6 +84,14 @@ class MockUartComponent : public uart::UARTComponent, public Component { }; std::vector<PeriodicRx> periodic_rx_; + // Staged RX - bytes that are pending delivery after a delay + // Simulates transport-level latency (e.g., USB packet delivery) + struct StagedRx { + std::vector<uint8_t> data; + uint32_t available_at_ms; // millis() time when bytes become available + }; + std::deque<StagedRx> staged_rx_; + // Observability uint32_t tx_count_{0}; uint32_t rx_count_{0}; diff --git a/tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml b/tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml new file mode 100644 index 0000000000..e3e8c8c8da --- /dev/null +++ b/tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml @@ -0,0 +1,64 @@ +esphome: + name: uart-mock-modbus-no-thresh + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +# Simulate a non-hardware UART (e.g., USB UART) by not setting rx_full_threshold. +# This leaves it at the default sentinel value (0), triggering the 50ms fallback timeout. +uart_mock: + - id: virtual_uart_dev + baud_rate: 9600 + auto_start: false + debug: + on_tx: + - then: + - if: + condition: #Read 80 input registers on device 2, starting at address 0 (SDM meter request) + lambda: "return data == std::vector<uint8_t>({0x02,0x04,0x00,0x00,0x00,0x50,0xF0,0x05});" + then: + - uart_mock.inject_rx: # First USB packet: SDM meter response part 1 + !lambda return {0x02,0x04,0xA0,0x43,0x73,0x19,0x9A,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3F,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; + - uart_mock.inject_rx: # Second USB packet: rest of response (staged with 40ms latency) + delay: 40ms + data: !lambda return{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x42,0x6F,0xCC,0xCD,0x43,0x7C,0xB8,0x10,0x3D,0x38,0x51,0xEC, + 0x43,0x81,0x1B,0xE7,0x3B,0x03,0x12,0x6F,0x50,0x1B}; + +modbus: + uart_id: virtual_uart_dev + turnaround_time: 10ms + +sensor: + - platform: sdm_meter + address: 2 + update_interval: 1s + phase_a: + voltage: + name: sdm_voltage + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: 'id(virtual_uart_dev).start_scenario();' diff --git a/tests/integration/test_uart_mock_modbus.py b/tests/integration/test_uart_mock_modbus.py index 6901dc27fe..e341d86f53 100644 --- a/tests/integration/test_uart_mock_modbus.py +++ b/tests/integration/test_uart_mock_modbus.py @@ -5,6 +5,10 @@ test_uart_mock_modbus : 1. Read a single register and parse successfully (basic_register) 2. Read multiple registers from SDM meter and parse successfully (sdm_voltage), with some intermediate delay to simulate UART buffer time. +test_uart_mock_modbus_no_threshold : + Test modbus with no rx_full_threshold set (simulating USB UART / non-hardware UART). + Verifies the 50ms fallback timeout handles chunked data with USB packet gaps. + """ from __future__ import annotations @@ -218,3 +222,78 @@ async def test_uart_mock_modbus_timing( f"Timeout waiting for SDM voltage change. Received sensor states:\n" f" sdm_voltage: {sensor_states['sdm_voltage']}\n" ) + + +@pytest.mark.asyncio +async def test_uart_mock_modbus_no_threshold( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test modbus with no rx_full_threshold (simulating USB UART). + + Without the 50ms fallback timeout, the chunked response with a 40ms gap + between USB packets would cause a false timeout and CRC failure cascade. + """ + # Replace external component path placeholder + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track sensor state updates (after initial state is swallowed) + sensor_states: dict[str, list[float]] = { + "sdm_voltage": [], + } + + voltage_changed = loop.create_future() + + def on_state(state: EntityState) -> None: + if isinstance(state, SensorState) and not state.missing_state: + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in sensor_states: + sensor_states[sensor_name].append(state.state) + # Check if this is a good voltage reading (243V) + if ( + sensor_name == "sdm_voltage" + and state.state > 200.0 + and not voltage_changed.done() + ): + voltage_changed.set_result(True) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + + # Build key mappings for all sensor types + all_names = list(sensor_states.keys()) + key_to_sensor = build_key_to_entity_mapping(entities, all_names) + + # Set up initial state helper + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + + # Wait for voltage to be updated with successful parse + try: + await asyncio.wait_for(voltage_changed, timeout=2.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for SDM voltage change. Received sensor states:\n" + f" sdm_voltage: {sensor_states['sdm_voltage']}\n" + ) From c11ad7f0e6fdbce4c65eb714e794c0791a8c7352 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 14:32:35 -1000 Subject: [PATCH 471/609] [rp2040] Wrap printf/vprintf/fprintf to eliminate _vfprintf_r (~9.2 KB flash) (#14622) --- esphome/components/rp2040/__init__.py | 17 ++++- esphome/components/rp2040/const.py | 1 + esphome/components/rp2040/printf_stubs.cpp | 74 ++++++++++++++++++++ tests/components/rp2040/test.rp2040-ard.yaml | 3 + 4 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 esphome/components/rp2040/printf_stubs.cpp diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 1442a0a7f7..54e1db27aa 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -21,7 +21,13 @@ from esphome.const import ( from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority from esphome.helpers import copy_file_if_changed, read_file, write_file_if_changed -from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns +from .const import ( + CONF_ENABLE_FULL_PRINTF, + KEY_BOARD, + KEY_PIO_FILES, + KEY_RP2040, + rp2040_ns, +) # force import gpio to register pin schema from .gpio import rp2040_pin_to_code # noqa @@ -153,6 +159,7 @@ CONFIG_SCHEMA = cv.All( cv.positive_time_period_milliseconds, cv.Range(max=cv.TimePeriod(milliseconds=8388)), ), + cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean, } ), set_core_data, @@ -190,6 +197,14 @@ async def to_code(config): ], ) + # Wrap FILE*-based printf functions to eliminate newlib's _vfprintf_r + # (~9.2 KB). See printf_stubs.cpp for implementation. + if config.get(CONF_ENABLE_FULL_PRINTF): + cg.add_define("USE_FULL_PRINTF") + else: + for symbol in ("vprintf", "printf", "fprintf"): + cg.add_build_flag(f"-Wl,--wrap={symbol}") + cg.add_platformio_option("board_build.core", "earlephilhower") cg.add_platformio_option("board_build.filesystem_size", "1m") diff --git a/esphome/components/rp2040/const.py b/esphome/components/rp2040/const.py index ab5f42d757..7eeddffc76 100644 --- a/esphome/components/rp2040/const.py +++ b/esphome/components/rp2040/const.py @@ -1,5 +1,6 @@ import esphome.codegen as cg +CONF_ENABLE_FULL_PRINTF = "enable_full_printf" KEY_BOARD = "board" KEY_RP2040 = "rp2040" KEY_PIO_FILES = "pio_files" diff --git a/esphome/components/rp2040/printf_stubs.cpp b/esphome/components/rp2040/printf_stubs.cpp new file mode 100644 index 0000000000..c2174a1dec --- /dev/null +++ b/esphome/components/rp2040/printf_stubs.cpp @@ -0,0 +1,74 @@ +/* + * Linker wrap stubs for FILE*-based printf functions. + * + * The RP2040 Arduino framework and libraries may reference printf(), + * vprintf(), and fprintf() which pull in newlib's _vfprintf_r (~8.9 KB). + * ESPHome never uses these — all logging goes through the logger component + * which uses snprintf/vsnprintf, so the libc FILE*-based printf path is + * dead code. + * + * These stubs redirect through vsnprintf() (which is already in the binary) + * and fwrite(), allowing the linker to dead-code eliminate _vfprintf_r. + * + * Saves ~8.9 KB of flash. + */ + +#if defined(USE_RP2040) && !defined(USE_FULL_PRINTF) +#include <cstdarg> +#include <cstdio> +#include <cstdlib> + +namespace esphome::rp2040 {} + +static constexpr size_t PRINTF_BUFFER_SIZE = 512; + +// These stubs are essentially dead code at runtime — ESPHome uses its own +// logging through snprintf/vsnprintf, not libc printf. +// The buffer overflow check is purely defensive and should never trigger. +static int write_printf_buffer(FILE *stream, char *buf, int len) { + if (len < 0) { + return len; + } + size_t write_len = len; + if (write_len >= PRINTF_BUFFER_SIZE) { + fwrite(buf, 1, PRINTF_BUFFER_SIZE - 1, stream); + // Use fwrite for the message to avoid recursive __wrap_printf call + static const char msg[] = "\nprintf buffer overflow\n"; + fwrite(msg, 1, sizeof(msg) - 1, stream); + abort(); + } + if (fwrite(buf, 1, write_len, stream) < write_len || ferror(stream)) { + return -1; + } + return len; +} + +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +extern "C" { + +int __wrap_vprintf(const char *fmt, va_list ap) { + char buf[PRINTF_BUFFER_SIZE]; + return write_printf_buffer(stdout, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); +} + +int __wrap_printf(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int len = __wrap_vprintf(fmt, ap); + va_end(ap); + return len; +} + +int __wrap_fprintf(FILE *stream, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + char buf[PRINTF_BUFFER_SIZE]; + int len = write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); + va_end(ap); + return len; +} + +} // extern "C" +// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) + +#endif // USE_RP2040 && !USE_FULL_PRINTF diff --git a/tests/components/rp2040/test.rp2040-ard.yaml b/tests/components/rp2040/test.rp2040-ard.yaml index 039a261016..1eb315a3b4 100644 --- a/tests/components/rp2040/test.rp2040-ard.yaml +++ b/tests/components/rp2040/test.rp2040-ard.yaml @@ -1,3 +1,6 @@ +rp2040: + enable_full_printf: false + logger: level: VERBOSE From e1c849d5d22651a6e7d3457f2d5dbe206d21c386 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 14:32:47 -1000 Subject: [PATCH 472/609] [esp8266] Wrap printf/vprintf/fprintf to eliminate _vfiprintf_r (~1.6 KB flash) (#14621) --- esphome/components/esp8266/__init__.py | 10 +++ esphome/components/esp8266/const.py | 1 + esphome/components/esp8266/printf_stubs.cpp | 71 +++++++++++++++++++ .../components/esp8266/test.esp8266-ard.yaml | 3 + 4 files changed, 85 insertions(+) create mode 100644 esphome/components/esp8266/printf_stubs.cpp diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 927a59fd61..1ef4f5e037 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -23,6 +23,7 @@ from esphome.helpers import copy_file_if_changed from .boards import BOARDS, ESP8266_LD_SCRIPTS from .const import ( CONF_EARLY_PIN_INIT, + CONF_ENABLE_FULL_PRINTF, CONF_ENABLE_SERIAL, CONF_ENABLE_SERIAL1, CONF_RESTORE_FROM_FLASH, @@ -179,6 +180,7 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_ENABLE_SERIAL): cv.boolean, cv.Optional(CONF_ENABLE_SERIAL1): cv.boolean, + cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean, } ), set_core_data, @@ -260,6 +262,14 @@ async def to_code(config): if CORE.testing_mode: cg.add_build_flag("-DESPHOME_TESTING_MODE") + # Wrap FILE*-based printf functions to eliminate newlib's _vfiprintf_r + # (~1.6 KB). See printf_stubs.cpp for implementation. + if config.get(CONF_ENABLE_FULL_PRINTF): + cg.add_define("USE_FULL_PRINTF") + else: + for symbol in ("vprintf", "printf", "fprintf"): + cg.add_build_flag(f"-Wl,--wrap={symbol}") + cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE]) ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] diff --git a/esphome/components/esp8266/const.py b/esphome/components/esp8266/const.py index 229ac61f24..57eb54f0f8 100644 --- a/esphome/components/esp8266/const.py +++ b/esphome/components/esp8266/const.py @@ -6,6 +6,7 @@ KEY_BOARD = "board" KEY_PIN_INITIAL_STATES = "pin_initial_states" CONF_RESTORE_FROM_FLASH = "restore_from_flash" CONF_EARLY_PIN_INIT = "early_pin_init" +CONF_ENABLE_FULL_PRINTF = "enable_full_printf" CONF_ENABLE_SERIAL = "enable_serial" CONF_ENABLE_SERIAL1 = "enable_serial1" KEY_FLASH_SIZE = "flash_size" diff --git a/esphome/components/esp8266/printf_stubs.cpp b/esphome/components/esp8266/printf_stubs.cpp new file mode 100644 index 0000000000..e6d4a74866 --- /dev/null +++ b/esphome/components/esp8266/printf_stubs.cpp @@ -0,0 +1,71 @@ +/* + * Linker wrap stubs for FILE*-based printf functions. + * + * The ESP8266 Arduino framework and libraries may reference printf(), + * vprintf(), and fprintf() which pull in newlib's _vfprintf_r (~900 bytes). + * ESPHome never uses these — all logging writes directly to the UART via + * Arduino's Serial, so the libc FILE*-based printf path is dead code. + * + * These stubs redirect through vsnprintf() (which is already in the binary + * for ESPHome's logging) and fwrite(), allowing the linker to dead-code + * eliminate _vfprintf_r. + * + * Saves ~1.6 KB of flash. + */ + +#if defined(USE_ESP8266) && !defined(USE_FULL_PRINTF) +#include <cstdarg> +#include <cstdio> +#include <cstdlib> + +namespace esphome::esp8266 {} + +static constexpr size_t PRINTF_BUFFER_SIZE = 512; + +// These stubs are essentially dead code at runtime — ESPHome writes directly +// to the UART via Arduino's Serial, and Serial.printf() has its own implementation. +// The buffer overflow check is purely defensive and should never trigger. +static int write_printf_buffer(FILE *stream, char *buf, int len) { + if (len < 0) { + return len; + } + size_t write_len = len; + if (write_len >= PRINTF_BUFFER_SIZE) { + fwrite(buf, 1, PRINTF_BUFFER_SIZE - 1, stream); + abort(); + } + if (fwrite(buf, 1, write_len, stream) < write_len || ferror(stream)) { + return -1; + } + return len; +} + +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +extern "C" { + +int __wrap_vprintf(const char *fmt, va_list ap) { + char buf[PRINTF_BUFFER_SIZE]; + return write_printf_buffer(stdout, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); +} + +int __wrap_printf(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int len = __wrap_vprintf(fmt, ap); + va_end(ap); + return len; +} + +int __wrap_fprintf(FILE *stream, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + char buf[PRINTF_BUFFER_SIZE]; + int len = write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); + va_end(ap); + return len; +} + +} // extern "C" +// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) + +#endif // USE_ESP8266 && !USE_FULL_PRINTF diff --git a/tests/components/esp8266/test.esp8266-ard.yaml b/tests/components/esp8266/test.esp8266-ard.yaml index 039a261016..c77218f7a3 100644 --- a/tests/components/esp8266/test.esp8266-ard.yaml +++ b/tests/components/esp8266/test.esp8266-ard.yaml @@ -1,3 +1,6 @@ +esp8266: + enable_full_printf: false + logger: level: VERBOSE From aef2d74e41123f77900bc5cab37a75cfb9b6e2b1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 14:32:59 -1000 Subject: [PATCH 473/609] [ld2450] Add integration tests with mock UART (#14611) --- .../fixtures/uart_mock_ld2450.yaml | 221 ++++++++++++++++++ tests/integration/state_utils.py | 28 ++- tests/integration/test_uart_mock_ld2450.py | 204 ++++++++++++++++ 3 files changed, 448 insertions(+), 5 deletions(-) create mode 100644 tests/integration/fixtures/uart_mock_ld2450.yaml create mode 100644 tests/integration/test_uart_mock_ld2450.py diff --git a/tests/integration/fixtures/uart_mock_ld2450.yaml b/tests/integration/fixtures/uart_mock_ld2450.yaml new file mode 100644 index 0000000000..269136da68 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_ld2450.yaml @@ -0,0 +1,221 @@ +esphome: + name: uart-mock-ld2450-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy ld2450's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + id: mock_uart + baud_rate: 256000 + auto_start: false + responses: + # Catch-all response: match any command footer (04 03 02 01). + # Returns a generic ACK to unblock setup commands. + # + # Response layout: + # [0-3] FD FC FB FA = header + # [4-5] 04 00 = length 4 + # [6] FF = cmd (handled as CMD_ENABLE_CONF) + # [7] 01 = status (ACK) + # [8-9] 00 00 = error = 0 + # [10-13] 04 03 02 01 = footer + - expect_tx: [0x04, 0x03, 0x02, 0x01] + inject_rx: + [ + 0xFD, 0xFC, 0xFB, 0xFA, + 0x04, 0x00, + 0xFF, 0x01, + 0x00, 0x00, + 0x04, 0x03, 0x02, 0x01, + ] + + injections: + # Phase 1 (t=100ms): Valid LD2450 periodic data frame - happy path + # The buffer is clean at this point, so this frame should parse correctly. + # + # Target 1: X=-500mm, Y=1000mm, Speed=-50mm/s (approaching), Res=320mm + # X: magnitude=500 (0x01F4), negative → high=0x01, low=0xF4 + # Y: magnitude=1000 (0x03E8), positive → high=0x83, low=0xE8 + # Speed: raw=5, negative (approaching) → high=0x00, low=0x05, decoded=-50mm/s + # Resolution: 320 → low=0x40, high=0x01 + # Distance: sqrt(500²+1000²) = sqrt(1250000) ≈ 1118mm + # + # Target 2: X=200mm, Y=500mm, Speed=0 (stationary), Res=100mm + # X: magnitude=200 (0x00C8), positive → high=0x80, low=0xC8 + # Y: magnitude=500 (0x01F4), positive → high=0x81, low=0xF4 + # Speed: 0 → 0x00, 0x00 + # Resolution: 100 → low=0x64, high=0x00 + # Distance: sqrt(200²+500²) = sqrt(290000) ≈ 538mm + # + # Target 3: No target (all zeros) + # Distance: 0 → sensors publish unknown/NaN + # + # Counts: target_count=2, moving_target_count=1, still_target_count=1 + # + # Frame layout (30 bytes): + # [0-3] AA FF 03 00 = periodic data header + # [4-11] Target 1 (8 bytes): X_L X_H Y_L Y_H SPD_L SPD_H RES_L RES_H + # [12-19] Target 2 (8 bytes) + # [20-27] Target 3 (8 bytes) + # [28-29] 55 CC = periodic data footer + - delay: 100ms + inject_rx: + [ + 0xAA, 0xFF, 0x03, 0x00, + 0xF4, 0x01, 0xE8, 0x83, 0x05, 0x00, 0x40, 0x01, + 0xC8, 0x80, 0xF4, 0x81, 0x00, 0x00, 0x64, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x55, 0xCC, + ] + + # Phase 2 (t=300ms): Garbage bytes + # LD2450's readline_ does NOT reject bytes at position 0 (unlike LD2412), + # so these bytes accumulate in the buffer. buffer_pos_ goes from 0 to 7. + - delay: 200ms + inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22] + + # Phase 3 (t=400ms): Truncated frame (header + partial data, no footer) + # More bytes accumulating in the buffer without a footer match. + # After this, buffer_pos_ = 7 + 8 = 15. + - delay: 100ms + inject_rx: [0xAA, 0xFF, 0x03, 0x00, 0x01, 0x02, 0x03, 0x04] + + # Phase 4 (t=600ms): Overflow - inject 75 bytes of 0xFF (MAX_LINE_LENGTH=45) + # Buffer has 15 bytes from phases 2+3. + # readline_() stores bytes while buffer_pos_ < 44. When buffer_pos_ == 44, + # the next byte triggers overflow: logs warning, resets buffer_pos_ to 0, + # and discards that byte. + # + # First overflow: 29 bytes fill positions 15-43 (buffer_pos_=44), byte 30 + # triggers overflow (discarded). Total consumed: 30 bytes. + # Second overflow: 44 bytes fill positions 0-43 (buffer_pos_=44), byte 45 + # triggers overflow (discarded). Total consumed: 30+45 = 75 bytes. + # After both overflows, buffer_pos_ = 0 (clean state for recovery frame). + - delay: 200ms + inject_rx: + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ] + + # Phase 5 (t=700ms): Valid frame after overflow - recovery test + # Buffer was reset by overflow. This valid frame should parse correctly. + # + # Target 1: X=300mm, Y=400mm, Speed=30mm/s (moving away), Res=100mm + # X: magnitude=300 (0x012C), positive → high=0x81, low=0x2C + # Y: magnitude=400 (0x0190), positive → high=0x81, low=0x90 + # Speed: raw=3, positive (moving away) → high=0x80, low=0x03, decoded=30mm/s + # Resolution: 100 → low=0x64, high=0x00 + # Distance: sqrt(300²+400²) = 500mm + # + # Target 2 & 3: No target (all zeros) + # Counts: target_count=1, moving_target_count=1, still_target_count=0 + - delay: 100ms + inject_rx: + [ + 0xAA, 0xFF, 0x03, 0x00, + 0x2C, 0x81, 0x90, 0x81, 0x03, 0x80, 0x64, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x55, 0xCC, + ] + +ld2450: + id: ld2450_dev + uart_id: mock_uart + +sensor: + - platform: ld2450 + ld2450_id: ld2450_dev + target_count: + name: "Target Count" + filters: &sensor_filters + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + still_target_count: + name: "Still Target Count" + filters: *sensor_filters + moving_target_count: + name: "Moving Target Count" + filters: *sensor_filters + target_1: + x: + name: "Target 1 X" + filters: *sensor_filters + y: + name: "Target 1 Y" + filters: *sensor_filters + speed: + name: "Target 1 Speed" + filters: *sensor_filters + distance: + name: "Target 1 Distance" + filters: *sensor_filters + resolution: + name: "Target 1 Resolution" + filters: *sensor_filters + angle: + name: "Target 1 Angle" + filters: *sensor_filters + target_2: + x: + name: "Target 2 X" + filters: *sensor_filters + y: + name: "Target 2 Y" + filters: *sensor_filters + speed: + name: "Target 2 Speed" + filters: *sensor_filters + distance: + name: "Target 2 Distance" + filters: *sensor_filters + +binary_sensor: + - platform: ld2450 + ld2450_id: ld2450_dev + has_target: + name: "Has Target" + filters: &binary_sensor_filters + - settle: 50ms + has_moving_target: + name: "Has Moving Target" + filters: *binary_sensor_filters + has_still_target: + name: "Has Still Target" + filters: *binary_sensor_filters + +text_sensor: + - platform: ld2450 + ld2450_id: ld2450_dev + target_1: + direction: + name: "Target 1 Direction" + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: 'id(mock_uart).start_scenario();' diff --git a/tests/integration/state_utils.py b/tests/integration/state_utils.py index e8c2cc5e66..ab9fdb01bb 100644 --- a/tests/integration/state_utils.py +++ b/tests/integration/state_utils.py @@ -13,6 +13,7 @@ from aioesphomeapi import ( EntityInfo, EntityState, SensorState, + TextSensorState, ) _LOGGER = logging.getLogger(__name__) @@ -244,12 +245,13 @@ class InitialStateHelper: class SensorStateCollector: - """Collects sensor and binary sensor state updates and provides wait helpers. + """Collects sensor, binary sensor, and text sensor state updates with wait helpers. Usage: collector = SensorStateCollector( sensor_names=["moving_distance", "still_distance"], binary_sensor_names=["has_target"], + text_sensor_names=["direction"], ) # Use collector.on_state as the callback (or wrap it) client.subscribe_states(helper.on_state_wrapper(collector.on_state)) @@ -259,18 +261,23 @@ class SensorStateCollector: # Access collected states assert collector.sensor_states["moving_distance"][0] == approx(100.0) + assert collector.text_sensor_states["direction"][0] == "Approaching" """ def __init__( self, sensor_names: list[str], binary_sensor_names: list[str] | None = None, + text_sensor_names: list[str] | None = None, entities: list[EntityInfo] | None = None, ) -> None: self.sensor_states: dict[str, list[float]] = {name: [] for name in sensor_names} self.binary_states: dict[str, list[bool]] = { name: [] for name in (binary_sensor_names or []) } + self.text_sensor_states: dict[str, list[str]] = { + name: [] for name in (text_sensor_names or []) + } self._key_to_sensor: dict[int, str] = {} self._waiters: list[tuple[Callable[[], bool], asyncio.Future[bool]]] = [] @@ -279,7 +286,11 @@ class SensorStateCollector: def build_key_mapping(self, entities: list[EntityInfo]) -> None: """Build key-to-name mapping from entities. Sorted by descending length.""" - all_names = list(self.sensor_states.keys()) + list(self.binary_states.keys()) + all_names = ( + list(self.sensor_states.keys()) + + list(self.binary_states.keys()) + + list(self.text_sensor_states.keys()) + ) all_names.sort(key=len, reverse=True) self._key_to_sensor = build_key_to_entity_mapping(entities, all_names) @@ -295,6 +306,11 @@ class SensorStateCollector: if sensor_name and sensor_name in self.binary_states: self.binary_states[sensor_name].append(state.state) self._check_waiters() + elif isinstance(state, TextSensorState) and not state.missing_state: + sensor_name = self._key_to_sensor.get(state.key) + if sensor_name and sensor_name in self.text_sensor_states: + self.text_sensor_states[sensor_name].append(state.state) + self._check_waiters() def _check_waiters(self) -> None: """Check all pending waiters and resolve any whose condition is met.""" @@ -303,9 +319,11 @@ class SensorStateCollector: future.set_result(True) def _all_have_values(self) -> bool: - """Check if all sensor and binary sensor lists have at least one value.""" - return all(len(v) >= 1 for v in self.sensor_states.values()) and all( - len(v) >= 1 for v in self.binary_states.values() + """Check if all sensor, binary sensor, and text sensor lists have at least one value.""" + return ( + all(len(v) >= 1 for v in self.sensor_states.values()) + and all(len(v) >= 1 for v in self.binary_states.values()) + and all(len(v) >= 1 for v in self.text_sensor_states.values()) ) async def wait_for_all(self, timeout: float = 3.0) -> None: diff --git a/tests/integration/test_uart_mock_ld2450.py b/tests/integration/test_uart_mock_ld2450.py new file mode 100644 index 0000000000..b1aa2f6952 --- /dev/null +++ b/tests/integration/test_uart_mock_ld2450.py @@ -0,0 +1,204 @@ +"""Integration test for LD2450 component with mock UART. + +Tests: +test_uart_mock_ld2450: + 1. Happy path - valid periodic data frame publishes correct target sensor values + 2. Multi-target tracking - verifies target count, moving/still counts + 3. Target coordinate decoding - signed X/Y coordinates with sign-magnitude encoding + 4. Speed decoding - approaching (negative) and stationary (zero) targets + 5. Distance calculation - computed from X/Y via sqrt(x²+y²) + 6. Direction text sensor - "Approaching" for negative speed target + 7. Garbage resilience - random bytes don't crash the component + 8. Truncated frame handling - partial frame doesn't corrupt state + 9. Buffer overflow recovery - overflow resets the parser + 10. Post-overflow parsing - next valid frame after overflow is parsed correctly + 11. TX logging - verifies LD2450 sends expected setup commands +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path + +from aioesphomeapi import ButtonInfo +import pytest + +from .state_utils import InitialStateHelper, SensorStateCollector, find_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_uart_mock_ld2450( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test LD2450 data parsing with happy path, garbage, overflow, and recovery.""" + # Replace external component path placeholder + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track overflow warning in logs + overflow_seen = loop.create_future() + + # Track TX data logged by the mock for assertions + tx_log_lines: list[str] = [] + + def line_callback(line: str) -> None: + if "Max command length exceeded" in line and not overflow_seen.done(): + overflow_seen.set_result(True) + # Capture all TX log lines from uart_mock + if "uart_mock" in line and "TX " in line: + tx_log_lines.append(line) + + collector = SensorStateCollector( + sensor_names=[ + "target_1_x", + "target_1_y", + "target_1_speed", + "target_1_distance", + "target_1_resolution", + "target_1_angle", + "target_2_x", + "target_2_y", + "target_2_speed", + "target_2_distance", + "target_count", + "still_target_count", + "moving_target_count", + ], + binary_sensor_names=[ + "has_target", + "has_moving_target", + "has_still_target", + ], + text_sensor_names=[ + "target_1_direction", + ], + ) + + # Signal when we see recovery frame values (target 1 distance ≈ 500mm) + recovery_received = collector.add_waiter( + lambda: ( + pytest.approx(500.0, abs=1.0) + in collector.sensor_states["target_1_distance"] + ) + ) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + collector.build_key_mapping(entities) + + # Set up initial state helper + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states( + initial_state_helper.on_state_wrapper(collector.on_state) + ) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + + # Wait for Phase 1 - all sensors and binary sensors have at least one value + try: + await collector.wait_for_all(timeout=5.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 1 frame. Received:\n" + f" sensor_states: {collector.sensor_states}\n" + f" binary_states: {collector.binary_states}\n" + f" text_states: {collector.text_sensor_states}" + ) + + # Phase 1 values: + # Target 1: X=-500, Y=1000, Speed=-50 (approaching), Res=320 + # Distance = sqrt(500²+1000²) ≈ 1118mm + assert collector.sensor_states["target_1_x"][0] == pytest.approx(-500.0) + assert collector.sensor_states["target_1_y"][0] == pytest.approx(1000.0) + assert collector.sensor_states["target_1_speed"][0] == pytest.approx(-50.0) + assert collector.sensor_states["target_1_resolution"][0] == pytest.approx(320.0) + # Distance computed from X/Y + assert collector.sensor_states["target_1_distance"][0] == pytest.approx( + 1118.0, abs=1.0 + ) + + # Target 2: X=200, Y=500, Speed=0 (stationary), Res=100 + # Distance = sqrt(200²+500²) ≈ 538mm + assert collector.sensor_states["target_2_x"][0] == pytest.approx(200.0) + assert collector.sensor_states["target_2_y"][0] == pytest.approx(500.0) + assert collector.sensor_states["target_2_speed"][0] == pytest.approx(0.0) + assert collector.sensor_states["target_2_distance"][0] == pytest.approx( + 538.0, abs=1.0 + ) + + # Target counts: 2 targets total, 1 moving, 1 still + assert collector.sensor_states["target_count"][0] == pytest.approx(2.0) + assert collector.sensor_states["moving_target_count"][0] == pytest.approx(1.0) + assert collector.sensor_states["still_target_count"][0] == pytest.approx(1.0) + + # Binary sensors: all true (targets detected) + assert collector.binary_states["has_target"][0] is True + assert collector.binary_states["has_moving_target"][0] is True + assert collector.binary_states["has_still_target"][0] is True + + # Direction text sensor: Target 1 is approaching (speed < 0) + assert collector.text_sensor_states["target_1_direction"][0] == "Approaching" + + # Wait for the recovery frame (Phase 5) to be parsed + # This proves the component survived garbage + truncated + overflow + try: + await asyncio.wait_for(recovery_received, timeout=5.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for recovery frame. Received:\n" + f" sensor_states: {collector.sensor_states}" + ) + + # Verify overflow warning was logged + assert overflow_seen.done(), ( + "Expected 'Max command length exceeded' warning in logs" + ) + + # Verify LD2450 sent setup commands (TX logging) + assert len(tx_log_lines) > 0, "Expected TX log lines from uart_mock" + tx_data = " ".join(tx_log_lines) + # Verify command frame header appears (FD:FC:FB:FA) + assert "FD:FC:FB:FA" in tx_data, ( + "Expected LD2450 command frame header FD:FC:FB:FA in TX log" + ) + # Verify command frame footer appears (04:03:02:01) + assert "04:03:02:01" in tx_data, ( + "Expected LD2450 command frame footer 04:03:02:01 in TX log" + ) + + # Recovery frame values (Phase 5, after overflow): + # Target 1: X=300, Y=400, Distance=500, Speed=30 (moving away) + # target_count=1, moving=1, still=0 + # + # Note: throttle filters cause sensor lists to have different lengths, + # so we check each value appeared somewhere rather than using a shared index. + assert ( + pytest.approx(500.0, abs=1.0) + in collector.sensor_states["target_1_distance"] + ) + assert pytest.approx(300.0) in collector.sensor_states["target_1_x"] + assert pytest.approx(400.0) in collector.sensor_states["target_1_y"] + assert pytest.approx(30.0) in collector.sensor_states["target_1_speed"] + assert pytest.approx(1.0) in collector.sensor_states["target_count"] + assert pytest.approx(1.0) in collector.sensor_states["moving_target_count"] + assert pytest.approx(0.0) in collector.sensor_states["still_target_count"] From b05dbfccd31ec769bfa873783506b469ac6def27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 14:52:21 -1000 Subject: [PATCH 474/609] [api] Bump noise-c to 0.1.11 (#14632) --- .clang-tidy.hash | 2 +- esphome/components/api/__init__.py | 2 +- platformio.ini | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index adcebadeb4..ff25675918 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -b6f8c16c1ddd222134bf4a71910b4c832e764e23caf49f9bce3280b079955fcf +e4b9c4b54e705d3c9400e1cdda8ba0b32634780cfa5f32271832e911bdcafe7e diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index dd99862cc2..c7dec6e78b 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -453,7 +453,7 @@ async def to_code(config: ConfigType) -> None: # and plaintext disabled. Only a factory reset can remove it. cg.add_define("USE_API_PLAINTEXT") cg.add_define("USE_API_NOISE") - cg.add_library("esphome/noise-c", "0.1.10") + cg.add_library("esphome/noise-c", "0.1.11") else: cg.add_define("USE_API_PLAINTEXT") diff --git a/platformio.ini b/platformio.ini index 87f992759c..deee23d049 100644 --- a/platformio.ini +++ b/platformio.ini @@ -46,7 +46,7 @@ lib_deps_base = lib_deps = ${common.lib_deps_base} - esphome/noise-c@0.1.10 ; api + esphome/noise-c@0.1.11 ; api improv/Improv@1.2.4 ; improv_serial / esp32_improv kikuchan98/pngle@1.1.0 ; online_image ; Using the repository directly, otherwise ESP-IDF can't use the library @@ -542,7 +542,7 @@ build_unflags = extends = common platform = platformio/native lib_deps = - esphome/noise-c@0.1.10 ; used by api + esphome/noise-c@0.1.11 ; used by api build_flags = ${common.build_flags} -DUSE_HOST From 9547a54fac5de13f36a98d670e60ef18b1e88fe8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 14:52:40 -1000 Subject: [PATCH 475/609] [const] Move CONF_ENABLE_FULL_PRINTF to const.py (#14633) --- esphome/components/esp32/__init__.py | 2 +- esphome/components/esp8266/__init__.py | 2 +- esphome/components/esp8266/const.py | 1 - esphome/components/rp2040/__init__.py | 9 ++------- esphome/components/rp2040/const.py | 1 - esphome/const.py | 1 + 6 files changed, 5 insertions(+), 11 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 8ad84656ed..52e70501dc 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -14,6 +14,7 @@ from esphome.const import ( CONF_BOARD, CONF_COMPONENTS, CONF_DISABLED, + CONF_ENABLE_FULL_PRINTF, CONF_ENABLE_OTA_ROLLBACK, CONF_ESPHOME, CONF_FRAMEWORK, @@ -954,7 +955,6 @@ CONF_HEAP_IN_IRAM = "heap_in_iram" CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" CONF_USE_FULL_CERTIFICATE_BUNDLE = "use_full_certificate_bundle" CONF_DISABLE_DEBUG_STUBS = "disable_debug_stubs" -CONF_ENABLE_FULL_PRINTF = "enable_full_printf" CONF_DISABLE_OCD_AWARE = "disable_ocd_aware" CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY = "disable_usb_serial_jtag_secondary" CONF_DISABLE_DEV_NULL_VFS = "disable_dev_null_vfs" diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 1ef4f5e037..16043b6d69 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -6,6 +6,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_BOARD, CONF_BOARD_FLASH_MODE, + CONF_ENABLE_FULL_PRINTF, CONF_FRAMEWORK, CONF_PLATFORM_VERSION, CONF_SOURCE, @@ -23,7 +24,6 @@ from esphome.helpers import copy_file_if_changed from .boards import BOARDS, ESP8266_LD_SCRIPTS from .const import ( CONF_EARLY_PIN_INIT, - CONF_ENABLE_FULL_PRINTF, CONF_ENABLE_SERIAL, CONF_ENABLE_SERIAL1, CONF_RESTORE_FROM_FLASH, diff --git a/esphome/components/esp8266/const.py b/esphome/components/esp8266/const.py index 57eb54f0f8..229ac61f24 100644 --- a/esphome/components/esp8266/const.py +++ b/esphome/components/esp8266/const.py @@ -6,7 +6,6 @@ KEY_BOARD = "board" KEY_PIN_INITIAL_STATES = "pin_initial_states" CONF_RESTORE_FROM_FLASH = "restore_from_flash" CONF_EARLY_PIN_INIT = "early_pin_init" -CONF_ENABLE_FULL_PRINTF = "enable_full_printf" CONF_ENABLE_SERIAL = "enable_serial" CONF_ENABLE_SERIAL1 = "enable_serial1" KEY_FLASH_SIZE = "flash_size" diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 54e1db27aa..359337adfb 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -6,6 +6,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( CONF_BOARD, + CONF_ENABLE_FULL_PRINTF, CONF_FRAMEWORK, CONF_PLATFORM_VERSION, CONF_SOURCE, @@ -21,13 +22,7 @@ from esphome.const import ( from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority from esphome.helpers import copy_file_if_changed, read_file, write_file_if_changed -from .const import ( - CONF_ENABLE_FULL_PRINTF, - KEY_BOARD, - KEY_PIO_FILES, - KEY_RP2040, - rp2040_ns, -) +from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns # force import gpio to register pin schema from .gpio import rp2040_pin_to_code # noqa diff --git a/esphome/components/rp2040/const.py b/esphome/components/rp2040/const.py index 7eeddffc76..ab5f42d757 100644 --- a/esphome/components/rp2040/const.py +++ b/esphome/components/rp2040/const.py @@ -1,6 +1,5 @@ import esphome.codegen as cg -CONF_ENABLE_FULL_PRINTF = "enable_full_printf" KEY_BOARD = "board" KEY_RP2040 = "rp2040" KEY_PIO_FILES = "pio_files" diff --git a/esphome/const.py b/esphome/const.py index 88e3c33fbc..d409514f3c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -356,6 +356,7 @@ CONF_EFFECT = "effect" CONF_EFFECTS = "effects" CONF_ELSE = "else" CONF_ENABLE_BTM = "enable_btm" +CONF_ENABLE_FULL_PRINTF = "enable_full_printf" CONF_ENABLE_IPV6 = "enable_ipv6" CONF_ENABLE_ON_BOOT = "enable_on_boot" CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" From d0285cdc41d479ed0aa7dc0984a71215ee4ff696 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 15:11:15 -1000 Subject: [PATCH 476/609] [core] Pack entity flags into configure_entity_() and protect setters (#14564) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/core/entity_base.cpp | 14 +- esphome/core/entity_base.h | 31 ++- esphome/core/entity_helpers.py | 84 ++++++- .../binary_sensor/test_binary_sensor.py | 8 +- tests/component_tests/button/test_button.py | 8 +- tests/component_tests/helpers.py | 19 ++ tests/component_tests/sensor/test_sensor.py | 12 +- tests/component_tests/text/test_text.py | 10 +- .../text_sensor/test_text_sensor.py | 20 +- tests/unit_tests/core/test_entity_helpers.py | 222 +++++++++++++++++- 10 files changed, 354 insertions(+), 74 deletions(-) create mode 100644 tests/component_tests/helpers.py diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 3274640eb3..818dae06de 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -11,7 +11,7 @@ static const char *const TAG = "entity_base"; // Entity Name const StringRef &EntityBase::get_name() const { return this->name_; } -void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_strings_packed) { +void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_fields) { this->name_ = StringRef(name); if (this->name_.empty()) { #ifdef USE_DEVICES @@ -44,17 +44,19 @@ void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, ui this->calc_object_id_(); } } - // Unpack entity string table indices. - // Packed: [23..16] icon | [15..8] UoM | [7..0] device_class (each 8 bits) + // Unpack entity string table indices and flags from entity_fields. #ifdef USE_ENTITY_DEVICE_CLASS - this->device_class_idx_ = entity_strings_packed & 0xFF; + this->device_class_idx_ = (entity_fields >> ENTITY_FIELD_DC_SHIFT) & 0xFF; #endif #ifdef USE_ENTITY_UNIT_OF_MEASUREMENT - this->uom_idx_ = (entity_strings_packed >> 8) & 0xFF; + this->uom_idx_ = (entity_fields >> ENTITY_FIELD_UOM_SHIFT) & 0xFF; #endif #ifdef USE_ENTITY_ICON - this->icon_idx_ = (entity_strings_packed >> 16) & 0xFF; + this->icon_idx_ = (entity_fields >> ENTITY_FIELD_ICON_SHIFT) & 0xFF; #endif + this->flags_.internal = (entity_fields >> ENTITY_FIELD_INTERNAL_SHIFT) & 1; + this->flags_.disabled_by_default = (entity_fields >> ENTITY_FIELD_DISABLED_BY_DEFAULT_SHIFT) & 1; + this->flags_.entity_category = (entity_fields >> ENTITY_FIELD_ENTITY_CATEGORY_SHIFT) & 0x3; } // Weak default lookup functions — overridden by generated code in main.cpp diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index accd532b0d..cccbafd2c3 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -55,6 +55,15 @@ enum EntityCategory : uint8_t { ENTITY_CATEGORY_DIAGNOSTIC = 2, }; +// Bit layout for entity_fields parameter in configure_entity_(). +// Keep in sync with _*_SHIFT constants in esphome/core/entity_helpers.py +static constexpr uint8_t ENTITY_FIELD_DC_SHIFT = 0; +static constexpr uint8_t ENTITY_FIELD_UOM_SHIFT = 8; +static constexpr uint8_t ENTITY_FIELD_ICON_SHIFT = 16; +static constexpr uint8_t ENTITY_FIELD_INTERNAL_SHIFT = 24; +static constexpr uint8_t ENTITY_FIELD_DISABLED_BY_DEFAULT_SHIFT = 25; +static constexpr uint8_t ENTITY_FIELD_ENTITY_CATEGORY_SHIFT = 26; + // The generic Entity base class that provides an interface common to all Entities. class EntityBase { public: @@ -88,21 +97,16 @@ class EntityBase { /// Useful for building compound strings without intermediate buffer size_t write_object_id_to(char *buf, size_t buf_size) const; - // Get/set whether this Entity should be hidden outside ESPHome + // Get whether this Entity should be hidden outside ESPHome bool is_internal() const { return this->flags_.internal; } - void set_internal(bool internal) { this->flags_.internal = internal; } // Check if this object is declared to be disabled by default. // That means that when the device gets added to Home Assistant (or other clients) it should // not be added to the default view by default, and a user action is necessary to manually add it. bool is_disabled_by_default() const { return this->flags_.disabled_by_default; } - void set_disabled_by_default(bool disabled_by_default) { this->flags_.disabled_by_default = disabled_by_default; } - // Get/set the entity category. + // Get the entity category. EntityCategory get_entity_category() const { return static_cast<EntityCategory>(this->flags_.entity_category); } - void set_entity_category(EntityCategory entity_category) { - this->flags_.entity_category = static_cast<uint8_t>(entity_category); - } // Get this entity's device class into a stack buffer. // On non-ESP8266: returns pointer to PROGMEM string directly (buffer unused). @@ -164,14 +168,13 @@ class EntityBase { #endif #ifdef USE_DEVICES - // Get/set this entity's device id + // Get this entity's device id uint32_t get_device_id() const { if (this->device_ == nullptr) { return 0; // No device set, return 0 } return this->device_->get_device_id(); } - void set_device(Device *device) { this->device_ = device; } // Get the device this entity belongs to (nullptr if main device) Device *get_device() const { return this->device_; } #endif @@ -228,8 +231,14 @@ class EntityBase { friend void ::setup(); friend void ::original_setup(); - /// Combined entity setup from codegen: set name, object_id hash, and entity string indices. - void configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_strings_packed); + /// Combined entity setup from codegen: set name, object_id hash, entity string indices, and flags. + /// Bit layout of entity_fields is defined by the ENTITY_FIELD_*_SHIFT constants above. + void configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_fields); + +#ifdef USE_DEVICES + // Codegen-only setter — only accessible from setup() via friend declaration. + void set_device_(Device *device) { this->device_ = device; } +#endif /// Non-template helper for make_entity_preference() to avoid code bloat. /// When preference hash algorithm changes, migration logic goes here. diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 4fa109fb0e..0589b92364 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -34,11 +34,19 @@ _KEY_ICON_IDX = "_entity_icon_idx" _KEY_ENTITY_NAME = "_entity_name" _KEY_OBJECT_ID_HASH = "_entity_object_id_hash" -# Bit layout for entity_strings_packed in configure_entity_() — must match C++ in entity_base.h: -# [23..16] icon (8 bits) | [15..8] UoM (8 bits) | [7..0] device_class (8 bits) +# Bit layout for entity_fields in configure_entity_(). +# Keep in sync with ENTITY_FIELD_*_SHIFT constants in esphome/core/entity_base.h _DC_SHIFT = 0 _UOM_SHIFT = 8 _ICON_SHIFT = 16 +_INTERNAL_SHIFT = 24 +_DISABLED_BY_DEFAULT_SHIFT = 25 +_ENTITY_CATEGORY_SHIFT = 26 + +# Private config keys for storing flags +_KEY_INTERNAL = "_entity_internal" +_KEY_DISABLED_BY_DEFAULT = "_entity_disabled_by_default" +_KEY_ENTITY_CATEGORY = "_entity_category" # Maximum unique strings per category (8-bit index, 0 = not set) _MAX_DEVICE_CLASSES = 0xFF # 255 @@ -220,8 +228,39 @@ def setup_unit_of_measurement(config: ConfigType) -> None: config[_KEY_UOM_IDX] = idx +def _sanitize_comment(text: str) -> str: + r"""Sanitize a string for safe inclusion in a C++ // line comment. + + Dangerous characters: + - \n, \r: break out of line comment, next line becomes code + - \: at end of line, splices next line into comment (eats real code) + """ + return text.replace("\\", "/").replace("\n", " ").replace("\r", "") + + +def _describe_packed_flags(config: ConfigType, entity_category: int) -> str: + """Build a human-readable description of packed entity flags for C++ comments.""" + parts: list[str] = [] + if config.get(_KEY_INTERNAL): + parts.append("internal") + if config.get(_KEY_DISABLED_BY_DEFAULT): + parts.append("disabled_by_default") + entity_cat_keys = list(cv.ENTITY_CATEGORIES) + if entity_category < len(entity_cat_keys) and ( + cat_name := entity_cat_keys[entity_category] + ): + parts.append(f"category:{cat_name}") + if config.get(_KEY_DC_IDX) and (dc := config.get(CONF_DEVICE_CLASS)): + parts.append(f"dc:{_sanitize_comment(dc)}") + if config.get(_KEY_UOM_IDX) and (uom := config.get(CONF_UNIT_OF_MEASUREMENT)): + parts.append(f"uom:{_sanitize_comment(uom)}") + if config.get(_KEY_ICON_IDX) and (icon := config.get(CONF_ICON)): + parts.append(f"icon:{_sanitize_comment(icon)}") + return ", ".join(parts) + + def finalize_entity_strings(var: MockObj, config: ConfigType) -> None: - """Emit a single configure_entity_() call with name, hash, and packed string indices. + """Emit a single configure_entity_() call with name, hash, packed string indices, and flags. Call this at the end of each component's setup function, after setup_entity() and any register_device_class/register_unit_of_measurement calls. @@ -231,8 +270,24 @@ def finalize_entity_strings(var: MockObj, config: ConfigType) -> None: dc_idx = config.get(_KEY_DC_IDX, 0) uom_idx = config.get(_KEY_UOM_IDX, 0) icon_idx = config.get(_KEY_ICON_IDX, 0) - packed = (dc_idx << _DC_SHIFT) | (uom_idx << _UOM_SHIFT) | (icon_idx << _ICON_SHIFT) - add(var.configure_entity_(entity_name, object_id_hash, packed)) + internal = config.get(_KEY_INTERNAL, 0) + disabled_by_default = config.get(_KEY_DISABLED_BY_DEFAULT, 0) + entity_category = config.get(_KEY_ENTITY_CATEGORY, 0) + packed = ( + (dc_idx << _DC_SHIFT) + | (uom_idx << _UOM_SHIFT) + | (icon_idx << _ICON_SHIFT) + | (internal << _INTERNAL_SHIFT) + | (disabled_by_default << _DISABLED_BY_DEFAULT_SHIFT) + | (entity_category << _ENTITY_CATEGORY_SHIFT) + ) + # Build inline comment describing the packed flags for readability + comment = _describe_packed_flags(config, entity_category) + expr = var.configure_entity_(entity_name, object_id_hash, packed) + if comment: + add(RawStatement(f"{expr}; // {comment}")) + else: + add(expr) def get_base_entity_object_id( @@ -332,7 +387,7 @@ async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) -> # Get device info if configured if device_id_obj := config.get(CONF_DEVICE_ID): device: MockObj = await get_variable(device_id_obj) - add(var.set_device(device)) + add(var.set_device_(device)) # Pre-compute entity name and object_id hash for configure_entity_() # which is emitted later by finalize_entity_strings(). @@ -343,18 +398,25 @@ async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) -> object_id_hash = fnv1_hash_object_id(entity_name) if entity_name else 0 config[_KEY_ENTITY_NAME] = entity_name config[_KEY_OBJECT_ID_HASH] = object_id_hash - # Only set disabled_by_default if True (default is False) - if config[CONF_DISABLED_BY_DEFAULT]: - add(var.set_disabled_by_default(True)) + # Store flags for packing into configure_entity_() + config[_KEY_DISABLED_BY_DEFAULT] = int(config[CONF_DISABLED_BY_DEFAULT]) if CONF_INTERNAL in config: - add(var.set_internal(config[CONF_INTERNAL])) + config[_KEY_INTERNAL] = int(config[CONF_INTERNAL]) icon_idx = 0 if CONF_ICON in config: # Add USE_ENTITY_ICON define when icons are used cg.add_define("USE_ENTITY_ICON") icon_idx = register_icon(config[CONF_ICON]) if CONF_ENTITY_CATEGORY in config: - add(var.set_entity_category(config[CONF_ENTITY_CATEGORY])) + # Derive integer value from key position in cv.ENTITY_CATEGORIES + # (must match C++ EntityCategory enum in entity_base.h) + entity_cat_str = str(config[CONF_ENTITY_CATEGORY]) + entity_cat_keys = list(cv.ENTITY_CATEGORIES) + config[_KEY_ENTITY_CATEGORY] = ( + entity_cat_keys.index(entity_cat_str) + if entity_cat_str in entity_cat_keys + else 0 + ) # Store icon index for finalize_entity_strings config[_KEY_ICON_IDX] = icon_idx diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py index fbc2f37d9a..10d7f80834 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.py +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -1,5 +1,7 @@ """Tests for the binary sensor component.""" +from tests.component_tests.helpers import INTERNAL_BIT, extract_packed_value + def test_binary_sensor_is_setup(generate_main): """ @@ -44,9 +46,9 @@ def test_binary_sensor_config_value_internal_set(generate_main): "tests/component_tests/binary_sensor/test_binary_sensor.yaml" ) - # Then - assert "bs_1->set_internal(true);" in main_cpp - assert "bs_2->set_internal(false);" in main_cpp + # Then: bs_1 has internal: true, bs_2 has internal: false + assert extract_packed_value(main_cpp, "bs_1") & INTERNAL_BIT != 0 + assert extract_packed_value(main_cpp, "bs_2") & INTERNAL_BIT == 0 def test_binary_sensor_config_value_use_raw_set(generate_main): diff --git a/tests/component_tests/button/test_button.py b/tests/component_tests/button/test_button.py index 9f94d61c8c..a35994a682 100644 --- a/tests/component_tests/button/test_button.py +++ b/tests/component_tests/button/test_button.py @@ -1,5 +1,7 @@ """Tests for the button component""" +from tests.component_tests.helpers import INTERNAL_BIT, extract_packed_value + def test_button_is_setup(generate_main): """ @@ -39,6 +41,6 @@ def test_button_config_value_internal_set(generate_main): # When main_cpp = generate_main("tests/component_tests/button/test_button.yaml") - # Then - assert "wol_1->set_internal(true);" in main_cpp - assert "wol_2->set_internal(false);" in main_cpp + # Then: wol_1 has internal: true, wol_2 has internal: false + assert extract_packed_value(main_cpp, "wol_1") & INTERNAL_BIT != 0 + assert extract_packed_value(main_cpp, "wol_2") & INTERNAL_BIT == 0 diff --git a/tests/component_tests/helpers.py b/tests/component_tests/helpers.py new file mode 100644 index 0000000000..568d1639d0 --- /dev/null +++ b/tests/component_tests/helpers.py @@ -0,0 +1,19 @@ +"""Shared helpers for component tests.""" + +from __future__ import annotations + +import re + +INTERNAL_BIT = 1 << 24 + + +def extract_packed_value(main_cpp: str, var_name: str) -> int: + """Extract the third (packed) argument from a configure_entity_ call.""" + pattern = ( + rf"{re.escape(var_name)}->configure_entity_\(" + r'"(?:\\.|[^"\\])*"' + r",\s*\w+,\s*(\d+)\)" + ) + match = re.search(pattern, main_cpp) + assert match, f"configure_entity_ call not found for {var_name}" + return int(match.group(1)) diff --git a/tests/component_tests/sensor/test_sensor.py b/tests/component_tests/sensor/test_sensor.py index d9ab3a022c..9d18fa36b8 100644 --- a/tests/component_tests/sensor/test_sensor.py +++ b/tests/component_tests/sensor/test_sensor.py @@ -1,14 +1,6 @@ """Tests for the sensor component.""" -import re - - -def _extract_packed_value(main_cpp, var_name): - """Extract the third (packed) argument from a configure_entity_ call.""" - pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)" - match = re.search(pattern, main_cpp) - assert match, f"configure_entity_ call not found for {var_name}" - return int(match.group(1)) +from tests.component_tests.helpers import extract_packed_value def test_sensor_device_class_set(generate_main): @@ -21,5 +13,5 @@ def test_sensor_device_class_set(generate_main): main_cpp = generate_main("tests/component_tests/sensor/test_sensor.yaml") # Then: device_class: voltage means packed value must be non-zero - packed = _extract_packed_value(main_cpp, "s_1") + packed = extract_packed_value(main_cpp, "s_1") assert packed != 0 diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 3ceaa9b8f8..c74dfb8a47 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -1,4 +1,6 @@ -"""Tests for the binary sensor component.""" +"""Tests for the text component.""" + +from tests.component_tests.helpers import INTERNAL_BIT, extract_packed_value def test_text_is_setup(generate_main): @@ -37,9 +39,9 @@ def test_text_config_value_internal_set(generate_main): # When main_cpp = generate_main("tests/component_tests/text/test_text.yaml") - # Then - assert "it_2->set_internal(false);" in main_cpp - assert "it_3->set_internal(true);" in main_cpp + # Then: it_2 has internal: false, it_3 has internal: true + assert extract_packed_value(main_cpp, "it_2") & INTERNAL_BIT == 0 + assert extract_packed_value(main_cpp, "it_3") & INTERNAL_BIT != 0 def test_text_config_value_mode_set(generate_main): diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py index f30b820e94..1ff31ab96b 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.py +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -1,14 +1,6 @@ """Tests for the text sensor component.""" -import re - - -def _extract_packed_value(main_cpp, var_name): - """Extract the third (packed) argument from a configure_entity_ call.""" - pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)" - match = re.search(pattern, main_cpp) - assert match, f"configure_entity_ call not found for {var_name}" - return int(match.group(1)) +from tests.component_tests.helpers import INTERNAL_BIT, extract_packed_value def test_text_sensor_is_setup(generate_main): @@ -49,9 +41,9 @@ def test_text_sensor_config_value_internal_set(generate_main): # When main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") - # Then - assert "ts_2->set_internal(true);" in main_cpp - assert "ts_3->set_internal(false);" in main_cpp + # Then: ts_2 has internal: true, ts_3 has internal: false + assert extract_packed_value(main_cpp, "ts_2") & INTERNAL_BIT != 0 + assert extract_packed_value(main_cpp, "ts_3") & INTERNAL_BIT == 0 def test_text_sensor_device_class_set(generate_main): @@ -65,7 +57,7 @@ def test_text_sensor_device_class_set(generate_main): # Then: ts_2 has device_class: timestamp, ts_3 has device_class: date # so their packed values must be non-zero - packed_ts_2 = _extract_packed_value(main_cpp, "ts_2") + packed_ts_2 = extract_packed_value(main_cpp, "ts_2") assert packed_ts_2 != 0 - packed_ts_3 = _extract_packed_value(main_cpp, "ts_3") + packed_ts_3 = extract_packed_value(main_cpp, "ts_3") assert packed_ts_3 != 0 diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index 3f6faaee54..d6cbb8c6be 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -9,6 +9,7 @@ import pytest from esphome.config_validation import Invalid from esphome.const import ( + CONF_DEVICE_CLASS, CONF_DEVICE_ID, CONF_DISABLED_BY_DEFAULT, CONF_ENTITY_CATEGORY, @@ -16,16 +17,20 @@ from esphome.const import ( CONF_ID, CONF_INTERNAL, CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, ) from esphome.core import CORE, ID, entity_helpers from esphome.core.entity_helpers import ( _register_string, _setup_entity_impl, entity_duplicate_validator, + finalize_entity_strings, get_base_entity_object_id, register_device_class, register_icon, + setup_device_class, setup_entity, + setup_unit_of_measurement, ) from esphome.cpp_generator import MockObj from esphome.helpers import sanitize, snake_case @@ -486,8 +491,6 @@ async def test_setup_entity_disabled_by_default( ) -> None: """Test setup_entity sets disabled_by_default correctly.""" - added_expressions = setup_test_environment - var = MockObj("sensor1") config = { @@ -497,10 +500,8 @@ async def test_setup_entity_disabled_by_default( await _setup_entity_impl(var, config, "sensor") - # Check disabled_by_default was set - assert any( - "sensor1.set_disabled_by_default(true)" in expr for expr in added_expressions - ) + # disabled_by_default is now packed into config for configure_entity_() + assert config.get("_entity_disabled_by_default") == 1 def test_entity_duplicate_validator() -> None: @@ -785,8 +786,8 @@ async def test_setup_entity_empty_name_with_device( entity_helpers.get_variable = original_get_variable - # Check that set_device was called - assert any("sensor1.set_device" in expr for expr in added_expressions) + # Check that set_device_ was called (separate protected call, accessible via friend) + assert any("sensor1.set_device_" in expr for expr in added_expressions) # For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime assert config.get("_entity_name") == "" @@ -928,7 +929,7 @@ def test_register_device_class_max_length() -> None: async def test_setup_entity_with_entity_category( setup_test_environment: list[str], ) -> None: - """Test setup_entity sets entity_category correctly.""" + """Test entity_category is packed correctly through the full setup flow.""" added_expressions = setup_test_environment var = MockObj("sensor1") config = { @@ -937,9 +938,10 @@ async def test_setup_entity_with_entity_category( CONF_ENTITY_CATEGORY: "diagnostic", } await _setup_entity_impl(var, config, "sensor") - assert any( - 'set_entity_category("diagnostic")' in expr for expr in added_expressions - ) + finalize_entity_strings(var, config) + packed = _extract_packed_value(added_expressions) + assert packed != 0 + assert "category:diagnostic" in added_expressions[0] @pytest.mark.asyncio @@ -988,3 +990,199 @@ async def test_setup_entity_decorator_mode(setup_test_environment: list[str]) -> assert body_called object_id = extract_object_id_from_expressions(added_expressions) assert object_id == "temperature" + + +# Tests for finalize_entity_strings packing +# +# These tests verify that flags and string indices produce non-zero packed values +# and correct inline comments. The actual bit layout correctness (Python _*_SHIFT +# matching C++ ENTITY_FIELD_*_SHIFT) is verified end-to-end by the integration +# test test_host_mode_entity_fields, which compiles firmware and checks values +# via the native API. + + +def _extract_packed_value(expressions: list[str]) -> int: + """Extract the third argument (packed value) from a configure_entity_() call.""" + for expr in expressions: + if "configure_entity_" in expr: + # Match the last integer argument before the closing ");" + match = re.search(r",\s*(\d+)\s*\)", expr) + if match: + return int(match.group(1)) + raise AssertionError("No configure_entity_ call found") + + +@pytest.mark.asyncio +async def test_finalize_no_flags(setup_test_environment: list[str]) -> None: + """Test entity with no special flags — packed value is 0, no comment.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + config = { + CONF_NAME: "Test", + CONF_DISABLED_BY_DEFAULT: False, + } + await _setup_entity_impl(var, config, "sensor") + finalize_entity_strings(var, config) + packed = _extract_packed_value(added_expressions) + assert packed == 0 + assert "//" not in added_expressions[0] + + +@pytest.mark.asyncio +async def test_finalize_internal(setup_test_environment: list[str]) -> None: + """Test entity with internal=True packs the internal flag.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + config = { + CONF_NAME: "Test", + CONF_DISABLED_BY_DEFAULT: False, + CONF_INTERNAL: True, + } + await _setup_entity_impl(var, config, "sensor") + finalize_entity_strings(var, config) + packed = _extract_packed_value(added_expressions) + assert packed != 0 + assert "// internal" in added_expressions[0] + + +@pytest.mark.asyncio +async def test_finalize_disabled_by_default( + setup_test_environment: list[str], +) -> None: + """Test entity with disabled_by_default=True packs the flag.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + config = { + CONF_NAME: "Test", + CONF_DISABLED_BY_DEFAULT: True, + } + await _setup_entity_impl(var, config, "sensor") + finalize_entity_strings(var, config) + packed = _extract_packed_value(added_expressions) + assert packed != 0 + assert "// disabled_by_default" in added_expressions[0] + + +@pytest.mark.asyncio +async def test_finalize_entity_category( + setup_test_environment: list[str], +) -> None: + """Test entity_category values are packed and described in comment.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + + # Test diagnostic + config = { + CONF_NAME: "Test", + CONF_DISABLED_BY_DEFAULT: False, + CONF_ENTITY_CATEGORY: "diagnostic", + } + await _setup_entity_impl(var, config, "sensor") + finalize_entity_strings(var, config) + packed_diag = _extract_packed_value(added_expressions) + assert packed_diag != 0 + assert "category:diagnostic" in added_expressions[0] + + # Test config — different packed value + added_expressions.clear() + config2 = { + CONF_NAME: "Test2", + CONF_DISABLED_BY_DEFAULT: False, + CONF_ENTITY_CATEGORY: "config", + } + await _setup_entity_impl(var, config2, "sensor") + finalize_entity_strings(var, config2) + packed_cfg = _extract_packed_value(added_expressions) + assert packed_cfg != 0 + assert packed_cfg != packed_diag + assert "category:config" in added_expressions[0] + + +@pytest.mark.asyncio +async def test_finalize_string_indices( + setup_test_environment: list[str], +) -> None: + """Test device_class, unit_of_measurement, and icon produce non-zero packed value.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + config = { + CONF_NAME: "Test", + CONF_DISABLED_BY_DEFAULT: False, + CONF_DEVICE_CLASS: "temperature", + CONF_UNIT_OF_MEASUREMENT: "°C", + CONF_ICON: "mdi:thermometer", + } + await _setup_entity_impl(var, config, "sensor") + setup_device_class(config) + setup_unit_of_measurement(config) + finalize_entity_strings(var, config) + packed = _extract_packed_value(added_expressions) + assert packed != 0 + comment = added_expressions[0] + assert "dc:temperature" in comment + assert "uom:°C" in comment + assert "icon:mdi:thermometer" in comment + + +@pytest.mark.asyncio +async def test_finalize_all_fields( + setup_test_environment: list[str], +) -> None: + """Test all fields set: flags, string indices, and comment.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + config = { + CONF_NAME: "Test", + CONF_DISABLED_BY_DEFAULT: True, + CONF_INTERNAL: True, + CONF_ENTITY_CATEGORY: "diagnostic", + CONF_DEVICE_CLASS: "temperature", + CONF_UNIT_OF_MEASUREMENT: "°C", + CONF_ICON: "mdi:thermometer", + } + await _setup_entity_impl(var, config, "sensor") + setup_device_class(config) + setup_unit_of_measurement(config) + finalize_entity_strings(var, config) + packed = _extract_packed_value(added_expressions) + assert packed != 0 + # Verify comment contains all flags with actual string values + comment_line = added_expressions[0] + assert ( + "// internal, disabled_by_default, category:diagnostic," + " dc:temperature, uom:°C, icon:mdi:thermometer" in comment_line + ) + + +@pytest.mark.asyncio +async def test_finalize_comment_sanitization( + setup_test_environment: list[str], +) -> None: + """Test that user strings in comments are sanitized against injection.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + config = { + CONF_NAME: "Test", + CONF_DISABLED_BY_DEFAULT: False, + # Backslash at end would cause line splice eating next code line + CONF_ICON: "mdi:evil\\", + } + await _setup_entity_impl(var, config, "sensor") + finalize_entity_strings(var, config) + comment_line = added_expressions[0] + # Backslash must be replaced to prevent line splice + assert "\\" not in comment_line + assert "mdi:evil/" in comment_line + + added_expressions.clear() + config2 = { + CONF_NAME: "Test2", + CONF_DISABLED_BY_DEFAULT: False, + CONF_ICON: "mdi:evil\nINJECTED_CODE();", + } + await _setup_entity_impl(var, config2, "sensor") + finalize_entity_strings(var, config2) + comment_line = added_expressions[0] + # Newline must be replaced to prevent breaking out of comment + assert "\n" not in comment_line + assert "INJECTED_CODE" in comment_line # still visible but safe in comment From c681dc8872f88f894bb3b71026ffbec013c1bdc9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 15:11:24 -1000 Subject: [PATCH 477/609] [socket] Add socket wake support for RP2040 (#14498) --- .../components/socket/lwip_raw_tcp_impl.cpp | 73 ++++++++++++++++++- esphome/components/socket/socket.h | 12 ++- esphome/core/application.cpp | 8 +- esphome/core/application.h | 8 +- esphome/core/component.cpp | 4 +- 5 files changed, 93 insertions(+), 12 deletions(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index d697bd47a5..445a57809d 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -11,6 +11,9 @@ #ifdef USE_ESP8266 #include <coredecls.h> // For esp_schedule() +#elif defined(USE_RP2040) +#include <hardware/sync.h> // For __sev(), __wfe() +#include <pico/time.h> // For add_alarm_in_ms(), cancel_alarm() #endif namespace esphome::socket { @@ -40,6 +43,72 @@ void IRAM_ATTR socket_wake() { s_socket_woke = true; esp_schedule(); } +#elif defined(USE_RP2040) +// RP2040 (non-FreeRTOS) socket wake using hardware WFE/SEV instructions. +// +// Same pattern as ESP8266's esp_delay()/esp_schedule(): set a one-shot timer, +// then sleep with __wfe(). Wake on either: +// - Timer alarm fires → callback calls __sev() → __wfe() returns → timeout +// - Socket data arrives → LWIP callback calls socket_wake() → __sev() → __wfe() returns → early wake +// +// CYW43 WiFi chip communicates via SPI interrupts on core 0. When data arrives, +// the GPIO interrupt fires → async_context pendsv processes CYW43/LWIP → recv/accept +// callbacks call socket_wake() → __sev() wakes the main loop from __wfe() sleep. +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +static volatile bool s_socket_woke = false; +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +static volatile bool s_delay_expired = false; + +static int64_t alarm_callback(alarm_id_t id, void *user_data) { + (void) id; + (void) user_data; + s_delay_expired = true; + // Wake the main loop from __wfe() sleep — timeout expired. + __sev(); + // Return 0 = don't reschedule (one-shot) + return 0; +} + +void socket_delay(uint32_t ms) { + if (ms == 0) { + yield(); + return; + } + // If a wake was already signalled, consume it and return immediately + // instead of going to sleep. This avoids losing a wake that arrived + // between loop iterations. + if (s_socket_woke) { + s_socket_woke = false; + return; + } + s_socket_woke = false; + s_delay_expired = false; + // Set a one-shot timer to wake us after the timeout. + // add_alarm_in_ms returns >0 on success, 0 if time already passed, <0 on error. + alarm_id_t alarm = add_alarm_in_ms(ms, alarm_callback, nullptr, true); + if (alarm <= 0) { + delay(ms); + return; + } + // Sleep until woken by either the timer alarm or socket_wake(). + // __wfe() may return spuriously (stale event register, other interrupts), + // so we loop checking both flags. + while (!s_socket_woke && !s_delay_expired) { + __wfe(); + } + // Cancel timer if we woke early (socket data arrived before timeout) + if (!s_delay_expired) + cancel_alarm(alarm); +} + +// No IRAM_ATTR equivalent needed: on RP2040, CYW43 async_context runs LWIP +// callbacks via pendsv (not hard IRQ), so they execute from flash safely. +void socket_wake() { + s_socket_woke = true; + // Wake the main loop from __wfe() sleep. __sev() is a global event that + // wakes any core sleeping in __wfe(). This is ISR-safe. + __sev(); +} #endif static const char *const TAG = "socket.lwip"; @@ -371,7 +440,7 @@ err_t LWIPRawImpl::recv_fn(struct pbuf *pb, err_t err) { } else { pbuf_cat(this->rx_buf_, pb); } -#ifdef USE_ESP8266 +#if (defined(USE_ESP8266) || defined(USE_RP2040)) // Wake the main loop immediately so it can process the received data. socket_wake(); #endif @@ -650,7 +719,7 @@ err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) { sock->init(); this->accepted_sockets_[this->accepted_socket_count_++] = std::move(sock); LWIP_LOG("Accepted connection, queue size: %d", this->accepted_socket_count_); -#ifdef USE_ESP8266 +#if (defined(USE_ESP8266) || defined(USE_RP2040)) // Wake the main loop immediately so it can accept the new connection. socket_wake(); #endif diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 0884e4ba3e..a21bd64730 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -120,13 +120,17 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po /// Format sockaddr into caller-provided buffer, returns length written (excluding null) size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span<char, SOCKADDR_STR_LEN> buf); -#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) +#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) /// Delay that can be woken early by socket activity. -/// On ESP8266, lwip callbacks set a flag and call esp_schedule() to wake the delay. +/// On ESP8266, uses esp_delay() with a callback that checks socket activity. +/// On RP2040, uses __wfe() (Wait For Event) to truly sleep until an interrupt +/// (for example, CYW43 GPIO or a timer alarm) fires and wakes the CPU. void socket_delay(uint32_t ms); -/// Signal socket/IO activity and wake the main loop from esp_delay() early. -/// ISR-safe: uses IRAM_ATTR internally and only sets a volatile flag + esp_schedule(). +/// Signal socket/IO activity and wake the main loop early. +/// On ESP8266: sets flag + esp_schedule(). +/// On RP2040: sets flag + __sev() (Send Event) to wake from __wfe(). +/// ISR-safe on both platforms. void socket_wake(); // NOLINT(readability-redundant-declaration) #endif diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index db1c8a0c0a..8685bff360 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -32,7 +32,7 @@ #include "esphome/components/status_led/status_led.h" #endif -#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) +#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) #include "esphome/components/socket/socket.h" #endif @@ -713,8 +713,10 @@ void Application::yield_with_select_(uint32_t delay_ms) { } // No sockets registered or select() failed - use regular delay delay(delay_ms); -#elif defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) - // No select support but can wake on socket activity via esp_schedule() +#elif (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) + // No select support but can wake on socket activity + // ESP8266: via esp_schedule() + // RP2040: via __sev()/__wfe() hardware sleep/wake socket::socket_delay(delay_ms); #else // No select support, use regular delay diff --git a/esphome/core/application.h b/esphome/core/application.h index 49253b6324..f357c6b1a3 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -34,7 +34,7 @@ #endif #endif #endif // USE_SOCKET_SELECT_SUPPORT -#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) +#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) namespace esphome::socket { void socket_wake(); // NOLINT(readability-redundant-declaration) } // namespace esphome::socket @@ -565,8 +565,12 @@ class Application { #if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) /// Wake the main event loop from any context (ISR, thread, or main loop). - /// On ESP8266: sets the socket wake flag and calls esp_schedule() to exit esp_delay() early. + /// Sets the socket wake flag and calls esp_schedule() to exit esp_delay() early. static void IRAM_ATTR wake_loop_any_context() { socket::socket_wake(); } +#elif defined(USE_RP2040) && defined(USE_SOCKET_IMPL_LWIP_TCP) + /// Wake the main event loop from any context. + /// Sets the socket wake flag and calls __sev() to exit __wfe() early. + static void wake_loop_any_context() { socket::socket_wake(); } #endif protected: diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 2879d4b5ab..cce0c7b3e0 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -322,11 +322,13 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { // 8. Race condition with main loop is handled by clearing flag before processing this->pending_enable_loop_ = true; App.has_pending_enable_loop_requests_ = true; -#if (defined(USE_LWIP_FAST_SELECT) && defined(USE_ESP32)) || (defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP)) +#if (defined(USE_LWIP_FAST_SELECT) && defined(USE_ESP32)) || \ + ((defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP)) // Wake the main loop from sleep. Without this, the main loop would not // wake until the select/delay timeout expires (~16ms). // ESP32: uses xPortInIsrContext() to choose the correct FreeRTOS notify API. // ESP8266: sets socket wake flag and calls esp_schedule() to exit esp_delay() early. + // RP2040: sets socket wake flag and calls __sev() to exit __wfe() early. Application::wake_loop_any_context(); #endif } From 6ba5c9a7056a07fe431ebf3689dbda307892ea85 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 15:22:39 -1000 Subject: [PATCH 478/609] [api] Skip state_action_() call in noise data path (#14629) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/api/api_frame_helper.h | 11 +++++++++++ .../components/api/api_frame_helper_noise.cpp | 18 ++++-------------- .../api/api_frame_helper_plaintext.cpp | 14 +++++++------- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 2b4e9ea3cd..151314658e 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -232,6 +232,17 @@ class APIFrameHelper { EXPLICIT_REJECT = 8, // Noise only }; + // Fast inline state check for read_packet/write_protobuf_messages hot path. + // Returns OK only in DATA state; maps CLOSED/FAILED to BAD_STATE and any + // other intermediate state to WOULD_BLOCK. + inline APIError ESPHOME_ALWAYS_INLINE check_data_state_() const { + if (this->state_ == State::DATA) + return APIError::OK; + if (this->state_ == State::CLOSED || this->state_ == State::FAILED) + return APIError::BAD_STATE; + return APIError::WOULD_BLOCK; + } + // Containers (size varies, but typically 12+ bytes on 32-bit) std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_; std::vector<uint8_t> rx_buf_; diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index ba4f2f0642..62523fb835 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -397,14 +397,9 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso state_ = orig_state; } APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { - APIError aerr = this->state_action_(); - if (aerr != APIError::OK) { + APIError aerr = this->check_data_state_(); + if (aerr != APIError::OK) return aerr; - } - - if (this->state_ != State::DATA) { - return APIError::WOULD_BLOCK; - } aerr = this->try_read_frame_(); if (aerr != APIError::OK) @@ -461,14 +456,9 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuff } APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) { - APIError aerr = state_action_(); - if (aerr != APIError::OK) { + APIError aerr = this->check_data_state_(); + if (aerr != APIError::OK) return aerr; - } - - if (state_ != State::DATA) { - return APIError::WOULD_BLOCK; - } if (messages.empty()) { return APIError::OK; diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index e2bb56e0ac..3c54ed7c70 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -195,11 +195,11 @@ APIError APIPlaintextFrameHelper::try_read_frame_() { } APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { - if (this->state_ != State::DATA) { - return APIError::WOULD_BLOCK; - } + APIError aerr = this->check_data_state_(); + if (aerr != APIError::OK) + return aerr; - APIError aerr = this->try_read_frame_(); + aerr = this->try_read_frame_(); if (aerr != APIError::OK) { if (aerr == APIError::BAD_INDICATOR) { // Make sure to tell the remote that we don't @@ -244,9 +244,9 @@ APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWrite APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) { - if (state_ != State::DATA) { - return APIError::BAD_STATE; - } + APIError aerr = this->check_data_state_(); + if (aerr != APIError::OK) + return aerr; if (messages.empty()) { return APIError::OK; From cac751e9e8d1a185ede1f001bb19cc397a3b6a7f Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Mon, 9 Mar 2026 02:29:41 +0100 Subject: [PATCH 479/609] [nextion] Add configurable HTTP parameters for TFT upload (#14234) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/components/nextion/base_component.py | 3 + esphome/components/nextion/display.py | 66 +++++++++++++++++-- esphome/components/nextion/nextion.cpp | 12 ++++ esphome/components/nextion/nextion.h | 31 +++++++++ .../nextion/nextion_upload_arduino.cpp | 10 +-- .../nextion/nextion_upload_esp32.cpp | 8 ++- .../components/nextion/common_tft_upload.yaml | 5 ++ .../nextion/common_tft_upload_watchdog.yaml | 3 + tests/components/nextion/test.esp32-ard.yaml | 6 +- tests/components/nextion/test.esp32-idf.yaml | 6 +- .../components/nextion/test.esp8266-ard.yaml | 5 +- 11 files changed, 132 insertions(+), 23 deletions(-) create mode 100644 tests/components/nextion/common_tft_upload.yaml create mode 100644 tests/components/nextion/common_tft_upload_watchdog.yaml diff --git a/esphome/components/nextion/base_component.py b/esphome/components/nextion/base_component.py index 86551cbe23..7705b21b0b 100644 --- a/esphome/components/nextion/base_component.py +++ b/esphome/components/nextion/base_component.py @@ -27,6 +27,9 @@ CONF_PRECISION = "precision" CONF_SKIP_CONNECTION_HANDSHAKE = "skip_connection_handshake" CONF_START_UP_PAGE = "start_up_page" CONF_STARTUP_OVERRIDE_MS = "startup_override_ms" +CONF_TFT_UPLOAD_HTTP_RETRIES = "tft_upload_http_retries" +CONF_TFT_UPLOAD_HTTP_TIMEOUT = "tft_upload_http_timeout" +CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT = "tft_upload_watchdog_timeout" CONF_TFT_URL = "tft_url" CONF_TOUCH_SLEEP_TIMEOUT = "touch_sleep_timeout" CONF_VARIABLE_NAME = "variable_name" diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 3bfcc95995..b8fcd5d8cf 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -33,15 +33,24 @@ from .base_component import ( CONF_SKIP_CONNECTION_HANDSHAKE, CONF_START_UP_PAGE, CONF_STARTUP_OVERRIDE_MS, + CONF_TFT_UPLOAD_HTTP_RETRIES, + CONF_TFT_UPLOAD_HTTP_TIMEOUT, + CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT, CONF_TFT_URL, CONF_TOUCH_SLEEP_TIMEOUT, CONF_WAKE_UP_PAGE, ) CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"] - DEPENDENCIES = ["uart"] -AUTO_LOAD = ["binary_sensor", "switch", "sensor", "text_sensor"] + + +def AUTO_LOAD() -> list[str]: + base = ["binary_sensor", "switch", "sensor", "text_sensor"] + if CORE.is_esp32: + base.append("watchdog") + return base + NextionSetBrightnessAction = nextion_ns.class_( "NextionSetBrightnessAction", automation.Action @@ -55,7 +64,24 @@ BufferOverflowTrigger = nextion_ns.class_( "BufferOverflowTrigger", automation.Trigger.template() ) -CONFIG_SCHEMA = ( + +def _validate_tft_upload(config): + has_tft_url = CONF_TFT_URL in config + for conf_key in ( + CONF_TFT_UPLOAD_HTTP_TIMEOUT, + CONF_TFT_UPLOAD_HTTP_RETRIES, + CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT, + ): + if conf_key in config and not has_tft_url: + raise cv.Invalid(f"{conf_key} requires {CONF_TFT_URL} to be set") + if CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT in config and not CORE.is_esp32: + raise cv.Invalid( + f"{CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT} is only available on ESP32" + ) + return config + + +CONFIG_SCHEMA = cv.All( display.BASIC_DISPLAY_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(Nextion), @@ -115,6 +141,14 @@ CONFIG_SCHEMA = ( ), ), cv.Optional(CONF_START_UP_PAGE): cv.uint8_t, + cv.Optional(CONF_TFT_UPLOAD_HTTP_RETRIES): cv.int_range(min=1, max=255), + cv.Optional(CONF_TFT_UPLOAD_HTTP_TIMEOUT): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=TimePeriod(milliseconds=65535)), + ), + cv.Optional( + CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT + ): cv.positive_time_period_milliseconds, cv.Optional(CONF_TFT_URL): cv.url, cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.Any( 0, cv.int_range(min=3, max=65535) @@ -123,7 +157,8 @@ CONFIG_SCHEMA = ( } ) .extend(cv.polling_component_schema("5s")) - .extend(uart.UART_DEVICE_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA), + _validate_tft_upload, ) @@ -176,6 +211,29 @@ async def to_code(config): if CONF_TFT_URL in config: cg.add_define("USE_NEXTION_TFT_UPLOAD") cg.add(var.set_tft_url(config[CONF_TFT_URL])) + + # TFT upload HTTP timeout (default: 4.5s) + if CONF_TFT_UPLOAD_HTTP_TIMEOUT in config: + cg.add( + var.set_tft_upload_http_timeout( + config[CONF_TFT_UPLOAD_HTTP_TIMEOUT].total_milliseconds + ) + ) + + # TFT upload HTTP retries (default: 5) + if CONF_TFT_UPLOAD_HTTP_RETRIES in config: + cg.add( + var.set_tft_upload_http_retries(config[CONF_TFT_UPLOAD_HTTP_RETRIES]) + ) + + # TFT upload watchdog timeout (default: 0 = no adjustment) + if CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT in config: + cg.add( + var.set_tft_upload_watchdog_timeout( + config[CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT].total_milliseconds + ) + ) + if CORE.is_esp32: # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time) esp32.include_builtin_idf_component("esp_http_client") diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index c8c1b6fa41..cb20c34005 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -191,6 +191,18 @@ void Nextion::dump_config() { #ifdef USE_NEXTION_MAX_QUEUE_SIZE ESP_LOGCONFIG(TAG, " Max queue size: %zu", this->max_queue_size_); #endif +#ifdef USE_NEXTION_TFT_UPLOAD + ESP_LOGCONFIG(TAG, + " TFT URL: %s\n" + " TFT upload HTTP timeout: %" PRIu16 "ms\n" + " TFT upload HTTP retries: %u", + this->tft_url_.c_str(), this->tft_upload_http_timeout_, this->tft_upload_http_retries_); +#ifdef USE_ESP32 + if (this->tft_upload_watchdog_timeout_ > 0) { + ESP_LOGCONFIG(TAG, " TFT upload WDT timeout: %" PRIu32 "ms", this->tft_upload_watchdog_timeout_); + } +#endif // USE_ESP32 +#endif // USE_NEXTION_TFT_UPLOAD } void Nextion::update() { diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index c42ddba9b5..7999e3c4e3 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1071,6 +1071,33 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe bool send_command_printf(const char *format, ...) __attribute__((format(printf, 2, 3))); #ifdef USE_NEXTION_TFT_UPLOAD + /** + * @brief Set the HTTP timeout for TFT upload requests. + * @param timeout_ms Timeout in milliseconds. Defaults to 4500ms (4.5s). + */ + void set_tft_upload_http_timeout(uint16_t timeout_ms) { this->tft_upload_http_timeout_ = timeout_ms; } + +#ifdef USE_ESP32 + /** + * @brief Set the watchdog timeout during TFT upload. + * + * The system watchdog timeout is temporarily adjusted to this value + * during the entire TFT transfer process and restored to the original + * value after the transfer completes (whether successful or not). + * + * A value of 0 means no watchdog adjustment (default). + * + * @param timeout_ms Watchdog timeout in milliseconds. 0 = no adjustment. + */ + void set_tft_upload_watchdog_timeout(uint32_t timeout_ms) { this->tft_upload_watchdog_timeout_ = timeout_ms; } +#endif // USE_ESP32 + + /** + * @brief Set the number of HTTP retries for TFT upload requests. + * @param retries Number of retries. Defaults to 5. Range: 1-255. + */ + void set_tft_upload_http_retries(uint8_t retries) { this->tft_upload_http_retries_ = retries; } + /** * Set the tft file URL. */ @@ -1439,8 +1466,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe int tft_size_ = 0; uint32_t original_baud_rate_ = 0; bool upload_first_chunk_sent_ = false; + uint16_t tft_upload_http_timeout_{4500}; ///< HTTP timeout in ms (default: 4.5s) + uint8_t tft_upload_http_retries_{5}; ///< HTTP retry count (default: 5) #ifdef USE_ESP32 + uint32_t tft_upload_watchdog_timeout_{0}; ///< WDT timeout in ms (0 = no adjustment) + /** * will request 4096 bytes chunks from the web server * and send each to Nextion diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index 46a04c1b2e..e03f1f470b 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -166,7 +166,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { // Define the configuration for the HTTP client ESP_LOGV(TAG, "Init HTTP client, heap: %" PRIu32, EspClass::getFreeHeap()); HTTPClient http_client; - http_client.setTimeout(15000); // Yes 15 seconds.... Helps 8266s along + http_client.setTimeout(this->tft_upload_http_timeout_); bool begin_status = false; #ifdef USE_ESP8266 @@ -192,15 +192,15 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { http_client.collectHeaders(header_names, 1); ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str()); http_client.setReuse(true); - // try up to 5 times. DNS sometimes needs a second try or so + int tries = 1; int code = http_client.GET(); delay(100); // NOLINT App.feed_wdt(); - while (code != 200 && code != 206 && tries <= 5) { - ESP_LOGW(TAG, "HTTP fail: URL: %s; Error: %s, retry %d/5", this->tft_url_.c_str(), - HTTPClient::errorToString(code).c_str(), tries); + while (code != 200 && code != 206 && tries <= this->tft_upload_http_retries_) { + ESP_LOGW(TAG, "HTTP fail: URL: %s; Error: %s, retry %d/%u", this->tft_url_.c_str(), + HTTPClient::errorToString(code).c_str(), tries, this->tft_upload_http_retries_); delay(250); // NOLINT App.feed_wdt(); diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index 43f59a8d4b..1014c728a8 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -7,6 +7,7 @@ #include <esp_http_client.h> #include <cinttypes> #include "esphome/components/network/util.h" +#include "esphome/components/watchdog/watchdog.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" @@ -68,7 +69,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r int partial_read_len = 0; uint8_t retries = 0; // Attempt to read the chunk with retries. - while (retries < 5 && read_len < buffer_size) { + while (retries < this->tft_upload_http_retries_ && read_len < buffer_size) { partial_read_len = esp_http_client_read(http_client, reinterpret_cast<char *>(buffer) + read_len, buffer_size - read_len); if (partial_read_len > 0) { @@ -167,6 +168,9 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { return false; } + // Temporarily adjust watchdog timeout for the duration of the TFT upload + watchdog::WatchdogManager wdm(this->tft_upload_watchdog_timeout_); + this->connection_state_.is_updating_ = true; if (exit_reparse) { @@ -190,7 +194,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { .url = this->tft_url_.c_str(), .cert_pem = nullptr, .method = HTTP_METHOD_HEAD, - .timeout_ms = 15000, + .timeout_ms = static_cast<int>(this->tft_upload_http_timeout_), .disable_auto_redirect = false, .max_redirection_count = 10, }; diff --git a/tests/components/nextion/common_tft_upload.yaml b/tests/components/nextion/common_tft_upload.yaml new file mode 100644 index 0000000000..190abbc7b1 --- /dev/null +++ b/tests/components/nextion/common_tft_upload.yaml @@ -0,0 +1,5 @@ +display: + - id: !extend main_lcd + tft_url: http://esphome.io/default35.tft + tft_upload_http_timeout: 20s + tft_upload_http_retries: 10 diff --git a/tests/components/nextion/common_tft_upload_watchdog.yaml b/tests/components/nextion/common_tft_upload_watchdog.yaml new file mode 100644 index 0000000000..385fee359e --- /dev/null +++ b/tests/components/nextion/common_tft_upload_watchdog.yaml @@ -0,0 +1,3 @@ +display: + - id: !extend main_lcd + tft_upload_watchdog_timeout: 30s diff --git a/tests/components/nextion/test.esp32-ard.yaml b/tests/components/nextion/test.esp32-ard.yaml index 7e94a9b4a5..a4f71628da 100644 --- a/tests/components/nextion/test.esp32-ard.yaml +++ b/tests/components/nextion/test.esp32-ard.yaml @@ -1,7 +1,5 @@ packages: uart: !include ../../test_build_components/common/uart/esp32-ard.yaml base: !include common.yaml - -display: - - id: !extend main_lcd - tft_url: http://esphome.io/default35.tft + tft_upload: !include common_tft_upload.yaml + tft_upload_watchdog: !include common_tft_upload_watchdog.yaml diff --git a/tests/components/nextion/test.esp32-idf.yaml b/tests/components/nextion/test.esp32-idf.yaml index 99820f0f8d..259b71c9d0 100644 --- a/tests/components/nextion/test.esp32-idf.yaml +++ b/tests/components/nextion/test.esp32-idf.yaml @@ -1,7 +1,5 @@ packages: uart: !include ../../test_build_components/common/uart/esp32-idf.yaml base: !include common.yaml - -display: - - id: !extend main_lcd - tft_url: http://esphome.io/default35.tft + tft_upload: !include common_tft_upload.yaml + tft_upload_watchdog: !include common_tft_upload_watchdog.yaml diff --git a/tests/components/nextion/test.esp8266-ard.yaml b/tests/components/nextion/test.esp8266-ard.yaml index 49f79b2f4c..a2b0e727cc 100644 --- a/tests/components/nextion/test.esp8266-ard.yaml +++ b/tests/components/nextion/test.esp8266-ard.yaml @@ -1,7 +1,4 @@ packages: uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml base: !include common.yaml - -display: - - id: !extend main_lcd - tft_url: http://esphome.io/default35.tft + tft_upload: !include common_tft_upload.yaml From 5b9cab02bee18da518c522fbdcf26b0b12fb17f6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:37:54 -0400 Subject: [PATCH 480/609] [multiple] Add default initializers to uninitialized member variables (#14636) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/duty_time/duty_time_sensor.h | 6 +++--- esphome/components/growatt_solar/growatt_solar.h | 4 ++-- esphome/components/hte501/hte501.h | 4 ++-- esphome/components/select/select_call.h | 2 +- esphome/components/uponor_smatrix/uponor_smatrix.h | 4 ++-- esphome/components/wifi_info/wifi_info_text_sensor.h | 2 +- esphome/components/x9c/x9c.h | 6 +++--- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/esphome/components/duty_time/duty_time_sensor.h b/esphome/components/duty_time/duty_time_sensor.h index d9fb2a6d60..d21802ebb6 100644 --- a/esphome/components/duty_time/duty_time_sensor.h +++ b/esphome/components/duty_time/duty_time_sensor.h @@ -41,9 +41,9 @@ class DutyTimeSensor : public sensor::Sensor, public PollingComponent { sensor::Sensor *last_duty_time_sensor_{nullptr}; ESPPreferenceObject pref_; - uint32_t total_sec_; - uint32_t last_time_; - uint32_t edge_time_; + uint32_t total_sec_{0}; + uint32_t last_time_{0}; + uint32_t edge_time_{0}; bool last_state_{false}; bool restore_; }; diff --git a/esphome/components/growatt_solar/growatt_solar.h b/esphome/components/growatt_solar/growatt_solar.h index b0ddd4b99d..833d6a36dd 100644 --- a/esphome/components/growatt_solar/growatt_solar.h +++ b/esphome/components/growatt_solar/growatt_solar.h @@ -56,8 +56,8 @@ class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { } protected: - bool waiting_to_update_; - uint32_t last_send_; + bool waiting_to_update_{false}; + uint32_t last_send_{0}; struct GrowattPhase { sensor::Sensor *voltage_sensor_{nullptr}; diff --git a/esphome/components/hte501/hte501.h b/esphome/components/hte501/hte501.h index b47daf9157..7f29885f49 100644 --- a/esphome/components/hte501/hte501.h +++ b/esphome/components/hte501/hte501.h @@ -18,8 +18,8 @@ class HTE501Component : public PollingComponent, public i2c::I2CDevice { void update() override; protected: - sensor::Sensor *temperature_sensor_; - sensor::Sensor *humidity_sensor_; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE}; }; diff --git a/esphome/components/select/select_call.h b/esphome/components/select/select_call.h index c9abbc69a0..fbe7b82e92 100644 --- a/esphome/components/select/select_call.h +++ b/esphome/components/select/select_call.h @@ -43,7 +43,7 @@ class SelectCall { Select *const parent_; optional<size_t> index_; SelectOperation operation_{SELECT_OP_NONE}; - bool cycle_; + bool cycle_{false}; }; } // namespace esphome::select diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.h b/esphome/components/uponor_smatrix/uponor_smatrix.h index bd760f0d77..bd20e9b6a0 100644 --- a/esphome/components/uponor_smatrix/uponor_smatrix.h +++ b/esphome/components/uponor_smatrix/uponor_smatrix.h @@ -89,8 +89,8 @@ class UponorSmatrixComponent : public uart::UARTDevice, public Component { std::vector<uint8_t> rx_buffer_; std::queue<std::vector<uint8_t>> tx_queue_; - uint32_t last_rx_; - uint32_t last_tx_; + uint32_t last_rx_{0}; + uint32_t last_tx_{0}; #ifdef USE_TIME time::RealTimeClock *time_id_{nullptr}; diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 8ef35a5f5d..7ade170c02 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -23,7 +23,7 @@ class IPAddressWiFiInfo final : public Component, public text_sensor::TextSensor const network::IPAddress &dns2) override; protected: - std::array<text_sensor::TextSensor *, 5> ip_sensors_; + std::array<text_sensor::TextSensor *, 5> ip_sensors_{}; }; class DNSAddressWifiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiIPStateListener { diff --git a/esphome/components/x9c/x9c.h b/esphome/components/x9c/x9c.h index 66c3df14e1..7dcd79bb7c 100644 --- a/esphome/components/x9c/x9c.h +++ b/esphome/components/x9c/x9c.h @@ -25,9 +25,9 @@ class X9cOutput : public output::FloatOutput, public Component { InternalGPIOPin *cs_pin_; InternalGPIOPin *inc_pin_; InternalGPIOPin *ud_pin_; - float initial_value_; - float pot_value_; - int step_delay_; + float initial_value_{0.0f}; + float pot_value_{0.0f}; + int step_delay_{0}; }; } // namespace x9c From 5d3893368d003a0904554e9170a1645f5c687efe Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 8 Mar 2026 23:16:32 -0400 Subject: [PATCH 481/609] [multiple] Add array bounds checks (#14635) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .../addressable_light/addressable_light_display.cpp | 5 ++++- esphome/components/bme680_bsec/bme680_bsec.cpp | 2 +- esphome/components/bme68x_bsec2/bme68x_bsec2.cpp | 1 + esphome/components/dac7678/dac7678_output.cpp | 2 ++ esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp | 5 +++-- esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp | 6 ++++++ 6 files changed, 17 insertions(+), 4 deletions(-) diff --git a/esphome/components/addressable_light/addressable_light_display.cpp b/esphome/components/addressable_light/addressable_light_display.cpp index 16fab15b17..329620bcf0 100644 --- a/esphome/components/addressable_light/addressable_light_display.cpp +++ b/esphome/components/addressable_light/addressable_light_display.cpp @@ -58,7 +58,10 @@ void HOT AddressableLightDisplay::draw_absolute_pixel_internal(int x, int y, Col if (this->pixel_mapper_f_.has_value()) { // Params are passed by reference, so they may be modified in call. - this->addressable_light_buffer_[(*this->pixel_mapper_f_)(x, y)] = color; + int index = (*this->pixel_mapper_f_)(x, y); + if (index < 0 || static_cast<size_t>(index) >= this->addressable_light_buffer_.size()) + return; + this->addressable_light_buffer_[index] = color; } else { this->addressable_light_buffer_[y * this->get_width_internal() + x] = color; } diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index 392d071b31..454be0c0fe 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -383,7 +383,7 @@ void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_out switch (outputs[i].sensor_id) { case BSEC_OUTPUT_IAQ: case BSEC_OUTPUT_STATIC_IAQ: { - uint8_t accuracy = outputs[i].accuracy; + uint8_t accuracy = std::min<uint8_t>(outputs[i].accuracy, std::size(IAQ_ACCURACY_STATES) - 1); this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); }); this->queue_push_([this, accuracy]() { this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]); diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index 1a42c9d54b..0210d1e67d 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -438,6 +438,7 @@ void BME68xBSEC2Component::publish_(const bsec_output_t *outputs, uint8_t num_ou } } if (update_accuracy) { + max_accuracy = std::min<uint8_t>(max_accuracy, std::size(IAQ_ACCURACY_STATES) - 1); #ifdef USE_SENSOR this->queue_push_( [this, max_accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, max_accuracy, true); }); diff --git a/esphome/components/dac7678/dac7678_output.cpp b/esphome/components/dac7678/dac7678_output.cpp index 83f8722e7f..27ab54f0be 100644 --- a/esphome/components/dac7678/dac7678_output.cpp +++ b/esphome/components/dac7678/dac7678_output.cpp @@ -62,6 +62,8 @@ void DAC7678Output::register_channel(DAC7678Channel *channel) { } void DAC7678Output::set_channel_value_(uint8_t channel, uint16_t value) { + if (channel >= std::size(this->dac_input_reg_)) + return; if (this->dac_input_reg_[channel] != value) { ESP_LOGV(TAG, "Channel %01u: input_reg=%04u ", channel, value); diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp index 99d519b434..263603704a 100644 --- a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp @@ -452,7 +452,8 @@ void MR24HPC1Component::r24_frame_parse_open_underlying_information_(uint8_t *da } break; case 0x83: - if (this->custom_presence_of_detection_sensor_ != nullptr) { + if (this->custom_presence_of_detection_sensor_ != nullptr && + data[FRAME_DATA_INDEX] < std::size(S_PRESENCE_OF_DETECTION_RANGE_STR)) { this->custom_presence_of_detection_sensor_->publish_state( S_PRESENCE_OF_DETECTION_RANGE_STR[data[FRAME_DATA_INDEX]]); } @@ -646,7 +647,7 @@ void MR24HPC1Component::r24_frame_parse_human_information_(uint8_t *data) { #ifdef USE_BINARY_SENSOR case 0x01: case 0x81: - if (this->has_target_binary_sensor_ != nullptr) { + if (this->has_target_binary_sensor_ != nullptr && data[FRAME_DATA_INDEX] < std::size(S_SOMEONE_EXISTS_STR)) { this->has_target_binary_sensor_->publish_state(S_SOMEONE_EXISTS_STR[data[FRAME_DATA_INDEX]]); } break; diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp index c6527a948e..e24e9b338e 100644 --- a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp @@ -334,6 +334,8 @@ void MR60FDA2Component::process_frame_() { // Send Heartbeat Packet Command void MR60FDA2Component::set_install_height(uint8_t index) { + if (index >= std::size(INSTALL_HEIGHT)) + return; uint8_t send_data[13] = {0x01, 0x00, 0x00, 0x00, 0x04, 0x0E, 0x04, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00}; float_to_bytes(INSTALL_HEIGHT[index], &send_data[8]); send_data[12] = calculate_checksum(send_data + 8, 4); @@ -345,6 +347,8 @@ void MR60FDA2Component::set_install_height(uint8_t index) { } void MR60FDA2Component::set_height_threshold(uint8_t index) { + if (index >= std::size(HEIGHT_THRESHOLD)) + return; uint8_t send_data[13] = {0x01, 0x00, 0x00, 0x00, 0x04, 0x0E, 0x08, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00}; float_to_bytes(HEIGHT_THRESHOLD[index], &send_data[8]); send_data[12] = calculate_checksum(send_data + 8, 4); @@ -356,6 +360,8 @@ void MR60FDA2Component::set_height_threshold(uint8_t index) { } void MR60FDA2Component::set_sensitivity(uint8_t index) { + if (index >= std::size(SENSITIVITY)) + return; uint8_t send_data[13] = {0x01, 0x00, 0x00, 0x00, 0x04, 0x0E, 0x0A, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00}; int_to_bytes(SENSITIVITY[index], &send_data[8]); From 088a8a4338940c061fa181b55845a5d25c6268ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 17:23:58 -1000 Subject: [PATCH 482/609] [ci] Match symbols with changed signatures in memory impact analysis (#14600) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- script/ci_memory_impact_comment.py | 75 ++++++++++++++ tests/unit_tests/analyze_memory/__init__.py | 0 .../test_ci_memory_impact_comment.py | 99 +++++++++++++++++++ 3 files changed, 174 insertions(+) create mode 100644 tests/unit_tests/analyze_memory/__init__.py create mode 100644 tests/unit_tests/analyze_memory/test_ci_memory_impact_comment.py diff --git a/script/ci_memory_impact_comment.py b/script/ci_memory_impact_comment.py index a296130645..01316da27f 100755 --- a/script/ci_memory_impact_comment.py +++ b/script/ci_memory_impact_comment.py @@ -160,6 +160,76 @@ def format_change(before: int, after: int, threshold: float | None = None) -> st return f"{emoji} {delta_str} ({pct_str})" +def _sig_base(sym: str) -> str: + """Strip argument types from a symbol name for fuzzy matching. + + Removes the entire outermost parenthesized argument list (including + the parentheses) from the symbol string. + + This makes, for example, "foo(int)::nested" and "foo(float)::nested" + share the same key "foo::nested", while "foo(int)" maps to "foo" and + therefore does NOT collide with "foo(int)::nested". + """ + start = sym.find("(") + if start == -1: + return sym + end = sym.rfind(")") + if end == -1: + return sym + return sym[:start] + sym[end + 1 :] + + +_AMBIGUOUS = object() + + +def _match_signature_changes( + changed_symbols: list[tuple[str, int, int, int]], + new_symbols: list[tuple[str, int]], + removed_symbols: list[tuple[str, int]], +) -> tuple[ + list[tuple[str, int, int, int]], + list[tuple[str, int]], + list[tuple[str, int]], +]: + """Match new/removed symbol pairs that only differ in argument types. + + When a function's argument types change (e.g. foo(vector<>&) -> foo(Buffer&)), + it appears as a new + removed symbol. This matches them by base name and moves + them to changed_symbols. Only matches unambiguous 1:1 pairs. + """ + if not new_symbols or not removed_symbols: + return changed_symbols, new_symbols, removed_symbols + + # Build base -> entry maps; mark ambiguous bases with sentinel + new_by_base: dict[str, tuple[str, int] | object] = {} + for entry in new_symbols: + base = _sig_base(entry[0]) + new_by_base[base] = _AMBIGUOUS if base in new_by_base else entry + removed_by_base: dict[str, tuple[str, int] | object] = {} + for entry in removed_symbols: + base = _sig_base(entry[0]) + removed_by_base[base] = _AMBIGUOUS if base in removed_by_base else entry + + matched: set[str] = set() # matched base keys + for base, new_entry in new_by_base.items(): + if new_entry is _AMBIGUOUS: + continue + rem_entry = removed_by_base.get(base) + if rem_entry is None or rem_entry is _AMBIGUOUS: + continue + pr_sym, pr_size = new_entry + _rm_sym, target_size = rem_entry + delta = pr_size - target_size + if delta != 0: + changed_symbols.append((pr_sym, target_size, pr_size, delta)) + matched.add(base) + + if matched: + new_symbols = [e for e in new_symbols if _sig_base(e[0]) not in matched] + removed_symbols = [e for e in removed_symbols if _sig_base(e[0]) not in matched] + return changed_symbols, new_symbols, removed_symbols + + def prepare_symbol_changes_data( target_symbols: dict | None, pr_symbols: dict | None ) -> dict | None: @@ -200,6 +270,11 @@ def prepare_symbol_changes_data( delta = pr_size - target_size changed_symbols.append((symbol, target_size, pr_size, delta)) + # Match new/removed symbols that only differ in argument types + changed_symbols, new_symbols, removed_symbols = _match_signature_changes( + changed_symbols, new_symbols, removed_symbols + ) + if not changed_symbols and not new_symbols and not removed_symbols: return None diff --git a/tests/unit_tests/analyze_memory/__init__.py b/tests/unit_tests/analyze_memory/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit_tests/analyze_memory/test_ci_memory_impact_comment.py b/tests/unit_tests/analyze_memory/test_ci_memory_impact_comment.py new file mode 100644 index 0000000000..8399ac0303 --- /dev/null +++ b/tests/unit_tests/analyze_memory/test_ci_memory_impact_comment.py @@ -0,0 +1,99 @@ +"""Tests for script/ci_memory_impact_comment.py symbol matching.""" + +from pathlib import Path +import sys + +# Add script directory to path so we can import the module +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "script")) + +from ci_memory_impact_comment import prepare_symbol_changes_data # noqa: E402 + + +def test_prepare_symbol_changes_signature_match() -> None: + """Symbols with same base name but different args are matched as changed.""" + target = { + "Foo::bar(std::vector<unsigned char>&, int)": 300, + "unchanged()": 50, + } + pr = { + "Foo::bar(ProtoByteBuffer&, int)": 320, + "unchanged()": 50, + } + result = prepare_symbol_changes_data(target, pr) + assert result is not None + assert len(result["changed_symbols"]) == 1 + assert len(result["new_symbols"]) == 0 + assert len(result["removed_symbols"]) == 0 + sym, t_size, p_size, delta = result["changed_symbols"][0] + assert sym == "Foo::bar(ProtoByteBuffer&, int)" + assert t_size == 300 + assert p_size == 320 + assert delta == 20 + + +def test_prepare_symbol_changes_ambiguous_overloads_not_matched() -> None: + """Multiple overloads with same base name stay as new/removed.""" + target = { + "Foo::bar(int)": 100, + "Foo::bar(float)": 200, + } + pr = { + "Foo::bar(double)": 150, + "Foo::bar(long)": 250, + } + result = prepare_symbol_changes_data(target, pr) + assert result is not None + assert len(result["changed_symbols"]) == 0 + assert len(result["new_symbols"]) == 2 + assert len(result["removed_symbols"]) == 2 + + +def test_prepare_symbol_changes_no_parens_not_matched() -> None: + """Symbols without parens (variables) are not fuzzy-matched.""" + target = {"my_global_var": 100} + pr = {"my_global_var_v2": 120} + result = prepare_symbol_changes_data(target, pr) + assert result is not None + assert len(result["changed_symbols"]) == 0 + assert len(result["new_symbols"]) == 1 + assert len(result["removed_symbols"]) == 1 + + +def test_prepare_symbol_changes_nested_symbols_matched_separately() -> None: + """Nested symbols like ::__pstr__ don't collide with parent function.""" + target = { + "Foo::bar(std::vector<unsigned char>&, int)": 300, + "Foo::bar(std::vector<unsigned char>&, int)::__pstr__": 19, + } + pr = { + "Foo::bar(ProtoByteBuffer&, int)": 320, + "Foo::bar(ProtoByteBuffer&, int)::__pstr__": 19, + } + result = prepare_symbol_changes_data(target, pr) + assert result is not None + # Both the function and its nested __pstr__ should be matched (not new/removed) + assert len(result["new_symbols"]) == 0 + assert len(result["removed_symbols"]) == 0 + # __pstr__ has delta=0 so it's silently dropped, only the function shows + assert len(result["changed_symbols"]) == 1 + sym, t_size, p_size, delta = result["changed_symbols"][0] + assert sym == "Foo::bar(ProtoByteBuffer&, int)" + assert delta == 20 + + +def test_prepare_symbol_changes_exact_match_preferred() -> None: + """Exact name matches are found before fuzzy matching runs.""" + target = { + "Foo::bar(int)": 100, + } + pr = { + "Foo::bar(int)": 120, + } + result = prepare_symbol_changes_data(target, pr) + assert result is not None + assert len(result["changed_symbols"]) == 1 + assert len(result["new_symbols"]) == 0 + assert len(result["removed_symbols"]) == 0 + sym, t_size, p_size, delta = result["changed_symbols"][0] + assert sym == "Foo::bar(int)" + assert delta == 20 From f3ca86b67017991a13a1cb27b242b373e64ed29e Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:48:03 +1100 Subject: [PATCH 483/609] [ci-custom] Directions on constant hoisting (#14637) --- script/ci-custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/ci-custom.py b/script/ci-custom.py index 8e1652b505..06fcdadb8c 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -519,7 +519,7 @@ def lint_constants_usage(): continue errs.append( f"Constant {highlight(constant)} is defined in {len(uses)} files. Please move all definitions of the " - f"constant to const.py (Uses: {', '.join(str(u) for u in uses)}) in a separate PR. " + f"constant to esphome/components/const/__init__.py (Uses: {', '.join(str(u) for u in uses)}) in a separate PR. " "See https://developers.esphome.io/contributing/code/#python" ) return errs From 0db9137d9101a04c4b2a8836e13308de60280c35 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 00:10:48 -0400 Subject: [PATCH 484/609] [multiple] Add division by zero guards (#14634) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick+github@koston.org> --- esphome/components/bl0942/bl0942.cpp | 2 +- esphome/components/combination/combination.cpp | 11 ++++++++++- esphome/components/graph/graph.cpp | 2 +- esphome/components/mics_4514/mics_4514.cpp | 6 ++++++ esphome/components/tsl2561/tsl2561.cpp | 4 ++++ esphome/components/ufire_ec/ufire_ec.cpp | 14 +++++++++++--- esphome/components/ufire_ec/ufire_ec.h | 2 +- esphome/components/xxtea/xxtea.cpp | 4 ++++ 8 files changed, 38 insertions(+), 7 deletions(-) diff --git a/esphome/components/bl0942/bl0942.cpp b/esphome/components/bl0942/bl0942.cpp index 16ad33141d..7d38597423 100644 --- a/esphome/components/bl0942/bl0942.cpp +++ b/esphome/components/bl0942/bl0942.cpp @@ -173,7 +173,7 @@ void BL0942::received_package_(DataPacket *data) { float i_rms = (uint24_t) data->i_rms / current_reference_; float watt = (int24_t) data->watt / power_reference_; float total_energy_consumption = cf_cnt / energy_reference_; - float frequency = 1000000.0f / data->frequency; + float frequency = data->frequency != 0 ? 1000000.0f / data->frequency : NAN; if (voltage_sensor_ != nullptr) { voltage_sensor_->publish_state(v_rms); diff --git a/esphome/components/combination/combination.cpp b/esphome/components/combination/combination.cpp index ece7cca482..2f0bd26a02 100644 --- a/esphome/components/combination/combination.cpp +++ b/esphome/components/combination/combination.cpp @@ -163,7 +163,7 @@ void MeanCombinationComponent::handle_new_value(float value) { return; float sum = 0.0; - size_t count = 0.0; + size_t count = 0; for (const auto &sensor : this->sensors_) { if (std::isfinite(sensor->state)) { @@ -172,6 +172,10 @@ void MeanCombinationComponent::handle_new_value(float value) { } } + if (count == 0) { + this->publish_state(NAN); + return; + } float mean = sum / count; this->publish_state(mean); @@ -238,6 +242,11 @@ void RangeCombinationComponent::handle_new_value(float value) { } } + if (sensor_states.empty()) { + this->publish_state(NAN); + return; + } + sort(sensor_states.begin(), sensor_states.end()); float range = sensor_states.back() - sensor_states.front(); diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index c43cd07fe0..801c97e3f5 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -171,7 +171,7 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo bool prev_b = false; int16_t prev_y = 0; for (uint32_t i = 0; i < this->width_; i++) { - float v = (trace->get_tracedata()->get_value(i) - ymin) / yrange; + float v = yrange != 0 ? (trace->get_tracedata()->get_value(i) - ymin) / yrange : NAN; if (!std::isnan(v) && (thick > 0)) { int16_t x = this->width_ - 1 - i + x_offset; uint8_t bit = 1 << ((i % (thick * LineType::PATTERN_LENGTH)) / thick); diff --git a/esphome/components/mics_4514/mics_4514.cpp b/esphome/components/mics_4514/mics_4514.cpp index 60413b32d7..ce63a7d062 100644 --- a/esphome/components/mics_4514/mics_4514.cpp +++ b/esphome/components/mics_4514/mics_4514.cpp @@ -59,6 +59,12 @@ void MICS4514Component::update() { return; } + if (this->red_calibration_ == 0 || this->ox_calibration_ == 0) { + ESP_LOGW(TAG, "Calibration values are zero, retrying"); + this->status_set_warning(); + this->initial_ = true; + return; + } float red_f = (float) (power - red) / this->red_calibration_; float ox_f = (float) (power - ox) / this->ox_calibration_; diff --git a/esphome/components/tsl2561/tsl2561.cpp b/esphome/components/tsl2561/tsl2561.cpp index cb4c38a83c..bccff1fb26 100644 --- a/esphome/components/tsl2561/tsl2561.cpp +++ b/esphome/components/tsl2561/tsl2561.cpp @@ -70,6 +70,10 @@ float TSL2561Sensor::calculate_lx_(uint16_t ch0, uint16_t ch1) { return NAN; } + if (ch0 == 0) { + ESP_LOGVV(TAG, "No light detected"); + return 0.0f; + } float d0 = ch0, d1 = ch1; float ratio = d1 / d0; diff --git a/esphome/components/ufire_ec/ufire_ec.cpp b/esphome/components/ufire_ec/ufire_ec.cpp index a1c3568a1a..40e3be2757 100644 --- a/esphome/components/ufire_ec/ufire_ec.cpp +++ b/esphome/components/ufire_ec/ufire_ec.cpp @@ -1,5 +1,6 @@ #include "esphome/core/log.h" #include "ufire_ec.h" +#include <cmath> namespace esphome { namespace ufire_ec { @@ -60,9 +61,15 @@ float UFireECComponent::measure_temperature_() { return this->read_data_(REGISTE float UFireECComponent::measure_ms_() { return this->read_data_(REGISTER_MS); } -void UFireECComponent::set_solution_(float solution, float temperature) { - solution /= (1 - (this->temperature_coefficient_ * (temperature - 25))); +bool UFireECComponent::set_solution_(float solution, float temperature) { + float denom = 1 - (this->temperature_coefficient_ * (temperature - 25)); + if (std::abs(denom) < 1e-6f) { + ESP_LOGE(TAG, "Temperature compensation denominator is zero"); + return false; + } + solution /= denom; this->write_data_(REGISTER_SOLUTION, solution); + return true; } void UFireECComponent::set_compensation_(float temperature) { this->write_data_(REGISTER_COMPENSATION, temperature); } @@ -72,7 +79,8 @@ void UFireECComponent::set_coefficient_(float coefficient) { this->write_data_(R void UFireECComponent::set_temperature_(float temperature) { this->write_data_(REGISTER_TEMP, temperature); } void UFireECComponent::calibrate_probe(float solution, float temperature) { - this->set_solution_(solution, temperature); + if (!this->set_solution_(solution, temperature)) + return; this->write_byte(REGISTER_TASK, COMMAND_CALIBRATE_PROBE); } diff --git a/esphome/components/ufire_ec/ufire_ec.h b/esphome/components/ufire_ec/ufire_ec.h index bfbed1b43e..8a648b5038 100644 --- a/esphome/components/ufire_ec/ufire_ec.h +++ b/esphome/components/ufire_ec/ufire_ec.h @@ -44,7 +44,7 @@ class UFireECComponent : public PollingComponent, public i2c::I2CDevice { protected: float measure_temperature_(); float measure_ms_(); - void set_solution_(float solution, float temperature); + bool set_solution_(float solution, float temperature); void set_compensation_(float temperature); void set_coefficient_(float coefficient); void set_temperature_(float temperature); diff --git a/esphome/components/xxtea/xxtea.cpp b/esphome/components/xxtea/xxtea.cpp index aae663ee01..ba17530b24 100644 --- a/esphome/components/xxtea/xxtea.cpp +++ b/esphome/components/xxtea/xxtea.cpp @@ -7,6 +7,8 @@ static const uint32_t DELTA = 0x9e3779b9; #define MX ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^ ((sum ^ y) + (k[(p ^ e) & 7] ^ z))) void encrypt(uint32_t *v, size_t n, const uint32_t *k) { + if (n == 0) + return; uint32_t z, y, sum, e; size_t p; size_t q = 6 + 52 / n; @@ -25,6 +27,8 @@ void encrypt(uint32_t *v, size_t n, const uint32_t *k) { } void decrypt(uint32_t *v, size_t n, const uint32_t *k) { + if (n == 0) + return; uint32_t z, y, sum, e; size_t p; size_t q = 6 + 52 / n; From 31f4b4d00d5242979ca83a98acc3b60b0bf0c84b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 07:33:08 -0400 Subject: [PATCH 485/609] [multiple] Fix undefined behavior across components (#14639) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/e131/e131_packet.cpp | 3 ++- .../components/globals/globals_component.h | 2 +- .../grove_tb6612fng/grove_tb6612fng.cpp | 3 ++- esphome/components/midea/appliance_base.h | 6 +++-- esphome/components/nextion/nextion.cpp | 12 +++------ esphome/components/ruuvi_ble/ruuvi_ble.cpp | 26 +++++++++---------- .../components/tormatic/tormatic_protocol.h | 2 +- 7 files changed, 26 insertions(+), 28 deletions(-) diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index b90e6d5c91..600793f5d3 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -1,3 +1,4 @@ +#include <cstddef> #include <cstring> #include "e131.h" #ifdef USE_NETWORK @@ -57,7 +58,7 @@ union E131RawPacket { // We need to have at least one `1` value // Get the offset of `property_values[1]` -const size_t E131_MIN_PACKET_SIZE = reinterpret_cast<size_t>(&((E131RawPacket *) nullptr)->property_values[1]); +const size_t E131_MIN_PACKET_SIZE = offsetof(E131RawPacket, property_values) + sizeof(uint8_t); bool E131Component::join_igmp_groups_() { if (this->listen_method_ != E131_MULTICAST) diff --git a/esphome/components/globals/globals_component.h b/esphome/components/globals/globals_component.h index 3db29bea35..520c068e6f 100644 --- a/esphome/components/globals/globals_component.h +++ b/esphome/components/globals/globals_component.h @@ -84,7 +84,7 @@ template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public P this->rtc_ = global_preferences->make_preference<uint8_t[SZ]>(1944399030U ^ this->name_hash_); bool hasdata = this->rtc_.load(&temp); if (hasdata) { - this->value_.assign(temp + 1, temp[0]); + this->value_.assign(temp + 1, static_cast<uint8_t>(temp[0])); } this->last_checked_value_.assign(this->value_); } diff --git a/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp index 428c8ec4a8..c10fa4cf25 100644 --- a/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp @@ -139,7 +139,8 @@ void GroveMotorDriveTB6612FNG::stepper_run(StepperModeTypeT mode, int16_t steps, } void GroveMotorDriveTB6612FNG::stepper_stop() { - if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP, nullptr, 1) != i2c::ERROR_OK) { + uint8_t status = 0; + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP, &status, 1) != i2c::ERROR_OK) { ESP_LOGW(TAG, "Send stop stepper failed!"); this->status_set_warning(); return; diff --git a/esphome/components/midea/appliance_base.h b/esphome/components/midea/appliance_base.h index 060cbd996b..660d185b49 100644 --- a/esphome/components/midea/appliance_base.h +++ b/esphome/components/midea/appliance_base.h @@ -28,12 +28,14 @@ class UARTStream : public Stream { int available() override { return this->uart_->available(); } int read() override { uint8_t data; - this->uart_->read_byte(&data); + if (!this->uart_->read_byte(&data)) + return -1; return data; } int peek() override { uint8_t data; - this->uart_->peek_byte(&data); + if (!this->uart_->peek_byte(&data)) + return -1; return data; } size_t write(uint8_t data) override { diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index cb20c34005..7ae4d50fc8 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -651,11 +651,7 @@ void Nextion::process_nextion_commands_() { break; } - int value = 0; - - for (int i = 0; i < 4; ++i) { - value += to_process[i] << (8 * i); - } + int value = static_cast<int>(encode_uint32(to_process[3], to_process[2], to_process[1], to_process[0])); NextionQueue *nb = this->nextion_queue_.front(); if (!nb || !nb->component) { @@ -751,10 +747,8 @@ void Nextion::process_nextion_commands_() { index = to_process.find('\0'); variable_name = to_process.substr(0, index); // // Get variable name - int value = 0; - for (int i = 0; i < 4; ++i) { - value += to_process[i + index + 1] << (8 * i); - } + int value = static_cast<int>( + encode_uint32(to_process[index + 4], to_process[index + 3], to_process[index + 2], to_process[index + 1])); ESP_LOGN(TAG, "Sensor: %s=%d", variable_name.c_str(), value); diff --git a/esphome/components/ruuvi_ble/ruuvi_ble.cpp b/esphome/components/ruuvi_ble/ruuvi_ble.cpp index bf088873ce..07f870b60c 100644 --- a/esphome/components/ruuvi_ble/ruuvi_ble.cpp +++ b/esphome/components/ruuvi_ble/ruuvi_ble.cpp @@ -21,11 +21,11 @@ bool parse_ruuvi_data_byte(const esp32_ble_tracker::adv_data_t &adv_data, RuuviP const float temperature = temp_sign == 0 ? temp_val : -1 * temp_val; const float humidity = data[0] * 0.5f; - const float pressure = (uint16_t(data[3] << 8) + uint16_t(data[4]) + 50000.0f) / 100.0f; - const float acceleration_x = (int16_t(data[5] << 8) + int16_t(data[6])) / 1000.0f; - const float acceleration_y = (int16_t(data[7] << 8) + int16_t(data[8])) / 1000.0f; - const float acceleration_z = (int16_t(data[9] << 8) + int16_t(data[10])) / 1000.0f; - const float battery_voltage = (uint16_t(data[11] << 8) + uint16_t(data[12])) / 1000.0f; + const float pressure = (encode_uint16(data[3], data[4]) + 50000.0f) / 100.0f; + const float acceleration_x = static_cast<int16_t>(encode_uint16(data[5], data[6])) / 1000.0f; + const float acceleration_y = static_cast<int16_t>(encode_uint16(data[7], data[8])) / 1000.0f; + const float acceleration_z = static_cast<int16_t>(encode_uint16(data[9], data[10])) / 1000.0f; + const float battery_voltage = encode_uint16(data[11], data[12]) / 1000.0f; result.humidity = humidity; result.temperature = temperature; @@ -43,19 +43,19 @@ bool parse_ruuvi_data_byte(const esp32_ble_tracker::adv_data_t &adv_data, RuuviP if (adv_data.size() != 24) return false; - const float temperature = (int16_t(data[0] << 8) + int16_t(data[1])) * 0.005f; - const float humidity = (uint16_t(data[2] << 8) | uint16_t(data[3])) / 400.0f; - const float pressure = ((uint16_t(data[4] << 8) | uint16_t(data[5])) + 50000.0f) / 100.0f; - const float acceleration_x = (int16_t(data[6] << 8) + int16_t(data[7])) / 1000.0f; - const float acceleration_y = (int16_t(data[8] << 8) + int16_t(data[9])) / 1000.0f; - const float acceleration_z = (int16_t(data[10] << 8) + int16_t(data[11])) / 1000.0f; + const float temperature = static_cast<int16_t>(encode_uint16(data[0], data[1])) * 0.005f; + const float humidity = encode_uint16(data[2], data[3]) / 400.0f; + const float pressure = (encode_uint16(data[4], data[5]) + 50000.0f) / 100.0f; + const float acceleration_x = static_cast<int16_t>(encode_uint16(data[6], data[7])) / 1000.0f; + const float acceleration_y = static_cast<int16_t>(encode_uint16(data[8], data[9])) / 1000.0f; + const float acceleration_z = static_cast<int16_t>(encode_uint16(data[10], data[11])) / 1000.0f; - const uint16_t power_info = (uint16_t(data[12] << 8) | data[13]); + const uint16_t power_info = encode_uint16(data[12], data[13]); const float battery_voltage = ((power_info >> 5) + 1600.0f) / 1000.0f; const float tx_power = ((power_info & 0x1F) * 2.0f) - 40.0f; const float movement_counter = float(data[14]); - const float measurement_sequence_number = float(uint16_t(data[15] << 8) | uint16_t(data[16])); + const float measurement_sequence_number = float(encode_uint16(data[15], data[16])); result.temperature = data[0] == 0x7F && data[1] == 0xFF ? NAN : temperature; result.humidity = data[2] == 0xFF && data[3] == 0xFF ? NAN : humidity; diff --git a/esphome/components/tormatic/tormatic_protocol.h b/esphome/components/tormatic/tormatic_protocol.h index 057713b884..26a634b630 100644 --- a/esphome/components/tormatic/tormatic_protocol.h +++ b/esphome/components/tormatic/tormatic_protocol.h @@ -99,7 +99,7 @@ struct MessageHeader { // payload_size returns the amount of payload bytes to be read from the uart // buffer after reading the header. - uint32_t payload_size() { return this->len - sizeof(this->type); } + uint32_t payload_size() { return this->len > sizeof(this->type) ? this->len - sizeof(this->type) : 0; } } __attribute__((packed)); // StatusType denotes which 'page' of information needs to be retrieved. From 019db745828289f5d105475a816f6dc1cabd814d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:44:27 +0000 Subject: [PATCH 486/609] Bump setuptools from 82.0.0 to 82.0.1 (#14665) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c6a2c22a5d..2e3a247768 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==82.0.0", "wheel>=0.43,<0.47"] +requires = ["setuptools==82.0.1", "wheel>=0.43,<0.47"] build-backend = "setuptools.build_meta" [project] From a379e5a6357340a696d9cbc67a8ad48a0bad0924 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:16:29 -0400 Subject: [PATCH 487/609] [runtime_image][st7701s] Fix BMP decoder and LCD init bugs (#14663) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .../components/runtime_image/bmp_decoder.cpp | 18 +++++++++++++++--- esphome/components/st7701s/st7701s.cpp | 6 ++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/esphome/components/runtime_image/bmp_decoder.cpp b/esphome/components/runtime_image/bmp_decoder.cpp index 1a56484c60..7003f4da2f 100644 --- a/esphome/components/runtime_image/bmp_decoder.cpp +++ b/esphome/components/runtime_image/bmp_decoder.cpp @@ -63,7 +63,8 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { switch (this->bits_per_pixel_) { case 1: - this->width_bytes_ = (this->width_ % 8 == 0) ? (this->width_ / 8) : (this->width_ / 8 + 1); + this->width_bytes_ = (this->width_ + 7) / 8; + this->padding_bytes_ = (4 - (this->width_bytes_ % 4)) % 4; break; case 24: this->width_bytes_ = this->width_ * 3; @@ -92,15 +93,26 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { case 1: { while (index < size) { uint8_t current_byte = buffer[index]; + bool end_of_row = false; for (uint8_t i = 0; i < 8; i++) { - size_t x = (this->paint_index_ % static_cast<size_t>(this->width_)) + i; + size_t x = this->paint_index_ % static_cast<size_t>(this->width_); size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / static_cast<size_t>(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_++; + // End of pixel row: skip remaining bits in this byte + if (x + 1 >= static_cast<size_t>(this->width_)) { + end_of_row = true; + break; + } } - this->paint_index_ += 8; this->current_index_++; index++; + // End of pixel row: skip row padding bytes (4-byte alignment) + if (end_of_row && this->padding_bytes_ > 0) { + index += this->padding_bytes_; + this->current_index_ += this->padding_bytes_; + } } break; } diff --git a/esphome/components/st7701s/st7701s.cpp b/esphome/components/st7701s/st7701s.cpp index 221fe39b9d..ecce4eb4b2 100644 --- a/esphome/components/st7701s/st7701s.cpp +++ b/esphome/components/st7701s/st7701s.cpp @@ -36,11 +36,13 @@ void ST7701S::setup() { config.de_gpio_num = this->de_pin_->get_pin(); config.pclk_gpio_num = this->pclk_pin_->get_pin(); esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_); - ESP_ERROR_CHECK(esp_lcd_panel_reset(this->handle_)); - ESP_ERROR_CHECK(esp_lcd_panel_init(this->handle_)); if (err != ESP_OK) { esph_log_e(TAG, "lcd_new_rgb_panel failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; } + ESP_ERROR_CHECK(esp_lcd_panel_reset(this->handle_)); + ESP_ERROR_CHECK(esp_lcd_panel_init(this->handle_)); } void ST7701S::loop() { From 75f55adbfa0cd1a75c9e33a16cc6a95d7195c50c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:17:31 -0400 Subject: [PATCH 488/609] [api][at581x][vl53l0x] Fix bounds check issues in 3 components (#14660) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/api/api_frame_helper_noise.cpp | 2 ++ esphome/components/at581x/at581x.cpp | 5 +++++ esphome/components/vl53l0x/vl53l0x_sensor.cpp | 4 ++-- esphome/components/vl53l0x/vl53l0x_sensor.h | 3 +-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 62523fb835..256357ce6a 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -375,6 +375,7 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso #ifdef USE_STORE_LOG_STR_IN_FLASH // On ESP8266 with flash strings, we need to use PROGMEM-aware functions size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason)); + reason_len = std::min(reason_len, sizeof(data) - 1); if (reason_len > 0) { memcpy_P(data + 1, reinterpret_cast<PGM_P>(reason), reason_len); } @@ -382,6 +383,7 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso // Normal memory access const char *reason_str = LOG_STR_ARG(reason); size_t reason_len = strlen(reason_str); + reason_len = std::min(reason_len, sizeof(data) - 1); if (reason_len > 0) { // NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string std::memcpy(data + 1, reason_str, reason_len); diff --git a/esphome/components/at581x/at581x.cpp b/esphome/components/at581x/at581x.cpp index 728fbe20c6..6fc85b0790 100644 --- a/esphome/components/at581x/at581x.cpp +++ b/esphome/components/at581x/at581x.cpp @@ -135,6 +135,11 @@ bool AT581XComponent::i2c_write_config() { } // Set gain + if (this->gain_ < 0 || static_cast<size_t>(this->gain_) >= ARRAY_SIZE(GAIN5C_TABLE) || + static_cast<size_t>(this->gain_ >> 1) >= ARRAY_SIZE(GAIN63_TABLE)) { + ESP_LOGE(TAG, "AT581X gain index out of range: %d", this->gain_); + return false; + } if (!this->i2c_write_reg(GAIN_ADDR_TABLE[0], GAIN5C_TABLE[this->gain_]) || !this->i2c_write_reg(GAIN_ADDR_TABLE[1], GAIN63_TABLE[this->gain_ >> 1])) { ESP_LOGE(TAG, "Failed to write AT581X gain registers"); diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp index e833657fc4..0b2b40d723 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.cpp +++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp @@ -87,9 +87,9 @@ void VL53L0XSensor::setup() { reg(0x94) = 0x6B; reg(0x83) = 0x00; - this->timeout_start_us_ = micros(); + uint32_t timeout_start_us = micros(); while (reg(0x83).get() == 0x00) { - if (this->timeout_us_ > 0 && ((uint16_t) (micros() - this->timeout_start_us_) > this->timeout_us_)) { + if (this->timeout_us_ > 0 && (micros() - timeout_start_us > this->timeout_us_)) { ESP_LOGE(TAG, "'%s' - setup timeout", this->name_.c_str()); this->mark_failed(); return; diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.h b/esphome/components/vl53l0x/vl53l0x_sensor.h index 2bf90015fe..f533005b5b 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.h +++ b/esphome/components/vl53l0x/vl53l0x_sensor.h @@ -64,8 +64,7 @@ class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c bool waiting_for_interrupt_{false}; uint8_t stop_variable_; - uint16_t timeout_start_us_; - uint16_t timeout_us_{}; + uint32_t timeout_us_{}; static std::list<VL53L0XSensor *> vl53_sensors; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool enable_pin_setup_complete; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) From b721cd48e5d8d41ad12eedf9e4ec941483cd99c3 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:18:07 -0400 Subject: [PATCH 489/609] [hmc5883l][mmc5603][honeywellabp2][xgzp68xx][max9611] Fix uninitialized members (#14659) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/hmc5883l/hmc5883l.h | 2 +- esphome/components/honeywellabp2_i2c/honeywellabp2.h | 4 ++-- esphome/components/max9611/sensor.py | 4 +++- esphome/components/mmc5603/mmc5603.h | 4 ++-- esphome/components/xgzp68xx/sensor.py | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/hmc5883l/hmc5883l.h b/esphome/components/hmc5883l/hmc5883l.h index 8eae0f7a50..b5cf93e62b 100644 --- a/esphome/components/hmc5883l/hmc5883l.h +++ b/esphome/components/hmc5883l/hmc5883l.h @@ -61,7 +61,7 @@ class HMC5883LComponent : public PollingComponent, public i2c::I2CDevice { NONE = 0, COMMUNICATION_FAILED, ID_REGISTERS, - } error_code_; + } error_code_{NONE}; HighFrequencyLoopRequester high_freq_; }; diff --git a/esphome/components/honeywellabp2_i2c/honeywellabp2.h b/esphome/components/honeywellabp2_i2c/honeywellabp2.h index 274de847ac..d29ebb855d 100644 --- a/esphome/components/honeywellabp2_i2c/honeywellabp2.h +++ b/esphome/components/honeywellabp2_i2c/honeywellabp2.h @@ -45,8 +45,8 @@ class HONEYWELLABP2Sensor : public PollingComponent, public i2c::I2CDevice { const float max_count_b_ = 11744051.2; // (70% of 2^24 counts or 0xB33333) const float min_count_b_ = 5033164.8; // (30% of 2^24 counts or 0x4CCCCC) - float max_count_; - float min_count_; + float max_count_{max_count_a_}; + float min_count_{min_count_a_}; bool measurement_running_ = false; uint8_t raw_data_[7]; // holds output data diff --git a/esphome/components/max9611/sensor.py b/esphome/components/max9611/sensor.py index 8405a3f75a..b3a73d8c10 100644 --- a/esphome/components/max9611/sensor.py +++ b/esphome/components/max9611/sensor.py @@ -35,7 +35,9 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(MAX9611Component), - cv.Required(CONF_SHUNT_RESISTANCE): cv.resistance, + cv.Required(CONF_SHUNT_RESISTANCE): cv.All( + cv.resistance, cv.Range(min=1e-6) + ), cv.Required(CONF_GAIN): cv.enum(MAX9611_GAIN, upper=True), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, diff --git a/esphome/components/mmc5603/mmc5603.h b/esphome/components/mmc5603/mmc5603.h index f827e27e04..9a77b78bc1 100644 --- a/esphome/components/mmc5603/mmc5603.h +++ b/esphome/components/mmc5603/mmc5603.h @@ -27,7 +27,7 @@ class MMC5603Component : public PollingComponent, public i2c::I2CDevice { void set_auto_set_reset(bool auto_set_reset) { auto_set_reset_ = auto_set_reset; } protected: - MMC5603Datarate datarate_; + MMC5603Datarate datarate_{MMC5603_DATARATE_75_0_HZ}; sensor::Sensor *x_sensor_{nullptr}; sensor::Sensor *y_sensor_{nullptr}; sensor::Sensor *z_sensor_{nullptr}; @@ -37,7 +37,7 @@ class MMC5603Component : public PollingComponent, public i2c::I2CDevice { NONE = 0, COMMUNICATION_FAILED, ID_REGISTERS, - } error_code_; + } error_code_{NONE}; }; } // namespace mmc5603 diff --git a/esphome/components/xgzp68xx/sensor.py b/esphome/components/xgzp68xx/sensor.py index 2b38392a02..6b83012eb4 100644 --- a/esphome/components/xgzp68xx/sensor.py +++ b/esphome/components/xgzp68xx/sensor.py @@ -56,7 +56,7 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Optional(CONF_K_VALUE, default=4096): cv.uint16_t, + cv.Optional(CONF_K_VALUE, default=4096): cv.int_range(min=1, max=65535), } ) .extend(cv.polling_component_schema("60s")) From 08a0608a48965edfd7ef11301d2e38faefe2f5ab Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:18:21 -0400 Subject: [PATCH 490/609] [wifi][captive_portal][heatpumpir][es8388] Fix wrong behavior in 4 components (#14657) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .../captive_portal/dns_server_esp32_idf.cpp | 5 +++-- esphome/components/es8388/es8388.cpp | 4 ++-- esphome/components/heatpumpir/heatpumpir.cpp | 2 +- .../wifi/wifi_component_esp8266.cpp | 20 ++++--------------- 4 files changed, 10 insertions(+), 21 deletions(-) diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.cpp b/esphome/components/captive_portal/dns_server_esp32_idf.cpp index bd9989a40c..7b75f04241 100644 --- a/esphome/components/captive_portal/dns_server_esp32_idf.cpp +++ b/esphome/components/captive_portal/dns_server_esp32_idf.cpp @@ -14,6 +14,7 @@ static const char *const TAG = "captive_portal.dns"; // DNS constants static constexpr uint16_t DNS_PORT = 53; static constexpr uint16_t DNS_QR_FLAG = 1 << 15; +static constexpr uint16_t DNS_AA_FLAG = 1 << 10; static constexpr uint16_t DNS_OPCODE_MASK = 0x7800; static constexpr uint16_t DNS_QTYPE_A = 0x0001; static constexpr uint16_t DNS_QCLASS_IN = 0x0001; @@ -162,8 +163,8 @@ void DNSServer::process_next_request() { } // Build DNS response by modifying the request in-place - header->flags = htons(DNS_QR_FLAG | 0x8000); // Response + Authoritative - header->an_count = htons(1); // One answer + header->flags = htons(DNS_QR_FLAG | DNS_AA_FLAG); // Response + Authoritative + header->an_count = htons(1); // One answer // Add answer section after the question size_t question_len = (ptr + sizeof(DNSQuestion)) - this->buffer_ - sizeof(DNSHeader); diff --git a/esphome/components/es8388/es8388.cpp b/esphome/components/es8388/es8388.cpp index 72026a2a84..c252cdb707 100644 --- a/esphome/components/es8388/es8388.cpp +++ b/esphome/components/es8388/es8388.cpp @@ -152,7 +152,7 @@ void ES8388::dump_config() { bool ES8388::set_volume(float volume) { volume = clamp(volume, 0.0f, 1.0f); - uint8_t value = remap<uint8_t, float>(volume, 0.0f, 1.0f, -96, 0); + uint8_t value = remap<uint8_t, float>(volume, 0.0f, 1.0f, 192, 0); ESP_LOGD(TAG, "Setting ES8388_DACCONTROL4 / ES8388_DACCONTROL5 to 0x%02X (volume: %f)", value, volume); ES8388_ERROR_CHECK(this->write_byte(ES8388_DACCONTROL4, value)); ES8388_ERROR_CHECK(this->write_byte(ES8388_DACCONTROL5, value)); @@ -163,7 +163,7 @@ bool ES8388::set_volume(float volume) { float ES8388::volume() { uint8_t value; ES8388_ERROR_CHECK(this->read_byte(ES8388_DACCONTROL4, &value)); - return remap<float, uint8_t>(value, -96, 0, 0.0f, 1.0f); + return remap<float, uint8_t>(value, 192, 0, 0.0f, 1.0f); } bool ES8388::set_mute_state_(bool mute_state) { diff --git a/esphome/components/heatpumpir/heatpumpir.cpp b/esphome/components/heatpumpir/heatpumpir.cpp index 6b73a24dc4..11e7672dc1 100644 --- a/esphome/components/heatpumpir/heatpumpir.cpp +++ b/esphome/components/heatpumpir/heatpumpir.cpp @@ -114,7 +114,7 @@ void HeatpumpIRClimate::setup() { this->current_temperature = state; IRSenderESPHome esp_sender(this->transmitter_); - this->heatpump_ir_->send(esp_sender, uint8_t(lround(this->current_temperature + 0.5))); + this->heatpump_ir_->send(esp_sender, uint8_t(lround(this->current_temperature))); // current temperature changed, publish state this->publish_state(); diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index a9b26c5935..0bf7934878 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -638,8 +638,6 @@ WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { return WiFiSTAConnectStatus::IDLE; } bool WiFiComponent::wifi_scan_start_(bool passive) { - static bool first_scan = false; - // enable STA if (!this->wifi_mode_(true, {})) return false; @@ -656,23 +654,13 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { config.show_hidden = 1; #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE; - if (first_scan) { - if (passive) { - config.scan_time.passive = 200; - } else { - config.scan_time.active.min = 100; - config.scan_time.active.max = 200; - } + if (passive) { + config.scan_time.passive = 500; } else { - if (passive) { - config.scan_time.passive = 500; - } else { - config.scan_time.active.min = 400; - config.scan_time.active.max = 500; - } + config.scan_time.active.min = 400; + config.scan_time.active.max = 500; } #endif - first_scan = false; bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback); if (!ret) { ESP_LOGV(TAG, "wifi_station_scan failed"); From 9418f35cc32e0a6216b09b813664aa40bcc3e216 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:18:44 -0400 Subject: [PATCH 491/609] [multiple] Remove unnecessary heap allocations in 4 components (#14656) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/daikin_arc/daikin_arc.cpp | 9 +++++---- esphome/components/pn7150_i2c/pn7150_i2c.cpp | 3 ++- esphome/components/pn7160_i2c/pn7160_i2c.cpp | 3 ++- esphome/components/toshiba/toshiba.cpp | 6 +++--- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/esphome/components/daikin_arc/daikin_arc.cpp b/esphome/components/daikin_arc/daikin_arc.cpp index c45fa307a7..adb7b9fec7 100644 --- a/esphome/components/daikin_arc/daikin_arc.cpp +++ b/esphome/components/daikin_arc/daikin_arc.cpp @@ -350,8 +350,9 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) { valid_daikin_frame = true; size_t bytes_count = data.size() / 2 / 8; - size_t buf_size = bytes_count * 3 + 1; - std::unique_ptr<char[]> buf(new char[buf_size]()); // value-initialize (zero-fill) + // Header (20) + state (19) = 39 bytes max; truncates gracefully via buf_append_printf + char buf[40 * 3 + 1] = {}; + constexpr size_t buf_size = sizeof(buf); size_t buf_pos = 0; for (size_t i = 0; i < bytes_count; i++) { uint8_t byte = 0; @@ -363,9 +364,9 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { break; } } - buf_pos = buf_append_printf(buf.get(), buf_size, buf_pos, "%02x ", byte); + buf_pos = buf_append_printf(buf, buf_size, buf_pos, "%02x ", byte); } - ESP_LOGD(TAG, "WHOLE FRAME %s size: %d", buf.get(), data.size()); + ESP_LOGD(TAG, "WHOLE FRAME %s size: %d", buf, data.size()); } if (!valid_daikin_frame) { char sbuf[16 * 10 + 1] = {0}; diff --git a/esphome/components/pn7150_i2c/pn7150_i2c.cpp b/esphome/components/pn7150_i2c/pn7150_i2c.cpp index 38b3102b37..4ae884595b 100644 --- a/esphome/components/pn7150_i2c/pn7150_i2c.cpp +++ b/esphome/components/pn7150_i2c/pn7150_i2c.cpp @@ -34,7 +34,8 @@ uint8_t PN7150I2C::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) { } uint8_t PN7150I2C::write_nfcc(nfc::NciMessage &tx) { - if (this->write(tx.encode().data(), tx.encode().size()) == i2c::ERROR_OK) { + auto encoded = tx.encode(); + if (this->write(encoded.data(), encoded.size()) == i2c::ERROR_OK) { return nfc::STATUS_OK; } return nfc::STATUS_FAILED; diff --git a/esphome/components/pn7160_i2c/pn7160_i2c.cpp b/esphome/components/pn7160_i2c/pn7160_i2c.cpp index 7c6da9dd06..e33c6c793d 100644 --- a/esphome/components/pn7160_i2c/pn7160_i2c.cpp +++ b/esphome/components/pn7160_i2c/pn7160_i2c.cpp @@ -34,7 +34,8 @@ uint8_t PN7160I2C::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) { } uint8_t PN7160I2C::write_nfcc(nfc::NciMessage &tx) { - if (this->write(tx.encode().data(), tx.encode().size()) == i2c::ERROR_OK) { + auto encoded = tx.encode(); + if (this->write(encoded.data(), encoded.size()) == i2c::ERROR_OK) { return nfc::STATUS_OK; } return nfc::STATUS_FAILED; diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index e0c150537a..53114cc50f 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -951,10 +951,10 @@ void ToshibaClimate::transmit_ras_2819t_() { } uint8_t ToshibaClimate::is_valid_rac_pt1411hwru_header_(const uint8_t *message) { - const std::vector<uint8_t> header{RAC_PT1411HWRU_MESSAGE_HEADER0, RAC_PT1411HWRU_CS_HEADER, - RAC_PT1411HWRU_SWING_HEADER}; + static constexpr uint8_t HEADERS[] = {RAC_PT1411HWRU_MESSAGE_HEADER0, RAC_PT1411HWRU_CS_HEADER, + RAC_PT1411HWRU_SWING_HEADER}; - for (auto i : header) { + for (auto i : HEADERS) { if ((message[0] == i) && (message[1] == static_cast<uint8_t>(~i))) return i; } From fecedeb01833d01cbc26ad331597a554e7904086 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:20:09 -0400 Subject: [PATCH 492/609] [multiple] Fix crashes from malformed external input (batch 2) (#14651) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 5 ++++ .../modbus_controller/modbus_controller.cpp | 23 +++++++++++++++++++ .../nextion/nextion_upload_arduino.cpp | 6 +++++ .../nextion/nextion_upload_esp32.cpp | 6 +++++ .../seeed_mr60bha2/seeed_mr60bha2.cpp | 2 +- .../components/usb_host/usb_host_client.cpp | 5 ++++ 6 files changed, 46 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 73a298d279..0e2a515b40 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -514,6 +514,11 @@ void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) { continue; // Possible zero padded advertisement data } + // Validate field fits in remaining payload + if (offset + field_length > len) { + break; + } + // first byte of adv record is adv record type const uint8_t record_type = payload[offset++]; const uint8_t *record = &payload[offset]; diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 7f0eb230e0..f77f51a20d 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -59,6 +59,10 @@ bool ModbusController::send_next_command_() { // Queue incoming response void ModbusController::on_modbus_data(const std::vector<uint8_t> &data) { + if (this->command_queue_.empty()) { + ESP_LOGW(TAG, "Received modbus data but command queue is empty"); + return; + } auto ¤t_command = this->command_queue_.front(); if (current_command != nullptr) { if (this->module_offline_) { @@ -92,6 +96,9 @@ void ModbusController::process_modbus_data_(const ModbusCommandItem *response) { void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_code) { ESP_LOGE(TAG, "Modbus error function code: 0x%X exception: %d ", function_code, exception_code); + if (this->command_queue_.empty()) { + return; + } // Remove pending command waiting for a response auto ¤t_command = this->command_queue_.front(); if (current_command != nullptr) { @@ -175,6 +182,11 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st uint16_t payload_offset; if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { + if (data.size() < 5) { + ESP_LOGW(TAG, "Write multiple registers data too short (%zu bytes)", data.size()); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); + return; + } number_of_registers = uint16_t(data[3]) | (uint16_t(data[2]) << 8); if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_WRITE) { ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers); @@ -188,8 +200,19 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); return; } + if (data.size() < 5 + payload_size) { + ESP_LOGW(TAG, "Write multiple registers payload truncated (%zu bytes, expected %u)", data.size(), + 5 + payload_size); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); + return; + } payload_offset = 5; } else if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER) { + if (data.size() < 4) { + ESP_LOGW(TAG, "Write single register data too short (%zu bytes)", data.size()); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); + return; + } number_of_registers = 1; payload_offset = 2; } else { diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index e03f1f470b..6c454ab745 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -86,6 +86,12 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { ESP_LOGD(TAG, "Upload: %0.2f%% (%" PRIu32 " left, heap: %" PRIu32 ")", upload_percentage, this->content_length_, EspClass::getFreeHeap()); upload_first_chunk_sent_ = true; + if (recv_string.empty()) { + ESP_LOGW(TAG, "No response from display during upload"); + allocator.deallocate(buffer, 4096); + buffer = nullptr; + return -1; + } if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)]; ESP_LOGD( diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index 1014c728a8..166bbcc86a 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -108,6 +108,12 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r static_cast<uint32_t>(esp_get_free_heap_size())); #endif upload_first_chunk_sent_ = true; + if (recv_string.empty()) { + ESP_LOGW(TAG, "No response from display during upload"); + allocator.deallocate(buffer, 4096); + buffer = nullptr; + return -1; + } if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)]; ESP_LOGD( diff --git a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp index 8628faac5a..1b5eaf6367 100644 --- a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp +++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp @@ -199,7 +199,7 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c } break; case DISTANCE_TYPE_BUFFER: - if (data[0] != 0) { + if (length >= 1 && data[0] != 0) { if (this->distance_sensor_ != nullptr && length >= 8) { uint32_t current_distance_int = encode_uint32(data[7], data[6], data[5], data[4]); float distance_float; diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index c77d738ace..2a460d1a07 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -492,6 +492,11 @@ bool USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u ESP_LOGE(TAG, "Too many requests queued"); return false; } + if (length > trq->transfer->data_buffer_size) { + ESP_LOGE(TAG, "transfer_in: data length %u exceeds buffer size %u", length, trq->transfer->data_buffer_size); + this->release_trq(trq); + return false; + } trq->callback = callback; trq->transfer->callback = transfer_callback; trq->transfer->bEndpointAddress = ep_address | USB_DIR_IN; From 7c1b9f0cb4b7862764ace94d4ccb2119b1024419 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:22:06 -0400 Subject: [PATCH 493/609] [multiple] Fix wrong behavior in 5 components (#14647) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/anova/anova.cpp | 9 ++++++--- esphome/components/binary_sensor/filter.cpp | 1 - esphome/components/bl0906/bl0906.cpp | 8 +++----- .../components/esp32_ble_tracker/esp32_ble_tracker.cpp | 2 +- esphome/components/ledc/ledc_output.cpp | 3 +++ 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index b625f92115..f21230b075 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -144,9 +144,12 @@ void Anova::update() { return; if (this->current_request_ < 2) { - auto *pkt = this->codec_->get_read_device_status_request(); - if (this->current_request_ == 0) - this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c'); + AnovaPacket *pkt; + if (this->current_request_ == 0) { + pkt = this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c'); + } else { + pkt = this->codec_->get_read_device_status_request(); + } auto status = esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 25a69c413a..5d525e967d 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -136,7 +136,6 @@ optional<bool> SettleFilter::new_value(bool value) { return {}; } else { this->steady_ = false; - this->output(value); this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->steady_ = true; }); return value; } diff --git a/esphome/components/bl0906/bl0906.cpp b/esphome/components/bl0906/bl0906.cpp index 7b643bba98..dcae4a2591 100644 --- a/esphome/components/bl0906/bl0906.cpp +++ b/esphome/components/bl0906/bl0906.cpp @@ -190,11 +190,9 @@ void BL0906::bias_correction_(uint8_t address, float measurements, float correct float i_rms0 = measurements * ki; float i_rms = correction * ki; int32_t value = (i_rms * i_rms - i_rms0 * i_rms0) / 256; - data.l = value << 24 >> 24; - data.m = value << 16 >> 24; - if (value < 0) { - data.h = (value << 8 >> 24) | 0b10000000; - } + data.l = value & 0xFF; + data.m = (value >> 8) & 0xFF; + data.h = (value >> 16) & 0xFF; data.address = bl0906_checksum(address, &data); ESP_LOGV(TAG, "RMSOS:%02X%02X%02X%02X%02X%02X", BL0906_WRITE_COMMAND, address, data.l, data.m, data.h, data.address); this->write_byte(BL0906_WRITE_COMMAND); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 0e2a515b40..5a43cf7e49 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -549,7 +549,7 @@ void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) { // CSS 1.5 TX POWER LEVEL // "The TX Power Level data type indicates the transmitted power level of the packet containing the data type." // CSS 1: Optional in this context (may appear more than once in a block). - this->tx_powers_.push_back(*payload); + this->tx_powers_.push_back(*record); break; } case ESP_BLE_AD_TYPE_APPEARANCE: { diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index 21e0682257..763de851da 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -76,6 +76,9 @@ esp_err_t configure_timer_frequency(ledc_mode_t speed_mode, ledc_timer_t timer_n init_result = ledc_timer_config(&timer_conf); if (init_result != ESP_OK) { ESP_LOGW(TAG, "Unable to initialize timer with frequency %.1f and bit depth of %u", frequency, bit_depth); + if (bit_depth <= 1) { + break; + } // try again with a lower bit depth timer_conf.duty_resolution = static_cast<ledc_timer_bit_t>(--bit_depth); } From 9902447834e8b997bf8831b569484f0c164c8f33 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:51:50 -0400 Subject: [PATCH 494/609] [multiple] Fix minor bugs in 8 components (#14650) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/bl0906/bl0906.cpp | 35 ++++++++++--------- esphome/components/bmi160/bmi160.cpp | 3 +- .../components/esp32_camera/esp32_camera.cpp | 7 ++++ esphome/components/ld2450/ld2450.cpp | 2 +- esphome/components/ld2450/ld2450.h | 2 +- .../light/addressable_light_effect.h | 2 ++ .../mopeka_std_check/mopeka_std_check.cpp | 18 +++++----- .../mopeka_std_check/mopeka_std_check.h | 2 +- esphome/components/rtttl/rtttl.cpp | 13 ++++--- 9 files changed, 49 insertions(+), 35 deletions(-) diff --git a/esphome/components/bl0906/bl0906.cpp b/esphome/components/bl0906/bl0906.cpp index dcae4a2591..70db235a37 100644 --- a/esphome/components/bl0906/bl0906.cpp +++ b/esphome/components/bl0906/bl0906.cpp @@ -138,23 +138,24 @@ void BL0906::read_data_(const uint8_t address, const float reference, sensor::Se this->write_byte(BL0906_READ_COMMAND); this->write_byte(address); - if (this->read_array((uint8_t *) &buffer, sizeof(buffer) - 1)) { - if (bl0906_checksum(address, &buffer) == buffer.checksum) { - if (signed_result) { - data_s24.l = buffer.l; - data_s24.m = buffer.m; - data_s24.h = buffer.h; - } else { - data_u24.l = buffer.l; - data_u24.m = buffer.m; - data_u24.h = buffer.h; - } - } else { - ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); - while (read() >= 0) - ; - return; - } + if (!this->read_array((uint8_t *) &buffer, sizeof(buffer) - 1)) { + ESP_LOGW(TAG, "Read failed"); + return; + } + if (bl0906_checksum(address, &buffer) != buffer.checksum) { + ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); + while (read() >= 0) + ; + return; + } + if (signed_result) { + data_s24.l = buffer.l; + data_s24.m = buffer.m; + data_s24.h = buffer.h; + } else { + data_u24.l = buffer.l; + data_u24.m = buffer.m; + data_u24.h = buffer.h; } // Power if (reference == BL0906_PREF) { diff --git a/esphome/components/bmi160/bmi160.cpp b/esphome/components/bmi160/bmi160.cpp index 1e8c91d7b7..ed92979d24 100644 --- a/esphome/components/bmi160/bmi160.cpp +++ b/esphome/components/bmi160/bmi160.cpp @@ -6,6 +6,7 @@ namespace esphome { namespace bmi160 { static const char *const TAG = "bmi160"; +static constexpr uint32_t GYRO_WAKEUP_TIMEOUT_MS = 100; const uint8_t BMI160_REGISTER_CHIPID = 0x00; @@ -144,7 +145,7 @@ void BMI160Component::internal_setup_(int stage) { } ESP_LOGV(TAG, " Waiting for gyroscope to wake up"); // wait between 51 & 81ms, doing 100 to be safe - this->set_timeout(10, [this]() { this->internal_setup_(2); }); + this->set_timeout(GYRO_WAKEUP_TIMEOUT_MS, [this]() { this->internal_setup_(2); }); break; case 2: diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 655ae54f0a..085feb8c8a 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -146,6 +146,10 @@ void ESP32Camera::dump_config() { } sensor_t *s = esp_camera_sensor_get(); + if (s == nullptr) { + ESP_LOGE(TAG, " Camera sensor not available"); + return; + } auto st = s->status; ESP_LOGCONFIG(TAG, " JPEG Quality: %u\n" @@ -483,6 +487,9 @@ void ESP32Camera::request_image(camera::CameraRequester requester) { this->singl camera::CameraImageReader *ESP32Camera::create_image_reader() { return new ESP32CameraImageReader; } void ESP32Camera::update_camera_parameters() { sensor_t *s = esp_camera_sensor_get(); + if (s == nullptr) { + return; + } /* update image */ s->set_vflip(s, this->vertical_flip_); s->set_hmirror(s, this->horizontal_mirror_); diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index eb17cc7de7..f9701cbdf6 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -133,7 +133,7 @@ static constexpr uint8_t DATA_FRAME_FOOTER[2] = {0x55, 0xCC}; // MAC address the module uses when Bluetooth is disabled static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01}; -static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; }; +static inline uint32_t convert_seconds_to_ms(uint16_t value) { return (uint32_t) value * 1000; }; static inline void convert_int_values_to_hex(const int *values, uint8_t *bytes) { for (uint8_t i = 0; i < 4; i++) { diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 39b0ebd9da..9409dfc21d 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -168,7 +168,7 @@ class LD2450Component : public Component, public uart::UARTDevice { uint32_t presence_millis_ = 0; uint32_t still_presence_millis_ = 0; uint32_t moving_presence_millis_ = 0; - uint16_t timeout_ = 5; + uint32_t timeout_ = 5; uint8_t buffer_data_[MAX_LINE_LENGTH]; uint8_t mac_address_[6] = {0, 0, 0, 0, 0, 0}; uint8_t version_[6] = {0, 0, 0, 0, 0, 0}; diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 461ddbc085..283b037aca 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -324,6 +324,8 @@ class AddressableFireworksEffect : public AddressableLightEffect { target *= 170; view = target; } + if (it.size() < 2) + return; int last = it.size() - 1; it[0].set(it[0].get() + (it[1].get() * 128)); for (int i = 1; i < last; i++) { diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.cpp b/esphome/components/mopeka_std_check/mopeka_std_check.cpp index 88bd7b02fd..a4a31b8260 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.cpp +++ b/esphome/components/mopeka_std_check/mopeka_std_check.cpp @@ -126,18 +126,18 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) // Copy measurements over into my array. { u_int8_t measurements_index = 0; - for (u_int8_t i = 0; i < 3; i++) { - measurements_time[measurements_index] = mopeka_data->val[i].time_0 + 1; - measurements_value[measurements_index] = mopeka_data->val[i].value_0; + for (const auto &val : mopeka_data->val) { + measurements_time[measurements_index] = val.time_0 + 1; + measurements_value[measurements_index] = val.value_0; measurements_index++; - measurements_time[measurements_index] = mopeka_data->val[i].time_1 + 1; - measurements_value[measurements_index] = mopeka_data->val[i].value_1; + measurements_time[measurements_index] = val.time_1 + 1; + measurements_value[measurements_index] = val.value_1; measurements_index++; - measurements_time[measurements_index] = mopeka_data->val[i].time_2 + 1; - measurements_value[measurements_index] = mopeka_data->val[i].value_2; + measurements_time[measurements_index] = val.time_2 + 1; + measurements_value[measurements_index] = val.value_2; measurements_index++; - measurements_time[measurements_index] = mopeka_data->val[i].time_3 + 1; - measurements_value[measurements_index] = mopeka_data->val[i].value_3; + measurements_time[measurements_index] = val.time_3 + 1; + measurements_value[measurements_index] = val.value_3; measurements_index++; } } diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.h b/esphome/components/mopeka_std_check/mopeka_std_check.h index 45588988c5..c0a02f27f2 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.h +++ b/esphome/components/mopeka_std_check/mopeka_std_check.h @@ -40,7 +40,7 @@ struct mopeka_std_package { // NOLINT(readability-identifier-naming,altera-stru bool slow_update_rate : 1; bool sync_pressed : 1; - mopeka_std_values val[4]; + mopeka_std_values val[3]; } __attribute__((packed)); class MopekaStdCheck : public Component, public esp32_ble_tracker::ESPBTDeviceListener { diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 9bf0450993..01f5aad810 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -146,16 +146,19 @@ void Rtttl::loop() { } #endif // USE_SPEAKER + // Align to note: most rtttl's out there does not add any space after the ',' separator but just in case + while (this->position_ < this->rtttl_.length()) { + char c = this->rtttl_[this->position_]; + if (c != ',' && c != ' ') + break; + this->position_++; + } + if (this->position_ >= this->rtttl_.length()) { this->finish_(); return; } - // Align to note: most rtttl's out there does not add any space after the ',' separator but just in case - while (this->rtttl_[this->position_] == ',' || this->rtttl_[this->position_] == ' ') { - this->position_++; - } - // First, get note duration, if available uint8_t note_denominator = this->get_integer_(); From 470d9160a512b41042710bf4eb48455dbbd17007 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:57:02 -0400 Subject: [PATCH 495/609] [demo] Fix alarm control panel auth bypass when code is omitted (#14645) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/demo/demo_alarm_control_panel.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/demo/demo_alarm_control_panel.h b/esphome/components/demo/demo_alarm_control_panel.h index 76cb24c2f4..9976e5c7f0 100644 --- a/esphome/components/demo/demo_alarm_control_panel.h +++ b/esphome/components/demo/demo_alarm_control_panel.h @@ -32,8 +32,8 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component { auto code = call.get_code(); switch (state) { case ACP_STATE_ARMED_AWAY: - if (this->get_requires_code_to_arm() && code.has_value()) { - if (*code != "1234") { + if (this->get_requires_code_to_arm()) { + if (!code.has_value() || *code != "1234") { this->status_momentary_error("invalid_code", 5000); return; } @@ -41,8 +41,8 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component { this->publish_state(ACP_STATE_ARMED_AWAY); break; case ACP_STATE_DISARMED: - if (this->get_requires_code() && code.has_value()) { - if (*code != "1234") { + if (this->get_requires_code()) { + if (!code.has_value() || *code != "1234") { this->status_momentary_error("invalid_code", 5000); return; } From 308e8e78cd0fb549b64b1ed16a30dcd1be26c9e3 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:59:36 -0400 Subject: [PATCH 496/609] [ble_scanner] Escape special characters in JSON output (#14664) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/ble_scanner/ble_scanner.h | 23 ++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/esphome/components/ble_scanner/ble_scanner.h b/esphome/components/ble_scanner/ble_scanner.h index 7061b6d336..c6d7f24cce 100644 --- a/esphome/components/ble_scanner/ble_scanner.h +++ b/esphome/components/ble_scanner/ble_scanner.h @@ -16,12 +16,27 @@ namespace ble_scanner { class BLEScanner : public text_sensor::TextSensor, public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - // Format JSON using stack buffer to avoid heap allocations from string concatenation - char buf[128]; char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + // Escape special characters in the device name for valid JSON + const char *name = device.get_name().c_str(); + char escaped_name[128]; + size_t pos = 0; + for (; *name != '\0' && pos < sizeof(escaped_name) - 7; name++) { + uint8_t c = static_cast<uint8_t>(*name); + if (c == '"' || c == '\\') { + escaped_name[pos++] = '\\'; + escaped_name[pos++] = c; + } else if (c < 0x20) { + pos += snprintf(escaped_name + pos, sizeof(escaped_name) - pos, "\\u%04x", c); + } else { + escaped_name[pos++] = c; + } + } + escaped_name[pos] = '\0'; + + char buf[256]; snprintf(buf, sizeof(buf), "{\"timestamp\":%" PRId64 ",\"address\":\"%s\",\"rssi\":%d,\"name\":\"%s\"}", - static_cast<int64_t>(::time(nullptr)), device.address_str_to(addr_buf), device.get_rssi(), - device.get_name().c_str()); + static_cast<int64_t>(::time(nullptr)), device.address_str_to(addr_buf), device.get_rssi(), escaped_name); this->publish_state(buf); return true; } From b3fc43c13c5bee4b60c0bae0dbc1b244e4f4c60c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:00:17 -0400 Subject: [PATCH 497/609] [multiple] Fix wrong behavior in sensor calculations and drivers (#14644) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/bme280_base/bme280_base.cpp | 7 +++++-- esphome/components/bme680/bme680.h | 4 ++-- esphome/components/cse7766/cse7766.cpp | 9 +++++---- esphome/components/hitachi_ac344/hitachi_ac344.h | 2 +- esphome/components/rx8130/rx8130.cpp | 4 ++-- esphome/components/usb_uart/cp210x.cpp | 2 +- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/esphome/components/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp index f396888fd1..addbfe618d 100644 --- a/esphome/components/bme280_base/bme280_base.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -147,8 +147,11 @@ void BME280Component::setup() { this->calibration_.h1 = read_u8_(BME280_REGISTER_DIG_H1); this->calibration_.h2 = read_s16_le_(BME280_REGISTER_DIG_H2); this->calibration_.h3 = read_u8_(BME280_REGISTER_DIG_H3); - this->calibration_.h4 = read_u8_(BME280_REGISTER_DIG_H4) << 4 | (read_u8_(BME280_REGISTER_DIG_H4 + 1) & 0x0F); - this->calibration_.h5 = read_u8_(BME280_REGISTER_DIG_H5 + 1) << 4 | (read_u8_(BME280_REGISTER_DIG_H5) >> 4); + // h4 and h5 are signed 12-bit values; shift left then arithmetic right shift to sign-extend + int16_t h4_raw = read_u8_(BME280_REGISTER_DIG_H4) << 4 | (read_u8_(BME280_REGISTER_DIG_H4 + 1) & 0x0F); + this->calibration_.h4 = static_cast<int16_t>(h4_raw << 4) >> 4; + int16_t h5_raw = read_u8_(BME280_REGISTER_DIG_H5 + 1) << 4 | (read_u8_(BME280_REGISTER_DIG_H5) >> 4); + this->calibration_.h5 = static_cast<int16_t>(h5_raw << 4) >> 4; this->calibration_.h6 = read_u8_(BME280_REGISTER_DIG_H6); uint8_t humid_control_val = 0; diff --git a/esphome/components/bme680/bme680.h b/esphome/components/bme680/bme680.h index d48a42823b..239823fa8c 100644 --- a/esphome/components/bme680/bme680.h +++ b/esphome/components/bme680/bme680.h @@ -32,8 +32,8 @@ enum BME680Oversampling { /// Struct for storing calibration data for the BME680. struct BME680CalibrationData { uint16_t t1; - uint16_t t2; - uint8_t t3; + int16_t t2; + int8_t t3; uint16_t p1; int16_t p2; diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 806b79e19e..ce77b62b7b 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -2,6 +2,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include <cmath> namespace esphome::cse7766 { @@ -192,12 +193,12 @@ void CSE7766Component::parse_data_() { this->apparent_power_sensor_->publish_state(apparent_power); } if (have_power && this->reactive_power_sensor_ != nullptr) { - const float reactive_power = apparent_power - power; - if (reactive_power < 0.0f) { - ESP_LOGD(TAG, "Impossible reactive power: %.4f is negative", reactive_power); + const float q_squared = apparent_power * apparent_power - power * power; + if (q_squared < 0.0f) { + ESP_LOGD(TAG, "Impossible reactive power: S^2-P^2 is negative (%.4f)", q_squared); this->reactive_power_sensor_->publish_state(0.0f); } else { - this->reactive_power_sensor_->publish_state(reactive_power); + this->reactive_power_sensor_->publish_state(std::sqrt(q_squared)); } } if (this->power_factor_sensor_ != nullptr && (have_power || power_cycle_exceeds_range)) { diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.h b/esphome/components/hitachi_ac344/hitachi_ac344.h index c34f033d92..0877b83261 100644 --- a/esphome/components/hitachi_ac344/hitachi_ac344.h +++ b/esphome/components/hitachi_ac344/hitachi_ac344.h @@ -96,7 +96,7 @@ class HitachiClimate : public climate_ir::ClimateIR { void set_power_(bool on); uint8_t get_mode_(); void set_mode_(uint8_t mode); - void set_temp_(uint8_t celsius, bool set_previous = false); + void set_temp_(uint8_t celsius, bool set_previous = true); uint8_t get_fan_(); void set_fan_(uint8_t speed); void set_swing_v_toggle_(bool on); diff --git a/esphome/components/rx8130/rx8130.cpp b/esphome/components/rx8130/rx8130.cpp index ba092a4834..9e6f05ee15 100644 --- a/esphome/components/rx8130/rx8130.cpp +++ b/esphome/components/rx8130/rx8130.cpp @@ -75,7 +75,7 @@ void RX8130Component::read_time() { .second = bcd2dec(date[0] & 0x7f), .minute = bcd2dec(date[1] & 0x7f), .hour = bcd2dec(date[2] & 0x3f), - .day_of_week = bcd2dec(date[3] & 0x7f), + .day_of_week = static_cast<uint8_t>((date[3] & 0x7f) ? __builtin_ctz(date[3] & 0x7f) + 1 : 1), .day_of_month = bcd2dec(date[4] & 0x3f), .day_of_year = 1, // ignored by recalc_timestamp_utc(false) .month = bcd2dec(date[5] & 0x1f), @@ -103,7 +103,7 @@ void RX8130Component::write_time() { buff[0] = dec2bcd(now.second); buff[1] = dec2bcd(now.minute); buff[2] = dec2bcd(now.hour); - buff[3] = dec2bcd(now.day_of_week); + buff[3] = 1 << (now.day_of_week - 1); buff[4] = dec2bcd(now.day_of_month); buff[5] = dec2bcd(now.month); buff[6] = dec2bcd(now.year % 100); diff --git a/esphome/components/usb_uart/cp210x.cpp b/esphome/components/usb_uart/cp210x.cpp index ae9170c5fb..261f40c0db 100644 --- a/esphome/components/usb_uart/cp210x.cpp +++ b/esphome/components/usb_uart/cp210x.cpp @@ -65,7 +65,7 @@ std::vector<CdcEps> USBUartTypeCP210X::parse_descriptors(usb_device_handle_t dev } for (uint8_t i = 0; i != config_desc->bNumInterfaces; i++) { - auto data_desc = usb_parse_interface_descriptor(config_desc, 0, 0, &conf_offset); + auto data_desc = usb_parse_interface_descriptor(config_desc, i, 0, &conf_offset); if (!data_desc) { ESP_LOGE(TAG, "data_desc: usb_parse_interface_descriptor failed"); break; From 468ce74c8e2822a87c5367f22ca2c8732732e940 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Mon, 9 Mar 2026 17:04:47 -0500 Subject: [PATCH 498/609] [api][serial_proxy] Fix dangling pointer (#14640) --- esphome/components/api/api_connection.cpp | 12 ++++++++++++ esphome/components/serial_proxy/serial_proxy.h | 3 +++ 2 files changed, 15 insertions(+) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 28a770a4fb..7bd5d5120b 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -155,6 +155,18 @@ APIConnection::~APIConnection() { voice_assistant::global_voice_assistant->client_subscription(this, false); } #endif +#ifdef USE_ZWAVE_PROXY + if (zwave_proxy::global_zwave_proxy != nullptr && zwave_proxy::global_zwave_proxy->get_api_connection() == this) { + zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE); + } +#endif +#ifdef USE_SERIAL_PROXY + for (auto *proxy : App.get_serial_proxies()) { + if (proxy->get_api_connection() == this) { + proxy->serial_proxy_request(this, enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE); + } + } +#endif } void APIConnection::destroy_active_iterator_() { diff --git a/esphome/components/serial_proxy/serial_proxy.h b/esphome/components/serial_proxy/serial_proxy.h index 52f0654ff0..62f942b19d 100644 --- a/esphome/components/serial_proxy/serial_proxy.h +++ b/esphome/components/serial_proxy/serial_proxy.h @@ -74,6 +74,9 @@ class SerialProxy : public uart::UARTDevice, public Component { /// @param data_size Number of data bits (5-8) void configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint8_t stop_bits, uint8_t data_size); + /// Get the currently subscribed API connection (nullptr if none) + api::APIConnection *get_api_connection() { return this->api_connection_; } + /// Handle a subscribe/unsubscribe request from an API client void serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type); From d2686b49bef3fdf1d56f7f222e1532e8c25d4380 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:15:33 -0400 Subject: [PATCH 499/609] [canbus] Fix multiple MCP component bugs (#14461) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/canbus/canbus.cpp | 3 ++- esphome/components/mcp23x08_base/mcp23x08_base.cpp | 4 ++-- esphome/components/mcp23x17_base/mcp23x17_base.cpp | 4 ++-- esphome/components/mcp2515/mcp2515.cpp | 1 + esphome/components/mcp4461/mcp4461.cpp | 12 ++++++------ esphome/components/mcp4728/mcp4728.h | 2 +- 6 files changed, 14 insertions(+), 12 deletions(-) diff --git a/esphome/components/canbus/canbus.cpp b/esphome/components/canbus/canbus.cpp index e208b0fd66..ce48bfbba5 100644 --- a/esphome/components/canbus/canbus.cpp +++ b/esphome/components/canbus/canbus.cpp @@ -1,4 +1,5 @@ #include "canbus.h" +#include <algorithm> #include "esphome/core/log.h" namespace esphome { @@ -82,7 +83,7 @@ void Canbus::loop() { std::vector<uint8_t> data; // show data received - for (int i = 0; i < can_message.can_data_length_code; i++) { + for (int i = 0; i < std::min(can_message.can_data_length_code, CAN_MAX_DATA_LENGTH); i++) { ESP_LOGV(TAG, " can_message.data[%d]=%02x", i, can_message.data[i]); data.push_back(can_message.data[i]); } diff --git a/esphome/components/mcp23x08_base/mcp23x08_base.cpp b/esphome/components/mcp23x08_base/mcp23x08_base.cpp index 1593c376cd..92228be62c 100644 --- a/esphome/components/mcp23x08_base/mcp23x08_base.cpp +++ b/esphome/components/mcp23x08_base/mcp23x08_base.cpp @@ -47,12 +47,12 @@ void MCP23X08Base::pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterr case mcp23xxx_base::MCP23XXX_RISING: this->update_reg(pin, true, gpinten); this->update_reg(pin, true, intcon); - this->update_reg(pin, true, defval); + this->update_reg(pin, false, defval); break; case mcp23xxx_base::MCP23XXX_FALLING: this->update_reg(pin, true, gpinten); this->update_reg(pin, true, intcon); - this->update_reg(pin, false, defval); + this->update_reg(pin, true, defval); break; case mcp23xxx_base::MCP23XXX_NO_INTERRUPT: this->update_reg(pin, false, gpinten); diff --git a/esphome/components/mcp23x17_base/mcp23x17_base.cpp b/esphome/components/mcp23x17_base/mcp23x17_base.cpp index b1f1f260b4..6f95ee98fd 100644 --- a/esphome/components/mcp23x17_base/mcp23x17_base.cpp +++ b/esphome/components/mcp23x17_base/mcp23x17_base.cpp @@ -59,12 +59,12 @@ void MCP23X17Base::pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterr case mcp23xxx_base::MCP23XXX_RISING: this->update_reg(pin, true, gpinten); this->update_reg(pin, true, intcon); - this->update_reg(pin, true, defval); + this->update_reg(pin, false, defval); break; case mcp23xxx_base::MCP23XXX_FALLING: this->update_reg(pin, true, gpinten); this->update_reg(pin, true, intcon); - this->update_reg(pin, false, defval); + this->update_reg(pin, true, defval); break; case mcp23xxx_base::MCP23XXX_NO_INTERRUPT: this->update_reg(pin, false, gpinten); diff --git a/esphome/components/mcp2515/mcp2515.cpp b/esphome/components/mcp2515/mcp2515.cpp index 77bfaf9224..c2db9228c8 100644 --- a/esphome/components/mcp2515/mcp2515.cpp +++ b/esphome/components/mcp2515/mcp2515.cpp @@ -506,6 +506,7 @@ canbus::Error MCP2515::set_bitrate_(canbus::CanSpeed can_speed, CanClock can_clo cfg3 = MCP_12MHZ_40KBPS_CFG3; break; case (canbus::CAN_50KBPS): // 50Kbps + cfg1 = MCP_12MHZ_50KBPS_CFG1; cfg2 = MCP_12MHZ_50KBPS_CFG2; cfg3 = MCP_12MHZ_50KBPS_CFG3; break; diff --git a/esphome/components/mcp4461/mcp4461.cpp b/esphome/components/mcp4461/mcp4461.cpp index dc7e7019aa..48d90377df 100644 --- a/esphome/components/mcp4461/mcp4461.cpp +++ b/esphome/components/mcp4461/mcp4461.cpp @@ -79,8 +79,8 @@ void Mcp4461Component::dump_config() { // reworked to be a one-line intentionally, as output would not be in order if (i < 4) { ESP_LOGCONFIG(TAG, " ├── Volatile wiper [%u] level: %u, Status: %s, HW: %s, A: %s, B: %s, W: %s", i, - this->reg_[i].state, ONOFF(this->reg_[i].terminal_hw), ONOFF(this->reg_[i].terminal_a), - ONOFF(this->reg_[i].terminal_b), ONOFF(this->reg_[i].terminal_w), ONOFF(this->reg_[i].enabled)); + this->reg_[i].state, ONOFF(this->reg_[i].enabled), ONOFF(this->reg_[i].terminal_hw), + ONOFF(this->reg_[i].terminal_a), ONOFF(this->reg_[i].terminal_b), ONOFF(this->reg_[i].terminal_w)); } else { ESP_LOGCONFIG(TAG, " ├── Nonvolatile wiper [%u] level: %u", i, this->reg_[i].state); } @@ -315,9 +315,9 @@ void Mcp4461Component::disable_wiper_(Mcp4461WiperIdx wiper) { return; } ESP_LOGV(TAG, "Disabling wiper %u", wiper_idx); - this->reg_[wiper_idx].enabled = true; + this->reg_[wiper_idx].enabled = false; if (wiper_idx < 4) { - this->reg_[wiper_idx].terminal_hw = true; + this->reg_[wiper_idx].terminal_hw = false; this->reg_[wiper_idx].update_terminal = true; } } @@ -490,7 +490,7 @@ void Mcp4461Component::enable_terminal_(Mcp4461WiperIdx wiper, char terminal) { ESP_LOGW(TAG, "Unknown terminal %c specified", terminal); return; } - this->reg_[wiper_idx].update_terminal = false; + this->reg_[wiper_idx].update_terminal = true; } void Mcp4461Component::disable_terminal_(Mcp4461WiperIdx wiper, char terminal) { @@ -517,7 +517,7 @@ void Mcp4461Component::disable_terminal_(Mcp4461WiperIdx wiper, char terminal) { ESP_LOGW(TAG, "Unknown terminal %c specified", terminal); return; } - this->reg_[wiper_idx].update_terminal = false; + this->reg_[wiper_idx].update_terminal = true; } uint16_t Mcp4461Component::get_eeprom_value(Mcp4461EepromLocation location) { diff --git a/esphome/components/mcp4728/mcp4728.h b/esphome/components/mcp4728/mcp4728.h index f2262f4a35..d657408081 100644 --- a/esphome/components/mcp4728/mcp4728.h +++ b/esphome/components/mcp4728/mcp4728.h @@ -58,7 +58,7 @@ class MCP4728Component : public Component, public i2c::I2CDevice { void select_gain_(MCP4728ChannelIdx channel, MCP4728Gain gain); private: - DACInputData reg_[4]; + DACInputData reg_[4]{}; bool store_in_eeprom_ = false; bool update_ = false; }; From d96be88ff58d3dac9454364f6da24ff4ff0e3561 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:32:57 -0400 Subject: [PATCH 500/609] [multiple] Fix reliability issues in 5 components (#14655) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/components/bme680_bsec/bme680_bsec.cpp | 8 +++++++- esphome/components/hlk_fm22x/hlk_fm22x.cpp | 16 ++++++++++++++++ esphome/components/lvgl/lvgl_esphome.cpp | 6 ++++++ esphome/components/mqtt/mqtt_client.cpp | 4 +++- esphome/components/usb_uart/usb_uart.cpp | 5 +++++ 5 files changed, 37 insertions(+), 2 deletions(-) diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index 454be0c0fe..75efb6835a 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -271,10 +271,16 @@ void BME680BSECComponent::read_() { int64_t curr_time_ns = this->get_time_ns_(); if (this->bme680_settings_.trigger_measurement) { + uint32_t start = millis(); while (this->bme680_.power_mode != BME680_SLEEP_MODE) { + if (millis() - start > 50) { + ESP_LOGE(TAG, "Timeout waiting for BME680 to enter sleep mode"); + return; + } this->bme680_status_ = bme680_get_sensor_mode(&this->bme680_); if (this->bme680_status_ != BME680_OK) { - ESP_LOGW(TAG, "Failed to get sensor mode (BME680 Error Code %d)", this->bme680_status_); + ESP_LOGE(TAG, "Failed to get sensor mode (BME680 Error Code %d)", this->bme680_status_); + return; } } } diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.cpp b/esphome/components/hlk_fm22x/hlk_fm22x.cpp index 7c7c8782de..7a0dc0690c 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.cpp +++ b/esphome/components/hlk_fm22x/hlk_fm22x.cpp @@ -6,6 +6,7 @@ namespace esphome::hlk_fm22x { static const char *const TAG = "hlk_fm22x"; +static constexpr uint32_t PAYLOAD_TIMEOUT_MS = 20; void HlkFm22xComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X..."); @@ -133,6 +134,21 @@ void HlkFm22xComponent::recv_command_() { checksum ^= byte; length |= byte; + // Wait for remaining data (payload + checksum) to arrive. + // Header bytes are already consumed, so we must finish reading this message. + uint32_t start = millis(); + while (this->available() < length + 1) { + if (millis() - start > PAYLOAD_TIMEOUT_MS) { + ESP_LOGE(TAG, "Timeout waiting for payload (%u bytes)", length); + // Drain any partial payload bytes to resync the parser + while (this->available() > 0) { + this->read(); + } + return; + } + delay(1); + } + // Read up to buffer size; discard excess bytes while still computing checksum // GET_ALL_FACE_IDS can return all enrolled face data (hundreds of bytes) // but handlers only need the first few bytes diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 66cb25b864..5400054bb1 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -163,8 +163,11 @@ void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t ti void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { if (this->pages_.empty() || (this->current_page_ == this->pages_.size() - 1 && !this->page_wrap_)) return; + size_t start = this->current_page_; do { this->current_page_ = (this->current_page_ + 1) % this->pages_.size(); + if (this->current_page_ == start) + return; // all pages have skip=true (guaranteed not to happen by YAML validation) } while (this->pages_[this->current_page_]->skip); // skip empty pages() this->show_page(this->current_page_, anim, time); } @@ -172,8 +175,11 @@ void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) { if (this->pages_.empty() || (this->current_page_ == 0 && !this->page_wrap_)) return; + size_t start = this->current_page_; do { this->current_page_ = (this->current_page_ + this->pages_.size() - 1) % this->pages_.size(); + if (this->current_page_ == start) + return; // all pages have skip=true (guaranteed not to happen by YAML validation) } while (this->pages_[this->current_page_]->skip); // skip empty pages() this->show_page(this->current_page_, anim, time); } diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 1a03c5329e..38daf8f8f6 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -44,8 +44,10 @@ MQTTClientComponent::MQTTClientComponent() { void MQTTClientComponent::setup() { this->mqtt_backend_.set_on_message( [this](const char *topic, const char *payload, size_t len, size_t index, size_t total) { - if (index == 0) + if (index == 0) { + this->payload_buffer_.clear(); this->payload_buffer_.reserve(total); + } // append new payload, may contain incomplete MQTT message this->payload_buffer_.append(payload, len); diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 83de0b39fc..3d35f368fb 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -106,10 +106,15 @@ std::vector<CdcEps> USBUartTypeCdcAcm::parse_descriptors(usb_device_handle_t dev } void RingBuffer::push(uint8_t item) { + if (this->get_free_space() == 0) + return; this->buffer_[this->insert_pos_] = item; this->insert_pos_ = (this->insert_pos_ + 1) % this->buffer_size_; } void RingBuffer::push(const uint8_t *data, size_t len) { + size_t free = this->get_free_space(); + if (len > free) + len = free; for (size_t i = 0; i != len; i++) { this->buffer_[this->insert_pos_] = *data++; this->insert_pos_ = (this->insert_pos_ + 1) % this->buffer_size_; From dadbdd0f7b2081031d395a2778f92706ab98647a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Mar 2026 12:34:31 -1000 Subject: [PATCH 501/609] [ci] Make codeowner label update non-fatal for fork PRs (#14668) --- .../codeowner-approved-label-update.yml | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/codeowner-approved-label-update.yml b/.github/workflows/codeowner-approved-label-update.yml index c2eb886913..0bce33ebe2 100644 --- a/.github/workflows/codeowner-approved-label-update.yml +++ b/.github/workflows/codeowner-approved-label-update.yml @@ -55,18 +55,24 @@ jobs: return; } - if (action === LabelAction.ADD) { - await github.rest.issues.addLabels({ - owner, repo, issue_number: pr_number, labels: [LABEL_NAME] - }); - console.log(`Added '${LABEL_NAME}' label`); - } else if (action === LabelAction.REMOVE) { - try { + try { + if (action === LabelAction.ADD) { + await github.rest.issues.addLabels({ + owner, repo, issue_number: pr_number, labels: [LABEL_NAME] + }); + console.log(`Added '${LABEL_NAME}' label`); + } else if (action === LabelAction.REMOVE) { await github.rest.issues.removeLabel({ owner, repo, issue_number: pr_number, name: LABEL_NAME }); console.log(`Removed '${LABEL_NAME}' label`); - } catch (error) { - if (error.status !== 404) throw error; + } + } catch (error) { + if (error.status === 403) { + console.log(`Warning: insufficient permissions to update label (expected for fork PRs)`); + } else if (error.status === 404) { + console.log(`Label '${LABEL_NAME}' not present, nothing to remove`); + } else { + throw error; } } From d6ce5dda81d6fdf966392e80b68a8c8cac0a3192 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Mar 2026 12:54:56 -1000 Subject: [PATCH 502/609] [ci] Skip YAML anchor keys in integration fixture component extraction (#14670) --- script/helpers.py | 6 ++++-- tests/script/test_helpers.py | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/script/helpers.py b/script/helpers.py index 202ac9b5fc..d372d2a7ec 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -705,8 +705,10 @@ def get_components_from_integration_fixtures() -> set[str]: if not config: continue - # Add all top-level component keys - components.update(config.keys()) + # Add all top-level component keys (skip YAML anchor keys starting with '.') + components.update( + k for k in config if isinstance(k, str) and not k.startswith(".") + ) # Add platform components (e.g., output.template) for value in config.values(): diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 7e60ba41fc..2953a9fd42 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -1057,6 +1057,30 @@ def test_get_components_from_integration_fixtures() -> None: assert components == expected_components +def test_get_components_from_integration_fixtures_skips_yaml_anchors() -> None: + """Test that YAML anchor keys (starting with '.') are excluded.""" + yaml_content = { + "sensor": [{"platform": "template", "name": "test"}], + "esphome": {"name": "test"}, + ".sensor_filters": {"filters": [{"timeout": "50ms"}]}, + ".binary_filters": {"filters": [{"settle": "50ms"}]}, + } + + mock_yaml_file = Mock() + + with ( + patch("pathlib.Path.glob") as mock_glob, + patch("esphome.yaml_util.load_yaml", return_value=yaml_content), + ): + mock_glob.return_value = [mock_yaml_file] + + components = helpers.get_components_from_integration_fixtures() + + assert ".sensor_filters" not in components + assert ".binary_filters" not in components + assert components == {"sensor", "esphome", "template"} + + @pytest.mark.parametrize( "output,expected", [ From c31ac662bd08cdb99859819dc92a8a9295fb828c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:39:58 -0400 Subject: [PATCH 503/609] [multiple] Fix crashes from malformed external input (#14643) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/b_parasite/b_parasite.cpp | 10 ++ .../components/kamstrup_kmp/kamstrup_kmp.cpp | 12 +- esphome/components/ld2412/ld2412.cpp | 39 ++-- esphome/components/nextion/nextion.cpp | 4 +- esphome/components/pipsolar/pipsolar.cpp | 9 +- esphome/components/smt100/smt100.cpp | 25 ++- .../uart_mock_ld2412_engineering.yaml | 93 +++------- ...art_mock_ld2412_engineering_truncated.yaml | 167 ++++++++++++++++++ tests/integration/test_uart_mock_ld2412.py | 125 +++++++++++++ 9 files changed, 390 insertions(+), 94 deletions(-) create mode 100644 tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml diff --git a/esphome/components/b_parasite/b_parasite.cpp b/esphome/components/b_parasite/b_parasite.cpp index 356f396476..7be26efa7f 100644 --- a/esphome/components/b_parasite/b_parasite.cpp +++ b/esphome/components/b_parasite/b_parasite.cpp @@ -38,6 +38,11 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { const auto &data = service_data.data; + if (data.size() < 10) { + ESP_LOGW(TAG, "Service data too short: %zu", data.size()); + return false; + } + const uint8_t protocol_version = data[0] >> 4; if (protocol_version != 1 && protocol_version != 2) { ESP_LOGE(TAG, "Unsupported protocol version: %u", protocol_version); @@ -47,6 +52,11 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { // Some b-parasite versions have an (optional) illuminance sensor. bool has_illuminance = data[0] & 0x1; + if (has_illuminance && data.size() < 18) { + ESP_LOGW(TAG, "Service data too short for illuminance: %zu", data.size()); + return false; + } + // Counter for deduplicating messages. uint8_t counter = data[1] & 0x0f; if (last_processed_counter_ == counter) { diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp index 29de651255..9f2557243c 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp @@ -110,9 +110,17 @@ void KamstrupKMPComponent::send_message_(const uint8_t *msg, int msg_len) { for (int i = 0; i < buffer_len; i++) { if (buffer[i] == 0x06 || buffer[i] == 0x0d || buffer[i] == 0x1b || buffer[i] == 0x40 || buffer[i] == 0x80) { + if (tx_msg_len + 2 >= static_cast<int>(sizeof(tx_msg))) { + ESP_LOGE(TAG, "TX message overflow"); + return; + } tx_msg[tx_msg_len++] = 0x1b; tx_msg[tx_msg_len++] = buffer[i] ^ 0xff; } else { + if (tx_msg_len + 1 >= static_cast<int>(sizeof(tx_msg))) { + ESP_LOGE(TAG, "TX message overflow"); + return; + } tx_msg[tx_msg_len++] = buffer[i]; } } @@ -216,8 +224,8 @@ void KamstrupKMPComponent::parse_command_message_(uint16_t command, const uint8_ uint8_t unit_idx = msg[4]; uint8_t mantissa_range = msg[5]; - if (mantissa_range > 4) { - ESP_LOGE(TAG, "Received invalid message (mantissa size too large %d, expected 4)", mantissa_range); + if (mantissa_range > 4 || msg_len < 7 + mantissa_range) { + ESP_LOGE(TAG, "Received invalid message (mantissa size %d, msg_len %d)", mantissa_range, msg_len); return; } diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index ef0915d0bc..37578dd8da 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -413,24 +413,29 @@ void LD2412Component::handle_periodic_data_() { this->detection_distance_sensor_->publish_state_if_not_dup(new_detect_distance); } if (engineering_mode) { - /* - Moving distance range: 18th byte - Still distance range: 19th byte - Moving energy: 20~28th bytes - */ - for (uint8_t i = 0; i < TOTAL_GATES; i++) { - SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]) + // Engineering mode needs at least LIGHT_SENSOR + 1 bytes + if (this->buffer_pos_ < LIGHT_SENSOR + 1) { + ESP_LOGW(TAG, "Engineering mode packet too short: %u", this->buffer_pos_); + } else { + /* + Moving distance range: 18th byte + Still distance range: 19th byte + Moving energy: 20~28th bytes + */ + for (uint8_t i = 0; i < TOTAL_GATES; i++) { + SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]) + } + /* + Still energy: 29~37th bytes + */ + for (uint8_t i = 0; i < TOTAL_GATES; i++) { + SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]) + } + /* + Light sensor value + */ + SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]) } - /* - Still energy: 29~37th bytes - */ - for (uint8_t i = 0; i < TOTAL_GATES; i++) { - SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]) - } - /* - Light sensor: 38th bytes - */ - SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]) } else { for (auto &gate_move_sensor : this->gate_move_sensors_) { SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 7ae4d50fc8..01ceb3d765 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -646,8 +646,8 @@ void Nextion::process_nextion_commands_() { break; } - if (to_process_length == 0) { - ESP_LOGE(TAG, "Numeric return but no data"); + if (to_process_length < 4) { + ESP_LOGE(TAG, "Numeric return but insufficient data (need 4, got %zu)", to_process_length); break; } diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index eb6d3931e0..c304d206c0 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -192,8 +192,13 @@ bool Pipsolar::send_next_command_() { if (!this->command_queue_[this->command_queue_position_].empty()) { const char *command = this->command_queue_[this->command_queue_position_].c_str(); uint8_t byte_command[16]; - uint8_t length = this->command_queue_[this->command_queue_position_].length(); - for (uint8_t i = 0; i < length; i++) { + size_t length = this->command_queue_[this->command_queue_position_].length(); + if (length > sizeof(byte_command)) { + ESP_LOGE(TAG, "Command too long: %zu", length); + this->command_queue_[this->command_queue_position_].clear(); + return false; + } + for (size_t i = 0; i < length; i++) { byte_command[i] = (uint8_t) this->command_queue_[this->command_queue_position_].at(i); } this->state_ = STATE_COMMAND; diff --git a/esphome/components/smt100/smt100.cpp b/esphome/components/smt100/smt100.cpp index 105cc06edb..6eb6416447 100644 --- a/esphome/components/smt100/smt100.cpp +++ b/esphome/components/smt100/smt100.cpp @@ -14,11 +14,26 @@ void SMT100Component::update() { void SMT100Component::loop() { while (this->available() != 0) { if (this->readline_(this->read(), this->readline_buffer_, MAX_LINE_LENGTH) > 0) { - int counts = (int) strtol((strtok(this->readline_buffer_, ",")), nullptr, 10); - float permittivity = (float) strtod((strtok(nullptr, ",")), nullptr); - float moisture = (float) strtod((strtok(nullptr, ",")), nullptr); - float temperature = (float) strtod((strtok(nullptr, ",")), nullptr); - float voltage = (float) strtod((strtok(nullptr, ",")), nullptr); + char *token = strtok(this->readline_buffer_, ","); + if (!token) + continue; + int counts = (int) strtol(token, nullptr, 10); + token = strtok(nullptr, ","); + if (!token) + continue; + float permittivity = (float) strtod(token, nullptr); + token = strtok(nullptr, ","); + if (!token) + continue; + float moisture = (float) strtod(token, nullptr); + token = strtok(nullptr, ","); + if (!token) + continue; + float temperature = (float) strtod(token, nullptr); + token = strtok(nullptr, ","); + if (!token) + continue; + float voltage = (float) strtod(token, nullptr); if (this->counts_sensor_ != nullptr) { counts_sensor_->publish_state(counts); diff --git a/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml b/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml index 103dbed132..a69e18888e 100644 --- a/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml +++ b/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml @@ -102,6 +102,18 @@ uart_mock: 0xF8, 0xF7, 0xF6, 0xF5, ] +# Common filter definitions +.sensor_filters: &sensor_filters + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + +.binary_filters: &binary_filters + filters: + - settle: 50ms + ld2412: id: ld2412_dev uart_id: mock_uart @@ -111,107 +123,56 @@ sensor: ld2412_id: ld2412_dev moving_distance: name: "Moving Distance" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters still_distance: name: "Still Distance" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters moving_energy: name: "Moving Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters still_energy: name: "Still Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters detection_distance: name: "Detection Distance" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters light: name: "Light" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters gate_0: move_energy: name: "Gate 0 Move Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters still_energy: name: "Gate 0 Still Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters gate_1: move_energy: name: "Gate 1 Move Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters still_energy: name: "Gate 1 Still Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters gate_2: move_energy: name: "Gate 2 Move Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters still_energy: name: "Gate 2 Still Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters binary_sensor: - platform: ld2412 ld2412_id: ld2412_dev has_target: name: "Has Target" - filters: - - settle: 50ms + <<: *binary_filters has_moving_target: name: "Has Moving Target" - filters: - - settle: 50ms + <<: *binary_filters has_still_target: name: "Has Still Target" - filters: - - settle: 50ms + <<: *binary_filters button: - platform: template diff --git a/tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml b/tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml new file mode 100644 index 0000000000..c0bd514762 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml @@ -0,0 +1,167 @@ +esphome: + name: uart-mock-ld2412-eng-trunc + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy ld2412's DEPENDENCIES = ["uart"] +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + id: mock_uart + baud_rate: 256000 + auto_start: false + injections: + # Phase 1 (t=100ms): Valid engineering mode frame (52 bytes, buffer_pos_=52) + # Establishes baseline: gate_0_move=100, light=87 + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x2A, 0x00, + 0x01, 0xAA, + 0x03, + 0x1E, 0x00, + 0x64, + 0x1E, 0x00, + 0x64, + 0x00, 0x00, + 0x64, 0x41, 0x06, 0x0E, 0x2B, 0x16, 0x03, 0x03, 0x07, 0x05, 0x09, 0x08, 0x07, 0x06, + 0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x50, 0x40, 0x30, 0x20, 0x10, + 0x57, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 2 (t=200ms): Truncated engineering mode frame (24 bytes, buffer_pos_=24) + # This frame has data_type=0x01 (engineering) but only enough data for the + # basic target fields, not the gate energies or light sensor. + # buffer_pos_=24 passes the old check (>= 12) but fails the new check (< 46). + # Without the fix, indices 17-45 would read stale buffer data from Phase 1. + # + # Layout (24 bytes): + # [0-3] F4 F3 F2 F1 = data frame header + # [4-5] 0E 00 = length 14 + # [6] 01 = data type (engineering mode) + # [7] AA = data header marker + # [8] 03 = target states (moving+still) + # [9-10] 1E 00 = moving distance 30 + # [11] 50 = moving energy 80 + # [12-13] 1E 00 = still distance 30 + # [14] 50 = still energy 80 + # [15-16] FF FF = garbage detection distance bytes + # [17] FF = padding (would be gate data in full frame) + # [18] 55 = data footer marker (at buffer_pos_ - 6) + # [19] 00 = check byte + # [20-23] F8 F7 F6 F5 = data frame footer + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x0E, 0x00, + 0x01, 0xAA, + 0x03, + 0x1E, 0x00, + 0x50, + 0x1E, 0x00, + 0x50, + 0xFF, 0xFF, + 0xFF, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 3 (t=300ms): Valid recovery frame with different values + # gate_0_move=50, light=42 — proves component recovered + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x2A, 0x00, + 0x01, 0xAA, + 0x03, + 0x1E, 0x00, + 0x64, + 0x1E, 0x00, + 0x64, + 0x00, 0x00, + 0x32, 0x20, 0x06, 0x0E, 0x2B, 0x16, 0x03, 0x03, 0x07, 0x05, 0x09, 0x08, 0x07, 0x06, + 0x00, 0x00, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x28, 0x20, 0x18, 0x10, 0x08, + 0x2A, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + +# Common filter definitions +.sensor_filters: &sensor_filters + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + +.binary_filters: &binary_filters + filters: + - settle: 50ms + +ld2412: + id: ld2412_dev + uart_id: mock_uart + +sensor: + - platform: ld2412 + ld2412_id: ld2412_dev + moving_distance: + name: "Moving Distance" + <<: *sensor_filters + still_distance: + name: "Still Distance" + <<: *sensor_filters + moving_energy: + name: "Moving Energy" + <<: *sensor_filters + still_energy: + name: "Still Energy" + <<: *sensor_filters + detection_distance: + name: "Detection Distance" + <<: *sensor_filters + light: + name: "Light" + <<: *sensor_filters + gate_0: + move_energy: + name: "Gate 0 Move Energy" + <<: *sensor_filters + still_energy: + name: "Gate 0 Still Energy" + <<: *sensor_filters + +binary_sensor: + - platform: ld2412 + ld2412_id: ld2412_dev + has_target: + name: "Has Target" + <<: *binary_filters + has_moving_target: + name: "Has Moving Target" + <<: *binary_filters + has_still_target: + name: "Has Still Target" + <<: *binary_filters + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: 'id(mock_uart).start_scenario();' diff --git a/tests/integration/test_uart_mock_ld2412.py b/tests/integration/test_uart_mock_ld2412.py index a964ba0073..9b928ef14f 100644 --- a/tests/integration/test_uart_mock_ld2412.py +++ b/tests/integration/test_uart_mock_ld2412.py @@ -14,6 +14,12 @@ test_uart_mock_ld2412_engineering (engineering mode): 2. Multi-byte still distance (291cm) using high byte > 0 3. Gate energy sensor values 4. Detection distance computed from target state + +test_uart_mock_ld2412_engineering_truncated (truncated engineering mode): + 1. Valid engineering frame establishes baseline sensor values + 2. Truncated engineering frame (24 bytes) is rejected — gate/light sensors + must not receive garbage from stale buffer data or frame footer bytes + 3. Recovery frame with different values proves the component survived """ from __future__ import annotations @@ -273,3 +279,122 @@ async def test_uart_mock_ld2412_engineering( ) assert pytest.approx(291.0) in collector.sensor_states["detection_distance"] + + +@pytest.mark.asyncio +async def test_uart_mock_ld2412_engineering_truncated( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that truncated engineering mode frames don't corrupt sensor values. + + Without the fix, a 24-byte engineering mode frame passes the old buffer_pos_ >= 12 + check but reads indices 17-45 from stale buffer data, publishing garbage values + (e.g. frame footer bytes 0xF8=248 as gate energy). + """ + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track the truncated frame warning + truncated_warning_seen = loop.create_future() + + def line_callback(line: str) -> None: + if ( + "Engineering mode packet too short" in line + and not truncated_warning_seen.done() + ): + truncated_warning_seen.set_result(True) + + collector = SensorStateCollector( + sensor_names=[ + "moving_distance", + "still_distance", + "moving_energy", + "still_energy", + "detection_distance", + "light", + "gate_0_move_energy", + "gate_0_still_energy", + ], + binary_sensor_names=[ + "has_target", + "has_moving_target", + "has_still_target", + ], + ) + + # Signal when we see Phase 3 recovery values (gate_0_move=50) + recovery_received = collector.add_waiter( + lambda: pytest.approx(50.0) in collector.sensor_states["gate_0_move_energy"] + ) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + collector.build_key_mapping(entities) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states( + initial_state_helper.on_state_wrapper(collector.on_state) + ) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + + # Wait for Phase 1 — valid engineering frame establishes baseline + try: + await collector.wait_for_all(timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 1 frame. Received:\n" + f" sensor_states: {collector.sensor_states}\n" + f" binary_states: {collector.binary_states}" + ) + + # Phase 1 baseline: gate_0_move=100, light=87 + assert collector.sensor_states["gate_0_move_energy"][0] == pytest.approx(100.0) + assert collector.sensor_states["light"][0] == pytest.approx(87.0) + + # Wait for Phase 3 recovery frame (gate_0_move=50) + try: + await asyncio.wait_for(recovery_received, timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for recovery frame. Received:\n" + f" gate_0_move_energy: {collector.sensor_states['gate_0_move_energy']}\n" + f" light: {collector.sensor_states['light']}" + ) + + # Verify the truncated frame warning was logged + assert truncated_warning_seen.done(), ( + "Expected 'Engineering mode packet too short' warning in logs" + ) + + # Phase 3 recovery: gate_0_move=50, light=42 + assert pytest.approx(50.0) in collector.sensor_states["gate_0_move_energy"] + assert pytest.approx(42.0) in collector.sensor_states["light"] + + # The critical assertion: gate_0_move_energy must never have received + # garbage values from the truncated frame. Without the fix, + # buffer_data_[17] = 0xFF = 255 would be published as gate_0_move. + for value in collector.sensor_states["gate_0_move_energy"]: + assert value == pytest.approx(100.0) or value == pytest.approx(50.0), ( + f"gate_0_move_energy got unexpected value {value} — " + f"truncated frame likely leaked stale buffer data. " + f"All values: {collector.sensor_states['gate_0_move_energy']}" + ) From 00f809f5f001a1428f099f3e0846d2c5d848cdf3 Mon Sep 17 00:00:00 2001 From: Tobias Stanzel <tobi.stanzel@gmail.com> Date: Tue, 10 Mar 2026 02:45:20 +0100 Subject: [PATCH 504/609] [sen6x] fix memory leak issue (#14623) Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick+github@koston.org> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/sen6x/sen6x.cpp | 338 ++++++++++++++++------------- esphome/components/sen6x/sen6x.h | 6 + 2 files changed, 188 insertions(+), 156 deletions(-) diff --git a/esphome/components/sen6x/sen6x.cpp b/esphome/components/sen6x/sen6x.cpp index baaadd6463..2a6ea64735 100644 --- a/esphome/components/sen6x/sen6x.cpp +++ b/esphome/components/sen6x/sen6x.cpp @@ -2,13 +2,16 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" #include <cmath> -#include <functional> -#include <memory> namespace esphome::sen6x { static const char *const TAG = "sen6x"; +static constexpr uint8_t POLL_RETRIES = 24; // 24 attempts +static constexpr uint32_t I2C_READ_DELAY = 20; // 20 ms to wait for I2C read to complete +static constexpr uint32_t POLL_INTERVAL = 50; // 50 ms between poll attempts +// Single numeric timeout ID — the chain is sequential so only one is active at a time. +static constexpr uint32_t TIMEOUT_POLL = 1; static constexpr uint16_t SEN6X_CMD_GET_DATA_READY_STATUS = 0x0202; static constexpr uint16_t SEN6X_CMD_GET_FIRMWARE_VERSION = 0xD100; static constexpr uint16_t SEN6X_CMD_GET_PRODUCT_NAME = 0xD014; @@ -182,179 +185,202 @@ void SEN6XComponent::update() { return; } - uint16_t read_cmd; - uint8_t read_words; - set_read_command_and_words(this->sen6x_type_, read_cmd, read_words); + // Cancel any in-flight polling from a previous update() cycle. + this->cancel_timeout(TIMEOUT_POLL); - const uint8_t poll_retries = 24; - auto poll_ready = std::make_shared<std::function<void(uint8_t)>>(); - *poll_ready = [this, poll_ready, read_cmd, read_words](uint8_t retries_left) { - const uint8_t attempt = static_cast<uint8_t>(poll_retries - retries_left + 1); - ESP_LOGV(TAG, "Data ready polling attempt %u", attempt); + set_read_command_and_words(this->sen6x_type_, this->read_cmd_, this->read_words_); - if (!this->write_command(SEN6X_CMD_GET_DATA_READY_STATUS)) { + // Polling uses chained timeouts to guarantee each I2C operation completes + // before the next begins. The flow is: + // + // poll_data_ready_() + // -> write_command (data ready status) + // -> timeout I2C_READ_DELAY + // -> read_data (check ready flag) + // -> if not ready: timeout POLL_INTERVAL -> poll_data_ready_() (retry) + // -> if ready: read_measurements_() + // -> write_command (read measurement) + // -> timeout I2C_READ_DELAY + // -> parse_and_publish_measurements_() + // + // All timeouts share a single ID (TIMEOUT_POLL) since only one is active + // at a time. cancel_timeout in update() stops any in-flight chain. + this->poll_retries_remaining_ = POLL_RETRIES; + this->poll_data_ready_(); +} + +void SEN6XComponent::poll_data_ready_() { + if (this->poll_retries_remaining_ == 0) { + this->status_set_warning(); + ESP_LOGD(TAG, "Data not ready"); + return; + } + ESP_LOGV(TAG, "Data ready polling attempt %u", + static_cast<unsigned>(POLL_RETRIES - this->poll_retries_remaining_ + 1)); + this->poll_retries_remaining_--; + + if (!this->write_command(SEN6X_CMD_GET_DATA_READY_STATUS)) { + this->status_set_warning(); + ESP_LOGD(TAG, "write data ready status error (%d)", this->last_error_); + return; + } + + this->set_timeout(TIMEOUT_POLL, I2C_READ_DELAY, [this]() { + uint16_t raw_read_status; + if (!this->read_data(&raw_read_status, 1)) { this->status_set_warning(); - ESP_LOGD(TAG, "write data ready status error (%d)", this->last_error_); + ESP_LOGD(TAG, "read data ready status error (%d)", this->last_error_); return; } - this->set_timeout(20, [this, poll_ready, retries_left, read_cmd, read_words]() { - uint16_t raw_read_status; - if (!this->read_data(&raw_read_status, 1)) { - this->status_set_warning(); - ESP_LOGD(TAG, "read data ready status error (%d)", this->last_error_); - return; - } + if ((raw_read_status & 0x0001) == 0) { + // Not ready yet; schedule next attempt after POLL_INTERVAL. + this->set_timeout(TIMEOUT_POLL, POLL_INTERVAL, [this]() { this->poll_data_ready_(); }); + return; + } - if ((raw_read_status & 0x0001) == 0) { - if (retries_left == 0) { - this->status_set_warning(); - ESP_LOGD(TAG, "Data not ready"); - return; - } - this->set_timeout(50, [poll_ready, retries_left]() { (*poll_ready)(retries_left - 1); }); - return; - } + this->read_measurements_(); + }); +} - if (!this->write_command(read_cmd)) { - this->status_set_warning(); - ESP_LOGD(TAG, "Read measurement failed (%d)", this->last_error_); - return; - } +void SEN6XComponent::read_measurements_() { + if (!this->write_command(this->read_cmd_)) { + this->status_set_warning(); + ESP_LOGD(TAG, "Read measurement failed (%d)", this->last_error_); + return; + } - this->set_timeout(20, [this, read_words]() { - uint16_t measurements[10]; + this->set_timeout(TIMEOUT_POLL, I2C_READ_DELAY, [this]() { this->parse_and_publish_measurements_(); }); +} - if (!this->read_data(measurements, read_words)) { - this->status_set_warning(); - ESP_LOGD(TAG, "Read data failed (%d)", this->last_error_); - return; - } - int8_t voc_index = -1; - int8_t nox_index = -1; - int8_t hcho_index = -1; - int8_t co2_index = -1; - bool co2_uint16 = false; - switch (this->sen6x_type_) { - case SEN62: - break; - case SEN63C: - co2_index = 6; - break; - case SEN65: - voc_index = 6; - nox_index = 7; - break; - case SEN66: - voc_index = 6; - nox_index = 7; - co2_index = 8; - co2_uint16 = true; - break; - case SEN68: - voc_index = 6; - nox_index = 7; - hcho_index = 8; - break; - case SEN69C: - voc_index = 6; - nox_index = 7; - hcho_index = 8; - co2_index = 9; - break; - default: - break; - } +void SEN6XComponent::parse_and_publish_measurements_() { + uint16_t measurements[10]; - float pm_1_0 = measurements[0] / 10.0f; - if (measurements[0] == 0xFFFF) - pm_1_0 = NAN; - float pm_2_5 = measurements[1] / 10.0f; - if (measurements[1] == 0xFFFF) - pm_2_5 = NAN; - float pm_4_0 = measurements[2] / 10.0f; - if (measurements[2] == 0xFFFF) - pm_4_0 = NAN; - float pm_10_0 = measurements[3] / 10.0f; - if (measurements[3] == 0xFFFF) - pm_10_0 = NAN; - float humidity = static_cast<int16_t>(measurements[4]) / 100.0f; - if (measurements[4] == 0x7FFF) - humidity = NAN; - float temperature = static_cast<int16_t>(measurements[5]) / 200.0f; - if (measurements[5] == 0x7FFF) - temperature = NAN; + if (!this->read_data(measurements, this->read_words_)) { + this->status_set_warning(); + ESP_LOGD(TAG, "Read data failed (%d)", this->last_error_); + return; + } + int8_t voc_index = -1; + int8_t nox_index = -1; + int8_t hcho_index = -1; + int8_t co2_index = -1; + bool co2_uint16 = false; + switch (this->sen6x_type_) { + case SEN62: + break; + case SEN63C: + co2_index = 6; + break; + case SEN65: + voc_index = 6; + nox_index = 7; + break; + case SEN66: + voc_index = 6; + nox_index = 7; + co2_index = 8; + co2_uint16 = true; + break; + case SEN68: + voc_index = 6; + nox_index = 7; + hcho_index = 8; + break; + case SEN69C: + voc_index = 6; + nox_index = 7; + hcho_index = 8; + co2_index = 9; + break; + default: + break; + } - float voc = NAN; - float nox = NAN; - float hcho = NAN; - float co2 = NAN; + float pm_1_0 = measurements[0] / 10.0f; + if (measurements[0] == 0xFFFF) + pm_1_0 = NAN; + float pm_2_5 = measurements[1] / 10.0f; + if (measurements[1] == 0xFFFF) + pm_2_5 = NAN; + float pm_4_0 = measurements[2] / 10.0f; + if (measurements[2] == 0xFFFF) + pm_4_0 = NAN; + float pm_10_0 = measurements[3] / 10.0f; + if (measurements[3] == 0xFFFF) + pm_10_0 = NAN; + float humidity = static_cast<int16_t>(measurements[4]) / 100.0f; + if (measurements[4] == 0x7FFF) + humidity = NAN; + float temperature = static_cast<int16_t>(measurements[5]) / 200.0f; + if (measurements[5] == 0x7FFF) + temperature = NAN; - if (voc_index >= 0) { - voc = static_cast<int16_t>(measurements[voc_index]) / 10.0f; - if (measurements[voc_index] == 0x7FFF) - voc = NAN; - } - if (nox_index >= 0) { - nox = static_cast<int16_t>(measurements[nox_index]) / 10.0f; - if (measurements[nox_index] == 0x7FFF) - nox = NAN; - } + float voc = NAN; + float nox = NAN; + float hcho = NAN; + float co2 = NAN; - if (hcho_index >= 0) { - const uint16_t hcho_raw = measurements[hcho_index]; - hcho = hcho_raw / 10.0f; - if (hcho_raw == 0xFFFF) - hcho = NAN; - } + if (voc_index >= 0) { + voc = static_cast<int16_t>(measurements[voc_index]) / 10.0f; + if (measurements[voc_index] == 0x7FFF) + voc = NAN; + } + if (nox_index >= 0) { + nox = static_cast<int16_t>(measurements[nox_index]) / 10.0f; + if (measurements[nox_index] == 0x7FFF) + nox = NAN; + } - if (co2_index >= 0) { - if (co2_uint16) { - const uint16_t co2_raw = measurements[co2_index]; - co2 = static_cast<float>(co2_raw); - if (co2_raw == 0xFFFF) - co2 = NAN; - } else { - const int16_t co2_raw = static_cast<int16_t>(measurements[co2_index]); - co2 = static_cast<float>(co2_raw); - if (co2_raw == 0x7FFF) - co2 = NAN; - } - } + if (hcho_index >= 0) { + const uint16_t hcho_raw = measurements[hcho_index]; + hcho = hcho_raw / 10.0f; + if (hcho_raw == 0xFFFF) + hcho = NAN; + } - if (!this->startup_complete_) { - ESP_LOGD(TAG, "Startup delay, ignoring values"); - this->status_clear_warning(); - return; - } + if (co2_index >= 0) { + if (co2_uint16) { + const uint16_t co2_raw = measurements[co2_index]; + co2 = static_cast<float>(co2_raw); + if (co2_raw == 0xFFFF) + co2 = NAN; + } else { + const int16_t co2_raw = static_cast<int16_t>(measurements[co2_index]); + co2 = static_cast<float>(co2_raw); + if (co2_raw == 0x7FFF) + co2 = NAN; + } + } - if (this->pm_1_0_sensor_ != nullptr) - this->pm_1_0_sensor_->publish_state(pm_1_0); - if (this->pm_2_5_sensor_ != nullptr) - this->pm_2_5_sensor_->publish_state(pm_2_5); - if (this->pm_4_0_sensor_ != nullptr) - this->pm_4_0_sensor_->publish_state(pm_4_0); - if (this->pm_10_0_sensor_ != nullptr) - this->pm_10_0_sensor_->publish_state(pm_10_0); - if (this->temperature_sensor_ != nullptr) - this->temperature_sensor_->publish_state(temperature); - if (this->humidity_sensor_ != nullptr) - this->humidity_sensor_->publish_state(humidity); - if (this->voc_sensor_ != nullptr) - this->voc_sensor_->publish_state(voc); - if (this->nox_sensor_ != nullptr) - this->nox_sensor_->publish_state(nox); - if (this->hcho_sensor_ != nullptr) - this->hcho_sensor_->publish_state(hcho); - if (this->co2_sensor_ != nullptr) - this->co2_sensor_->publish_state(co2); + if (!this->startup_complete_) { + ESP_LOGD(TAG, "Startup delay, ignoring values"); + this->status_clear_warning(); + return; + } - this->status_clear_warning(); - }); - }); - }; + if (this->pm_1_0_sensor_ != nullptr) + this->pm_1_0_sensor_->publish_state(pm_1_0); + if (this->pm_2_5_sensor_ != nullptr) + this->pm_2_5_sensor_->publish_state(pm_2_5); + if (this->pm_4_0_sensor_ != nullptr) + this->pm_4_0_sensor_->publish_state(pm_4_0); + if (this->pm_10_0_sensor_ != nullptr) + this->pm_10_0_sensor_->publish_state(pm_10_0); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); + if (this->voc_sensor_ != nullptr) + this->voc_sensor_->publish_state(voc); + if (this->nox_sensor_ != nullptr) + this->nox_sensor_->publish_state(nox); + if (this->hcho_sensor_ != nullptr) + this->hcho_sensor_->publish_state(hcho); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(co2); - (*poll_ready)(poll_retries); + this->status_clear_warning(); } SEN6XComponent::Sen6xType SEN6XComponent::infer_type_from_product_name_(const std::string &product_name) { diff --git a/esphome/components/sen6x/sen6x.h b/esphome/components/sen6x/sen6x.h index 01e89dce1b..bc44611882 100644 --- a/esphome/components/sen6x/sen6x.h +++ b/esphome/components/sen6x/sen6x.h @@ -30,13 +30,19 @@ class SEN6XComponent : public PollingComponent, public sensirion_common::Sensiri protected: Sen6xType infer_type_from_product_name_(const std::string &product_name); + void poll_data_ready_(); + void read_measurements_(); + void parse_and_publish_measurements_(); bool initialized_{false}; std::string product_name_; Sen6xType sen6x_type_{UNKNOWN}; std::string serial_number_; + uint16_t read_cmd_{0}; uint8_t firmware_version_major_{0}; uint8_t firmware_version_minor_{0}; + uint8_t poll_retries_remaining_{0}; + uint8_t read_words_{0}; bool startup_complete_{false}; }; From e82f0f443223a4985bf2014fbf9b5f4130b2f4b8 Mon Sep 17 00:00:00 2001 From: Javier Peletier <jpeletier@users.noreply.github.com> Date: Tue, 10 Mar 2026 03:41:02 +0100 Subject: [PATCH 505/609] [cpptests] support testing platform components (#13075) Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/core/__init__.py | 15 + esphome/loader.py | 5 + script/cpp_unit_test.py | 153 +++++++---- script/helpers.py | 15 +- tests/components/README.md | 13 + .../binary_sensor/binary_sensor_test.cpp | 77 ++++++ .../components/packet_transport/cpp_test.yaml | 11 - .../packet_transport_test.cpp | 259 ------------------ .../packet_transport/sensor/sensor_test.cpp | 170 ++++++++++++ tests/script/test_helpers.py | 67 +++++ tests/unit_tests/test_core.py | 12 + 11 files changed, 467 insertions(+), 330 deletions(-) create mode 100644 tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp delete mode 100644 tests/components/packet_transport/cpp_test.yaml create mode 100644 tests/components/packet_transport/sensor/sensor_test.cpp diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 484f679369..a86478aca1 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -615,6 +615,10 @@ class EsphomeCore: self.address_cache: AddressCache | None = None # Cached config hash (computed lazily) self._config_hash: int | None = None + # True if compiling for C++ unit tests + self.cpp_testing = False + # Allowlist of components whose to_code should run during C++ testing + self.cpp_testing_codegen: set[str] = set() def reset(self): from esphome.pins import PIN_SCHEMA_REGISTRY @@ -644,6 +648,8 @@ class EsphomeCore: self.current_component = None self.address_cache = None self._config_hash = None + self.cpp_testing = False + self.cpp_testing_codegen = set() PIN_SCHEMA_REGISTRY.reset() @contextmanager @@ -987,6 +993,15 @@ class EsphomeCore: """ self.platform_counts[platform_name] += 1 + def testing_ensure_platform_registered(self, platform_name: str) -> None: + """Ensure a platform has at least one entity registered for testing. + + Used during C++ test builds to guarantee USE_* defines are emitted + without needing a real component variable. + """ + if not self.platform_counts[platform_name]: + self.platform_counts[platform_name] = 1 + def register_controller(self) -> None: """Track registration of a Controller for ControllerRegistry StaticVector sizing.""" controller_count = self.data.setdefault(KEY_CONTROLLER_REGISTRY_COUNT, 0) diff --git a/esphome/loader.py b/esphome/loader.py index 968c8cf3e0..5771e07473 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -71,6 +71,11 @@ class ComponentManifest: @property def to_code(self) -> Callable[[Any], None] | None: + if CORE.cpp_testing: + # During C++ testing, only run to_code for allowlisted components + name = self.module.__package__.rsplit(".", 1)[-1] + if name not in CORE.cpp_testing_codegen: + return None return getattr(self.module, "to_code", None) @property diff --git a/script/cpp_unit_test.py b/script/cpp_unit_test.py index b87261ab33..e11687dc16 100755 --- a/script/cpp_unit_test.py +++ b/script/cpp_unit_test.py @@ -10,9 +10,10 @@ from helpers import get_all_components, get_all_dependencies, root_path from esphome.__main__ import command_compile, parse_args from esphome.config import validate_config +from esphome.const import CONF_PLATFORM from esphome.core import CORE +from esphome.loader import get_component from esphome.platformio_api import get_idedata -from esphome.yaml_util import load_yaml # This must coincide with the version in /platformio.ini PLATFORMIO_GOOGLE_TEST_LIB = "google/googletest@^1.15.2" @@ -20,6 +21,13 @@ PLATFORMIO_GOOGLE_TEST_LIB = "google/googletest@^1.15.2" # Path to /tests/components COMPONENTS_TESTS_DIR: Path = Path(root_path) / "tests" / "components" +# Components whose to_code should run during C++ test builds. +# Most components don't need code generation for tests; only these +# essential ones (platform setup, logging, core config) are needed. +# Note: "core" is the esphome core config module (esphome/core/config.py), +# which registers under package name "core" not "esphome". +CPP_TESTING_CODEGEN_COMPONENTS = {"core", "host", "logger"} + def hash_components(components: list[str]) -> str: key = ",".join(components) @@ -30,12 +38,14 @@ def filter_components_without_tests(components: list[str]) -> list[str]: """Filter out components that do not have a corresponding test file. This is done by checking if the component's directory contains at - least a .cpp file. + least a .cpp or .h file. """ filtered_components: list[str] = [] for component in components: test_dir = COMPONENTS_TESTS_DIR / component - if test_dir.is_dir() and any(test_dir.glob("*.cpp")): + if test_dir.is_dir() and ( + any(test_dir.glob("*.cpp")) or any(test_dir.glob("*.h")) + ): filtered_components.append(component) else: print( @@ -45,38 +55,6 @@ def filter_components_without_tests(components: list[str]) -> list[str]: return filtered_components -# Name of optional per-component YAML config merged into the test build -# before validation so that platform defines (USE_SENSOR, etc.) are generated. -CPP_TEST_CONFIG_FILE = "cpp_test.yaml" - - -def load_component_test_configs(components: list[str]) -> dict: - """Load cpp_test.yaml files from test component directories. - - These configs are merged into the base test config *before* validation - so that entity registration runs during code generation, which causes - the corresponding USE_* defines to be emitted. - """ - merged: dict = {} - for component in components: - config_file = COMPONENTS_TESTS_DIR / component / CPP_TEST_CONFIG_FILE - if not config_file.exists(): - continue - component_config = load_yaml(config_file) - if not component_config: - continue - for key, value in component_config.items(): - if ( - key in merged - and isinstance(merged[key], list) - and isinstance(value, list) - ): - merged[key].extend(value) - else: - merged[key] = value - return merged - - def create_test_config(config_name: str, includes: list[str]) -> dict: """Create ESPHome test configuration for C++ unit tests. @@ -113,11 +91,52 @@ def create_test_config(config_name: str, includes: list[str]) -> dict: } +def get_platform_components(components: list[str]) -> list[str]: + """Discover platform sub-components referenced by test directory structure. + + For each component being tested, any sub-directory named after a platform + domain (e.g. ``sensor``, ``binary_sensor``) is treated as a request to + include that ``<domain>.<component>`` platform in the build. The sub- + directory must name a valid platform domain; anything else raises an error + so that typos are caught early. + + Returns: + List of ``"domain.component"`` strings, one per discovered sub-directory. + """ + platform_components: list[str] = [] + for component in components: + test_dir = COMPONENTS_TESTS_DIR / component + if not test_dir.is_dir(): + continue + # Each sub-directory name is expected to be a platform domain + # (e.g. tests/components/bthome/sensor/ → sensor.bthome). + for domain_dir in test_dir.iterdir(): + if not domain_dir.is_dir(): + continue + domain = domain_dir.name + domain_module = get_component(domain) + if domain_module is None or not domain_module.is_platform_component: + raise ValueError( + f"Component tests for '{component}' reference non-existing or invalid domain '{domain}'" + f" in its directory structure. See ({COMPONENTS_TESTS_DIR / component / domain})." + ) + platform_components.append(f"{domain}.{component}") + return platform_components + + +# Exit codes for run_tests +EXIT_OK = 0 +EXIT_SKIPPED = 1 +EXIT_COMPILE_ERROR = 2 +EXIT_CONFIG_ERROR = 3 +EXIT_NO_EXECUTABLE = 4 + + def run_tests(selected_components: list[str]) -> int: # Skip tests on Windows if os.name == "nt": print("Skipping esphome tests on Windows", file=sys.stderr) - return 1 + return EXIT_SKIPPED # Remove components that do not have tests components = filter_components_without_tests(selected_components) @@ -127,45 +146,63 @@ def run_tests(selected_components: list[str]) -> int: "No components specified or no tests found for the specified components.", file=sys.stderr, ) - return 0 + return EXIT_OK components = sorted(components) - # Obtain possible dependencies for the requested components. - # Always include 'time' because USE_TIME_TIMEZONE is defined as a build flag, - # which causes core/time.h to include components/time/posix_tz.h. - components_with_dependencies = sorted( - get_all_dependencies(set(components) | {"time"}) - ) - - # Build a list of include folders, one folder per component containing tests. - # A special replacement main.cpp is located in /tests/components/main.cpp + # Build a list of include folders relative to COMPONENTS_TESTS_DIR. These folders will + # be added along with their subfolders. + # "main.cpp" is a special entry that points to /tests/components/main.cpp, + # which provides a custom test runner entry-point replacing the default one. + # Each remaining entry is a component folder whose *.cpp files are compiled. includes: list[str] = ["main.cpp"] + components + # Obtain a list of platform components to be tested: + try: + platform_components = get_platform_components(components) + except ValueError as e: + print(f"Error obtaining platform components: {e}") + return EXIT_CONFIG_ERROR + + components = sorted(components + platform_components) + # Create a unique name for this config based on the actual components being tested # to maximize cache during testing config_name: str = "cpptests-" + hash_components(components) - config = create_test_config(config_name, includes) + # Obtain possible dependencies for the requested components. + # Always include 'time' because USE_TIME_TIMEZONE is defined as a build flag, + # which causes core/time.h to include components/time/posix_tz.h. + components_with_dependencies: list[str] = sorted( + get_all_dependencies(set(components) | {"time"}, cpp_testing=True) + ) - # Merge component-specific test configs (e.g. sensor instances) before - # validation so that entity registration and USE_* defines work. - extra_config = load_component_test_configs(components) - config.update(extra_config) + config = create_test_config(config_name, includes) CORE.config_path = COMPONENTS_TESTS_DIR / "dummy.yaml" CORE.dashboard = None + CORE.cpp_testing = True + CORE.cpp_testing_codegen = CPP_TESTING_CODEGEN_COMPONENTS # Validate config will expand the above with defaults: config = validate_config(config, {}) # Add all components and dependencies to the base configuration after validation, so their files - # are added to the build. Use setdefault to avoid overwriting entries that were - # already validated (e.g. sensor instances from cpp_test.yaml). - for key in components_with_dependencies: - config.setdefault(key, {}) + # are added to the build. + for component_name in components_with_dependencies: + if "." in component_name: + # Format is always "domain.component" (exactly one dot), + # as produced by get_platform_components(). + domain, component = component_name.split(".", maxsplit=1) + domain_list = config.setdefault(domain, []) + CORE.testing_ensure_platform_registered(domain) + domain_list.append({CONF_PLATFORM: component}) + else: + config.setdefault(component_name, []) - print(f"Testing components: {', '.join(components)}") + dependencies = set(components_with_dependencies) - set(components) + deps_str = ", ".join(dependencies) if dependencies else "None" + print(f"Testing components: {', '.join(components)}. Dependencies: {deps_str}") CORE.config = config args = parse_args(["program", "compile", str(CORE.config_path)]) try: @@ -178,13 +215,13 @@ def run_tests(selected_components: list[str]) -> int: print( f"Error compiling unit tests for {', '.join(components)}. Check path. : {e}" ) - return 2 + return EXIT_COMPILE_ERROR # After a successful compilation, locate the executable and run it: idedata = get_idedata(config) if idedata is None: print("Cannot find executable") - return 1 + return EXIT_NO_EXECUTABLE program_path: str = idedata.raw["prog_path"] run_cmd: list[str] = [program_path] diff --git a/script/helpers.py b/script/helpers.py index d372d2a7ec..6ee286a657 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -15,6 +15,8 @@ from typing import Any import colorama +from esphome.loader import get_platform + root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, "..", ".."))) basepath = os.path.join(root_path, "esphome") temp_folder = os.path.join(root_path, ".temp") @@ -624,11 +626,15 @@ def get_usable_cpu_count() -> int: ) -def get_all_dependencies(component_names: set[str]) -> set[str]: +def get_all_dependencies( + component_names: set[str], cpp_testing: bool = False +) -> set[str]: """Get all dependencies for a set of components. Args: component_names: Set of component names to get dependencies for + cpp_testing: If True, set CORE.cpp_testing so AUTO_LOAD callables that + conditionally include testing-only dependencies work correctly Returns: Set of all components including dependencies and auto-loaded components @@ -646,6 +652,7 @@ def get_all_dependencies(component_names: set[str]) -> set[str]: # Reset CORE to ensure clean state CORE.reset() + CORE.cpp_testing = cpp_testing # Set up fake config path for component loading root = Path(__file__).parent.parent @@ -660,7 +667,11 @@ def get_all_dependencies(component_names: set[str]) -> set[str]: new_components: set[str] = set() for comp_name in all_components: - comp = get_component(comp_name) + if "." in comp_name: + domain, platform = comp_name.split(".", maxsplit=1) + comp = get_platform(domain, platform) + else: + comp = get_component(comp_name) if not comp: continue diff --git a/tests/components/README.md b/tests/components/README.md index 0901f2ef17..6da0dadd25 100644 --- a/tests/components/README.md +++ b/tests/components/README.md @@ -7,10 +7,23 @@ testing binaries that combine many components. By convention, this unique namespace is `esphome::component::testing` (where "component" is the component under test), for example: `esphome::uart::testing`. +### Platform components + +For components that expose to a platform component, create a folder under your component test folder with the platform component name, e.g. `binary_sensor` and +include the relevant `.cpp` and `.h` test files there. + +### Override component code generation for testing + +When generating code for testing, ESPHome won't invoke the component's `to_code` function, since most components do not +need to generate configuration code for testing. + +If you do need to generate code to for example configure compilation flags or add libraries, +add the component name to the `CPP_TESTING_CODEGEN_COMPONENTS` allowlist in `script/cpp_unit_test.py`. ## Running component unit tests (from the repository root) + ```bash ./script/cpp_unit_test.py component1 component2 ... ``` diff --git a/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp b/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp new file mode 100644 index 0000000000..36af087d2c --- /dev/null +++ b/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp @@ -0,0 +1,77 @@ +#include "../common.h" + +namespace esphome::packet_transport::testing { + +TEST(PacketTransportBinarySensorTest, AddBinarySensor) { + TestablePacketTransport transport; + binary_sensor::BinarySensor bs; + transport.add_binary_sensor("motion", &bs); + ASSERT_EQ(transport.binary_sensors_.size(), 1u); + EXPECT_STREQ(transport.binary_sensors_[0].id, "motion"); + EXPECT_EQ(transport.binary_sensors_[0].sensor, &bs); +} + +TEST(PacketTransportBinarySensorTest, AddRemoteBinarySensor) { + TestablePacketTransport transport; + binary_sensor::BinarySensor bs; + transport.add_remote_binary_sensor("host1", "remote_motion", &bs); + EXPECT_TRUE(transport.providers_.contains("host1")); + EXPECT_EQ(transport.remote_binary_sensors_["host1"]["remote_motion"], &bs); +} + +TEST(PacketTransportBinarySensorTest, UnencryptedBinarySensorRoundTrip) { + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + binary_sensor::BinarySensor local_bs; + local_bs.state = true; + encoder.add_binary_sensor("motion", &local_bs); + + encoder.send_data_(true); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + binary_sensor::BinarySensor remote_bs; + decoder.add_remote_binary_sensor("sender", "motion", &remote_bs); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + EXPECT_TRUE(remote_bs.state); +} + +TEST(PacketTransportBinarySensorTest, MultipleSensorsRoundTrip) { + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + + sensor::Sensor s1, s2; + s1.state = 10.0f; + s2.state = 20.0f; + encoder.add_sensor("s1", &s1); + encoder.add_sensor("s2", &s2); + + binary_sensor::BinarySensor bs1; + bs1.state = true; + encoder.add_binary_sensor("bs1", &bs1); + + encoder.send_data_(true); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + sensor::Sensor rs1, rs2; + binary_sensor::BinarySensor rbs1; + rs1.state = -999.0f; + rs2.state = -999.0f; + decoder.add_remote_sensor("sender", "s1", &rs1); + decoder.add_remote_sensor("sender", "s2", &rs2); + decoder.add_remote_binary_sensor("sender", "bs1", &rbs1); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + + EXPECT_FLOAT_EQ(rs1.state, 10.0f); + EXPECT_FLOAT_EQ(rs2.state, 20.0f); + EXPECT_TRUE(rbs1.state); +} + +} // namespace esphome::packet_transport::testing diff --git a/tests/components/packet_transport/cpp_test.yaml b/tests/components/packet_transport/cpp_test.yaml deleted file mode 100644 index fa39df3c0a..0000000000 --- a/tests/components/packet_transport/cpp_test.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# Extra component configuration required by C++ unit tests. -# Loaded by cpp_unit_test.py and merged into the test build config -# before validation, so that platform defines (USE_SENSOR, etc.) are generated. - -sensor: - - platform: template - id: test_cpp_sensor - -binary_sensor: - - platform: template - id: test_cpp_binary_sensor diff --git a/tests/components/packet_transport/packet_transport_test.cpp b/tests/components/packet_transport/packet_transport_test.cpp index d8f11ca607..59c0a88ed7 100644 --- a/tests/components/packet_transport/packet_transport_test.cpp +++ b/tests/components/packet_transport/packet_transport_test.cpp @@ -65,198 +65,6 @@ TEST(PacketTransportTest, SetProviderEncryption) { EXPECT_EQ(transport.providers_["host1"].encryption_key, key); } -// --- Sensor management (requires USE_SENSOR / USE_BINARY_SENSOR) --- - -#ifdef USE_SENSOR -TEST(PacketTransportTest, AddSensor) { - TestablePacketTransport transport; - sensor::Sensor s; - transport.add_sensor("temp", &s); - ASSERT_EQ(transport.sensors_.size(), 1u); - EXPECT_STREQ(transport.sensors_[0].id, "temp"); - EXPECT_EQ(transport.sensors_[0].sensor, &s); - EXPECT_TRUE(transport.sensors_[0].updated); -} - -TEST(PacketTransportTest, AddRemoteSensor) { - TestablePacketTransport transport; - sensor::Sensor s; - transport.add_remote_sensor("host1", "remote_temp", &s); - EXPECT_TRUE(transport.providers_.contains("host1")); - EXPECT_EQ(transport.remote_sensors_["host1"]["remote_temp"], &s); -} -#endif - -#ifdef USE_BINARY_SENSOR -TEST(PacketTransportTest, AddBinarySensor) { - TestablePacketTransport transport; - binary_sensor::BinarySensor bs; - transport.add_binary_sensor("motion", &bs); - ASSERT_EQ(transport.binary_sensors_.size(), 1u); - EXPECT_STREQ(transport.binary_sensors_[0].id, "motion"); - EXPECT_EQ(transport.binary_sensors_[0].sensor, &bs); -} - -TEST(PacketTransportTest, AddRemoteBinarySensor) { - TestablePacketTransport transport; - binary_sensor::BinarySensor bs; - transport.add_remote_binary_sensor("host1", "remote_motion", &bs); - EXPECT_TRUE(transport.providers_.contains("host1")); - EXPECT_EQ(transport.remote_binary_sensors_["host1"]["remote_motion"], &bs); -} -#endif - -// --- Unencrypted round-trip tests (require USE_SENSOR / USE_BINARY_SENSOR) --- - -#ifdef USE_SENSOR -TEST(PacketTransportTest, UnencryptedSensorRoundTrip) { - // Encoder - TestablePacketTransport encoder; - encoder.init_for_test("sender"); - sensor::Sensor local_sensor; - local_sensor.state = 42.5f; - encoder.add_sensor("temp", &local_sensor); - - encoder.send_data_(true); - ASSERT_EQ(encoder.sent_packets.size(), 1u); - - // Decoder - TestablePacketTransport decoder; - decoder.init_for_test("receiver"); - sensor::Sensor remote_sensor; - remote_sensor.state = -999.0f; // sentinel - decoder.add_remote_sensor("sender", "temp", &remote_sensor); - - auto &packet = encoder.sent_packets[0]; - decoder.process_({packet.data(), packet.size()}); - EXPECT_FLOAT_EQ(remote_sensor.state, 42.5f); -} -#endif - -#ifdef USE_BINARY_SENSOR -TEST(PacketTransportTest, UnencryptedBinarySensorRoundTrip) { - TestablePacketTransport encoder; - encoder.init_for_test("sender"); - binary_sensor::BinarySensor local_bs; - local_bs.state = true; - encoder.add_binary_sensor("motion", &local_bs); - - encoder.send_data_(true); - ASSERT_EQ(encoder.sent_packets.size(), 1u); - - TestablePacketTransport decoder; - decoder.init_for_test("receiver"); - binary_sensor::BinarySensor remote_bs; - decoder.add_remote_binary_sensor("sender", "motion", &remote_bs); - - auto &packet = encoder.sent_packets[0]; - decoder.process_({packet.data(), packet.size()}); - EXPECT_TRUE(remote_bs.state); -} -#endif - -#if defined(USE_SENSOR) && defined(USE_BINARY_SENSOR) -TEST(PacketTransportTest, MultipleSensorsRoundTrip) { - TestablePacketTransport encoder; - encoder.init_for_test("sender"); - - sensor::Sensor s1, s2; - s1.state = 10.0f; - s2.state = 20.0f; - encoder.add_sensor("s1", &s1); - encoder.add_sensor("s2", &s2); - - binary_sensor::BinarySensor bs1; - bs1.state = true; - encoder.add_binary_sensor("bs1", &bs1); - - encoder.send_data_(true); - ASSERT_EQ(encoder.sent_packets.size(), 1u); - - TestablePacketTransport decoder; - decoder.init_for_test("receiver"); - sensor::Sensor rs1, rs2; - binary_sensor::BinarySensor rbs1; - rs1.state = -999.0f; - rs2.state = -999.0f; - decoder.add_remote_sensor("sender", "s1", &rs1); - decoder.add_remote_sensor("sender", "s2", &rs2); - decoder.add_remote_binary_sensor("sender", "bs1", &rbs1); - - auto &packet = encoder.sent_packets[0]; - decoder.process_({packet.data(), packet.size()}); - - EXPECT_FLOAT_EQ(rs1.state, 10.0f); - EXPECT_FLOAT_EQ(rs2.state, 20.0f); - EXPECT_TRUE(rbs1.state); -} -#endif - -// --- Encrypted round-trip --- - -#ifdef USE_SENSOR -TEST(PacketTransportTest, EncryptedSensorRoundTrip) { - std::vector<uint8_t> key(32); - for (int i = 0; i < 32; i++) - key[i] = i; - - TestablePacketTransport encoder; - encoder.init_for_test("sender"); - encoder.set_encryption_key(key); - sensor::Sensor local_sensor; - local_sensor.state = 99.9f; - encoder.add_sensor("temp", &local_sensor); - - encoder.send_data_(true); - ASSERT_EQ(encoder.sent_packets.size(), 1u); - - TestablePacketTransport decoder; - decoder.init_for_test("receiver"); - sensor::Sensor remote_sensor; - remote_sensor.state = -999.0f; - decoder.add_remote_sensor("sender", "temp", &remote_sensor); - decoder.set_provider_encryption("sender", key); - - auto &packet = encoder.sent_packets[0]; - decoder.process_({packet.data(), packet.size()}); - EXPECT_FLOAT_EQ(remote_sensor.state, 99.9f); -} - -// --- Selective send --- - -TEST(PacketTransportTest, SendDataOnlyUpdated) { - TestablePacketTransport encoder; - encoder.init_for_test("sender"); - - sensor::Sensor s1, s2; - s1.state = 1.0f; - s2.state = 2.0f; - encoder.add_sensor("s1", &s1); - encoder.add_sensor("s2", &s2); - - // Mark s1 as not updated, only s2 as updated - encoder.sensors_[0].updated = false; - encoder.sensors_[1].updated = true; - - encoder.send_data_(false); - ASSERT_EQ(encoder.sent_packets.size(), 1u); - - TestablePacketTransport decoder; - decoder.init_for_test("receiver"); - sensor::Sensor rs1, rs2; - rs1.state = -999.0f; - rs2.state = -999.0f; - decoder.add_remote_sensor("sender", "s1", &rs1); - decoder.add_remote_sensor("sender", "s2", &rs2); - - auto &packet = encoder.sent_packets[0]; - decoder.process_({packet.data(), packet.size()}); - - EXPECT_FLOAT_EQ(rs1.state, -999.0f); // not updated, not sent - EXPECT_FLOAT_EQ(rs2.state, 2.0f); // updated, sent -} -#endif - // --- Ping key tests --- TEST(PacketTransportTest, PingKeyStoredWhenEncrypted) { @@ -319,73 +127,6 @@ TEST(PacketTransportTest, PingKeyMaxLimit) { EXPECT_FALSE(transport.ping_keys_.contains("host4")); } -#ifdef USE_SENSOR -TEST(PacketTransportTest, PingKeyIncludedInTransmittedPacket) { - std::vector<uint8_t> key(32, 0xBB); - - // Responder: encrypted, owns a sensor - TestablePacketTransport responder; - responder.init_for_test("responder"); - responder.set_encryption_key(key); - sensor::Sensor local_sensor; - local_sensor.state = 77.7f; - responder.add_sensor("temp", &local_sensor); - - // Requester sends a MAGIC_PING that the responder processes - auto ping = build_ping_packet("requester", 0xDEADBEEF); - responder.process_({ping.data(), ping.size()}); - ASSERT_EQ(responder.ping_keys_.size(), 1u); - - // Responder sends sensor data — ping key should be embedded - responder.send_data_(true); - ASSERT_EQ(responder.sent_packets.size(), 1u); - - // Requester: encrypted provider, ping-pong enabled, expects key 0xDEADBEEF - TestablePacketTransport requester; - requester.init_for_test("requester"); - requester.set_ping_pong_enable(true); - requester.ping_key_ = 0xDEADBEEF; - sensor::Sensor remote_sensor; - remote_sensor.state = -999.0f; - requester.add_remote_sensor("responder", "temp", &remote_sensor); - requester.set_provider_encryption("responder", key); - - // The requester decrypts the packet and finds its ping key echoed back, - // which gates the sensor data — if the key is missing, data is blocked. - auto &packet = responder.sent_packets[0]; - requester.process_({packet.data(), packet.size()}); - EXPECT_FLOAT_EQ(remote_sensor.state, 77.7f); -} - -TEST(PacketTransportTest, MissingPingKeyBlocksSensorData) { - std::vector<uint8_t> key(32, 0xBB); - - // Responder sends data WITHOUT receiving any MAGIC_PING first — no ping keys - TestablePacketTransport responder; - responder.init_for_test("responder"); - responder.set_encryption_key(key); - sensor::Sensor local_sensor; - local_sensor.state = 77.7f; - responder.add_sensor("temp", &local_sensor); - responder.send_data_(true); - ASSERT_EQ(responder.sent_packets.size(), 1u); - - // Requester with ping-pong enabled expects a key that isn't in the packet - TestablePacketTransport requester; - requester.init_for_test("requester"); - requester.set_ping_pong_enable(true); - requester.ping_key_ = 0xDEADBEEF; - sensor::Sensor remote_sensor; - remote_sensor.state = -999.0f; - requester.add_remote_sensor("responder", "temp", &remote_sensor); - requester.set_provider_encryption("responder", key); - - auto &packet = responder.sent_packets[0]; - requester.process_({packet.data(), packet.size()}); - EXPECT_FLOAT_EQ(remote_sensor.state, -999.0f); // blocked — ping key not found -} -#endif - // --- Process error handling --- TEST(PacketTransportTest, ProcessShortBuffer) { diff --git a/tests/components/packet_transport/sensor/sensor_test.cpp b/tests/components/packet_transport/sensor/sensor_test.cpp new file mode 100644 index 0000000000..2f681aee58 --- /dev/null +++ b/tests/components/packet_transport/sensor/sensor_test.cpp @@ -0,0 +1,170 @@ +#include "../common.h" + +namespace esphome::packet_transport::testing { + +TEST(PacketTransportSensorTest, AddSensor) { + TestablePacketTransport transport; + sensor::Sensor s; + transport.add_sensor("temp", &s); + ASSERT_EQ(transport.sensors_.size(), 1u); + EXPECT_STREQ(transport.sensors_[0].id, "temp"); + EXPECT_EQ(transport.sensors_[0].sensor, &s); + EXPECT_TRUE(transport.sensors_[0].updated); +} + +TEST(PacketTransportSensorTest, AddRemoteSensor) { + TestablePacketTransport transport; + sensor::Sensor s; + transport.add_remote_sensor("host1", "remote_temp", &s); + EXPECT_TRUE(transport.providers_.contains("host1")); + EXPECT_EQ(transport.remote_sensors_["host1"]["remote_temp"], &s); +} + +TEST(PacketTransportSensorTest, UnencryptedSensorRoundTrip) { + // Encoder + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + sensor::Sensor local_sensor; + local_sensor.state = 42.5f; + encoder.add_sensor("temp", &local_sensor); + + encoder.send_data_(true); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + // Decoder + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + sensor::Sensor remote_sensor; + remote_sensor.state = -999.0f; // sentinel + decoder.add_remote_sensor("sender", "temp", &remote_sensor); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + EXPECT_FLOAT_EQ(remote_sensor.state, 42.5f); +} + +TEST(PacketTransportSensorTest, EncryptedSensorRoundTrip) { + std::vector<uint8_t> key(32); + for (int i = 0; i < 32; i++) + key[i] = i; + + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + encoder.set_encryption_key(key); + sensor::Sensor local_sensor; + local_sensor.state = 99.9f; + encoder.add_sensor("temp", &local_sensor); + + encoder.send_data_(true); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + sensor::Sensor remote_sensor; + remote_sensor.state = -999.0f; + decoder.add_remote_sensor("sender", "temp", &remote_sensor); + decoder.set_provider_encryption("sender", key); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + EXPECT_FLOAT_EQ(remote_sensor.state, 99.9f); +} + +TEST(PacketTransportSensorTest, SendDataOnlyUpdated) { + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + + sensor::Sensor s1, s2; + s1.state = 1.0f; + s2.state = 2.0f; + encoder.add_sensor("s1", &s1); + encoder.add_sensor("s2", &s2); + + // Mark s1 as not updated, only s2 as updated + encoder.sensors_[0].updated = false; + encoder.sensors_[1].updated = true; + + encoder.send_data_(false); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + sensor::Sensor rs1, rs2; + rs1.state = -999.0f; + rs2.state = -999.0f; + decoder.add_remote_sensor("sender", "s1", &rs1); + decoder.add_remote_sensor("sender", "s2", &rs2); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + + EXPECT_FLOAT_EQ(rs1.state, -999.0f); // not updated, not sent + EXPECT_FLOAT_EQ(rs2.state, 2.0f); // updated, sent +} + +TEST(PacketTransportSensorTest, PingKeyIncludedInTransmittedPacket) { + std::vector<uint8_t> key(32, 0xBB); + + // Responder: encrypted, owns a sensor + TestablePacketTransport responder; + responder.init_for_test("responder"); + responder.set_encryption_key(key); + sensor::Sensor local_sensor; + local_sensor.state = 77.7f; + responder.add_sensor("temp", &local_sensor); + + // Requester sends a MAGIC_PING that the responder processes + auto ping = build_ping_packet("requester", 0xDEADBEEF); + responder.process_({ping.data(), ping.size()}); + ASSERT_EQ(responder.ping_keys_.size(), 1u); + + // Responder sends sensor data — ping key should be embedded + responder.send_data_(true); + ASSERT_EQ(responder.sent_packets.size(), 1u); + + // Requester: encrypted provider, ping-pong enabled, expects key 0xDEADBEEF + TestablePacketTransport requester; + requester.init_for_test("requester"); + requester.set_ping_pong_enable(true); + requester.ping_key_ = 0xDEADBEEF; + sensor::Sensor remote_sensor; + remote_sensor.state = -999.0f; + requester.add_remote_sensor("responder", "temp", &remote_sensor); + requester.set_provider_encryption("responder", key); + + // The requester decrypts the packet and finds its ping key echoed back, + // which gates the sensor data — if the key is missing, data is blocked. + auto &packet = responder.sent_packets[0]; + requester.process_({packet.data(), packet.size()}); + EXPECT_FLOAT_EQ(remote_sensor.state, 77.7f); +} + +TEST(PacketTransportSensorTest, MissingPingKeyBlocksSensorData) { + std::vector<uint8_t> key(32, 0xBB); + + // Responder sends data WITHOUT receiving any MAGIC_PING first — no ping keys + TestablePacketTransport responder; + responder.init_for_test("responder"); + responder.set_encryption_key(key); + sensor::Sensor local_sensor; + local_sensor.state = 77.7f; + responder.add_sensor("temp", &local_sensor); + responder.send_data_(true); + ASSERT_EQ(responder.sent_packets.size(), 1u); + + // Requester with ping-pong enabled expects a key that isn't in the packet + TestablePacketTransport requester; + requester.init_for_test("requester"); + requester.set_ping_pong_enable(true); + requester.ping_key_ = 0xDEADBEEF; + sensor::Sensor remote_sensor; + remote_sensor.state = -999.0f; + requester.add_remote_sensor("responder", "temp", &remote_sensor); + requester.set_provider_encryption("responder", key); + + auto &packet = responder.sent_packets[0]; + requester.process_({packet.data(), packet.size()}); + EXPECT_FLOAT_EQ(remote_sensor.state, -999.0f); // blocked — ping key not found +} + +} // namespace esphome::packet_transport::testing diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 2953a9fd42..781054eb3b 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -1027,6 +1027,73 @@ def test_get_all_dependencies_empty_set() -> None: assert result == set() +def test_get_all_dependencies_platform_component() -> None: + """Platform components (domain.component) are looked up via get_platform, + not get_component.""" + platform_comp = Mock() + platform_comp.dependencies = [] + platform_comp.auto_load = [] + + with ( + patch("esphome.loader.get_component") as mock_get_component, + patch("helpers.get_platform") as mock_get_platform, + ): + mock_get_platform.return_value = platform_comp + mock_get_component.return_value = None + + result = helpers.get_all_dependencies({"sensor.bthome"}) + + mock_get_platform.assert_called_once_with("sensor", "bthome") + mock_get_component.assert_not_called() + assert result == {"sensor.bthome"} + + +def test_get_all_dependencies_platform_component_with_dependencies() -> None: + """Dependencies of a platform component are resolved transitively.""" + platform_comp = Mock() + platform_comp.dependencies = ["sensor"] + platform_comp.auto_load = [] + + sensor_comp = Mock() + sensor_comp.dependencies = [] + sensor_comp.auto_load = [] + + with ( + patch("esphome.loader.get_component") as mock_get_component, + patch("helpers.get_platform") as mock_get_platform, + ): + mock_get_platform.return_value = platform_comp + mock_get_component.side_effect = lambda name: ( + sensor_comp if name == "sensor" else None + ) + + result = helpers.get_all_dependencies({"sensor.bthome"}) + + assert result == {"sensor.bthome", "sensor"} + + +def test_get_all_dependencies_cpp_testing_flag() -> None: + """cpp_testing=True propagates to CORE.cpp_testing during resolution.""" + from esphome.core import CORE + + with ( + patch("esphome.loader.get_component") as mock_get_component, + patch("esphome.loader.get_platform"), + ): + observed: list[bool] = [] + + def capturing_get_component(name: str): + observed.append(CORE.cpp_testing) + + mock_get_component.side_effect = capturing_get_component + + helpers.get_all_dependencies({"some_comp"}, cpp_testing=True) + + assert observed and all(observed), ( + "CORE.cpp_testing should be True during resolution" + ) + + def test_get_components_from_integration_fixtures() -> None: """Test extraction of components from fixture YAML files.""" yaml_content = { diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 174b3fec85..22be59653a 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -841,6 +841,18 @@ class TestEsphomeCore: assert "WiFi" in target.platformio_libraries + def test_testing_ensure_platform_registered__sets_count(self, target): + """Test testing_ensure_platform_registered sets count to 1 for new platform.""" + assert target.platform_counts["sensor"] == 0 + target.testing_ensure_platform_registered("sensor") + assert target.platform_counts["sensor"] == 1 + + def test_testing_ensure_platform_registered__does_not_overwrite(self, target): + """Test testing_ensure_platform_registered preserves existing count.""" + target.platform_counts["sensor"] = 3 + target.testing_ensure_platform_registered("sensor") + assert target.platform_counts["sensor"] == 3 + def test_add_library__extracts_short_name_from_path(self, target): """Test add_library extracts short name from library paths like owner/lib.""" target.data[const.KEY_CORE] = { From 4b50d14496896181d261a3c8a9dc5b4d4a062bda Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Mar 2026 21:10:03 -1000 Subject: [PATCH 506/609] [serial_proxy] Reduce loop() overhead by disabling when idle and splitting read path (#14673) --- .../components/serial_proxy/serial_proxy.cpp | 29 ++++++++++++++----- .../components/serial_proxy/serial_proxy.h | 5 ++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/esphome/components/serial_proxy/serial_proxy.cpp b/esphome/components/serial_proxy/serial_proxy.cpp index 340f9b0cb8..00d822b75c 100644 --- a/esphome/components/serial_proxy/serial_proxy.cpp +++ b/esphome/components/serial_proxy/serial_proxy.cpp @@ -28,25 +28,38 @@ void SerialProxy::setup() { // instance_index_ is fixed at registration time; pre-set it so loop() only needs to update data this->outgoing_msg_.instance = this->instance_index_; #endif + // No subscriber at startup; disable loop until a client subscribes + this->disable_loop(); } void SerialProxy::loop() { #ifdef USE_API - // Detect subscriber disconnect - if (this->api_connection_ != nullptr && (this->api_connection_->is_marked_for_removal() || - !this->api_connection_->is_connection_setup() || !api_is_connected())) { - ESP_LOGW(TAG, "Subscriber disconnected"); - this->api_connection_ = nullptr; + // Safety check — loop should only run when subscribed, but guard against races + if (this->api_connection_ == nullptr) [[unlikely]] { + this->disable_loop(); + return; } - if (this->api_connection_ == nullptr) + // Detect subscriber disconnect + if (this->api_connection_->is_marked_for_removal() || !this->api_connection_->is_connection_setup() || + !api_is_connected()) { + ESP_LOGW(TAG, "Subscriber disconnected"); + this->api_connection_ = nullptr; + this->disable_loop(); return; + } // Read available data from UART and forward to subscribed client size_t available = this->available(); if (available == 0) return; + this->read_and_send_(available); +#endif +} + +#ifdef USE_API +void __attribute__((noinline)) SerialProxy::read_and_send_(size_t available) { // Read in chunks up to SERIAL_PROXY_MAX_READ_SIZE uint8_t buffer[SERIAL_PROXY_MAX_READ_SIZE]; size_t to_read = std::min(available, sizeof(buffer)); @@ -56,8 +69,8 @@ void SerialProxy::loop() { this->outgoing_msg_.set_data(buffer, to_read); this->api_connection_->send_serial_proxy_data(this->outgoing_msg_); -#endif } +#endif void SerialProxy::dump_config() { ESP_LOGCONFIG(TAG, @@ -166,6 +179,7 @@ void SerialProxy::serial_proxy_request(api::APIConnection *api_connection, api:: return; } this->api_connection_ = api_connection; + this->enable_loop(); ESP_LOGV(TAG, "API connection subscribed to serial proxy [%u]", this->instance_index_); break; case api::enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: @@ -174,6 +188,7 @@ void SerialProxy::serial_proxy_request(api::APIConnection *api_connection, api:: return; } this->api_connection_ = nullptr; + this->disable_loop(); ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%u]", this->instance_index_); break; default: diff --git a/esphome/components/serial_proxy/serial_proxy.h b/esphome/components/serial_proxy/serial_proxy.h index 62f942b19d..5adfa4fe53 100644 --- a/esphome/components/serial_proxy/serial_proxy.h +++ b/esphome/components/serial_proxy/serial_proxy.h @@ -101,6 +101,11 @@ class SerialProxy : public uart::UARTDevice, public Component { void set_dtr_pin(GPIOPin *pin) { this->dtr_pin_ = pin; } protected: +#ifdef USE_API + /// Read from UART and send to API client (slow path with 256-byte stack buffer) + void read_and_send_(size_t available); +#endif + /// Instance index for identifying this proxy in API messages uint32_t instance_index_{0}; From fba21e6dd4bd321d4b0ee3b31869e81fee75aab5 Mon Sep 17 00:00:00 2001 From: Anunay Kulshrestha <anunay@kul.sh> Date: Tue, 10 Mar 2026 20:14:19 +0530 Subject: [PATCH 507/609] [bl0940] Fix reset_calibration() declaration missing from header (#14676) Co-authored-by: Claude <noreply@anthropic.com> --- esphome/components/bl0940/bl0940.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/bl0940/bl0940.h b/esphome/components/bl0940/bl0940.h index 93d54003f5..e0ca748a22 100644 --- a/esphome/components/bl0940/bl0940.h +++ b/esphome/components/bl0940/bl0940.h @@ -69,10 +69,8 @@ class BL0940 : public PollingComponent, public uart::UARTDevice { void set_energy_calibration_number(number::Number *num) { this->energy_calibration_number_ = num; } #endif -#ifdef USE_BUTTON - // Resets all calibration values to defaults (can be triggered by a button) + // Resets all calibration values to defaults void reset_calibration(); -#endif // Core component methods void loop() override; From 06a127f64b49c7306815100cc667af8c9842c463 Mon Sep 17 00:00:00 2001 From: Diorcet Yann <diorcety@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:52:48 +0100 Subject: [PATCH 508/609] [core] ESP-IDF compilation fixes (#14541) --- esphome/build_gen/espidf.py | 6 ++-- esphome/espidf_api.py | 61 ++++++++++++++++++++++++++++++++----- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index f45efb82c1..9df9b1069c 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -72,7 +72,7 @@ def get_component_cmakelists(minimal: bool = False) -> str: # Extract compile definitions from build flags (-DXXX -> XXX) compile_defs = [flag[2:] for flag in CORE.build_flags if flag.startswith("-D")] - compile_defs_str = "\n ".join(compile_defs) if compile_defs else "" + compile_defs_str = "\n ".join(sorted(compile_defs)) if compile_defs else "" # Extract compile options (-W flags, excluding linker flags) compile_opts = [ @@ -80,11 +80,11 @@ def get_component_cmakelists(minimal: bool = False) -> str: for flag in CORE.build_flags if flag.startswith("-W") and not flag.startswith("-Wl,") ] - compile_opts_str = "\n ".join(compile_opts) if compile_opts else "" + compile_opts_str = "\n ".join(sorted(compile_opts)) if compile_opts else "" # Extract linker options (-Wl, flags) link_opts = [flag for flag in CORE.build_flags if flag.startswith("-Wl,")] - link_opts_str = "\n ".join(link_opts) if link_opts else "" + link_opts_str = "\n ".join(sorted(link_opts)) if link_opts else "" return f"""\ # Auto-generated by ESPHome diff --git a/esphome/espidf_api.py b/esphome/espidf_api.py index 9e9c57bfbd..9ebcc48513 100644 --- a/esphome/espidf_api.py +++ b/esphome/espidf_api.py @@ -8,7 +8,6 @@ import shutil import subprocess from esphome.components.esp32.const import KEY_ESP32, KEY_FLASH_SIZE -from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME from esphome.core import CORE, EsphomeError _LOGGER = logging.getLogger(__name__) @@ -102,6 +101,55 @@ def run_reconfigure() -> int: return run_idf_py("reconfigure") +def has_outdated_files(): + """Check if the build configuration is stale. + + Returns True if required build files are missing or if configuration inputs + are newer than the generated CMake/Ninja build artifacts. + """ + cmakecache_txt_path = CORE.relative_build_path("build/CMakeCache.txt") + + cmakelists_txt_build_path = CORE.relative_build_path("CMakeLists.txt") + cmakelists_txt_src_path = CORE.relative_src_path("CMakeLists.txt") + build_config_path = CORE.relative_build_path("build/config") + sdkconfig_internal_path = CORE.relative_build_path( + f"sdkconfig.{CORE.name}.esphomeinternal" + ) + dependency_lock_path = CORE.relative_build_path("dependencies.lock") + build_ninja_path = CORE.relative_build_path("build/build.ninja") + + if not os.path.isdir(build_config_path) or not os.listdir(build_config_path): + return True + if not os.path.isfile(cmakecache_txt_path): + return True + if not os.path.isfile(build_ninja_path): + return True + if os.path.isfile(dependency_lock_path) and os.path.getmtime( + dependency_lock_path + ) > os.path.getmtime(build_ninja_path): + return True + + cmakecache_txt_mtime = os.path.getmtime(cmakecache_txt_path) + return any( + os.path.getmtime(f) > cmakecache_txt_mtime + for f in [ + _get_idf_path(), + cmakelists_txt_build_path, + cmakelists_txt_src_path, + sdkconfig_internal_path, + build_config_path, + ] + if f and os.path.exists(f) + ) + + +def need_reconfigure() -> bool: + from esphome.build_gen.espidf import has_discovered_components + + # We need to reconfigure either if the files are outdated or if there is no component discovered + return has_outdated_files() or not has_discovered_components() + + def run_compile(config, verbose: bool) -> int: """Compile the ESP-IDF project. @@ -110,10 +158,10 @@ def run_compile(config, verbose: bool) -> int: 2. Regenerate CMakeLists.txt with discovered components 3. Run full build """ - from esphome.build_gen.espidf import has_discovered_components, write_project + from esphome.build_gen.espidf import write_project # Check if we need to do discovery phase - if not has_discovered_components(): + if need_reconfigure(): _LOGGER.info("Discovering available ESP-IDF components...") write_project(minimal=True) rc = run_reconfigure() @@ -124,15 +172,12 @@ def run_compile(config, verbose: bool) -> int: write_project(minimal=False) # Build - args = ["build"] + args = [] if verbose: args.append("-v") - # Add parallel job limit if configured - if CONF_COMPILE_PROCESS_LIMIT in config.get(CONF_ESPHOME, {}): - limit = config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT] - args.extend(["-j", str(limit)]) + args.append("build") # Set the sdkconfig file sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}") From 2c7ef4f758522ac324a5983dfc89f963fa22af70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 09:10:33 -1000 Subject: [PATCH 509/609] [rp2040] Use picotool for BOOTSEL upload and improve upload UX (#14483) --- esphome/__main__.py | 211 ++++++++++++++++- esphome/espota2.py | 26 +-- esphome/helpers.py | 27 +++ esphome/util.py | 70 ++++++ tests/unit_tests/test_main.py | 420 ++++++++++++++++++++++++++++++++++ tests/unit_tests/test_util.py | 132 +++++++++++ 6 files changed, 859 insertions(+), 27 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 0164e2eeb3..f33e7f4b42 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -9,6 +9,8 @@ import logging import os from pathlib import Path import re +import shutil +import subprocess import sys import time from typing import Protocol @@ -44,7 +46,9 @@ from esphome.const import ( CONF_SUBSTITUTIONS, CONF_TOPIC, ENV_NOGITIGNORE, + KEY_CORE, KEY_NATIVE_IDF, + KEY_TARGET_PLATFORM, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, @@ -56,7 +60,11 @@ from esphome.helpers import get_bool_env, indent, is_ip_address from esphome.log import AnsiFore, color, setup_log from esphome.types import ConfigType from esphome.util import ( + PICOTOOL_PACKAGE, + detect_rp2040_bootsel, + get_picotool_path, get_serial_ports, + is_picotool_usb_permission_error, list_yaml_files, run_external_command, run_external_process, @@ -68,6 +76,21 @@ _LOGGER = logging.getLogger(__name__) # Maximum buffer size for serial log reading to prevent unbounded memory growth SERIAL_BUFFER_MAX_SIZE = 65536 +_RP2040_BOOTSEL_INSTRUCTIONS = ( + "To enter BOOTSEL mode:\n" + " 1. Unplug the device\n" + " 2. Hold the BOOT/BOOTSEL button\n" + " 3. Plug in the USB cable while holding the button\n" + " 4. Release the button - the device should appear as a USB drive (RPI-RP2)\n" + "Then run the upload command again." +) + +_RP2040_UDEV_HINT = ( + "You may need to add a udev rule for RP2040 devices. " + "See: https://github.com/raspberrypi/picotool" + "/blob/master/udev/60-picotool.rules" +) + # Special non-component keys that appear in configs _NON_COMPONENT_KEYS = frozenset( { @@ -163,6 +186,7 @@ class PortType(StrEnum): NETWORK = "NETWORK" MQTT = "MQTT" MQTTIP = "MQTTIP" + BOOTSEL = "BOOTSEL" # Magic MQTT port types that require special handling @@ -241,6 +265,19 @@ def choose_upload_log_host( (f"{port.path} ({port.description})", port.path) for port in get_serial_ports() ] + # Add RP2040 BOOTSEL device option when uploading + bootsel_permission_error = False + if ( + purpose == Purpose.UPLOADING + and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040 + and (picotool := _find_picotool()) is not None + ): + bootsel = detect_rp2040_bootsel(picotool) + if bootsel.device_count > 0: + options.append(("RP2040 BOOTSEL (via picotool)", "BOOTSEL")) + elif bootsel.permission_error: + bootsel_permission_error = True + if purpose == Purpose.LOGGING: if has_mqtt_logging(): mqtt_config = CORE.config[CONF_MQTT] @@ -258,6 +295,25 @@ def choose_upload_log_host( if has_mqtt_ip_lookup(): options.append(("Over The Air (MQTT IP lookup)", "MQTTIP")) + # Show helpful BOOTSEL instructions for RP2040 when no BOOTSEL device is found + if ( + purpose == Purpose.UPLOADING + and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040 + and not any(get_port_type(opt[1]) == PortType.BOOTSEL for opt in options) + ): + if bootsel_permission_error: + _LOGGER.warning( + "An RP2040 device in BOOTSEL mode was detected but could " + "not be accessed due to USB permissions." + ) + if sys.platform.startswith("linux"): + _LOGGER.warning(_RP2040_UDEV_HINT) + if not options: + raise EsphomeError( + f"No RP2040 device found. {_RP2040_BOOTSEL_INSTRUCTIONS}" + ) + _LOGGER.info("Tip: %s", _RP2040_BOOTSEL_INSTRUCTIONS) + if check_default is not None and check_default in [opt[1] for opt in options]: return [check_default] return [choose_prompt(options, purpose=purpose)] @@ -404,10 +460,13 @@ def get_port_type(port: str) -> PortType: Returns: PortType.SERIAL for serial ports (/dev/ttyUSB0, COM1, etc.) + PortType.BOOTSEL for RP2040 BOOTSEL upload via picotool PortType.MQTT for MQTT logging PortType.MQTTIP for MQTT IP lookup PortType.NETWORK for IP addresses, hostnames, or mDNS names """ + if port == "BOOTSEL": + return PortType.BOOTSEL if port.startswith("/") or port.startswith("COM"): return PortType.SERIAL if port == "MQTT": @@ -695,15 +754,138 @@ def upload_using_esptool( return run_esptool(115200) -def upload_using_platformio(config: ConfigType, port: str): +def upload_using_platformio(config: ConfigType, port: str) -> int: from esphome import platformio_api + # RP2040 platform-raspberrypi build recipe expects firmware.bin.signed for + # the upload target, but 'nobuild' skips the build phase that creates it. + # Create it here so the upload doesn't fail. + if CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040: + idedata = platformio_api.get_idedata(config) + build_dir = Path(idedata.firmware_elf_path).parent + firmware_bin = build_dir / "firmware.bin" + signed_bin = build_dir / "firmware.bin.signed" + if firmware_bin.is_file() and not signed_bin.is_file(): + shutil.copy2(firmware_bin, signed_bin) + upload_args = ["-t", "upload", "-t", "nobuild"] if port is not None: upload_args += ["--upload-port", port] return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args) +def _find_picotool() -> Path | None: + """Find the picotool binary from PlatformIO packages.""" + from esphome import platformio_api + + try: + idedata = platformio_api.get_idedata(CORE.config) + except Exception: # noqa: BLE001 # pylint: disable=broad-except + return None + return get_picotool_path(idedata.cc_path) + + +def upload_using_picotool(config: ConfigType) -> int: + """Upload firmware to RP2040 in BOOTSEL mode using picotool. + + Uses picotool to load the ELF firmware directly via USB, avoiding + the mass storage copy approach that causes "disk not ejected properly" + warnings on macOS. + """ + from esphome import platformio_api + + idedata = platformio_api.get_idedata(config) + firmware_elf = Path(idedata.firmware_elf_path) + + if not firmware_elf.is_file(): + _LOGGER.error( + "Firmware ELF file not found at %s. " + "Make sure the project has been compiled first.", + firmware_elf, + ) + return 1 + + picotool = get_picotool_path(idedata.cc_path) + if picotool is None: + _LOGGER.error( + "picotool not found. Ensure the RP2040 PlatformIO platform " + "is installed (%s).", + PICOTOOL_PACKAGE, + ) + return 1 + + _LOGGER.info("Uploading firmware to RP2040 via picotool...") + try: + # Don't capture stdout — let picotool write directly to the terminal + # so progress bars display in real-time with \r updates. + # Capture stderr only so we can detect permission errors. + result = subprocess.run( + [str(picotool), "load", "-v", "-x", str(firmware_elf)], + stderr=subprocess.PIPE, + timeout=60, + check=False, + ) + except subprocess.TimeoutExpired: + _LOGGER.error("picotool upload timed out after 60 seconds.") + return 1 + except OSError as err: + _LOGGER.error("Failed to run picotool: %s", err) + return 1 + + if result.returncode != 0: + stderr = result.stderr.decode("utf-8", errors="replace").strip() + if stderr: + for line in stderr.splitlines(): + safe_print(line) + if is_picotool_usb_permission_error(stderr): + msg = "Permission denied accessing USB device." + if sys.platform.startswith("linux"): + msg += f" {_RP2040_UDEV_HINT}" + _LOGGER.error(msg) + else: + _LOGGER.error("picotool upload failed (exit code %d).", result.returncode) + return 1 + + return 0 + + +def _wait_for_serial_port( + port: str | None = None, + timeout: float = 30.0, + known_ports: set[str] | None = None, +) -> None: + """Wait for a serial port to appear, e.g. after a device reboot. + + USB-CDC devices disappear briefly after flashing while the device + reboots and re-enumerates on the USB bus. + + If port is given, wait for that specific path. If known_ports is + given, wait for a new port that wasn't in the set. Otherwise wait + for any serial port to appear. + """ + + def _port_found() -> bool: + ports = get_serial_ports() + if port is not None: + return any(p.path == port for p in ports) + if known_ports is not None: + return any(p.path not in known_ports for p in ports) + return bool(ports) + + if _port_found(): + return + if port is not None: + _LOGGER.info("Waiting for %s to come online...", port) + else: + _LOGGER.info("Waiting for device to reboot...") + start = time.monotonic() + while time.monotonic() - start < timeout: + time.sleep(0.05) + if _port_found(): + time.sleep(0.05) + return + + def check_permissions(port: str): if os.name == "posix" and get_port_type(port) == PortType.SERIAL: # Check if we can open selected serial port @@ -733,7 +915,15 @@ def upload_program( except AttributeError: pass - if get_port_type(host) == PortType.SERIAL: + port_type = get_port_type(host) + + if port_type == PortType.BOOTSEL: + exit_code = upload_using_picotool(config) + # Return None for device - BOOTSEL can't be used for logging, + # so command_run will show the interactive chooser for log source + return exit_code, None + + if port_type == PortType.SERIAL: check_permissions(host) exit_code = 1 @@ -787,6 +977,7 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int port_type = get_port_type(port) if port_type == PortType.SERIAL: + _wait_for_serial_port(port) check_permissions(port) return run_miniterm(config, port, args) @@ -925,6 +1116,9 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: purpose=Purpose.UPLOADING, ) + # Snapshot current serial ports before upload so we can detect new ones + pre_upload_ports = {p.path for p in get_serial_ports()} + exit_code, successful_device = upload_program(config, args, devices) if exit_code == 0: _LOGGER.info("Successfully uploaded program.") @@ -935,6 +1129,19 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: if args.no_logs: return 0 + # After BOOTSEL upload, wait for a new serial port to appear + # so it shows up in the log chooser + if ( + successful_device is None + and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040 + ): + _wait_for_serial_port(known_ports=pre_upload_ports) + # If exactly one new serial port appeared, use it directly + serial_ports = get_serial_ports() + new_ports = [p for p in serial_ports if p.path not in pre_upload_ports] + if len(new_ports) == 1: + successful_device = new_ports[0].path + # For logs, prefer the device we successfully uploaded to devices = choose_upload_log_host( default=successful_device, diff --git a/esphome/espota2.py b/esphome/espota2.py index c342eb4463..c412bb51ff 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -13,7 +13,7 @@ import time from typing import Any from esphome.core import EsphomeError -from esphome.helpers import resolve_ip_address +from esphome.helpers import ProgressBar, resolve_ip_address RESPONSE_OK = 0x00 RESPONSE_REQUEST_AUTH = 0x01 @@ -63,30 +63,6 @@ _AUTH_METHODS: dict[int, tuple[Callable[..., Any], int, str]] = { } -class ProgressBar: - def __init__(self): - self.last_progress = None - - def update(self, progress): - bar_length = 60 - status = "" - if progress >= 1: - progress = 1 - status = "Done...\r\n" - new_progress = int(progress * 100) - if new_progress == self.last_progress: - return - self.last_progress = new_progress - block = int(round(bar_length * progress)) - text = f"\rUploading: [{'=' * block + ' ' * (bar_length - block)}] {new_progress}% {status}" - sys.stderr.write(text) - sys.stderr.flush() - - def done(self): - sys.stderr.write("\n") - sys.stderr.flush() - - class OTAError(EsphomeError): pass diff --git a/esphome/helpers.py b/esphome/helpers.py index 145ebd4096..f41bec357d 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -9,6 +9,7 @@ import platform import re import shutil import stat +import sys import tempfile from typing import TYPE_CHECKING from urllib.parse import urlparse @@ -585,6 +586,32 @@ def sanitize(value): return _DISALLOWED_CHARS.sub("_", value) +class ProgressBar: + """A simple terminal progress bar for upload operations.""" + + def __init__(self) -> None: + self.last_progress: int | None = None + + def update(self, progress: float) -> None: + bar_length = 60 + status = "" + if progress >= 1: + progress = 1 + status = "Done...\r\n" + new_progress = int(progress * 100) + if new_progress == self.last_progress: + return + self.last_progress = new_progress + block = int(round(bar_length * progress)) + text = f"\rUploading: [{'=' * block + ' ' * (bar_length - block)}] {new_progress}% {status}" + sys.stderr.write(text) + sys.stderr.flush() + + def done(self) -> None: + sys.stderr.write("\n") + sys.stderr.flush() + + def docs_url(path: str) -> str: """Return the URL to the documentation for a given path.""" # Local import to avoid circular import diff --git a/esphome/util.py b/esphome/util.py index 686aa74306..6a21b4f627 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -1,5 +1,6 @@ import collections from collections.abc import Callable +from dataclasses import dataclass import io import logging from pathlib import Path @@ -355,6 +356,75 @@ def get_serial_ports() -> list[SerialPort]: return result +PICOTOOL_PACKAGE = "tool-picotool-rp2040-earlephilhower" + + +def get_picotool_path(cc_path: str) -> Path | None: + """Derive the picotool binary path from the PlatformIO toolchain cc_path. + + The cc_path from IDEData points to the toolchain package, e.g.: + ~/.platformio/packages/toolchain-rp2040-earlephilhower/bin/arm-none-eabi-gcc + Picotool is in a sibling package: + ~/.platformio/packages/tool-picotool-rp2040-earlephilhower/picotool + """ + cc = Path(cc_path) + # Go from .../packages/toolchain-.../bin/gcc up to .../packages/ + packages_dir = cc.parent.parent.parent + binary_name = "picotool.exe" if sys.platform == "win32" else "picotool" + picotool = packages_dir / PICOTOOL_PACKAGE / binary_name + if picotool.is_file(): + return picotool + return None + + +def is_picotool_usb_permission_error(output: str | bytes) -> bool: + """Check if picotool output indicates a USB permission error.""" + if isinstance(output, str): + return ( + "unable to connect" in output + or "LIBUSB_ERROR_ACCESS" in output + or "Permission denied" in output + ) + return ( + b"unable to connect" in output + or b"LIBUSB_ERROR_ACCESS" in output + or b"Permission denied" in output + ) + + +@dataclass +class BootselResult: + """Result of RP2040 BOOTSEL detection.""" + + device_count: int + permission_error: bool = False + + +def detect_rp2040_bootsel(picotool_path: str | Path) -> BootselResult: + """Detect RP2040/RP2350 devices in BOOTSEL mode using picotool. + + Returns a BootselResult with the number of devices found (by counting + 'type:' lines in output), and whether a permission error was detected. + """ + try: + result = subprocess.run( + [str(picotool_path), "info", "-d"], + capture_output=True, + timeout=10, + check=False, + ) + device_count = result.stdout.count(b"type:") + if device_count > 0: + return BootselResult(device_count) + # Check for permission issues — picotool can see the device + # on the USB bus but can't connect without proper permissions + if is_picotool_usb_permission_error(result.stderr + result.stdout): + return BootselResult(0, permission_error=True) + return BootselResult(0) + except (OSError, subprocess.TimeoutExpired): + return BootselResult(0) + + def get_esp32_arduino_flash_error_help() -> str | None: """Returns helpful message when ESP32 with Arduino runs out of flash space.""" from esphome.core import CORE diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index cef561c54b..b6f1a28086 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -8,6 +8,7 @@ import json import logging from pathlib import Path import re +import sys import time from typing import Any from unittest.mock import MagicMock, Mock, patch @@ -40,6 +41,8 @@ from esphome.__main__ import ( show_logs, upload_program, upload_using_esptool, + upload_using_picotool, + upload_using_platformio, ) from esphome.components.esp32 import KEY_ESP32, KEY_VARIANT, VARIANT_ESP32 from esphome.const import ( @@ -70,6 +73,7 @@ from esphome.const import ( PLATFORM_RP2040, ) from esphome.core import CORE, EsphomeError +from esphome.util import BootselResult def strip_ansi_codes(text: str) -> str: @@ -174,6 +178,13 @@ def mock_upload_using_platformio() -> Generator[Mock]: yield mock +@pytest.fixture +def mock_upload_using_picotool() -> Generator[Mock]: + """Mock upload_using_picotool for testing.""" + with patch("esphome.__main__.upload_using_picotool") as mock: + yield mock + + @pytest.fixture def mock_run_ota() -> Generator[Mock]: """Mock espota2.run_ota for testing.""" @@ -851,6 +862,221 @@ def test_choose_upload_log_host_no_address_with_ota_config() -> None: ) +@pytest.mark.usefixtures("mock_no_serial_ports") +def test_choose_upload_log_host_no_defaults_with_rp2040_bootsel( + mock_choose_prompt: Mock, +) -> None: + """Test interactive mode shows RP2040 BOOTSEL option via picotool.""" + setup_core(platform=PLATFORM_RP2040) + + with ( + patch( + "esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool") + ), + patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(1)), + ): + result = choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert result == ["/dev/ttyUSB0"] # mock_choose_prompt default + mock_choose_prompt.assert_called_once_with( + [("RP2040 BOOTSEL (via picotool)", "BOOTSEL")], + purpose=Purpose.UPLOADING, + ) + + +@pytest.mark.usefixtures("mock_no_serial_ports") +def test_choose_upload_log_host_rp2040_no_device_shows_bootsel_help() -> None: + """Test BOOTSEL instructions shown when no RP2040 device found.""" + setup_core(platform=PLATFORM_RP2040) + + with ( + patch( + "esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool") + ), + patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(0)), + pytest.raises(EsphomeError, match="BOOTSEL"), + ): + choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.UPLOADING, + ) + + +@pytest.mark.usefixtures("mock_no_serial_ports") +def test_choose_upload_log_host_rp2040_bootsel_tip_with_ota( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test BOOTSEL tip shown when only OTA options exist for RP2040.""" + setup_core( + platform=PLATFORM_RP2040, + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, + address="192.168.1.100", + ) + + with ( + patch( + "esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool") + ), + patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(0)), + patch( + "esphome.__main__.choose_prompt", + return_value="192.168.1.100", + ), + caplog.at_level(logging.INFO, logger="esphome.__main__"), + ): + choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert "BOOTSEL" in caplog.text + + +def test_choose_upload_log_host_rp2040_bootsel_tip_with_serial_ports( + caplog: pytest.LogCaptureFixture, + mock_choose_prompt: Mock, +) -> None: + """Test BOOTSEL tip shown when serial ports exist but no BOOTSEL device.""" + setup_core(platform=PLATFORM_RP2040) + + mock_ports = [MockSerialPort("/dev/ttyACM0", "RP2040 Serial")] + with ( + patch("esphome.__main__.get_serial_ports", return_value=mock_ports), + patch( + "esphome.__main__._find_picotool", + return_value=Path("/usr/bin/picotool"), + ), + patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(0)), + caplog.at_level(logging.INFO, logger="esphome.__main__"), + ): + choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert "BOOTSEL" in caplog.text + + +@pytest.mark.usefixtures("mock_no_serial_ports") +def test_choose_upload_log_host_rp2040_permission_error_no_options( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test permission warning shown when BOOTSEL device found but not accessible.""" + setup_core(platform=PLATFORM_RP2040) + + with ( + patch( + "esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool") + ), + patch( + "esphome.__main__.detect_rp2040_bootsel", + return_value=BootselResult(0, permission_error=True), + ), + patch("esphome.__main__.sys.platform", "linux"), + pytest.raises(EsphomeError, match="BOOTSEL"), + caplog.at_level(logging.WARNING, logger="esphome.__main__"), + ): + choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.UPLOADING, + ) + + assert "USB permissions" in caplog.text + assert "udev" in caplog.text + + +@pytest.mark.usefixtures("mock_no_serial_ports") +def test_choose_upload_log_host_rp2040_permission_error_with_ota( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test permission warning shown with OTA fallback available.""" + setup_core( + platform=PLATFORM_RP2040, + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, + address="192.168.1.100", + ) + + with ( + patch( + "esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool") + ), + patch( + "esphome.__main__.detect_rp2040_bootsel", + return_value=BootselResult(0, permission_error=True), + ), + patch( + "esphome.__main__.choose_prompt", + return_value="192.168.1.100", + ), + caplog.at_level(logging.WARNING, logger="esphome.__main__"), + ): + choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.UPLOADING, + ) + + assert "USB permissions" in caplog.text + + +def test_choose_upload_log_host_no_bootsel_for_non_rp2040( + mock_no_serial_ports: Mock, +) -> None: + """Test that BOOTSEL detection is not run for non-RP2040 platforms.""" + setup_core( + platform=PLATFORM_ESP32, + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, + address="192.168.1.100", + ) + + with ( + patch("esphome.__main__._find_picotool") as mock_find_picotool, + patch( + "esphome.__main__.choose_prompt", + return_value="192.168.1.100", + ), + ): + choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.UPLOADING, + ) + mock_find_picotool.assert_not_called() + + +def test_choose_upload_log_host_rp2040_serial_and_bootsel( + mock_choose_prompt: Mock, +) -> None: + """Test both serial ports and BOOTSEL option shown for RP2040.""" + setup_core(platform=PLATFORM_RP2040) + + mock_ports = [MockSerialPort("/dev/ttyACM0", "RP2040 Serial")] + with ( + patch("esphome.__main__.get_serial_ports", return_value=mock_ports), + patch( + "esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool") + ), + patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(1)), + ): + choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.UPLOADING, + ) + mock_choose_prompt.assert_called_once_with( + [ + ("/dev/ttyACM0 (RP2040 Serial)", "/dev/ttyACM0"), + ("RP2040 BOOTSEL (via picotool)", "BOOTSEL"), + ], + purpose=Purpose.UPLOADING, + ) + + @dataclass class MockArgs: """Mock args for testing.""" @@ -1060,6 +1286,46 @@ def test_upload_program_serial_platformio_platforms( mock_upload_using_platformio.assert_called_once_with(config, device) +def test_upload_using_platformio_creates_signed_bin_for_rp2040( + tmp_path: Path, +) -> None: + """Test that upload_using_platformio creates firmware.bin.signed for RP2040.""" + setup_core(platform=PLATFORM_RP2040) + + build_dir = tmp_path / "build" + build_dir.mkdir() + firmware_bin = build_dir / "firmware.bin" + firmware_bin.write_bytes(b"test firmware content") + firmware_elf = build_dir / "firmware.elf" + firmware_elf.write_bytes(b"elf") + + mock_idedata = MagicMock() + mock_idedata.firmware_elf_path = str(firmware_elf) + + with ( + patch("esphome.platformio_api.get_idedata", return_value=mock_idedata), + patch("esphome.platformio_api.run_platformio_cli_run", return_value=0), + ): + result = upload_using_platformio({}, "/dev/ttyACM0") + + assert result == 0 + signed_bin = build_dir / "firmware.bin.signed" + assert signed_bin.is_file() + assert signed_bin.read_bytes() == b"test firmware content" + + +def test_upload_using_platformio_skips_signed_bin_for_non_rp2040( + tmp_path: Path, +) -> None: + """Test that upload_using_platformio doesn't create signed bin for non-RP2040.""" + setup_core(platform=PLATFORM_ESP32) + + with patch("esphome.platformio_api.run_platformio_cli_run", return_value=0): + result = upload_using_platformio({}, "/dev/ttyUSB0") + + assert result == 0 + + def test_upload_program_serial_upload_failed( mock_upload_using_esptool: Mock, mock_get_port_type: Mock, @@ -1082,6 +1348,158 @@ def test_upload_program_serial_upload_failed( mock_upload_using_esptool.assert_called_once() +def test_upload_program_bootsel( + mock_upload_using_picotool: Mock, + mock_get_port_type: Mock, +) -> None: + """Test upload_program with BOOTSEL for RP2040.""" + setup_core(platform=PLATFORM_RP2040) + mock_get_port_type.return_value = "BOOTSEL" + mock_upload_using_picotool.return_value = 0 + + config = {} + args = MockArgs() + devices = ["BOOTSEL"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + # BOOTSEL device can't be used for logging, so host should be None + assert host is None + mock_upload_using_picotool.assert_called_once_with(config) + + +def test_upload_program_bootsel_failed( + mock_upload_using_picotool: Mock, + mock_get_port_type: Mock, +) -> None: + """Test upload_program when BOOTSEL upload fails.""" + setup_core(platform=PLATFORM_RP2040) + mock_get_port_type.return_value = "BOOTSEL" + mock_upload_using_picotool.return_value = 1 + + config = {} + args = MockArgs() + devices = ["BOOTSEL"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 1 + assert host is None + mock_upload_using_picotool.assert_called_once_with(config) + + +def test_upload_using_picotool_success(tmp_path: Path) -> None: + """Test upload_using_picotool succeeds.""" + setup_core(platform=PLATFORM_RP2040, tmp_path=tmp_path) + + build_dir = tmp_path / "build" + build_dir.mkdir() + firmware_elf = build_dir / "firmware.elf" + firmware_elf.write_bytes(b"\x00" * 1024) + + # Create picotool binary + packages_dir = tmp_path / "packages" + toolchain_bin = packages_dir / "toolchain-rp2040-earlephilhower" / "bin" + toolchain_bin.mkdir(parents=True) + picotool_dir = packages_dir / "tool-picotool-rp2040-earlephilhower" + picotool_dir.mkdir(parents=True) + binary_name = "picotool.exe" if sys.platform == "win32" else "picotool" + picotool = picotool_dir / binary_name + picotool.touch() + + mock_idedata = MagicMock() + mock_idedata.firmware_elf_path = str(firmware_elf) + mock_idedata.cc_path = str(toolchain_bin / "arm-none-eabi-gcc") + + mock_result = MagicMock() + mock_result.returncode = 0 + mock_result.stderr = b"" + + config = {} + with ( + patch("esphome.platformio_api.get_idedata", return_value=mock_idedata), + patch("subprocess.run", return_value=mock_result), + ): + exit_code = upload_using_picotool(config) + + assert exit_code == 0 + + +def test_upload_using_picotool_no_elf(tmp_path: Path) -> None: + """Test upload_using_picotool when ELF file is missing.""" + setup_core(platform=PLATFORM_RP2040, tmp_path=tmp_path) + + build_dir = tmp_path / "build" + build_dir.mkdir() + + mock_idedata = MagicMock() + mock_idedata.firmware_elf_path = str(build_dir / "firmware.elf") + mock_idedata.cc_path = "/fake/path/gcc" + + config = {} + with patch("esphome.platformio_api.get_idedata", return_value=mock_idedata): + exit_code = upload_using_picotool(config) + + assert exit_code == 1 + + +def test_upload_using_picotool_not_found(tmp_path: Path) -> None: + """Test upload_using_picotool when picotool binary not found.""" + setup_core(platform=PLATFORM_RP2040, tmp_path=tmp_path) + + build_dir = tmp_path / "build" + build_dir.mkdir() + firmware_elf = build_dir / "firmware.elf" + firmware_elf.write_bytes(b"\x00" * 512) + + mock_idedata = MagicMock() + mock_idedata.firmware_elf_path = str(firmware_elf) + mock_idedata.cc_path = "/fake/path/gcc" + + config = {} + with patch("esphome.platformio_api.get_idedata", return_value=mock_idedata): + exit_code = upload_using_picotool(config) + + assert exit_code == 1 + + +def test_upload_using_picotool_permission_error(tmp_path: Path) -> None: + """Test upload_using_picotool shows helpful message on permission error.""" + setup_core(platform=PLATFORM_RP2040, tmp_path=tmp_path) + + build_dir = tmp_path / "build" + build_dir.mkdir() + firmware_elf = build_dir / "firmware.elf" + firmware_elf.write_bytes(b"\x00" * 512) + + packages_dir = tmp_path / "packages" + toolchain_bin = packages_dir / "toolchain-rp2040-earlephilhower" / "bin" + toolchain_bin.mkdir(parents=True) + picotool_dir = packages_dir / "tool-picotool-rp2040-earlephilhower" + picotool_dir.mkdir(parents=True) + binary_name = "picotool.exe" if sys.platform == "win32" else "picotool" + picotool = picotool_dir / binary_name + picotool.touch() + + mock_idedata = MagicMock() + mock_idedata.firmware_elf_path = str(firmware_elf) + mock_idedata.cc_path = str(toolchain_bin / "arm-none-eabi-gcc") + + mock_result = MagicMock() + mock_result.returncode = 1 + mock_result.stderr = b"LIBUSB_ERROR_ACCESS" + + config = {} + with ( + patch("esphome.platformio_api.get_idedata", return_value=mock_idedata), + patch("subprocess.run", return_value=mock_result), + ): + exit_code = upload_using_picotool(config) + + assert exit_code == 1 + + def test_upload_program_ota_success( mock_run_ota: Mock, mock_get_port_type: Mock, @@ -1606,6 +2024,8 @@ def test_get_port_type() -> None: assert get_port_type("esphome-device.local") == "NETWORK" assert get_port_type("10.0.0.1") == "NETWORK" + assert get_port_type("BOOTSEL") == "BOOTSEL" + def test_has_mqtt_ip_lookup() -> None: """Test has_mqtt_ip_lookup function.""" diff --git a/tests/unit_tests/test_util.py b/tests/unit_tests/test_util.py index 85873caea8..ca3fd9b78a 100644 --- a/tests/unit_tests/test_util.py +++ b/tests/unit_tests/test_util.py @@ -3,6 +3,9 @@ from __future__ import annotations from pathlib import Path +import subprocess +import sys +from unittest.mock import MagicMock, patch import pytest @@ -402,3 +405,132 @@ def test_shlex_quote_edge_cases() -> None: assert util.shlex_quote("\t") == "'\t'" assert util.shlex_quote("\n") == "'\n'" assert util.shlex_quote(" ") == "' '" + + +def test_get_picotool_path_found(tmp_path: Path) -> None: + """Test picotool path derivation from cc_path.""" + # Create the expected directory structure + packages_dir = tmp_path / "packages" + toolchain_dir = packages_dir / "toolchain-rp2040-earlephilhower" / "bin" + toolchain_dir.mkdir(parents=True) + gcc = toolchain_dir / "arm-none-eabi-gcc" + gcc.touch() + + binary_name = "picotool.exe" if sys.platform == "win32" else "picotool" + picotool_dir = packages_dir / "tool-picotool-rp2040-earlephilhower" + picotool_dir.mkdir(parents=True) + picotool = picotool_dir / binary_name + picotool.touch() + + result = util.get_picotool_path(str(gcc)) + assert result == picotool + + +def test_get_picotool_path_not_found(tmp_path: Path) -> None: + """Test picotool path returns None when not installed.""" + packages_dir = tmp_path / "packages" + toolchain_dir = packages_dir / "toolchain-rp2040-earlephilhower" / "bin" + toolchain_dir.mkdir(parents=True) + gcc = toolchain_dir / "arm-none-eabi-gcc" + gcc.touch() + + result = util.get_picotool_path(str(gcc)) + assert result is None + + +def test_get_picotool_path_windows(tmp_path: Path) -> None: + """Test picotool path uses .exe on Windows.""" + packages_dir = tmp_path / "packages" + toolchain_dir = packages_dir / "toolchain-rp2040-earlephilhower" / "bin" + toolchain_dir.mkdir(parents=True) + gcc = toolchain_dir / "arm-none-eabi-gcc.exe" + gcc.touch() + + picotool_dir = packages_dir / "tool-picotool-rp2040-earlephilhower" + picotool_dir.mkdir(parents=True) + picotool = picotool_dir / "picotool.exe" + picotool.touch() + + with patch("esphome.util.sys.platform", "win32"): + result = util.get_picotool_path(str(gcc)) + assert result == picotool + + +def test_detect_rp2040_bootsel_found() -> None: + """Test BOOTSEL device detection when device is present.""" + mock_result = MagicMock() + mock_result.stdout = b"Device Information\n type: RP2040\n" + with patch("esphome.util.subprocess.run", return_value=mock_result): + result = util.detect_rp2040_bootsel("/usr/bin/picotool") + assert result.device_count == 1 + assert result.permission_error is False + + +def test_detect_rp2040_bootsel_multiple() -> None: + """Test BOOTSEL detection with multiple devices.""" + mock_result = MagicMock() + mock_result.stdout = b"type: RP2040\ntype: RP2350\n" + with patch("esphome.util.subprocess.run", return_value=mock_result): + result = util.detect_rp2040_bootsel("/usr/bin/picotool") + assert result.device_count == 2 + assert result.permission_error is False + + +def test_detect_rp2040_bootsel_none() -> None: + """Test BOOTSEL detection when no device found.""" + mock_result = MagicMock() + mock_result.stdout = ( + b"No accessible RP2040/RP2350 devices in BOOTSEL mode were found.\n" + ) + mock_result.stderr = b"" + with patch("esphome.util.subprocess.run", return_value=mock_result): + result = util.detect_rp2040_bootsel("/usr/bin/picotool") + assert result.device_count == 0 + assert result.permission_error is False + + +def test_detect_rp2040_bootsel_permission_error() -> None: + """Test BOOTSEL detection with device found but not accessible.""" + mock_result = MagicMock() + mock_result.stdout = ( + b"No accessible RP-series devices in BOOTSEL mode were found.\n" + ) + mock_result.stderr = ( + b"RP2040 device at bus 5, address 24 appears to be in BOOTSEL mode, " + b"but picotool was unable to connect. " + b"Maybe try 'sudo' or check your permissions.\n" + ) + with patch("esphome.util.subprocess.run", return_value=mock_result): + result = util.detect_rp2040_bootsel("/usr/bin/picotool") + assert result.device_count == 0 + assert result.permission_error is True + + +def test_detect_rp2040_bootsel_libusb_access_error() -> None: + """Test BOOTSEL detection with LIBUSB_ERROR_ACCESS.""" + mock_result = MagicMock() + mock_result.stdout = b"" + mock_result.stderr = b"LIBUSB_ERROR_ACCESS\n" + with patch("esphome.util.subprocess.run", return_value=mock_result): + result = util.detect_rp2040_bootsel("/usr/bin/picotool") + assert result.device_count == 0 + assert result.permission_error is True + + +def test_detect_rp2040_bootsel_oserror() -> None: + """Test BOOTSEL detection handles OSError.""" + with patch("esphome.util.subprocess.run", side_effect=OSError("not found")): + result = util.detect_rp2040_bootsel("/usr/bin/picotool") + assert result.device_count == 0 + assert result.permission_error is False + + +def test_detect_rp2040_bootsel_timeout() -> None: + """Test BOOTSEL detection handles timeout.""" + with patch( + "esphome.util.subprocess.run", + side_effect=subprocess.TimeoutExpired("picotool", 10), + ): + result = util.detect_rp2040_bootsel("/usr/bin/picotool") + assert result.device_count == 0 + assert result.permission_error is False From 6e468936ec670461e244aa93b7d3ea74f1fa2bef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 09:10:55 -1000 Subject: [PATCH 510/609] [api] Inline ProtoVarInt::parse fast path and return consumed in struct (#14638) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .../api/api_frame_helper_plaintext.cpp | 24 +- esphome/components/api/api_pb2.cpp | 430 +++++++++--------- esphome/components/api/api_pb2.h | 102 ++--- esphome/components/api/proto.cpp | 75 +-- esphome/components/api/proto.h | 118 ++--- script/api_protobuf/api_protobuf.py | 20 +- 6 files changed, 376 insertions(+), 393 deletions(-) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 3c54ed7c70..793cece3b8 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -128,37 +128,37 @@ APIError APIPlaintextFrameHelper::try_read_frame_() { // Skip indicator byte at position 0 uint8_t varint_pos = 1; - uint32_t consumed = 0; - auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed); + // rx_header_buf_pos_ >= 3 and varint_pos == 1, so len >= 2 + auto msg_size_varint = ProtoVarInt::parse_non_empty(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos); if (!msg_size_varint.has_value()) { // not enough data there yet continue; } - if (msg_size_varint->as_uint32() > MAX_MESSAGE_SIZE) { + if (msg_size_varint.value > MAX_MESSAGE_SIZE) { state_ = State::FAILED; - HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(), - MAX_MESSAGE_SIZE); + HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", + static_cast<uint32_t>(msg_size_varint.value), MAX_MESSAGE_SIZE); return APIError::BAD_DATA_PACKET; } - rx_header_parsed_len_ = msg_size_varint->as_uint16(); + rx_header_parsed_len_ = static_cast<uint16_t>(msg_size_varint.value); // Move to next varint position - varint_pos += consumed; + varint_pos += msg_size_varint.consumed; - auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed); + auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos); if (!msg_type_varint.has_value()) { // not enough data there yet continue; } - if (msg_type_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) { + if (msg_type_varint.value > std::numeric_limits<uint16_t>::max()) { state_ = State::FAILED; - HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(), - std::numeric_limits<uint16_t>::max()); + HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", + static_cast<uint32_t>(msg_type_varint.value), std::numeric_limits<uint16_t>::max()); return APIError::BAD_DATA_PACKET; } - rx_header_parsed_type_ = msg_type_varint->as_uint16(); + rx_header_parsed_type_ = static_cast<uint16_t>(msg_type_varint.value); rx_header_parsed_ = true; } // header reading done diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 6fce10ca0f..01993cc5e5 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -7,13 +7,13 @@ namespace esphome::api { -bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool HelloRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->api_version_major = value.as_uint32(); + this->api_version_major = value; break; case 3: - this->api_version_minor = value.as_uint32(); + this->api_version_minor = value; break; default: return false; @@ -316,20 +316,20 @@ uint32_t CoverStateResponse::calculate_size() const { #endif return size; } -bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool CoverCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 4: - this->has_position = value.as_bool(); + this->has_position = value != 0; break; case 6: - this->has_tilt = value.as_bool(); + this->has_tilt = value != 0; break; case 8: - this->stop = value.as_bool(); + this->stop = value != 0; break; #ifdef USE_DEVICES case 9: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -423,38 +423,38 @@ uint32_t FanStateResponse::calculate_size() const { #endif return size; } -bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool FanCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->has_state = value.as_bool(); + this->has_state = value != 0; break; case 3: - this->state = value.as_bool(); + this->state = value != 0; break; case 6: - this->has_oscillating = value.as_bool(); + this->has_oscillating = value != 0; break; case 7: - this->oscillating = value.as_bool(); + this->oscillating = value != 0; break; case 8: - this->has_direction = value.as_bool(); + this->has_direction = value != 0; break; case 9: - this->direction = static_cast<enums::FanDirection>(value.as_uint32()); + this->direction = static_cast<enums::FanDirection>(value); break; case 10: - this->has_speed_level = value.as_bool(); + this->has_speed_level = value != 0; break; case 11: - this->speed_level = value.as_int32(); + this->speed_level = static_cast<int32_t>(value); break; case 12: - this->has_preset_mode = value.as_bool(); + this->has_preset_mode = value != 0; break; #ifdef USE_DEVICES case 14: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -571,59 +571,59 @@ uint32_t LightStateResponse::calculate_size() const { #endif return size; } -bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool LightCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->has_state = value.as_bool(); + this->has_state = value != 0; break; case 3: - this->state = value.as_bool(); + this->state = value != 0; break; case 4: - this->has_brightness = value.as_bool(); + this->has_brightness = value != 0; break; case 22: - this->has_color_mode = value.as_bool(); + this->has_color_mode = value != 0; break; case 23: - this->color_mode = static_cast<enums::ColorMode>(value.as_uint32()); + this->color_mode = static_cast<enums::ColorMode>(value); break; case 20: - this->has_color_brightness = value.as_bool(); + this->has_color_brightness = value != 0; break; case 6: - this->has_rgb = value.as_bool(); + this->has_rgb = value != 0; break; case 10: - this->has_white = value.as_bool(); + this->has_white = value != 0; break; case 12: - this->has_color_temperature = value.as_bool(); + this->has_color_temperature = value != 0; break; case 24: - this->has_cold_white = value.as_bool(); + this->has_cold_white = value != 0; break; case 26: - this->has_warm_white = value.as_bool(); + this->has_warm_white = value != 0; break; case 14: - this->has_transition_length = value.as_bool(); + this->has_transition_length = value != 0; break; case 15: - this->transition_length = value.as_uint32(); + this->transition_length = value; break; case 16: - this->has_flash_length = value.as_bool(); + this->has_flash_length = value != 0; break; case 17: - this->flash_length = value.as_uint32(); + this->flash_length = value; break; case 18: - this->has_effect = value.as_bool(); + this->has_effect = value != 0; break; #ifdef USE_DEVICES case 28: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -787,14 +787,14 @@ uint32_t SwitchStateResponse::calculate_size() const { #endif return size; } -bool SwitchCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SwitchCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->state = value.as_bool(); + this->state = value != 0; break; #ifdef USE_DEVICES case 3: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -863,13 +863,13 @@ uint32_t TextSensorStateResponse::calculate_size() const { return size; } #endif -bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SubscribeLogsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->level = static_cast<enums::LogLevel>(value.as_uint32()); + this->level = static_cast<enums::LogLevel>(value); break; case 2: - this->dump_config = value.as_bool(); + this->dump_config = value != 0; break; default: return false; @@ -971,13 +971,13 @@ uint32_t HomeassistantActionRequest::calculate_size() const { } #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES -bool HomeassistantActionResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool HomeassistantActionResponse::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->call_id = value.as_uint32(); + this->call_id = value; break; case 2: - this->success = value.as_bool(); + this->success = value != 0; break; default: return false; @@ -1036,38 +1036,38 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel return true; } #endif -bool DSTRule::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool DSTRule::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->time_seconds = value.as_sint32(); + this->time_seconds = decode_zigzag32(static_cast<uint32_t>(value)); break; case 2: - this->day = value.as_uint32(); + this->day = value; break; case 3: - this->type = static_cast<enums::DSTRuleType>(value.as_uint32()); + this->type = static_cast<enums::DSTRuleType>(value); break; case 4: - this->month = value.as_uint32(); + this->month = value; break; case 5: - this->week = value.as_uint32(); + this->week = value; break; case 6: - this->day_of_week = value.as_uint32(); + this->day_of_week = value; break; default: return false; } return true; } -bool ParsedTimezone::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool ParsedTimezone::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->std_offset_seconds = value.as_sint32(); + this->std_offset_seconds = decode_zigzag32(static_cast<uint32_t>(value)); break; case 2: - this->dst_offset_seconds = value.as_sint32(); + this->dst_offset_seconds = decode_zigzag32(static_cast<uint32_t>(value)); break; default: return false; @@ -1142,22 +1142,22 @@ uint32_t ListEntitiesServicesResponse::calculate_size() const { size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->supports_response)); return size; } -bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool ExecuteServiceArgument::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->bool_ = value.as_bool(); + this->bool_ = value != 0; break; case 2: - this->legacy_int = value.as_int32(); + this->legacy_int = static_cast<int32_t>(value); break; case 5: - this->int_ = value.as_sint32(); + this->int_ = decode_zigzag32(static_cast<uint32_t>(value)); break; case 6: - this->bool_array.push_back(value.as_bool()); + this->bool_array.push_back(value != 0); break; case 7: - this->int_array.push_back(value.as_sint32()); + this->int_array.push_back(decode_zigzag32(static_cast<uint32_t>(value))); break; default: return false; @@ -1202,16 +1202,16 @@ void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) { this->string_array.init(count_string_array); ProtoDecodableMessage::decode(buffer, length); } -bool ExecuteServiceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool ExecuteServiceRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES case 3: - this->call_id = value.as_uint32(); + this->call_id = value; break; #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES case 4: - this->return_response = value.as_bool(); + this->return_response = value != 0; break; #endif default: @@ -1313,13 +1313,13 @@ uint32_t CameraImageResponse::calculate_size() const { #endif return size; } -bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool CameraImageRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->single = value.as_bool(); + this->single = value != 0; break; case 2: - this->stream = value.as_bool(); + this->stream = value != 0; break; default: return false; @@ -1468,53 +1468,53 @@ uint32_t ClimateStateResponse::calculate_size() const { #endif return size; } -bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool ClimateCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->has_mode = value.as_bool(); + this->has_mode = value != 0; break; case 3: - this->mode = static_cast<enums::ClimateMode>(value.as_uint32()); + this->mode = static_cast<enums::ClimateMode>(value); break; case 4: - this->has_target_temperature = value.as_bool(); + this->has_target_temperature = value != 0; break; case 6: - this->has_target_temperature_low = value.as_bool(); + this->has_target_temperature_low = value != 0; break; case 8: - this->has_target_temperature_high = value.as_bool(); + this->has_target_temperature_high = value != 0; break; case 12: - this->has_fan_mode = value.as_bool(); + this->has_fan_mode = value != 0; break; case 13: - this->fan_mode = static_cast<enums::ClimateFanMode>(value.as_uint32()); + this->fan_mode = static_cast<enums::ClimateFanMode>(value); break; case 14: - this->has_swing_mode = value.as_bool(); + this->has_swing_mode = value != 0; break; case 15: - this->swing_mode = static_cast<enums::ClimateSwingMode>(value.as_uint32()); + this->swing_mode = static_cast<enums::ClimateSwingMode>(value); break; case 16: - this->has_custom_fan_mode = value.as_bool(); + this->has_custom_fan_mode = value != 0; break; case 18: - this->has_preset = value.as_bool(); + this->has_preset = value != 0; break; case 19: - this->preset = static_cast<enums::ClimatePreset>(value.as_uint32()); + this->preset = static_cast<enums::ClimatePreset>(value); break; case 20: - this->has_custom_preset = value.as_bool(); + this->has_custom_preset = value != 0; break; case 22: - this->has_target_humidity = value.as_bool(); + this->has_target_humidity = value != 0; break; #ifdef USE_DEVICES case 24: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -1631,21 +1631,21 @@ uint32_t WaterHeaterStateResponse::calculate_size() const { size += ProtoSize::calc_float(1, this->target_temperature_high); return size; } -bool WaterHeaterCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool WaterHeaterCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->has_fields = value.as_uint32(); + this->has_fields = value; break; case 3: - this->mode = static_cast<enums::WaterHeaterMode>(value.as_uint32()); + this->mode = static_cast<enums::WaterHeaterMode>(value); break; #ifdef USE_DEVICES case 5: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif case 6: - this->state = value.as_uint32(); + this->state = value; break; default: return false; @@ -1731,11 +1731,11 @@ uint32_t NumberStateResponse::calculate_size() const { #endif return size; } -bool NumberCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool NumberCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_DEVICES case 3: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -1812,11 +1812,11 @@ uint32_t SelectStateResponse::calculate_size() const { #endif return size; } -bool SelectCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SelectCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_DEVICES case 3: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -1903,29 +1903,29 @@ uint32_t SirenStateResponse::calculate_size() const { #endif return size; } -bool SirenCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SirenCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->has_state = value.as_bool(); + this->has_state = value != 0; break; case 3: - this->state = value.as_bool(); + this->state = value != 0; break; case 4: - this->has_tone = value.as_bool(); + this->has_tone = value != 0; break; case 6: - this->has_duration = value.as_bool(); + this->has_duration = value != 0; break; case 7: - this->duration = value.as_uint32(); + this->duration = value; break; case 8: - this->has_volume = value.as_bool(); + this->has_volume = value != 0; break; #ifdef USE_DEVICES case 10: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -2011,17 +2011,17 @@ uint32_t LockStateResponse::calculate_size() const { #endif return size; } -bool LockCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool LockCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->command = static_cast<enums::LockCommand>(value.as_uint32()); + this->command = static_cast<enums::LockCommand>(value); break; case 3: - this->has_code = value.as_bool(); + this->has_code = value != 0; break; #ifdef USE_DEVICES case 5: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -2082,11 +2082,11 @@ uint32_t ListEntitiesButtonResponse::calculate_size() const { #endif return size; } -bool ButtonCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool ButtonCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_DEVICES case 2: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -2182,29 +2182,29 @@ uint32_t MediaPlayerStateResponse::calculate_size() const { #endif return size; } -bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->has_command = value.as_bool(); + this->has_command = value != 0; break; case 3: - this->command = static_cast<enums::MediaPlayerCommand>(value.as_uint32()); + this->command = static_cast<enums::MediaPlayerCommand>(value); break; case 4: - this->has_volume = value.as_bool(); + this->has_volume = value != 0; break; case 6: - this->has_media_url = value.as_bool(); + this->has_media_url = value != 0; break; case 8: - this->has_announcement = value.as_bool(); + this->has_announcement = value != 0; break; case 9: - this->announcement = value.as_bool(); + this->announcement = value != 0; break; #ifdef USE_DEVICES case 10: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -2238,10 +2238,10 @@ bool MediaPlayerCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value } #endif #ifdef USE_BLUETOOTH_PROXY -bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->flags = value.as_uint32(); + this->flags = value; break; default: return false; @@ -2274,19 +2274,19 @@ uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const { } return size; } -bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; case 2: - this->request_type = static_cast<enums::BluetoothDeviceRequestType>(value.as_uint32()); + this->request_type = static_cast<enums::BluetoothDeviceRequestType>(value); break; case 3: - this->has_address_type = value.as_bool(); + this->has_address_type = value != 0; break; case 4: - this->address_type = value.as_uint32(); + this->address_type = value; break; default: return false; @@ -2307,10 +2307,10 @@ uint32_t BluetoothDeviceConnectionResponse::calculate_size() const { size += ProtoSize::calc_int32(1, this->error); return size; } -bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; default: return false; @@ -2413,13 +2413,13 @@ uint32_t BluetoothGATTGetServicesDoneResponse::calculate_size() const { size += ProtoSize::calc_uint64(1, this->address); return size; } -bool BluetoothGATTReadRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothGATTReadRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; case 2: - this->handle = value.as_uint32(); + this->handle = value; break; default: return false; @@ -2438,16 +2438,16 @@ uint32_t BluetoothGATTReadResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->data_len_); return size; } -bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; case 2: - this->handle = value.as_uint32(); + this->handle = value; break; case 3: - this->response = value.as_bool(); + this->response = value != 0; break; default: return false; @@ -2466,26 +2466,26 @@ bool BluetoothGATTWriteRequest::decode_length(uint32_t field_id, ProtoLengthDeli } return true; } -bool BluetoothGATTReadDescriptorRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothGATTReadDescriptorRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; case 2: - this->handle = value.as_uint32(); + this->handle = value; break; default: return false; } return true; } -bool BluetoothGATTWriteDescriptorRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothGATTWriteDescriptorRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; case 2: - this->handle = value.as_uint32(); + this->handle = value; break; default: return false; @@ -2504,16 +2504,16 @@ bool BluetoothGATTWriteDescriptorRequest::decode_length(uint32_t field_id, Proto } return true; } -bool BluetoothGATTNotifyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothGATTNotifyRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; case 2: - this->handle = value.as_uint32(); + this->handle = value; break; case 3: - this->enable = value.as_bool(); + this->enable = value != 0; break; default: return false; @@ -2632,10 +2632,10 @@ uint32_t BluetoothScannerStateResponse::calculate_size() const { size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->configured_mode)); return size; } -bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->mode = static_cast<enums::BluetoothScannerMode>(value.as_uint32()); + this->mode = static_cast<enums::BluetoothScannerMode>(value); break; default: return false; @@ -2644,13 +2644,13 @@ bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarIn } #endif #ifdef USE_VOICE_ASSISTANT -bool SubscribeVoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SubscribeVoiceAssistantRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->subscribe = value.as_bool(); + this->subscribe = value != 0; break; case 2: - this->flags = value.as_uint32(); + this->flags = value; break; default: return false; @@ -2685,13 +2685,13 @@ uint32_t VoiceAssistantRequest::calculate_size() const { size += ProtoSize::calc_length(1, this->wake_word_phrase.size()); return size; } -bool VoiceAssistantResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool VoiceAssistantResponse::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->port = value.as_uint32(); + this->port = value; break; case 2: - this->error = value.as_bool(); + this->error = value != 0; break; default: return false; @@ -2713,10 +2713,10 @@ bool VoiceAssistantEventData::decode_length(uint32_t field_id, ProtoLengthDelimi } return true; } -bool VoiceAssistantEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool VoiceAssistantEventResponse::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->event_type = static_cast<enums::VoiceAssistantEvent>(value.as_uint32()); + this->event_type = static_cast<enums::VoiceAssistantEvent>(value); break; default: return false; @@ -2734,10 +2734,10 @@ bool VoiceAssistantEventResponse::decode_length(uint32_t field_id, ProtoLengthDe } return true; } -bool VoiceAssistantAudio::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool VoiceAssistantAudio::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->end = value.as_bool(); + this->end = value != 0; break; default: return false; @@ -2766,19 +2766,19 @@ uint32_t VoiceAssistantAudio::calculate_size() const { size += ProtoSize::calc_bool(1, this->end); return size; } -bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->event_type = static_cast<enums::VoiceAssistantTimerEvent>(value.as_uint32()); + this->event_type = static_cast<enums::VoiceAssistantTimerEvent>(value); break; case 4: - this->total_seconds = value.as_uint32(); + this->total_seconds = value; break; case 5: - this->seconds_left = value.as_uint32(); + this->seconds_left = value; break; case 6: - this->is_active = value.as_bool(); + this->is_active = value != 0; break; default: return false; @@ -2800,10 +2800,10 @@ bool VoiceAssistantTimerEventResponse::decode_length(uint32_t field_id, ProtoLen } return true; } -bool VoiceAssistantAnnounceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool VoiceAssistantAnnounceRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 4: - this->start_conversation = value.as_bool(); + this->start_conversation = value != 0; break; default: return false; @@ -2853,10 +2853,10 @@ uint32_t VoiceAssistantWakeWord::calculate_size() const { } return size; } -bool VoiceAssistantExternalWakeWord::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool VoiceAssistantExternalWakeWord::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 5: - this->model_size = value.as_uint32(); + this->model_size = value; break; default: return false; @@ -2990,14 +2990,14 @@ uint32_t AlarmControlPanelStateResponse::calculate_size() const { #endif return size; } -bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->command = static_cast<enums::AlarmControlPanelStateCommand>(value.as_uint32()); + this->command = static_cast<enums::AlarmControlPanelStateCommand>(value); break; #ifdef USE_DEVICES case 4: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -3082,11 +3082,11 @@ uint32_t TextStateResponse::calculate_size() const { #endif return size; } -bool TextCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool TextCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_DEVICES case 3: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -3167,20 +3167,20 @@ uint32_t DateStateResponse::calculate_size() const { #endif return size; } -bool DateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool DateCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->year = value.as_uint32(); + this->year = value; break; case 3: - this->month = value.as_uint32(); + this->month = value; break; case 4: - this->day = value.as_uint32(); + this->day = value; break; #ifdef USE_DEVICES case 5: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -3250,20 +3250,20 @@ uint32_t TimeStateResponse::calculate_size() const { #endif return size; } -bool TimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool TimeCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->hour = value.as_uint32(); + this->hour = value; break; case 3: - this->minute = value.as_uint32(); + this->minute = value; break; case 4: - this->second = value.as_uint32(); + this->second = value; break; #ifdef USE_DEVICES case 5: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -3393,17 +3393,17 @@ uint32_t ValveStateResponse::calculate_size() const { #endif return size; } -bool ValveCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool ValveCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->has_position = value.as_bool(); + this->has_position = value != 0; break; case 4: - this->stop = value.as_bool(); + this->stop = value != 0; break; #ifdef USE_DEVICES case 5: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -3472,11 +3472,11 @@ uint32_t DateTimeStateResponse::calculate_size() const { #endif return size; } -bool DateTimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool DateTimeCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_DEVICES case 3: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -3561,14 +3561,14 @@ uint32_t UpdateStateResponse::calculate_size() const { #endif return size; } -bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool UpdateCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->command = static_cast<enums::UpdateCommand>(value.as_uint32()); + this->command = static_cast<enums::UpdateCommand>(value); break; #ifdef USE_DEVICES case 3: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -3606,10 +3606,10 @@ uint32_t ZWaveProxyFrame::calculate_size() const { size += ProtoSize::calc_length(1, this->data_len); return size; } -bool ZWaveProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool ZWaveProxyRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->type = static_cast<enums::ZWaveProxyRequestType>(value.as_uint32()); + this->type = static_cast<enums::ZWaveProxyRequestType>(value); break; default: return false; @@ -3672,18 +3672,18 @@ uint32_t ListEntitiesInfraredResponse::calculate_size() const { } #endif #ifdef USE_IR_RF -bool InfraredRFTransmitRawTimingsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool InfraredRFTransmitRawTimingsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_DEVICES case 1: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif case 3: - this->carrier_frequency = value.as_uint32(); + this->carrier_frequency = value; break; case 4: - this->repeat_count = value.as_uint32(); + this->repeat_count = value; break; default: return false; @@ -3737,25 +3737,25 @@ uint32_t InfraredRFReceiveEvent::calculate_size() const { } #endif #ifdef USE_SERIAL_PROXY -bool SerialProxyConfigureRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SerialProxyConfigureRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->instance = value.as_uint32(); + this->instance = value; break; case 2: - this->baudrate = value.as_uint32(); + this->baudrate = value; break; case 3: - this->flow_control = value.as_bool(); + this->flow_control = value != 0; break; case 4: - this->parity = static_cast<enums::SerialProxyParity>(value.as_uint32()); + this->parity = static_cast<enums::SerialProxyParity>(value); break; case 5: - this->stop_bits = value.as_uint32(); + this->stop_bits = value; break; case 6: - this->data_size = value.as_uint32(); + this->data_size = value; break; default: return false; @@ -3772,10 +3772,10 @@ uint32_t SerialProxyDataReceived::calculate_size() const { size += ProtoSize::calc_length(1, this->data_len_); return size; } -bool SerialProxyWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SerialProxyWriteRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->instance = value.as_uint32(); + this->instance = value; break; default: return false; @@ -3794,23 +3794,23 @@ bool SerialProxyWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimi } return true; } -bool SerialProxySetModemPinsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SerialProxySetModemPinsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->instance = value.as_uint32(); + this->instance = value; break; case 2: - this->line_states = value.as_uint32(); + this->line_states = value; break; default: return false; } return true; } -bool SerialProxyGetModemPinsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SerialProxyGetModemPinsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->instance = value.as_uint32(); + this->instance = value; break; default: return false; @@ -3827,13 +3827,13 @@ uint32_t SerialProxyGetModemPinsResponse::calculate_size() const { size += ProtoSize::calc_uint32(1, this->line_states); return size; } -bool SerialProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SerialProxyRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->instance = value.as_uint32(); + this->instance = value; break; case 2: - this->type = static_cast<enums::SerialProxyRequestType>(value.as_uint32()); + this->type = static_cast<enums::SerialProxyRequestType>(value); break; default: return false; @@ -3856,22 +3856,22 @@ uint32_t SerialProxyRequestResponse::calculate_size() const { } #endif #ifdef USE_BLUETOOTH_PROXY -bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; case 2: - this->min_interval = value.as_uint32(); + this->min_interval = value; break; case 3: - this->max_interval = value.as_uint32(); + this->max_interval = value; break; case 4: - this->latency = value.as_uint32(); + this->latency = value; break; case 5: - this->timeout = value.as_uint32(); + this->timeout = value; break; default: return false; diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 5c712508b9..a4ee0adb8b 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -399,7 +399,7 @@ class HelloRequest final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class HelloResponse final : public ProtoMessage { public: @@ -688,7 +688,7 @@ class CoverCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_FAN @@ -756,7 +756,7 @@ class FanCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_LIGHT @@ -846,7 +846,7 @@ class LightCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_SENSOR @@ -936,7 +936,7 @@ class SwitchCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_TEXT_SENSOR @@ -988,7 +988,7 @@ class SubscribeLogsRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class SubscribeLogsResponse final : public ProtoMessage { public: @@ -1110,7 +1110,7 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_API_HOMEASSISTANT_STATES @@ -1176,7 +1176,7 @@ class DSTRule final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class ParsedTimezone final : public ProtoDecodableMessage { public: @@ -1190,7 +1190,7 @@ class ParsedTimezone final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class GetTimeResponse final : public ProtoDecodableMessage { public: @@ -1261,7 +1261,7 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class ExecuteServiceRequest final : public ProtoDecodableMessage { public: @@ -1286,7 +1286,7 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES @@ -1365,7 +1365,7 @@ class CameraImageRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_CLIMATE @@ -1464,7 +1464,7 @@ class ClimateCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_WATER_HEATER @@ -1528,7 +1528,7 @@ class WaterHeaterCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_NUMBER @@ -1584,7 +1584,7 @@ class NumberCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_SELECT @@ -1636,7 +1636,7 @@ class SelectCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_SIREN @@ -1696,7 +1696,7 @@ class SirenCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_LOCK @@ -1752,7 +1752,7 @@ class LockCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_BUTTON @@ -1785,7 +1785,7 @@ class ButtonCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_MEDIA_PLAYER @@ -1862,7 +1862,7 @@ class MediaPlayerCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_BLUETOOTH_PROXY @@ -1879,7 +1879,7 @@ class SubscribeBluetoothLEAdvertisementsRequest final : public ProtoDecodableMes #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothLERawAdvertisement final : public ProtoMessage { public: @@ -1929,7 +1929,7 @@ class BluetoothDeviceRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothDeviceConnectionResponse final : public ProtoMessage { public: @@ -1963,7 +1963,7 @@ class BluetoothGATTGetServicesRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothGATTDescriptor final : public ProtoMessage { public: @@ -2054,7 +2054,7 @@ class BluetoothGATTReadRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothGATTReadResponse final : public ProtoMessage { public: @@ -2097,7 +2097,7 @@ class BluetoothGATTWriteRequest final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage { public: @@ -2113,7 +2113,7 @@ class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage { public: @@ -2132,7 +2132,7 @@ class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothGATTNotifyRequest final : public ProtoDecodableMessage { public: @@ -2149,7 +2149,7 @@ class BluetoothGATTNotifyRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothGATTNotifyDataResponse final : public ProtoMessage { public: @@ -2329,7 +2329,7 @@ class BluetoothScannerSetModeRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_VOICE_ASSISTANT @@ -2347,7 +2347,7 @@ class SubscribeVoiceAssistantRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class VoiceAssistantAudioSettings final : public ProtoMessage { public: @@ -2396,7 +2396,7 @@ class VoiceAssistantResponse final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class VoiceAssistantEventData final : public ProtoDecodableMessage { public: @@ -2424,7 +2424,7 @@ class VoiceAssistantEventResponse final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class VoiceAssistantAudio final : public ProtoDecodableMessage { public: @@ -2444,7 +2444,7 @@ class VoiceAssistantAudio final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class VoiceAssistantTimerEventResponse final : public ProtoDecodableMessage { public: @@ -2465,7 +2465,7 @@ class VoiceAssistantTimerEventResponse final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class VoiceAssistantAnnounceRequest final : public ProtoDecodableMessage { public: @@ -2484,7 +2484,7 @@ class VoiceAssistantAnnounceRequest final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class VoiceAssistantAnnounceFinished final : public ProtoMessage { public: @@ -2530,7 +2530,7 @@ class VoiceAssistantExternalWakeWord final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class VoiceAssistantConfigurationRequest final : public ProtoDecodableMessage { public: @@ -2632,7 +2632,7 @@ class AlarmControlPanelCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_TEXT @@ -2687,7 +2687,7 @@ class TextCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_DATETIME_DATE @@ -2741,7 +2741,7 @@ class DateCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_DATETIME_TIME @@ -2795,7 +2795,7 @@ class TimeCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_EVENT @@ -2886,7 +2886,7 @@ class ValveCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_DATETIME_DATETIME @@ -2936,7 +2936,7 @@ class DateTimeCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_UPDATE @@ -2994,7 +2994,7 @@ class UpdateCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_ZWAVE_PROXY @@ -3034,7 +3034,7 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_INFRARED @@ -3079,7 +3079,7 @@ class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class InfraredRFReceiveEvent final : public ProtoMessage { public: @@ -3121,7 +3121,7 @@ class SerialProxyConfigureRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class SerialProxyDataReceived final : public ProtoMessage { public: @@ -3161,7 +3161,7 @@ class SerialProxyWriteRequest final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class SerialProxySetModemPinsRequest final : public ProtoDecodableMessage { public: @@ -3177,7 +3177,7 @@ class SerialProxySetModemPinsRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class SerialProxyGetModemPinsRequest final : public ProtoDecodableMessage { public: @@ -3192,7 +3192,7 @@ class SerialProxyGetModemPinsRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class SerialProxyGetModemPinsResponse final : public ProtoMessage { public: @@ -3225,7 +3225,7 @@ class SerialProxyRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class SerialProxyRequestResponse final : public ProtoMessage { public: @@ -3265,7 +3265,7 @@ class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothSetConnectionParamsResponse final : public ProtoMessage { public: diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index fb229928e5..4f5b3f0918 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -20,20 +20,40 @@ void ProtoWriteBuffer::encode_varint_raw_slow_(uint32_t value) { *this->pos_++ = static_cast<uint8_t>(value); } +ProtoVarIntResult ProtoVarInt::parse_slow(const uint8_t *buffer, uint32_t len) { + // Multi-byte varint: first byte already checked to have high bit set + uint32_t result32 = buffer[0] & 0x7F; #ifdef USE_API_VARINT64 -optional<ProtoVarInt> ProtoVarInt::parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed, - uint32_t result32) { + uint32_t limit = std::min(len, uint32_t(4)); +#else + uint32_t limit = std::min(len, uint32_t(5)); +#endif + for (uint32_t i = 1; i < limit; i++) { + uint8_t val = buffer[i]; + result32 |= uint32_t(val & 0x7F) << (i * 7); + if ((val & 0x80) == 0) { + return {result32, i + 1}; + } + } +#ifdef USE_API_VARINT64 + return parse_wide(buffer, len, result32); +#else + return {0, PROTO_VARINT_PARSE_FAILED}; +#endif +} + +#ifdef USE_API_VARINT64 +ProtoVarIntResult ProtoVarInt::parse_wide(const uint8_t *buffer, uint32_t len, uint32_t result32) { uint64_t result64 = result32; uint32_t limit = std::min(len, uint32_t(10)); for (uint32_t i = 4; i < limit; i++) { uint8_t val = buffer[i]; result64 |= uint64_t(val & 0x7F) << (i * 7); if ((val & 0x80) == 0) { - *consumed = i + 1; - return ProtoVarInt(result64); + return {result64, i + 1}; } } - return {}; + return {0, PROTO_VARINT_PARSE_FAILED}; } #endif @@ -43,18 +63,16 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size const uint8_t *end = buffer + length; while (ptr < end) { - uint32_t consumed; - - // Parse field header (tag) - auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed); + // Parse field header (tag) - ptr < end guarantees len >= 1 + auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr); if (!res.has_value()) { break; // Invalid data, stop counting } - uint32_t tag = res->as_uint32(); + uint32_t tag = static_cast<uint32_t>(res.value); uint32_t field_type = tag & WIRE_TYPE_MASK; uint32_t field_id = tag >> 3; - ptr += consumed; + ptr += res.consumed; // Count if this is the target field if (field_id == target_field_id) { @@ -64,20 +82,20 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size // Skip field data based on wire type switch (field_type) { case WIRE_TYPE_VARINT: { // VarInt - parse and skip - res = ProtoVarInt::parse(ptr, end - ptr, &consumed); + res = ProtoVarInt::parse(ptr, end - ptr); if (!res.has_value()) { return count; // Invalid data, return what we have } - ptr += consumed; + ptr += res.consumed; break; } case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited - parse length and skip data - res = ProtoVarInt::parse(ptr, end - ptr, &consumed); + res = ProtoVarInt::parse(ptr, end - ptr); if (!res.has_value()) { return count; } - uint32_t field_length = res->as_uint32(); - ptr += consumed; + uint32_t field_length = static_cast<uint32_t>(res.value); + ptr += res.consumed; if (field_length > static_cast<size_t>(end - ptr)) { return count; // Out of bounds } @@ -190,41 +208,40 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { const uint8_t *end = buffer + length; while (ptr < end) { - uint32_t consumed; - - // Parse field header - auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed); + // Parse field header - ptr < end guarantees len >= 1 + auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr); if (!res.has_value()) { ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer)); return; } - uint32_t tag = res->as_uint32(); + uint32_t tag = static_cast<uint32_t>(res.value); uint32_t field_type = tag & WIRE_TYPE_MASK; uint32_t field_id = tag >> 3; - ptr += consumed; + ptr += res.consumed; switch (field_type) { case WIRE_TYPE_VARINT: { // VarInt - res = ProtoVarInt::parse(ptr, end - ptr, &consumed); + res = ProtoVarInt::parse(ptr, end - ptr); if (!res.has_value()) { ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer)); return; } - if (!this->decode_varint(field_id, *res)) { - ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu32 "!", field_id, res->as_uint32()); + if (!this->decode_varint(field_id, res.value)) { + ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu64 "!", field_id, + static_cast<uint64_t>(res.value)); } - ptr += consumed; + ptr += res.consumed; break; } case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited - res = ProtoVarInt::parse(ptr, end - ptr, &consumed); + res = ProtoVarInt::parse(ptr, end - ptr); if (!res.has_value()) { ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer)); return; } - uint32_t field_length = res->as_uint32(); - ptr += consumed; + uint32_t field_length = static_cast<uint32_t>(res.value); + ptr += res.consumed; if (field_length > static_cast<size_t>(end - ptr)) { ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer)); return; diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index adde0a8a85..7050efb446 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -98,90 +98,56 @@ inline void encode_varint_to_buffer(uint32_t val, uint8_t *buffer) { * within the same function scope where temporaries are created. */ -/// Representation of a VarInt - in ProtoBuf should be 64bit but we only use 32bit +/// Type used for decoded varint values - uint64_t when BLE needs 64-bit addresses, uint32_t otherwise +#ifdef USE_API_VARINT64 +using proto_varint_value_t = uint64_t; +#else +using proto_varint_value_t = uint32_t; +#endif + +/// Sentinel value for consumed field indicating parse failure +inline constexpr uint32_t PROTO_VARINT_PARSE_FAILED = 0; + +/// Result of parsing a varint: value + number of bytes consumed. +/// consumed == PROTO_VARINT_PARSE_FAILED indicates parse failure (not enough data or invalid). +struct ProtoVarIntResult { + proto_varint_value_t value; + uint32_t consumed; // PROTO_VARINT_PARSE_FAILED = parse failed + + constexpr bool has_value() const { return this->consumed != PROTO_VARINT_PARSE_FAILED; } +}; + +/// Static varint parsing methods for the protobuf wire format. class ProtoVarInt { public: - ProtoVarInt() : value_(0) {} - explicit ProtoVarInt(uint64_t value) : value_(value) {} - - /// Parse a varint from buffer. consumed must be a valid pointer (not null). - static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) { + /// Parse a varint from buffer. Caller must ensure len >= 1. + /// Returns result with consumed=0 on failure (truncated multi-byte varint). + static inline ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse_non_empty(const uint8_t *buffer, uint32_t len) { #ifdef ESPHOME_DEBUG_API - assert(consumed != nullptr); + assert(len > 0); #endif - if (len == 0) - return {}; // Fast path: single-byte varints (0-127) are the most common case - // (booleans, small enums, field tags). Avoid loop overhead entirely. - if ((buffer[0] & 0x80) == 0) { - *consumed = 1; - return ProtoVarInt(buffer[0]); - } - // 32-bit phase: process remaining bytes with native 32-bit shifts. - // Without USE_API_VARINT64: cover bytes 1-4 (shifts 7, 14, 21, 28) — the uint32_t - // shift at byte 4 (shift by 28) may lose bits 32-34, but those are always zero for valid uint32 values. - // With USE_API_VARINT64: cover bytes 1-3 (shifts 7, 14, 21) so parse_wide handles - // byte 4+ with full 64-bit arithmetic (avoids truncating values > UINT32_MAX). - uint32_t result32 = buffer[0] & 0x7F; -#ifdef USE_API_VARINT64 - uint32_t limit = std::min(len, uint32_t(4)); -#else - uint32_t limit = std::min(len, uint32_t(5)); -#endif - for (uint32_t i = 1; i < limit; i++) { - uint8_t val = buffer[i]; - result32 |= uint32_t(val & 0x7F) << (i * 7); - if ((val & 0x80) == 0) { - *consumed = i + 1; - return ProtoVarInt(result32); - } - } - // 64-bit phase for remaining bytes (BLE addresses etc.) -#ifdef USE_API_VARINT64 - return parse_wide(buffer, len, consumed, result32); -#else - return {}; -#endif + // (booleans, small enums, field tags, small message sizes/types). + if ((buffer[0] & 0x80) == 0) [[likely]] + return {buffer[0], 1}; + return parse_slow(buffer, len); + } + + /// Parse a varint from buffer (safe for empty buffers). + /// Returns result with consumed=0 on failure (empty buffer or truncated varint). + static inline ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse(const uint8_t *buffer, uint32_t len) { + if (len == 0) + return {0, PROTO_VARINT_PARSE_FAILED}; + return parse_non_empty(buffer, len); } -#ifdef USE_API_VARINT64 protected: + // Slow path for multi-byte varints (>= 128), outlined to keep fast path small + static ProtoVarIntResult parse_slow(const uint8_t *buffer, uint32_t len) __attribute__((noinline)); + +#ifdef USE_API_VARINT64 /// Continue parsing varint bytes 4-9 with 64-bit arithmetic. - /// Separated to keep 64-bit shift code (__ashldi3 on 32-bit platforms) out of the common path. - static optional<ProtoVarInt> parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed, uint32_t result32) - __attribute__((noinline)); - - public: -#endif - - constexpr uint16_t as_uint16() const { return this->value_; } - constexpr uint32_t as_uint32() const { return this->value_; } - constexpr bool as_bool() const { return this->value_; } - constexpr int32_t as_int32() const { - // Not ZigZag encoded - return static_cast<int32_t>(this->value_); - } - constexpr int32_t as_sint32() const { - // with ZigZag encoding - return decode_zigzag32(static_cast<uint32_t>(this->value_)); - } -#ifdef USE_API_VARINT64 - constexpr uint64_t as_uint64() const { return this->value_; } - constexpr int64_t as_int64() const { - // Not ZigZag encoded - return static_cast<int64_t>(this->value_); - } - constexpr int64_t as_sint64() const { - // with ZigZag encoding - return decode_zigzag64(this->value_); - } -#endif - - protected: -#ifdef USE_API_VARINT64 - uint64_t value_; -#else - uint32_t value_; + static ProtoVarIntResult parse_wide(const uint8_t *buffer, uint32_t len, uint32_t result32) __attribute__((noinline)); #endif }; @@ -499,7 +465,7 @@ class ProtoDecodableMessage : public ProtoMessage { protected: ~ProtoDecodableMessage() = default; - virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; } + virtual bool decode_varint(uint32_t field_id, proto_varint_value_t value) { return false; } virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; } virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; } // NOTE: decode_64bit removed - wire type 1 not supported diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 206f8f558b..b4044c362c 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -461,7 +461,7 @@ class FloatType(TypeInfo): class Int64Type(TypeInfo): cpp_type = "int64_t" default_value = "0" - decode_varint = "value.as_int64()" + decode_varint = "static_cast<int64_t>(value)" encode_func = "encode_int64" wire_type = WireType.VARINT # Uses wire type 0 @@ -481,7 +481,7 @@ class Int64Type(TypeInfo): class UInt64Type(TypeInfo): cpp_type = "uint64_t" default_value = "0" - decode_varint = "value.as_uint64()" + decode_varint = "value" encode_func = "encode_uint64" wire_type = WireType.VARINT # Uses wire type 0 @@ -501,7 +501,7 @@ class UInt64Type(TypeInfo): class Int32Type(TypeInfo): cpp_type = "int32_t" default_value = "0" - decode_varint = "value.as_int32()" + decode_varint = "static_cast<int32_t>(value)" encode_func = "encode_int32" wire_type = WireType.VARINT # Uses wire type 0 @@ -573,7 +573,7 @@ class Fixed32Type(TypeInfo): class BoolType(TypeInfo): cpp_type = "bool" default_value = "false" - decode_varint = "value.as_bool()" + decode_varint = "value != 0" encode_func = "encode_bool" wire_type = WireType.VARINT # Uses wire type 0 @@ -1151,7 +1151,7 @@ class FixedArrayBytesType(TypeInfo): class UInt32Type(TypeInfo): cpp_type = "uint32_t" default_value = "0" - decode_varint = "value.as_uint32()" + decode_varint = "value" encode_func = "encode_uint32" wire_type = WireType.VARINT # Uses wire type 0 @@ -1175,7 +1175,7 @@ class EnumType(TypeInfo): @property def decode_varint(self) -> str: - return f"static_cast<{self.cpp_type}>(value.as_uint32())" + return f"static_cast<{self.cpp_type}>(value)" default_value = "" wire_type = WireType.VARINT # Uses wire type 0 @@ -1262,7 +1262,7 @@ class SFixed64Type(TypeInfo): class SInt32Type(TypeInfo): cpp_type = "int32_t" default_value = "0" - decode_varint = "value.as_sint32()" + decode_varint = "decode_zigzag32(static_cast<uint32_t>(value))" encode_func = "encode_sint32" wire_type = WireType.VARINT # Uses wire type 0 @@ -1282,7 +1282,7 @@ class SInt32Type(TypeInfo): class SInt64Type(TypeInfo): cpp_type = "int64_t" default_value = "0" - decode_varint = "value.as_sint64()" + decode_varint = "decode_zigzag64(value)" encode_func = "encode_sint64" wire_type = WireType.VARINT # Uses wire type 0 @@ -2205,7 +2205,7 @@ def build_message_type( cpp = "" if decode_varint: - o = f"bool {desc.name}::decode_varint(uint32_t field_id, ProtoVarInt value) {{\n" + o = f"bool {desc.name}::decode_varint(uint32_t field_id, proto_varint_value_t value) {{\n" o += " switch (field_id) {\n" o += indent("\n".join(decode_varint), " ") + "\n" o += " default: return false;\n" @@ -2213,7 +2213,7 @@ def build_message_type( o += " return true;\n" o += "}\n" cpp += o - prot = "bool decode_varint(uint32_t field_id, ProtoVarInt value) override;" + prot = "bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;" protected_content.insert(0, prot) if decode_length: o = f"bool {desc.name}::decode_length(uint32_t field_id, ProtoLengthDelimited value) {{\n" From c709010c4cb04c7276f93af781dee82bf566ca85 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 09:11:12 -1000 Subject: [PATCH 511/609] [api] Replace std::vector<uint8_t> with APIBuffer to skip zero-fill (#14593) --- esphome/components/api/api_buffer.cpp | 13 ++++ esphome/components/api/api_buffer.h | 67 +++++++++++++++++++ esphome/components/api/api_connection.cpp | 4 +- esphome/components/api/api_connection.h | 8 +-- esphome/components/api/api_frame_helper.h | 10 +-- .../components/api/api_frame_helper_noise.cpp | 7 +- .../components/api/api_frame_helper_noise.h | 4 +- .../api/api_frame_helper_plaintext.cpp | 4 +- esphome/components/api/api_server.h | 5 +- esphome/components/api/proto.h | 10 +-- tests/components/api/test.ln882x-ard.yaml | 5 ++ 11 files changed, 107 insertions(+), 30 deletions(-) create mode 100644 esphome/components/api/api_buffer.cpp create mode 100644 esphome/components/api/api_buffer.h create mode 100644 tests/components/api/test.ln882x-ard.yaml diff --git a/esphome/components/api/api_buffer.cpp b/esphome/components/api/api_buffer.cpp new file mode 100644 index 0000000000..6db18b0365 --- /dev/null +++ b/esphome/components/api/api_buffer.cpp @@ -0,0 +1,13 @@ +#include "api_buffer.h" + +namespace esphome::api { + +void APIBuffer::grow_(size_t n) { + auto new_data = make_buffer(n); + if (this->size_) + std::memcpy(new_data.get(), this->data_.get(), this->size_); + this->data_ = std::move(new_data); + this->capacity_ = n; +} + +} // namespace esphome::api diff --git a/esphome/components/api/api_buffer.h b/esphome/components/api/api_buffer.h new file mode 100644 index 0000000000..00801e3ee5 --- /dev/null +++ b/esphome/components/api/api_buffer.h @@ -0,0 +1,67 @@ +#pragma once + +#include <cstdint> +#include <cstring> +#include <memory> + +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +namespace esphome::api { + +/// Helper to use make_unique_for_overwrite where available (skips zero-fill), +/// falling back to make_unique on older GCC (ESP8266, LibreTiny). +inline std::unique_ptr<uint8_t[]> make_buffer(size_t n) { +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) + return std::make_unique<uint8_t[]>(n); +#else + return std::make_unique_for_overwrite<uint8_t[]>(n); +#endif +} + +/// Byte buffer that skips zero-initialization on resize(). +/// +/// std::vector<uint8_t>::resize() zero-fills new bytes via memset. For the +/// shared protobuf write buffer, every byte is overwritten by the encoder, +/// making the zero-fill pure waste. For the receive buffer, bytes are +/// overwritten by socket reads. +/// +/// Designed for bulk clear/resize/overwrite patterns. grow_() allocates +/// exactly the requested size (no growth factor) since callers resize to +/// known sizes rather than appending incrementally. +/// +/// Safe because: callers always write exactly the number of bytes they +/// resize for. In the protobuf write path, debug_check_bounds_ validates +/// writes in debug builds. +class APIBuffer { + public: + void clear() { this->size_ = 0; } + inline void reserve(size_t n) ESPHOME_ALWAYS_INLINE { + if (n > this->capacity_) + this->grow_(n); + } + inline void resize(size_t n) ESPHOME_ALWAYS_INLINE { + this->reserve(n); + this->size_ = n; // no zero-fill + } + uint8_t *data() { return this->data_.get(); } + const uint8_t *data() const { return this->data_.get(); } + size_t size() const { return this->size_; } + bool empty() const { return this->size_ == 0; } + uint8_t &operator[](size_t i) { return this->data_[i]; } + const uint8_t &operator[](size_t i) const { return this->data_[i]; } + /// Release all memory (equivalent to std::vector swap trick). + void release() { + this->data_.reset(); + this->size_ = 0; + this->capacity_ = 0; + } + + protected: + void grow_(size_t n); + std::unique_ptr<uint8_t[]> data_; + size_t size_{0}; + size_t capacity_{0}; +}; + +} // namespace esphome::api diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7bd5d5120b..dea3ba5460 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -2016,7 +2016,7 @@ uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncode if (total_calculated_size > remaining_size) return 0; // Doesn't fit - std::vector<uint8_t> &shared_buf = conn->parent_->get_shared_buffer_ref(); + auto &shared_buf = conn->parent_->get_shared_buffer_ref(); if (conn->flags_.batch_first_message) { // First message - buffer already prepared by caller, just clear flag @@ -2184,7 +2184,7 @@ void APIConnection::process_batch_() { // Separated from process_batch_() so the single-message fast path gets a minimal // stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array. -void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding, +void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding, uint8_t footer_size) { // Ensure MessageInfo remains trivially destructible for our placement new approach static_assert(std::is_trivially_destructible<MessageInfo>::value, diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index ccb51186d6..3356511684 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -288,7 +288,7 @@ class APIConnection final : public APIServerConnectionBase { } } - void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t header_padding, size_t total_size) { + void prepare_first_message_buffer(APIBuffer &shared_buf, size_t header_padding, size_t total_size) { shared_buf.clear(); // Reserve space for header padding + message + footer // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext) @@ -299,7 +299,7 @@ class APIConnection final : public APIServerConnectionBase { } // Convenience overload - computes frame overhead internally - void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t payload_size) { + void prepare_first_message_buffer(APIBuffer &shared_buf, size_t payload_size) { const uint8_t header_padding = this->helper_->frame_header_padding(); const uint8_t footer_size = this->helper_->frame_footer_size(); this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size); @@ -687,8 +687,8 @@ class APIConnection final : public APIServerConnectionBase { bool schedule_batch_(); void process_batch_(); - void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding, - uint8_t footer_size) __attribute__((noinline)); + void process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding, uint8_t footer_size) + __attribute__((noinline)); void clear_batch_() { this->deferred_batch_.clear(); this->flags_.batch_scheduled = false; diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 151314658e..98de24501e 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -5,10 +5,10 @@ #include <memory> #include <span> #include <utility> -#include <vector> #include "esphome/core/defines.h" #ifdef USE_API +#include "esphome/components/api/api_buffer.h" #include "esphome/components/socket/socket.h" #include "esphome/core/application.h" #include "esphome/core/log.h" @@ -178,8 +178,7 @@ class APIFrameHelper { // rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame // and clearing would lose partially received data. if (this->rx_buf_len_ == 0) { - // Use swap trick since shrink_to_fit() is non-binding and may be ignored - std::vector<uint8_t>().swap(this->rx_buf_); + this->rx_buf_.release(); } } @@ -206,9 +205,6 @@ class APIFrameHelper { // Common socket write error handling APIError handle_socket_write_error_(); - template<typename StateEnum> - APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf, - const std::string &info, StateEnum &state, StateEnum failed_state); // Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit) std::unique_ptr<socket::Socket> socket_; @@ -245,7 +241,7 @@ class APIFrameHelper { // Containers (size varies, but typically 12+ bytes on 32-bit) std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_; - std::vector<uint8_t> rx_buf_; + APIBuffer rx_buf_; // Client name buffer - stores name from Hello message or initial peername char client_name_[CLIENT_INFO_NAME_MAX_LEN]{}; diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 256357ce6a..3e6ecf9dc3 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -207,9 +207,7 @@ APIError APINoiseFrameHelper::try_read_frame_() { // During handshake, rx_buf_.size() is used in prologue construction, so // the buffer must be exactly msg_size to avoid prologue mismatch.) uint16_t alloc_size = msg_size + (is_data ? RX_BUF_NULL_TERMINATOR : 0); - if (this->rx_buf_.size() != alloc_size) { - this->rx_buf_.resize(alloc_size); - } + this->rx_buf_.resize(alloc_size); if (rx_buf_len_ < msg_size) { // more data to read @@ -571,8 +569,7 @@ APIError APINoiseFrameHelper::init_handshake_() { if (aerr != APIError::OK) return aerr; // set_prologue copies it into handshakestate, so we can get rid of it now - // Use swap idiom to actually release memory (= {} only clears size, not capacity) - std::vector<uint8_t>().swap(prologue_); + prologue_.release(); err = noise_handshakestate_start(handshake_); aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED); diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index 183b8c8a51..83410febb2 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -43,8 +43,8 @@ class APINoiseFrameHelper final : public APIFrameHelper { // Reference to noise context (4 bytes on 32-bit) APINoiseContext &ctx_; - // Vector (12 bytes on 32-bit) - std::vector<uint8_t> prologue_; + // Buffer for noise handshake prologue (released after handshake) + APIBuffer prologue_; // NoiseProtocolId (size depends on implementation) NoiseProtocolId nid_; diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 793cece3b8..007da7ef2b 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -165,9 +165,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_() { // Reserve space for body (+ null terminator so protobuf StringRef fields // can be safely null-terminated in-place after decode) - if (this->rx_buf_.size() != this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR) { - this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR); - } + this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR); if (rx_buf_len_ < rx_header_parsed_len_) { // more data to read diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index e5f371d8a1..69fc26cc00 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" #ifdef USE_API +#include "api_buffer.h" #include "api_noise_context.h" #include "api_pb2.h" #include "api_pb2_service.h" @@ -65,7 +66,7 @@ class APIServer : public Component, void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; } // Get reference to shared buffer for API connections - std::vector<uint8_t> &get_shared_buffer_ref() { return shared_write_buffer_; } + APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; } #ifdef USE_API_NOISE bool save_noise_psk(psk_t psk, bool make_active = true); @@ -276,7 +277,7 @@ class APIServer : public Component, // Not pre-allocated: all send paths call prepare_first_message_buffer() which // reserves the exact needed size. Pre-allocating here would cause heap fragmentation // since the buffer would almost always reallocate on first use. - std::vector<uint8_t> shared_write_buffer_; + APIBuffer shared_write_buffer_; #ifdef USE_API_HOMEASSISTANT_STATES std::vector<HomeAssistantStateSubscription> state_subs_; #endif diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 7050efb446..d1c955b1fb 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -1,6 +1,7 @@ #pragma once #include "api_pb2_defines.h" +#include "api_buffer.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -203,9 +204,8 @@ class Proto32Bit { class ProtoWriteBuffer { public: - ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {} - ProtoWriteBuffer(std::vector<uint8_t> *buffer, size_t write_pos) - : buffer_(buffer), pos_(buffer->data() + write_pos) {} + ProtoWriteBuffer(APIBuffer *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {} + ProtoWriteBuffer(APIBuffer *buffer, size_t write_pos) : buffer_(buffer), pos_(buffer->data() + write_pos) {} inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint32_t value) { if (value < 128) [[likely]] { this->debug_check_bounds_(1); @@ -340,7 +340,7 @@ class ProtoWriteBuffer { // Non-template core for encode_optional_sub_message. void encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value, void (*encode_fn)(const void *, ProtoWriteBuffer &)); - std::vector<uint8_t> *get_buffer() const { return buffer_; } + APIBuffer *get_buffer() const { return buffer_; } protected: // Slow path for encode_varint_raw values >= 128, outlined to keep fast path small @@ -353,7 +353,7 @@ class ProtoWriteBuffer { void debug_check_bounds_([[maybe_unused]] size_t bytes) {} #endif - std::vector<uint8_t> *buffer_; + APIBuffer *buffer_; uint8_t *pos_; }; diff --git a/tests/components/api/test.ln882x-ard.yaml b/tests/components/api/test.ln882x-ard.yaml new file mode 100644 index 0000000000..46c01d926f --- /dev/null +++ b/tests/components/api/test.ln882x-ard.yaml @@ -0,0 +1,5 @@ +<<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 From 9dd3ec258c2f1ebf636cd7765981df2f1711c4a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 09:11:28 -1000 Subject: [PATCH 512/609] [scheduler] Replace unique_ptr with raw pointers, add leak detection (#14620) --- esphome/core/scheduler.cpp | 179 +++++++++++------- esphome/core/scheduler.h | 94 +++++---- .../fixtures/scheduler_bulk_cleanup.yaml | 1 + .../fixtures/scheduler_defer_cancel.yaml | 1 + .../scheduler_defer_cancels_regular.yaml | 1 + .../fixtures/scheduler_defer_fifo_simple.yaml | 1 + .../fixtures/scheduler_defer_stress.yaml | 1 + .../fixtures/scheduler_heap_stress.yaml | 1 + .../scheduler_internal_id_no_collision.yaml | 1 + .../fixtures/scheduler_null_name.yaml | 1 + .../fixtures/scheduler_numeric_id_test.yaml | 1 + .../scheduler_rapid_cancellation.yaml | 1 + .../fixtures/scheduler_recursive_timeout.yaml | 1 + .../fixtures/scheduler_removed_item_race.yaml | 1 + .../fixtures/scheduler_retry_test.yaml | 1 + .../scheduler_simultaneous_callbacks.yaml | 1 + .../fixtures/scheduler_string_lifetime.yaml | 1 + .../scheduler_string_name_stress.yaml | 1 + 18 files changed, 172 insertions(+), 117 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index ca560e8250..63e1006b03 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -30,11 +30,6 @@ static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5; // max delay to start an interval sequence static constexpr uint32_t MAX_INTERVAL_DELAY = 5000; -// Prevent inlining of SchedulerItem deletion. On BK7231N (Thumb-1), GCC inlines -// ~unique_ptr<SchedulerItem> (~30 bytes each) at every destruction site. Defining -// the deleter in the .cpp file ensures a single copy of the destructor + operator delete. -void Scheduler::SchedulerItemDeleter::operator()(SchedulerItem *ptr) const noexcept { delete ptr; } - #if defined(ESPHOME_LOG_HAS_VERBOSE) || defined(ESPHOME_DEBUG_SCHEDULER) // Helper struct for formatting scheduler item names consistently in logs // Uses a stack buffer to avoid heap allocation @@ -122,8 +117,8 @@ uint32_t Scheduler::calculate_interval_offset_(uint32_t delay) { bool Scheduler::is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id) { for (auto *container : {&this->items_, &this->to_add_}) { - for (auto &item : *container) { - if (item && this->is_item_removed_locked_(item.get()) && + for (auto *item : *container) { + if (item != nullptr && this->is_item_removed_locked_(item) && this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT, /* match_retry= */ true, /* skip_removed= */ false)) { return true; @@ -147,17 +142,31 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type return; } - // Take lock early to protect scheduler_item_pool_ access + // Take lock early to protect scheduler_item_pool_ access and retry-cancelled check LockGuard guard{this->lock_}; + // For retries, check if there's a cancelled timeout first - before allocating an item. + // Skip check for anonymous retries (STATIC_STRING with nullptr) - they can't be cancelled by name + // Skip check for defer (delay=0) - deferred retries bypass the cancellation check + if (is_retry && delay != 0 && (name_type != NameType::STATIC_STRING || static_name != nullptr) && + type == SchedulerItem::TIMEOUT && + this->is_retry_cancelled_locked_(component, name_type, static_name, hash_or_id)) { +#ifdef ESPHOME_DEBUG_SCHEDULER + SchedulerNameLog skip_name_log; + ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", + skip_name_log.format(name_type, static_name, hash_or_id)); +#endif + return; + } + // Create and populate the scheduler item - auto item = this->get_item_from_pool_locked_(); + SchedulerItem *item = this->get_item_from_pool_locked_(); item->component = component; item->set_name(name_type, static_name, hash_or_id); item->type = type; item->callback = std::move(func); // Reset remove flag - recycled items may have been cancelled (remove=true) in previous use - this->set_item_removed_(item.get(), false); + this->set_item_removed_(item, false); item->is_retry = is_retry; // Determine target container: defer_queue_ for deferred items, to_add_ for everything else. @@ -193,29 +202,15 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type } #ifdef ESPHOME_DEBUG_SCHEDULER - this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now_64); + this->debug_log_timer_(item, name_type, static_name, hash_or_id, type, delay, now_64); #endif /* ESPHOME_DEBUG_SCHEDULER */ - - // For retries, check if there's a cancelled timeout first - // Skip check for anonymous retries (STATIC_STRING with nullptr) - they can't be cancelled by name - if (is_retry && (name_type != NameType::STATIC_STRING || static_name != nullptr) && - type == SchedulerItem::TIMEOUT && - this->is_retry_cancelled_locked_(component, name_type, static_name, hash_or_id)) { - // Skip scheduling - the retry was cancelled -#ifdef ESPHOME_DEBUG_SCHEDULER - SchedulerNameLog skip_name_log; - ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", - skip_name_log.format(name_type, static_name, hash_or_id)); -#endif - return; - } } // Common epilogue: atomic cancel-and-add (unless skip_cancel is true) if (!skip_cancel) { this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type); } - target->push_back(std::move(item)); + target->push_back(item); } void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, @@ -395,7 +390,7 @@ optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) { if (this->cleanup_() == 0) return {}; - auto &item = this->items_[0]; + SchedulerItem *item = this->items_[0]; const auto now_64 = this->millis_64_from_(now); const uint64_t next_exec = item->get_next_execution(); if (next_exec < now_64) @@ -414,13 +409,13 @@ void Scheduler::full_cleanup_removed_items_() { // Compact in-place: move valid items forward, recycle removed ones size_t write = 0; for (size_t read = 0; read < this->items_.size(); ++read) { - if (!is_item_removed_locked_(this->items_[read].get())) { + if (!is_item_removed_locked_(this->items_[read])) { if (write != read) { - this->items_[write] = std::move(this->items_[read]); + this->items_[write] = this->items_[read]; } ++write; } else { - this->recycle_item_main_loop_(std::move(this->items_[read])); + this->recycle_item_main_loop_(this->items_[read]); } } this->items_.erase(this->items_.begin() + write, this->items_.end()); @@ -444,7 +439,7 @@ void Scheduler::compact_defer_queue_locked_() { // and recycled on the next loop iteration. size_t remaining = this->defer_queue_.size() - this->defer_queue_front_; for (size_t i = 0; i < remaining; i++) { - this->defer_queue_[i] = std::move(this->defer_queue_[this->defer_queue_front_ + i]); + this->defer_queue_[i] = this->defer_queue_[this->defer_queue_front_ + i]; } // Use erase() instead of resize() to avoid instantiating _M_default_append // (saves ~156 bytes flash). Erasing from the end is O(1) - no shifting needed. @@ -469,26 +464,26 @@ void HOT Scheduler::call(uint32_t now) { if (now_64 - last_print > 2000) { last_print = now_64; - std::vector<SchedulerItemPtr> old_items; + std::vector<SchedulerItem *> old_items; ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64, this->items_.size(), this->scheduler_item_pool_.size(), now_64); // Cleanup before debug output this->cleanup_(); while (!this->items_.empty()) { - SchedulerItemPtr item; + SchedulerItem *item; { LockGuard guard{this->lock_}; item = this->pop_raw_locked_(); } SchedulerNameLog name_log; - bool is_cancelled = is_item_removed_(item.get()); + bool is_cancelled = is_item_removed_(item); ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64 "%s", item->get_type_str(), LOG_STR_ARG(item->get_source()), name_log.format(item->get_name_type(), item->get_name(), item->get_name_hash_or_id()), item->interval, item->get_next_execution() - now_64, item->get_next_execution(), is_cancelled ? " [CANCELLED]" : ""); - old_items.push_back(std::move(item)); + old_items.push_back(item); } ESP_LOGD(TAG, "\n"); @@ -512,7 +507,7 @@ void HOT Scheduler::call(uint32_t now) { } while (!this->items_.empty()) { // Don't copy-by value yet - auto &item = this->items_[0]; + SchedulerItem *item = this->items_[0]; if (item->get_next_execution() > now_64) { // Not reached timeout yet, done for this call break; @@ -532,7 +527,7 @@ void HOT Scheduler::call(uint32_t now) { // Multi-threaded platforms without atomics: must take lock to safely read remove flag { LockGuard guard{this->lock_}; - if (is_item_removed_locked_(item.get())) { + if (is_item_removed_locked_(item)) { this->recycle_item_main_loop_(this->pop_raw_locked_()); this->to_remove_--; continue; @@ -540,7 +535,7 @@ void HOT Scheduler::call(uint32_t now) { } #else // Single-threaded or multi-threaded with atomics: can check without lock - if (is_item_removed_(item.get())) { + if (is_item_removed_(item)) { LockGuard guard{this->lock_}; this->recycle_item_main_loop_(this->pop_raw_locked_()); this->to_remove_--; @@ -561,18 +556,18 @@ void HOT Scheduler::call(uint32_t now) { // Warning: During callback(), a lot of stuff can happen, including: // - timeouts/intervals get added, potentially invalidating vector pointers // - timeouts/intervals get cancelled - now = this->execute_item_(item.get(), now); + now = this->execute_item_(item, now); LockGuard guard{this->lock_}; // Only pop after function call, this ensures we were reachable // during the function call and know if we were cancelled. - auto executed_item = this->pop_raw_locked_(); + SchedulerItem *executed_item = this->pop_raw_locked_(); - if (this->is_item_removed_locked_(executed_item.get())) { + if (this->is_item_removed_locked_(executed_item)) { // We were removed/cancelled in the function call, recycle and continue this->to_remove_--; - this->recycle_item_main_loop_(std::move(executed_item)); + this->recycle_item_main_loop_(executed_item); continue; } @@ -580,10 +575,10 @@ void HOT Scheduler::call(uint32_t now) { executed_item->set_next_execution(now_64 + executed_item->interval); // Add new item directly to to_add_ // since we have the lock held - this->to_add_.push_back(std::move(executed_item)); + this->to_add_.push_back(executed_item); } else { // Timeout completed - recycle it - this->recycle_item_main_loop_(std::move(executed_item)); + this->recycle_item_main_loop_(executed_item); } has_added_items |= !this->to_add_.empty(); @@ -592,17 +587,33 @@ void HOT Scheduler::call(uint32_t now) { if (has_added_items) { this->process_to_add(); } + +#ifdef ESPHOME_DEBUG_SCHEDULER + // Verify no items were leaked during this call() cycle. + // All items must be in items_, to_add_, defer_queue_, or the pool. + // Safe to check here because: + // - process_defer_queue_ has already run its cleanup_defer_queue_locked_(), + // so defer_queue_ contains no nullptr slots inflating the count. + // - The while loop above has finished, so no items are held in local variables; + // every item has been returned to a container (items_, to_add_, or pool). + // Lock needed to get a consistent snapshot of all containers. + { + LockGuard guard{this->lock_}; + this->debug_verify_no_leak_(); + } +#endif } void HOT Scheduler::process_to_add() { LockGuard guard{this->lock_}; - for (auto &it : this->to_add_) { - if (is_item_removed_locked_(it.get())) { + for (auto *&it : this->to_add_) { + if (is_item_removed_locked_(it)) { // Recycle cancelled items - this->recycle_item_main_loop_(std::move(it)); + this->recycle_item_main_loop_(it); + it = nullptr; continue; } - this->items_.push_back(std::move(it)); + this->items_.push_back(it); std::push_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); } this->to_add_.clear(); @@ -628,20 +639,18 @@ size_t HOT Scheduler::cleanup_() { // leading to race conditions LockGuard guard{this->lock_}; while (!this->items_.empty()) { - auto &item = this->items_[0]; - if (!this->is_item_removed_locked_(item.get())) + SchedulerItem *item = this->items_[0]; + if (!this->is_item_removed_locked_(item)) break; this->to_remove_--; this->recycle_item_main_loop_(this->pop_raw_locked_()); } return this->items_.size(); } -Scheduler::SchedulerItemPtr HOT Scheduler::pop_raw_locked_() { +Scheduler::SchedulerItem *HOT Scheduler::pop_raw_locked_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); - // Move the item out before popping - this is the item that was at the front of the heap - auto item = std::move(this->items_.back()); - + SchedulerItem *item = this->items_.back(); this->items_.pop_back(); return item; } @@ -699,7 +708,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type return total_cancelled > 0; } -bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) { +bool HOT Scheduler::SchedulerItem::cmp(SchedulerItem *a, SchedulerItem *b) { // High bits are almost always equal (change only on 32-bit rollover ~49 days) // Optimize for common case: check low bits first when high bits are equal return (a->next_execution_high_ == b->next_execution_high_) ? (a->next_execution_low_ > b->next_execution_low_) @@ -710,23 +719,26 @@ bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const Schedule // IMPORTANT: Caller must hold the scheduler lock before calling this function. // This protects scheduler_item_pool_ from concurrent access by other threads // that may be acquiring items from the pool in set_timer_common_(). -void Scheduler::recycle_item_main_loop_(SchedulerItemPtr item) { - if (!item) +void Scheduler::recycle_item_main_loop_(SchedulerItem *item) { + if (item == nullptr) return; if (this->scheduler_item_pool_.size() < MAX_POOL_SIZE) { // Clear callback to release captured resources item->callback = nullptr; - this->scheduler_item_pool_.push_back(std::move(item)); + this->scheduler_item_pool_.push_back(item); #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Recycled item to pool (pool size now: %zu)", this->scheduler_item_pool_.size()); #endif } else { #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Pool full (size: %zu), deleting item", this->scheduler_item_pool_.size()); +#endif + delete item; +#ifdef ESPHOME_DEBUG_SCHEDULER + this->debug_live_items_--; #endif } - // else: unique_ptr will delete the item when it goes out of scope } #ifdef ESPHOME_DEBUG_SCHEDULER @@ -753,21 +765,54 @@ void Scheduler::debug_log_timer_(const SchedulerItem *item, NameType name_type, // Helper to get or create a scheduler item from the pool // IMPORTANT: Caller must hold the scheduler lock before calling this function. -Scheduler::SchedulerItemPtr Scheduler::get_item_from_pool_locked_() { - SchedulerItemPtr item; +Scheduler::SchedulerItem *Scheduler::get_item_from_pool_locked_() { if (!this->scheduler_item_pool_.empty()) { - item = std::move(this->scheduler_item_pool_.back()); + SchedulerItem *item = this->scheduler_item_pool_.back(); this->scheduler_item_pool_.pop_back(); #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Reused item from pool (pool size now: %zu)", this->scheduler_item_pool_.size()); #endif - } else { - item = SchedulerItemPtr(new SchedulerItem()); -#ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "Allocated new item (pool empty)"); -#endif + return item; } +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Allocated new item (pool empty)"); +#endif + auto *item = new SchedulerItem(); +#ifdef ESPHOME_DEBUG_SCHEDULER + this->debug_live_items_++; +#endif return item; } +#ifdef ESPHOME_DEBUG_SCHEDULER +bool Scheduler::debug_verify_no_leak_() const { + // Invariant: every live SchedulerItem must be in exactly one container. + // debug_live_items_ tracks allocations minus deletions. + size_t accounted = this->items_.size() + this->to_add_.size() + this->scheduler_item_pool_.size(); +#ifndef ESPHOME_THREAD_SINGLE + accounted += this->defer_queue_.size(); +#endif + if (accounted != this->debug_live_items_) { + ESP_LOGE(TAG, + "SCHEDULER LEAK DETECTED: live=%" PRIu32 " but accounted=%" PRIu32 " (items=%" PRIu32 " to_add=%" PRIu32 + " pool=%" PRIu32 +#ifndef ESPHOME_THREAD_SINGLE + " defer=%" PRIu32 +#endif + ")", + static_cast<uint32_t>(this->debug_live_items_), static_cast<uint32_t>(accounted), + static_cast<uint32_t>(this->items_.size()), static_cast<uint32_t>(this->to_add_.size()), + static_cast<uint32_t>(this->scheduler_item_pool_.size()) +#ifndef ESPHOME_THREAD_SINGLE + , + static_cast<uint32_t>(this->defer_queue_.size()) +#endif + ); + assert(false); + return false; + } + return true; +} +#endif + } // namespace esphome diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index eb6cea4f37..0476513bb9 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -2,7 +2,6 @@ #include "esphome/core/defines.h" #include <cstring> -#include <memory> #include <string> #include <vector> #ifdef ESPHOME_THREAD_MULTI_ATOMICS @@ -144,19 +143,6 @@ class Scheduler { }; protected: - struct SchedulerItem; - - // Custom deleter for SchedulerItem unique_ptr that prevents the compiler from - // inlining the destructor at every destruction site. On BK7231N (Thumb-1), GCC - // inlines ~unique_ptr<SchedulerItem> (~30 bytes: null check + ~std::function + - // operator delete) at every destruction site, while ESP32/ESP8266/RTL8720CF outline - // it into a single helper. This noinline deleter ensures only one copy exists. - // operator() is defined in scheduler.cpp to prevent inlining. - struct SchedulerItemDeleter { - void operator()(SchedulerItem *ptr) const noexcept; - }; - using SchedulerItemPtr = std::unique_ptr<SchedulerItem, SchedulerItemDeleter>; - struct SchedulerItem { // Ordered by size to minimize padding Component *component; @@ -219,14 +205,14 @@ class Scheduler { name_.static_name = nullptr; } - // Destructor - no dynamic memory to clean up + // Destructor - no dynamic memory to clean up (callback's std::function handles its own) ~SchedulerItem() = default; // Delete copy operations to prevent accidental copies SchedulerItem(const SchedulerItem &) = delete; SchedulerItem &operator=(const SchedulerItem &) = delete; - // Delete move operations: SchedulerItem objects are only managed via unique_ptr, never moved directly + // Delete move operations: SchedulerItem objects are managed via raw pointers, never moved directly SchedulerItem(SchedulerItem &&) = delete; SchedulerItem &operator=(SchedulerItem &&) = delete; @@ -250,7 +236,7 @@ class Scheduler { name_type_ = type; } - static bool cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b); + static bool cmp(SchedulerItem *a, SchedulerItem *b); // Note: We use 48 bits total (32 + 16), stored in a 64-bit value for API compatibility. // The upper 16 bits of the 64-bit value are always zero, which is fine since @@ -301,12 +287,13 @@ class Scheduler { // Returns the number of items remaining after cleanup // IMPORTANT: This method should only be called from the main thread (loop task). size_t cleanup_(); - // Remove and return the front item from the heap + // Remove and return the front item from the heap as a raw pointer. + // Caller takes ownership and must either recycle or delete the item. // IMPORTANT: Caller must hold the scheduler lock before calling this function. - SchedulerItemPtr pop_raw_locked_(); + SchedulerItem *pop_raw_locked_(); // Get or create a scheduler item from the pool // IMPORTANT: Caller must hold the scheduler lock before calling this function. - SchedulerItemPtr get_item_from_pool_locked_(); + SchedulerItem *get_item_from_pool_locked_(); private: // Helper to cancel items - must be called with lock held @@ -330,19 +317,16 @@ class Scheduler { // Helper function to check if item matches criteria for cancellation // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id // IMPORTANT: Must be called with scheduler lock held - inline bool HOT matches_item_locked_(const SchedulerItemPtr &item, Component *component, NameType name_type, + inline bool HOT matches_item_locked_(SchedulerItem *item, Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const { // THREAD SAFETY: Check for nullptr first to prevent LoadProhibited crashes. On multi-threaded - // platforms, items can be moved out of defer_queue_ during processing, leaving nullptr entries. - // PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_()), but this check - // provides defense-in-depth: helper - // functions should be safe regardless of caller behavior. + // platforms, items can be nulled in defer_queue_ during processing. // Fixes: https://github.com/esphome/esphome/issues/11940 - if (!item) + if (item == nullptr) return false; - if (item->component != component || item->type != type || - (skip_removed && this->is_item_removed_locked_(item.get())) || (match_retry && !item->is_retry)) { + if (item->component != component || item->type != type || (skip_removed && this->is_item_removed_locked_(item)) || + (match_retry && !item->is_retry)) { return false; } // Name type must match @@ -364,10 +348,12 @@ class Scheduler { } // Helper to recycle a SchedulerItem back to the pool. + // Takes a raw pointer — caller transfers ownership. The item is either added to the + // pool or deleted if the pool is full. // IMPORTANT: Only call from main loop context! Recycling clears the callback, // so calling from another thread while the callback is executing causes use-after-free. // IMPORTANT: Caller must hold the scheduler lock before calling this function. - void recycle_item_main_loop_(SchedulerItemPtr item); + void recycle_item_main_loop_(SchedulerItem *item); // Helper to perform full cleanup when too many items are cancelled void full_cleanup_removed_items_(); @@ -423,27 +409,28 @@ class Scheduler { // Merge lock acquisitions: instead of separate locks for move-out and recycle (2N+1 total), // recycle each item after re-acquiring the lock for the next iteration (N+1 total). // The lock is held across: recycle → loop condition → move-out, then released for execution. - SchedulerItemPtr item; + SchedulerItem *item; this->lock_.lock(); while (this->defer_queue_front_ < defer_queue_end) { - // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. - // This is intentional and safe because: + // Take ownership of the item, leaving nullptr in the vector slot. + // This is safe because: // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_) // 3. The lock protects concurrent access, but the nullptr remains until cleanup - item = std::move(this->defer_queue_[this->defer_queue_front_]); + item = this->defer_queue_[this->defer_queue_front_]; + this->defer_queue_[this->defer_queue_front_] = nullptr; this->defer_queue_front_++; this->lock_.unlock(); // Execute callback without holding lock to prevent deadlocks // if the callback tries to call defer() again - if (!this->should_skip_item_(item.get())) { - now = this->execute_item_(item.get(), now); + if (!this->should_skip_item_(item)) { + now = this->execute_item_(item, now); } this->lock_.lock(); - this->recycle_item_main_loop_(std::move(item)); + this->recycle_item_main_loop_(item); } // Clean up the queue (lock already held from last recycle or initial acquisition) this->cleanup_defer_queue_locked_(); @@ -523,18 +510,14 @@ class Scheduler { // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id // Returns the number of items marked for removal // IMPORTANT: Must be called with scheduler lock held - __attribute__((noinline)) size_t mark_matching_items_removed_locked_(std::vector<SchedulerItemPtr> &container, + __attribute__((noinline)) size_t mark_matching_items_removed_locked_(std::vector<SchedulerItem *> &container, Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry) { size_t count = 0; - for (auto &item : container) { - // Skip nullptr items (can happen in defer_queue_ when items are being processed) - // The defer_queue_ uses index-based processing: items are std::moved out but left in the - // vector as nullptr until cleanup. Even though this function is called with lock held, - // the vector can still contain nullptr items from the processing loop. This check prevents crashes. - if (item && this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) { - this->set_item_removed_(item.get(), true); + for (auto *item : container) { + if (this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) { + this->set_item_removed_(item, true); count++; } } @@ -542,15 +525,15 @@ class Scheduler { } Mutex lock_; - std::vector<SchedulerItemPtr> items_; - std::vector<SchedulerItemPtr> to_add_; + std::vector<SchedulerItem *> items_; + std::vector<SchedulerItem *> to_add_; #ifndef ESPHOME_THREAD_SINGLE // Single-core platforms don't need the defer queue and save ~32 bytes of RAM // Using std::vector instead of std::deque avoids 512-byte chunked allocations // Index tracking avoids O(n) erase() calls when draining the queue each loop - std::vector<SchedulerItemPtr> defer_queue_; // FIFO queue for defer() calls - size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items) -#endif /* ESPHOME_THREAD_SINGLE */ + std::vector<SchedulerItem *> defer_queue_; // FIFO queue for defer() calls + size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items) +#endif /* ESPHOME_THREAD_SINGLE */ uint32_t to_remove_{0}; // Memory pool for recycling SchedulerItem objects to reduce heap churn. @@ -561,7 +544,18 @@ class Scheduler { // - The pool significantly reduces heap fragmentation which is critical because heap allocation/deallocation // can stall the entire system, causing timing issues and dropped events for any components that need // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52) - std::vector<SchedulerItemPtr> scheduler_item_pool_; + std::vector<SchedulerItem *> scheduler_item_pool_; + +#ifdef ESPHOME_DEBUG_SCHEDULER + // Leak detection: tracks total live SchedulerItem allocations. + // Invariant: debug_live_items_ == items_.size() + to_add_.size() + defer_queue_.size() + scheduler_item_pool_.size() + // Verified periodically in call() to catch leaks early. + size_t debug_live_items_{0}; + + // Verify the scheduler memory invariant: all allocated items are accounted for. + // Returns true if no leak detected. Logs an error and asserts on failure. + bool debug_verify_no_leak_() const; +#endif }; } // namespace esphome diff --git a/tests/integration/fixtures/scheduler_bulk_cleanup.yaml b/tests/integration/fixtures/scheduler_bulk_cleanup.yaml index de876da8c4..3d2c47a0de 100644 --- a/tests/integration/fixtures/scheduler_bulk_cleanup.yaml +++ b/tests/integration/fixtures/scheduler_bulk_cleanup.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-bulk-cleanup external_components: diff --git a/tests/integration/fixtures/scheduler_defer_cancel.yaml b/tests/integration/fixtures/scheduler_defer_cancel.yaml index 9e3f927c33..92ae0062ac 100644 --- a/tests/integration/fixtures/scheduler_defer_cancel.yaml +++ b/tests/integration/fixtures/scheduler_defer_cancel.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-defer-cancel host: diff --git a/tests/integration/fixtures/scheduler_defer_cancels_regular.yaml b/tests/integration/fixtures/scheduler_defer_cancels_regular.yaml index fb6b1791dc..cf7f6ec733 100644 --- a/tests/integration/fixtures/scheduler_defer_cancels_regular.yaml +++ b/tests/integration/fixtures/scheduler_defer_cancels_regular.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-defer-cancel-regular host: diff --git a/tests/integration/fixtures/scheduler_defer_fifo_simple.yaml b/tests/integration/fixtures/scheduler_defer_fifo_simple.yaml index 7384082ac2..f69e5c6c67 100644 --- a/tests/integration/fixtures/scheduler_defer_fifo_simple.yaml +++ b/tests/integration/fixtures/scheduler_defer_fifo_simple.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-defer-fifo-simple host: diff --git a/tests/integration/fixtures/scheduler_defer_stress.yaml b/tests/integration/fixtures/scheduler_defer_stress.yaml index 0d9c1d1405..70eac01daf 100644 --- a/tests/integration/fixtures/scheduler_defer_stress.yaml +++ b/tests/integration/fixtures/scheduler_defer_stress.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-defer-stress-test external_components: diff --git a/tests/integration/fixtures/scheduler_heap_stress.yaml b/tests/integration/fixtures/scheduler_heap_stress.yaml index d4d340b68b..486a5d1276 100644 --- a/tests/integration/fixtures/scheduler_heap_stress.yaml +++ b/tests/integration/fixtures/scheduler_heap_stress.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-heap-stress-test external_components: diff --git a/tests/integration/fixtures/scheduler_internal_id_no_collision.yaml b/tests/integration/fixtures/scheduler_internal_id_no_collision.yaml index 46dbb8e728..e696e99efa 100644 --- a/tests/integration/fixtures/scheduler_internal_id_no_collision.yaml +++ b/tests/integration/fixtures/scheduler_internal_id_no_collision.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-internal-id-test on_boot: priority: -100 diff --git a/tests/integration/fixtures/scheduler_null_name.yaml b/tests/integration/fixtures/scheduler_null_name.yaml index 42eaacdd43..d5488761d6 100644 --- a/tests/integration/fixtures/scheduler_null_name.yaml +++ b/tests/integration/fixtures/scheduler_null_name.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-null-name host: diff --git a/tests/integration/fixtures/scheduler_numeric_id_test.yaml b/tests/integration/fixtures/scheduler_numeric_id_test.yaml index 1669f026f5..25decf20f5 100644 --- a/tests/integration/fixtures/scheduler_numeric_id_test.yaml +++ b/tests/integration/fixtures/scheduler_numeric_id_test.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-numeric-id-test on_boot: priority: -100 diff --git a/tests/integration/fixtures/scheduler_rapid_cancellation.yaml b/tests/integration/fixtures/scheduler_rapid_cancellation.yaml index 4824654c5c..530b8241f5 100644 --- a/tests/integration/fixtures/scheduler_rapid_cancellation.yaml +++ b/tests/integration/fixtures/scheduler_rapid_cancellation.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: sched-rapid-cancel-test external_components: diff --git a/tests/integration/fixtures/scheduler_recursive_timeout.yaml b/tests/integration/fixtures/scheduler_recursive_timeout.yaml index f1168802f6..66b6f4b19b 100644 --- a/tests/integration/fixtures/scheduler_recursive_timeout.yaml +++ b/tests/integration/fixtures/scheduler_recursive_timeout.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: sched-recursive-timeout external_components: diff --git a/tests/integration/fixtures/scheduler_removed_item_race.yaml b/tests/integration/fixtures/scheduler_removed_item_race.yaml index 2f8a7fb987..55d2197d7c 100644 --- a/tests/integration/fixtures/scheduler_removed_item_race.yaml +++ b/tests/integration/fixtures/scheduler_removed_item_race.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-removed-item-race host: diff --git a/tests/integration/fixtures/scheduler_retry_test.yaml b/tests/integration/fixtures/scheduler_retry_test.yaml index ffe9082a69..cdf71152bd 100644 --- a/tests/integration/fixtures/scheduler_retry_test.yaml +++ b/tests/integration/fixtures/scheduler_retry_test.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-retry-test on_boot: priority: -100 diff --git a/tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml b/tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml index 446ee7fdc0..c15edc3ffd 100644 --- a/tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml +++ b/tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: sched-simul-callbacks-test external_components: diff --git a/tests/integration/fixtures/scheduler_string_lifetime.yaml b/tests/integration/fixtures/scheduler_string_lifetime.yaml index ebd5052b8b..5ae5a1914e 100644 --- a/tests/integration/fixtures/scheduler_string_lifetime.yaml +++ b/tests/integration/fixtures/scheduler_string_lifetime.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-string-lifetime-test external_components: diff --git a/tests/integration/fixtures/scheduler_string_name_stress.yaml b/tests/integration/fixtures/scheduler_string_name_stress.yaml index d1ef55c8d5..8f68d1d102 100644 --- a/tests/integration/fixtures/scheduler_string_name_stress.yaml +++ b/tests/integration/fixtures/scheduler_string_name_stress.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: sched-string-name-stress external_components: From 89bb5d9e42fcc911a73af225af78390a96ad8e5f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 09:11:45 -1000 Subject: [PATCH 513/609] [core] Require explicit synchronous= for register_action (#14606) --- esphome/automation.py | 38 ++++++++++--- esphome/components/ags10/sensor.py | 2 + esphome/components/aic3204/audio_dac.py | 5 +- .../alarm_control_panel/__init__.py | 40 +++++++++++--- esphome/components/animation/__init__.py | 12 +++-- esphome/components/api/__init__.py | 1 + esphome/components/at581x/__init__.py | 2 + esphome/components/audio_adc/__init__.py | 5 +- esphome/components/audio_dac/__init__.py | 13 +++-- esphome/components/binary_sensor/__init__.py | 1 + esphome/components/bl0906/sensor.py | 1 + esphome/components/ble_client/__init__.py | 22 ++++++-- esphome/components/bm8563/time.py | 3 ++ esphome/components/canbus/__init__.py | 1 + esphome/components/cc1101/__init__.py | 27 +++++++--- esphome/components/climate/__init__.py | 5 +- esphome/components/cm1106/sensor.py | 1 + esphome/components/cs5460a/sensor.py | 1 + esphome/components/datetime/__init__.py | 3 ++ esphome/components/deep_sleep/__init__.py | 7 ++- esphome/components/dfplayer/__init__.py | 16 ++++++ .../components/dfrobot_sen0395/__init__.py | 2 + esphome/components/display/__init__.py | 3 ++ .../components/display_menu_base/__init__.py | 33 +++++++++--- esphome/components/ds1307/time.py | 2 + esphome/components/duty_time/sensor.py | 12 +++-- esphome/components/esp32_ble/__init__.py | 8 ++- .../components/esp32_ble_server/__init__.py | 3 ++ .../components/esp32_ble_tracker/__init__.py | 2 + esphome/components/esp8266_pwm/output.py | 1 + esphome/components/esp_ldo/__init__.py | 1 + esphome/components/espnow/__init__.py | 5 ++ esphome/components/event/__init__.py | 4 +- esphome/components/ezo_pmp/__init__.py | 28 ++++++++-- esphome/components/fan/__init__.py | 1 + .../components/fingerprint_grow/__init__.py | 6 +++ .../components/grove_tb6612fng/__init__.py | 6 +++ esphome/components/haier/climate.py | 49 +++++++++++++---- esphome/components/hbridge/fan/__init__.py | 1 + esphome/components/hc8/sensor.py | 5 +- esphome/components/hdc302x/sensor.py | 10 +++- esphome/components/hlk_fm22x/__init__.py | 5 ++ esphome/components/http_request/__init__.py | 15 ++++-- .../components/http_request/ota/__init__.py | 1 + esphome/components/htu21d/sensor.py | 2 + esphome/components/hub75/display.py | 1 + esphome/components/integration/sensor.py | 2 + esphome/components/key_collector/__init__.py | 2 + esphome/components/ld2410/__init__.py | 5 +- esphome/components/ledc/output.py | 1 + esphome/components/libretiny_pwm/output.py | 1 + esphome/components/light/automation.py | 5 +- esphome/components/lightwaverf/__init__.py | 1 + esphome/components/lock/__init__.py | 12 +++-- esphome/components/logger/__init__.py | 1 + esphome/components/lvgl/automation.py | 27 ++++++++-- esphome/components/lvgl/styles.py | 1 + esphome/components/lvgl/types.py | 1 + esphome/components/lvgl/widgets/animimg.py | 2 + .../components/lvgl/widgets/buttonmatrix.py | 1 + esphome/components/lvgl/widgets/canvas.py | 8 +++ esphome/components/lvgl/widgets/meter.py | 1 + esphome/components/lvgl/widgets/page.py | 3 ++ esphome/components/lvgl/widgets/spinbox.py | 2 + esphome/components/lvgl/widgets/tabview.py | 1 + esphome/components/lvgl/widgets/tileview.py | 1 + esphome/components/max17043/sensor.py | 4 +- esphome/components/max6956/__init__.py | 2 + esphome/components/max7219digit/display.py | 35 +++++++++--- esphome/components/media_player/__init__.py | 7 ++- esphome/components/mhz19/sensor.py | 20 +++++-- .../components/micro_wake_word/__init__.py | 13 ++++- esphome/components/microphone/__init__.py | 22 +++++--- esphome/components/midea/climate.py | 4 +- esphome/components/mixer/speaker/__init__.py | 1 + esphome/components/mqtt/__init__.py | 2 + esphome/components/nau7802/sensor.py | 3 ++ .../nextion/binary_sensor/__init__.py | 1 + esphome/components/nextion/display.py | 1 + esphome/components/nextion/sensor/__init__.py | 1 + esphome/components/nextion/switch/__init__.py | 1 + .../nextion/text_sensor/__init__.py | 1 + esphome/components/online_image/__init__.py | 9 +++- esphome/components/output/__init__.py | 2 + esphome/components/pcf85063/time.py | 2 + esphome/components/pcf8563/time.py | 2 + esphome/components/pid/climate.py | 3 ++ .../components/pipsolar/output/__init__.py | 1 + esphome/components/pmwcs3/sensor.py | 3 ++ esphome/components/pn7150/__init__.py | 43 +++++++++++---- esphome/components/pn7160/__init__.py | 43 +++++++++++---- esphome/components/pulse_counter/sensor.py | 1 + esphome/components/pulse_meter/sensor.py | 1 + esphome/components/pzemac/sensor.py | 1 + esphome/components/pzemdc/sensor.py | 1 + esphome/components/remote_base/__init__.py | 5 +- .../components/remote_transmitter/__init__.py | 5 +- esphome/components/rf_bridge/__init__.py | 22 ++++++-- esphome/components/rotary_encoder/sensor.py | 1 + esphome/components/rp2040_pwm/output.py | 1 + esphome/components/rtttl/__init__.py | 2 + esphome/components/rx8130/time.py | 2 + esphome/components/safe_mode/__init__.py | 1 + esphome/components/scd30/sensor.py | 1 + esphome/components/scd4x/sensor.py | 6 ++- esphome/components/script/__init__.py | 1 + esphome/components/sen5x/sensor.py | 5 +- esphome/components/senseair/sensor.py | 17 ++++-- esphome/components/servo/__init__.py | 2 + esphome/components/sim800l/__init__.py | 16 ++++-- esphome/components/sound_level/sensor.py | 8 ++- esphome/components/speaker/__init__.py | 23 +++++--- .../speaker/media_player/__init__.py | 1 + esphome/components/sprinkler/__init__.py | 53 +++++++++++++++---- esphome/components/sps30/sensor.py | 15 ++++-- esphome/components/stepper/__init__.py | 5 ++ esphome/components/sx126x/__init__.py | 30 ++++++++--- esphome/components/sx127x/__init__.py | 30 ++++++++--- .../template/binary_sensor/__init__.py | 1 + esphome/components/template/cover/__init__.py | 1 + esphome/components/template/lock/__init__.py | 1 + .../components/template/sensor/__init__.py | 1 + .../components/template/switch/__init__.py | 1 + .../template/text_sensor/__init__.py | 1 + esphome/components/template/valve/__init__.py | 1 + .../template/water_heater/__init__.py | 1 + esphome/components/tm1651/__init__.py | 12 ++++- esphome/components/uart/__init__.py | 1 + esphome/components/udp/__init__.py | 1 + esphome/components/ufire_ec/sensor.py | 2 + esphome/components/ufire_ise/sensor.py | 3 ++ esphome/components/update/__init__.py | 2 + esphome/components/valve/__init__.py | 20 +++++-- .../components/voice_assistant/__init__.py | 6 ++- esphome/components/wifi/__init__.py | 9 +++- esphome/components/wireguard/__init__.py | 2 + esphome/components/zigbee/__init__.py | 1 + 137 files changed, 852 insertions(+), 187 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index d9b8b2ec57..36ab30b654 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -1,3 +1,5 @@ +import logging + import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( @@ -57,22 +59,41 @@ def maybe_conf(conf, *validators): return validate +_LOGGER = logging.getLogger(__name__) + + def register_action( name: str, action_type: MockObjClass, schema: cv.Schema, *, - synchronous: bool = False, + synchronous: bool | None = None, ): """Register an action type. - Actions default to ``synchronous=False`` (safe default), meaning string - arguments use owning std::string to prevent dangling references. + All callers must pass ``synchronous`` explicitly. - Set ``synchronous=True`` only for actions that complete synchronously - and never store trigger arguments for later execution. This allows - the code generator to use non-owning StringRef for zero-copy access. + ``synchronous=True`` — the action never defers ``play_next_()`` to a + later point (callback, timer, or ``loop()``). Trigger arguments are + only used during the initial call, so string args can use non-owning + StringRef for zero-copy access. + + ``synchronous=False`` — the action defers ``play_next_()`` via a + callback, timer, or ``Component::loop()``. Trigger arguments must + outlive the initial call, so string args use owning std::string to + prevent dangling references. """ + if synchronous is None: + _LOGGER.warning( + "register_action('%s', ...) is missing the synchronous= parameter. " + "Defaulting to synchronous=False (safe but prevents StringRef " + "optimization). Check the C++ class: use synchronous=False if " + "play_next_() is deferred to a callback, timer, or loop(); " + "use synchronous=True if play_next_() always runs before the " + "initial play/play_complex call returns", + name, + ) + synchronous = False return ACTION_REGISTRY.register(name, action_type, schema, synchronous=synchronous) @@ -353,6 +374,7 @@ async def component_is_idle_condition_to_code( "delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds), + synchronous=False, ) async def delay_action_to_code( config: ConfigType, @@ -465,7 +487,7 @@ _validate_wait_until = cv.maybe_simple_value( ) -@register_action("wait_until", WaitUntilAction, _validate_wait_until) +@register_action("wait_until", WaitUntilAction, _validate_wait_until, synchronous=False) async def wait_until_action_to_code( config: ConfigType, action_id: ID, @@ -611,7 +633,7 @@ def has_non_synchronous_actions(actions: ConfigType) -> bool: Non-synchronous actions (delay, wait_until, script.wait, etc.) store trigger args for later execution, making non-owning types like StringRef - unsafe. Actions that haven't been audited default to non-synchronous. + unsafe. """ if isinstance(actions, list): return any(has_non_synchronous_actions(item) for item in actions) diff --git a/esphome/components/ags10/sensor.py b/esphome/components/ags10/sensor.py index 8f0f372951..4cfa9e67ec 100644 --- a/esphome/components/ags10/sensor.py +++ b/esphome/components/ags10/sensor.py @@ -92,6 +92,7 @@ AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value( "ags10.new_i2c_address", AGS10NewI2cAddressAction, AGS10_NEW_I2C_ADDRESS_SCHEMA, + synchronous=True, ) async def ags10newi2caddress_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -121,6 +122,7 @@ AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema( "ags10.set_zero_point", AGS10SetZeroPointAction, AGS10_SET_ZERO_POINT_SCHEMA, + synchronous=True, ) async def ags10setzeropoint_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/aic3204/audio_dac.py b/esphome/components/aic3204/audio_dac.py index da7a54df54..a644638f69 100644 --- a/esphome/components/aic3204/audio_dac.py +++ b/esphome/components/aic3204/audio_dac.py @@ -34,7 +34,10 @@ SET_AUTO_MUTE_ACTION_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "aic3204.set_auto_mute_mode", SetAutoMuteAction, SET_AUTO_MUTE_ACTION_SCHEMA + "aic3204.set_auto_mute_mode", + SetAutoMuteAction, + SET_AUTO_MUTE_ACTION_SCHEMA, + synchronous=True, ) async def aic3204_set_volume_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index b855586152..aefb18d25c 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -243,7 +243,10 @@ async def new_alarm_control_panel(config, *args): @automation.register_action( - "alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.arm_away", + ArmAwayAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) async def alarm_action_arm_away_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -255,7 +258,10 @@ async def alarm_action_arm_away_to_code(config, action_id, template_arg, args): @automation.register_action( - "alarm_control_panel.arm_home", ArmHomeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.arm_home", + ArmHomeAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) async def alarm_action_arm_home_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -267,7 +273,10 @@ async def alarm_action_arm_home_to_code(config, action_id, template_arg, args): @automation.register_action( - "alarm_control_panel.arm_night", ArmNightAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.arm_night", + ArmNightAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) async def alarm_action_arm_night_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -279,7 +288,10 @@ async def alarm_action_arm_night_to_code(config, action_id, template_arg, args): @automation.register_action( - "alarm_control_panel.disarm", DisarmAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.disarm", + DisarmAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) async def alarm_action_disarm_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -291,7 +303,10 @@ async def alarm_action_disarm_to_code(config, action_id, template_arg, args): @automation.register_action( - "alarm_control_panel.pending", PendingAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.pending", + PendingAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) async def alarm_action_pending_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -299,7 +314,10 @@ async def alarm_action_pending_to_code(config, action_id, template_arg, args): @automation.register_action( - "alarm_control_panel.triggered", TriggeredAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.triggered", + TriggeredAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) async def alarm_action_trigger_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -307,7 +325,10 @@ async def alarm_action_trigger_to_code(config, action_id, template_arg, args): @automation.register_action( - "alarm_control_panel.chime", ChimeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.chime", + ChimeAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) async def alarm_action_chime_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -315,7 +336,10 @@ async def alarm_action_chime_to_code(config, action_id, template_arg, args): @automation.register_action( - "alarm_control_panel.ready", ReadyAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.ready", + ReadyAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) @automation.register_condition( "alarm_control_panel.ready", diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py index c4ac7adb23..e9630f5266 100644 --- a/esphome/components/animation/__init__.py +++ b/esphome/components/animation/__init__.py @@ -69,9 +69,15 @@ SET_FRAME_SCHEMA = cv.Schema( ) -@automation.register_action("animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA) -@automation.register_action("animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA) -@automation.register_action("animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA) +@automation.register_action( + "animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA, synchronous=True +) +@automation.register_action( + "animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA, synchronous=True +) +@automation.register_action( + "animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True +) async def animation_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index c7dec6e78b..9772e6afca 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -712,6 +712,7 @@ API_RESPOND_ACTION_SCHEMA = cv.All( "api.respond", APIRespondAction, API_RESPOND_ACTION_SCHEMA, + synchronous=True, ) async def api_respond_to_code( config: ConfigType, diff --git a/esphome/components/at581x/__init__.py b/esphome/components/at581x/__init__.py index 117ada123d..0780814ea6 100644 --- a/esphome/components/at581x/__init__.py +++ b/esphome/components/at581x/__init__.py @@ -89,6 +89,7 @@ AT581XSettingsAction = at581x_ns.class_("AT581XSettingsAction", automation.Actio cv.Required(CONF_ID): cv.use_id(AT581XComponent), } ), + synchronous=True, ) async def at581x_reset_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -160,6 +161,7 @@ RADAR_SETTINGS_SCHEMA = cv.Schema( "at581x.settings", AT581XSettingsAction, RADAR_SETTINGS_SCHEMA, + synchronous=True, ) async def at581x_settings_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/audio_adc/__init__.py b/esphome/components/audio_adc/__init__.py index 2f95a039f5..3c9b32e610 100644 --- a/esphome/components/audio_adc/__init__.py +++ b/esphome/components/audio_adc/__init__.py @@ -23,7 +23,10 @@ SET_MIC_GAIN_ACTION_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "audio_adc.set_mic_gain", SetMicGainAction, SET_MIC_GAIN_ACTION_SCHEMA + "audio_adc.set_mic_gain", + SetMicGainAction, + SET_MIC_GAIN_ACTION_SCHEMA, + synchronous=True, ) async def audio_adc_set_mic_gain_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/audio_dac/__init__.py b/esphome/components/audio_dac/__init__.py index 92e6cb18fa..a950c1967b 100644 --- a/esphome/components/audio_dac/__init__.py +++ b/esphome/components/audio_dac/__init__.py @@ -31,15 +31,22 @@ SET_VOLUME_ACTION_SCHEMA = cv.maybe_simple_value( ) -@automation.register_action("audio_dac.mute_off", MuteOffAction, MUTE_ACTION_SCHEMA) -@automation.register_action("audio_dac.mute_on", MuteOnAction, MUTE_ACTION_SCHEMA) +@automation.register_action( + "audio_dac.mute_off", MuteOffAction, MUTE_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "audio_dac.mute_on", MuteOnAction, MUTE_ACTION_SCHEMA, synchronous=True +) async def audio_dac_mute_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_action( - "audio_dac.set_volume", SetVolumeAction, SET_VOLUME_ACTION_SCHEMA + "audio_dac.set_volume", + SetVolumeAction, + SET_VOLUME_ACTION_SCHEMA, + synchronous=True, ) async def audio_dac_set_volume_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 1f64118560..37cccc01be 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -685,6 +685,7 @@ async def to_code(config): }, key=CONF_ID, ), + synchronous=True, ) async def binary_sensor_invalidate_state_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/bl0906/sensor.py b/esphome/components/bl0906/sensor.py index 42c6f06092..059e10e962 100644 --- a/esphome/components/bl0906/sensor.py +++ b/esphome/components/bl0906/sensor.py @@ -143,6 +143,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( cv.Required(CONF_ID): cv.use_id(BL0906), } ), + synchronous=True, ) async def reset_energy_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 37db181584..56ac2ea147 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -172,7 +172,10 @@ BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema( @automation.register_action( - "ble_client.disconnect", BLEDisconnectAction, BLE_CONNECT_ACTION_SCHEMA + "ble_client.disconnect", + BLEDisconnectAction, + BLE_CONNECT_ACTION_SCHEMA, + synchronous=False, ) async def ble_disconnect_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) @@ -180,7 +183,10 @@ async def ble_disconnect_to_code(config, action_id, template_arg, args): @automation.register_action( - "ble_client.connect", BLEConnectAction, BLE_CONNECT_ACTION_SCHEMA + "ble_client.connect", + BLEConnectAction, + BLE_CONNECT_ACTION_SCHEMA, + synchronous=False, ) async def ble_connect_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) @@ -188,7 +194,10 @@ async def ble_connect_to_code(config, action_id, template_arg, args): @automation.register_action( - "ble_client.ble_write", BLEWriteAction, BLE_WRITE_ACTION_SCHEMA + "ble_client.ble_write", + BLEWriteAction, + BLE_WRITE_ACTION_SCHEMA, + synchronous=False, ) async def ble_write_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) @@ -247,6 +256,7 @@ async def ble_write_to_code(config, action_id, template_arg, args): "ble_client.numeric_comparison_reply", BLENumericComparisonReplyAction, BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA, + synchronous=True, ) async def numeric_comparison_reply_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) @@ -263,7 +273,10 @@ async def numeric_comparison_reply_to_code(config, action_id, template_arg, args @automation.register_action( - "ble_client.passkey_reply", BLEPasskeyReplyAction, BLE_PASSKEY_REPLY_ACTION_SCHEMA + "ble_client.passkey_reply", + BLEPasskeyReplyAction, + BLE_PASSKEY_REPLY_ACTION_SCHEMA, + synchronous=True, ) async def passkey_reply_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) @@ -283,6 +296,7 @@ async def passkey_reply_to_code(config, action_id, template_arg, args): "ble_client.remove_bond", BLERemoveBondAction, BLE_REMOVE_BOND_ACTION_SCHEMA, + synchronous=True, ) async def remove_bond_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/bm8563/time.py b/esphome/components/bm8563/time.py index 2785315af2..ba264f00bf 100644 --- a/esphome/components/bm8563/time.py +++ b/esphome/components/bm8563/time.py @@ -33,6 +33,7 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.use_id(BM8563), } ), + synchronous=True, ) async def bm8563_write_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -49,6 +50,7 @@ async def bm8563_write_time_to_code(config, action_id, template_arg, args): cv.Required(CONF_DURATION): cv.templatable(cv.positive_time_period_seconds), } ), + synchronous=True, ) async def bm8563_start_timer_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -66,6 +68,7 @@ async def bm8563_start_timer_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(BM8563), } ), + synchronous=True, ) async def bm8563_read_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index 7b51c2c45c..c94c8647a9 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -155,6 +155,7 @@ async def register_canbus(var, config): validate_id, key=CONF_DATA, ), + synchronous=True, ) async def canbus_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index e2e5986daf..2709290862 100644 --- a/esphome/components/cc1101/__init__.py +++ b/esphome/components/cc1101/__init__.py @@ -287,10 +287,18 @@ CC1101_ACTION_SCHEMA = cv.Schema( ) -@automation.register_action("cc1101.begin_tx", BeginTxAction, CC1101_ACTION_SCHEMA) -@automation.register_action("cc1101.begin_rx", BeginRxAction, CC1101_ACTION_SCHEMA) -@automation.register_action("cc1101.reset", ResetAction, CC1101_ACTION_SCHEMA) -@automation.register_action("cc1101.set_idle", SetIdleAction, CC1101_ACTION_SCHEMA) +@automation.register_action( + "cc1101.begin_tx", BeginTxAction, CC1101_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "cc1101.begin_rx", BeginRxAction, CC1101_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "cc1101.reset", ResetAction, CC1101_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "cc1101.set_idle", SetIdleAction, CC1101_ACTION_SCHEMA, synchronous=True +) async def cc1101_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) @@ -317,7 +325,10 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "cc1101.send_packet", SendPacketAction, SEND_PACKET_ACTION_SCHEMA + "cc1101.send_packet", + SendPacketAction, + SEND_PACKET_ACTION_SCHEMA, + synchronous=True, ) async def send_packet_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -419,9 +430,9 @@ def _register_setter_actions(): cg.add(getattr(var, _setter)(_map[data] if _map else data)) return var - automation.register_action(f"cc1101.{setter_name}", action_cls, schema)( - _setter_action_to_code - ) + automation.register_action( + f"cc1101.{setter_name}", action_cls, schema, synchronous=True + )(_setter_action_to_code) _register_setter_actions() diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index f5b91c502c..8cf5fa9b0c 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -476,7 +476,10 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( @automation.register_action( - "climate.control", ControlAction, CLIMATE_CONTROL_ACTION_SCHEMA + "climate.control", + ControlAction, + CLIMATE_CONTROL_ACTION_SCHEMA, + synchronous=True, ) async def climate_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/cm1106/sensor.py b/esphome/components/cm1106/sensor.py index 1d95bcc666..3c82fac977 100644 --- a/esphome/components/cm1106/sensor.py +++ b/esphome/components/cm1106/sensor.py @@ -65,6 +65,7 @@ CALIBRATION_ACTION_SCHEMA = maybe_simple_id( "cm1106.calibrate_zero", CM1106CalibrateZeroAction, CALIBRATION_ACTION_SCHEMA, + synchronous=True, ) async def cm1106_calibration_to_code(config, action_id, template_arg, args) -> None: """Service code generation entry point.""" diff --git a/esphome/components/cs5460a/sensor.py b/esphome/components/cs5460a/sensor.py index 07b5ea1c63..d2383bd01b 100644 --- a/esphome/components/cs5460a/sensor.py +++ b/esphome/components/cs5460a/sensor.py @@ -132,6 +132,7 @@ async def to_code(config): cv.Required(CONF_ID): cv.use_id(CS5460AComponent), } ), + synchronous=True, ) async def restart_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index 74c9d594f7..90835624bf 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -187,6 +187,7 @@ async def to_code(config): ), } ), + synchronous=True, ) async def datetime_date_set_to_code(config, action_id, template_arg, args): action_var = cg.new_Pvariable(action_id, template_arg) @@ -218,6 +219,7 @@ async def datetime_date_set_to_code(config, action_id, template_arg, args): ), } ), + synchronous=True, ) async def datetime_time_set_to_code(config, action_id, template_arg, args): action_var = cg.new_Pvariable(action_id, template_arg) @@ -249,6 +251,7 @@ async def datetime_time_set_to_code(config, action_id, template_arg, args): ), }, ), + synchronous=True, ) async def datetime_datetime_set_to_code(config, action_id, template_arg, args): action_var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 3cfe7aa641..4098fd3fb8 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -405,7 +405,10 @@ DEEP_SLEEP_ENTER_SCHEMA = cv.All( @automation.register_action( - "deep_sleep.enter", EnterDeepSleepAction, DEEP_SLEEP_ENTER_SCHEMA + "deep_sleep.enter", + EnterDeepSleepAction, + DEEP_SLEEP_ENTER_SCHEMA, + synchronous=True, ) async def deep_sleep_enter_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -428,11 +431,13 @@ async def deep_sleep_enter_to_code(config, action_id, template_arg, args): "deep_sleep.prevent", PreventDeepSleepAction, automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA), + synchronous=True, ) @automation.register_action( "deep_sleep.allow", AllowDeepSleepAction, automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA), + synchronous=True, ) async def deep_sleep_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index 53ebda6bcc..9df108c9c0 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -91,6 +91,7 @@ async def to_code(config): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_next_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -106,6 +107,7 @@ async def dfplayer_next_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_previous_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -123,6 +125,7 @@ async def dfplayer_previous_to_code(config, action_id, template_arg, args): }, key=CONF_FILE, ), + synchronous=True, ) async def dfplayer_play_mp3_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -143,6 +146,7 @@ async def dfplayer_play_mp3_to_code(config, action_id, template_arg, args): }, key=CONF_FILE, ), + synchronous=True, ) async def dfplayer_play_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -166,6 +170,7 @@ async def dfplayer_play_to_code(config, action_id, template_arg, args): cv.Optional(CONF_LOOP): cv.templatable(cv.boolean), } ), + synchronous=True, ) async def dfplayer_play_folder_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -191,6 +196,7 @@ async def dfplayer_play_folder_to_code(config, action_id, template_arg, args): }, key=CONF_DEVICE, ), + synchronous=True, ) async def dfplayer_set_device_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -210,6 +216,7 @@ async def dfplayer_set_device_to_code(config, action_id, template_arg, args): }, key=CONF_VOLUME, ), + synchronous=True, ) async def dfplayer_set_volume_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -227,6 +234,7 @@ async def dfplayer_set_volume_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_volume_up_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -242,6 +250,7 @@ async def dfplayer_volume_up_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_volume_down_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -259,6 +268,7 @@ async def dfplayer_volume_down_to_code(config, action_id, template_arg, args): }, key=CONF_EQ_PRESET, ), + synchronous=True, ) async def dfplayer_set_eq_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -276,6 +286,7 @@ async def dfplayer_set_eq_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_sleep_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -291,6 +302,7 @@ async def dfplayer_sleep_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_reset_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -306,6 +318,7 @@ async def dfplayer_reset_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_start_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -321,6 +334,7 @@ async def dfplayer_start_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_pause_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -336,6 +350,7 @@ async def dfplayer_pause_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_stop_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -351,6 +366,7 @@ async def dfplayer_stop_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_random_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/dfrobot_sen0395/__init__.py b/esphome/components/dfrobot_sen0395/__init__.py index d54b147036..ba77e56abb 100644 --- a/esphome/components/dfrobot_sen0395/__init__.py +++ b/esphome/components/dfrobot_sen0395/__init__.py @@ -52,6 +52,7 @@ async def to_code(config): cv.GenerateID(): cv.use_id(DfrobotSen0395Component), } ), + synchronous=True, ) async def dfrobot_sen0395_reset_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -151,6 +152,7 @@ MMWAVE_SETTINGS_SCHEMA = cv.Schema( "dfrobot_sen0395.settings", DfrobotSen0395SettingsAction, MMWAVE_SETTINGS_SCHEMA, + synchronous=True, ) async def dfrobot_sen0395_settings_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 695e7cde47..6367f88acc 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -159,6 +159,7 @@ async def register_display(var, config): cv.Required(CONF_ID): cv.templatable(cv.use_id(DisplayPage)), } ), + synchronous=True, ) async def display_page_show_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -179,6 +180,7 @@ async def display_page_show_to_code(config, action_id, template_arg, args): cv.GenerateID(CONF_ID): cv.templatable(cv.use_id(Display)), } ), + synchronous=True, ) async def display_page_show_next_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -193,6 +195,7 @@ async def display_page_show_next_to_code(config, action_id, template_arg, args): cv.GenerateID(CONF_ID): cv.templatable(cv.use_id(Display)), } ), + synchronous=True, ) async def display_page_show_previous_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/display_menu_base/__init__.py b/esphome/components/display_menu_base/__init__.py index 658292ec7a..c9a0c7ee93 100644 --- a/esphome/components/display_menu_base/__init__.py +++ b/esphome/components/display_menu_base/__init__.py @@ -294,50 +294,67 @@ MENU_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action("display_menu.up", UpAction, MENU_ACTION_SCHEMA) +@automation.register_action( + "display_menu.up", UpAction, MENU_ACTION_SCHEMA, synchronous=True +) async def menu_up_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("display_menu.down", DownAction, MENU_ACTION_SCHEMA) +@automation.register_action( + "display_menu.down", DownAction, MENU_ACTION_SCHEMA, synchronous=True +) async def menu_down_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("display_menu.left", LeftAction, MENU_ACTION_SCHEMA) +@automation.register_action( + "display_menu.left", LeftAction, MENU_ACTION_SCHEMA, synchronous=True +) async def menu_left_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("display_menu.right", RightAction, MENU_ACTION_SCHEMA) +@automation.register_action( + "display_menu.right", RightAction, MENU_ACTION_SCHEMA, synchronous=True +) async def menu_right_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("display_menu.enter", EnterAction, MENU_ACTION_SCHEMA) +@automation.register_action( + "display_menu.enter", EnterAction, MENU_ACTION_SCHEMA, synchronous=True +) async def menu_enter_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("display_menu.show", ShowAction, MENU_ACTION_SCHEMA) +@automation.register_action( + "display_menu.show", ShowAction, MENU_ACTION_SCHEMA, synchronous=True +) async def menu_show_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("display_menu.hide", HideAction, MENU_ACTION_SCHEMA) +@automation.register_action( + "display_menu.hide", HideAction, MENU_ACTION_SCHEMA, synchronous=True +) async def menu_hide_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_action( - "display_menu.show_main", ShowMainAction, MENU_ACTION_SCHEMA + "display_menu.show_main", + ShowMainAction, + MENU_ACTION_SCHEMA, + synchronous=True, ) async def menu_show_main_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/ds1307/time.py b/esphome/components/ds1307/time.py index 42b7184db9..0e7bb976a2 100644 --- a/esphome/components/ds1307/time.py +++ b/esphome/components/ds1307/time.py @@ -27,6 +27,7 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( cv.GenerateID(): cv.use_id(DS1307Component), } ), + synchronous=True, ) async def ds1307_write_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -42,6 +43,7 @@ async def ds1307_write_time_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DS1307Component), } ), + synchronous=True, ) async def ds1307_read_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/duty_time/sensor.py b/esphome/components/duty_time/sensor.py index 1907b3fcfe..456859f8e4 100644 --- a/esphome/components/duty_time/sensor.py +++ b/esphome/components/duty_time/sensor.py @@ -90,21 +90,27 @@ DUTY_TIME_ID_SCHEMA = maybe_simple_id( ) -@register_action("sensor.duty_time.start", StartAction, DUTY_TIME_ID_SCHEMA) +@register_action( + "sensor.duty_time.start", StartAction, DUTY_TIME_ID_SCHEMA, synchronous=True +) async def sensor_runtime_start_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var -@register_action("sensor.duty_time.stop", StopAction, DUTY_TIME_ID_SCHEMA) +@register_action( + "sensor.duty_time.stop", StopAction, DUTY_TIME_ID_SCHEMA, synchronous=True +) async def sensor_runtime_stop_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var -@register_action("sensor.duty_time.reset", ResetAction, DUTY_TIME_ID_SCHEMA) +@register_action( + "sensor.duty_time.reset", ResetAction, DUTY_TIME_ID_SCHEMA, synchronous=True +) async def sensor_runtime_reset_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 8b368afc2e..43208eb87e 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -604,11 +604,15 @@ async def ble_enabled_to_code(config, condition_id, template_arg, args): return cg.new_Pvariable(condition_id, template_arg) -@automation.register_action("ble.enable", BLEEnableAction, cv.Schema({})) +@automation.register_action( + "ble.enable", BLEEnableAction, cv.Schema({}), synchronous=True +) async def ble_enable_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg) -@automation.register_action("ble.disable", BLEDisableAction, cv.Schema({})) +@automation.register_action( + "ble.disable", BLEDisableAction, cv.Schema({}), synchronous=True +) async def ble_disable_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index b08e791e7e..827ddba955 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -622,6 +622,7 @@ async def to_code(config): ), validate_set_value_action, ), + synchronous=True, ) async def ble_server_characteristic_set_value(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -641,6 +642,7 @@ async def ble_server_characteristic_set_value(config, action_id, template_arg, a cv.Required(CONF_VALUE): value_schema(), } ), + synchronous=True, ) async def ble_server_descriptor_set_value(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -662,6 +664,7 @@ async def ble_server_descriptor_set_value(config, action_id, template_arg, args) ), validate_notify_action, ), + synchronous=True, ) async def ble_server_characteristic_notify(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 37e74672ed..c5e8f3178d 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -373,6 +373,7 @@ ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema( "esp32_ble_tracker.start_scan", ESP32BLEStartScanAction, ESP32_BLE_START_SCAN_ACTION_SCHEMA, + synchronous=True, ) async def esp32_ble_tracker_start_scan_action_to_code( config, action_id, template_arg, args @@ -396,6 +397,7 @@ ESP32_BLE_STOP_SCAN_ACTION_SCHEMA = automation.maybe_simple_id( "esp32_ble_tracker.stop_scan", ESP32BLEStopScanAction, ESP32_BLE_STOP_SCAN_ACTION_SCHEMA, + synchronous=True, ) async def esp32_ble_tracker_stop_scan_action_to_code( config, action_id, template_arg, args diff --git a/esphome/components/esp8266_pwm/output.py b/esphome/components/esp8266_pwm/output.py index a78831c516..b9b6dcc95a 100644 --- a/esphome/components/esp8266_pwm/output.py +++ b/esphome/components/esp8266_pwm/output.py @@ -57,6 +57,7 @@ async def to_code(config) -> None: cv.Required(CONF_FREQUENCY): cv.templatable(validate_frequency), } ), + synchronous=True, ) async def esp8266_set_frequency_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/esp_ldo/__init__.py b/esphome/components/esp_ldo/__init__.py index 5235a9411e..a489651b59 100644 --- a/esphome/components/esp_ldo/__init__.py +++ b/esphome/components/esp_ldo/__init__.py @@ -129,6 +129,7 @@ def adjusted_ldo_id(value): ), } ), + synchronous=True, ) async def ldo_voltage_adjust_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index faeccd910e..d1a85ae8fd 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -220,6 +220,7 @@ SEND_SCHEMA.add_extra(_validate_send_action) "espnow.send", SendAction, SEND_SCHEMA, + synchronous=False, ) @automation.register_action( "espnow.broadcast", @@ -232,6 +233,7 @@ SEND_SCHEMA.add_extra(_validate_send_action) ), key=CONF_DATA, ), + synchronous=False, ) async def send_action( config: ConfigType, @@ -271,6 +273,7 @@ async def send_action( PEER_SCHEMA, key=CONF_ADDRESS, ), + synchronous=True, ) @automation.register_action( "espnow.peer.delete", @@ -279,6 +282,7 @@ async def send_action( PEER_SCHEMA, key=CONF_ADDRESS, ), + synchronous=True, ) async def peer_action( config: ConfigType, @@ -303,6 +307,7 @@ async def peer_action( }, key=CONF_CHANNEL, ), + synchronous=True, ) async def channel_action( config: ConfigType, diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index 14cc1505ad..300902b8ca 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -129,7 +129,9 @@ TRIGGER_EVENT_SCHEMA = cv.Schema( ) -@automation.register_action("event.trigger", TriggerEventAction, TRIGGER_EVENT_SCHEMA) +@automation.register_action( + "event.trigger", TriggerEventAction, TRIGGER_EVENT_SCHEMA, synchronous=True +) async def event_fire_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) diff --git a/esphome/components/ezo_pmp/__init__.py b/esphome/components/ezo_pmp/__init__.py index c1f72bb05d..1538e303f1 100644 --- a/esphome/components/ezo_pmp/__init__.py +++ b/esphome/components/ezo_pmp/__init__.py @@ -81,7 +81,10 @@ EzoPMPArbitraryCommandAction = ezo_pmp_ns.class_( @automation.register_action( - "ezo_pmp.find", EzoPMPFindAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA + "ezo_pmp.find", + EzoPMPFindAction, + EZO_PMP_NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_find_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -92,6 +95,7 @@ async def ezo_pmp_find_to_code(config, action_id, template_arg, args): "ezo_pmp.dose_continuously", EzoPMPDoseContinuouslyAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_dose_continuously_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -102,6 +106,7 @@ async def ezo_pmp_dose_continuously_to_code(config, action_id, template_arg, arg "ezo_pmp.clear_total_volume_dosed", EzoPMPClearTotalVolumeDispensedAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_clear_total_volume_dosed_to_code( config, action_id, template_arg, args @@ -114,6 +119,7 @@ async def ezo_pmp_clear_total_volume_dosed_to_code( "ezo_pmp.clear_calibration", EzoPMPClearCalibrationAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_clear_calibration_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -121,7 +127,10 @@ async def ezo_pmp_clear_calibration_to_code(config, action_id, template_arg, arg @automation.register_action( - "ezo_pmp.pause_dosing", EzoPMPPauseDosingAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA + "ezo_pmp.pause_dosing", + EzoPMPPauseDosingAction, + EZO_PMP_NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_pause_dosing_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -129,7 +138,10 @@ async def ezo_pmp_pause_dosing_to_code(config, action_id, template_arg, args): @automation.register_action( - "ezo_pmp.stop_dosing", EzoPMPStopDosingAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA + "ezo_pmp.stop_dosing", + EzoPMPStopDosingAction, + EZO_PMP_NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_stop_dosing_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -149,7 +161,10 @@ EZO_PMP_DOSE_VOLUME_ACTION_SCHEMA = cv.All( @automation.register_action( - "ezo_pmp.dose_volume", EzoPMPDoseVolumeAction, EZO_PMP_DOSE_VOLUME_ACTION_SCHEMA + "ezo_pmp.dose_volume", + EzoPMPDoseVolumeAction, + EZO_PMP_DOSE_VOLUME_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_dose_volume_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -178,6 +193,7 @@ EZO_PMP_DOSE_VOLUME_OVER_TIME_ACTION_SCHEMA = cv.All( "ezo_pmp.dose_volume_over_time", EzoPMPDoseVolumeOverTimeAction, EZO_PMP_DOSE_VOLUME_OVER_TIME_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_dose_volume_over_time_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -209,6 +225,7 @@ EZO_PMP_DOSE_WITH_CONSTANT_FLOW_RATE_ACTION_SCHEMA = cv.All( "ezo_pmp.dose_with_constant_flow_rate", EzoPMPDoseWithConstantFlowRateAction, EZO_PMP_DOSE_WITH_CONSTANT_FLOW_RATE_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_dose_with_constant_flow_rate_to_code( config, action_id, template_arg, args @@ -239,6 +256,7 @@ EZO_PMP_SET_CALIBRATION_VOLUME_ACTION_SCHEMA = cv.All( "ezo_pmp.set_calibration_volume", EzoPMPSetCalibrationVolumeAction, EZO_PMP_SET_CALIBRATION_VOLUME_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_set_calibration_volume_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -262,6 +280,7 @@ EZO_PMP_CHANGE_I2C_ADDRESS_ACTION_SCHEMA = cv.All( "ezo_pmp.change_i2c_address", EzoPMPChangeI2CAddressAction, EZO_PMP_CHANGE_I2C_ADDRESS_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_change_i2c_address_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -285,6 +304,7 @@ EZO_PMP_ARBITRARY_COMMAND_ACTION_SCHEMA = cv.All( "ezo_pmp.arbitrary_command", EzoPMPArbitraryCommandAction, EZO_PMP_ARBITRARY_COMMAND_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_arbitrary_command_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index da28c577c8..df71c6ab3f 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -365,6 +365,7 @@ async def fan_turn_on_to_code(config, action_id, template_arg, args): cv.Optional(CONF_OFF_SPEED_CYCLE, default=True): cv.boolean, } ), + synchronous=True, ) async def fan_cycle_speed_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/fingerprint_grow/__init__.py b/esphome/components/fingerprint_grow/__init__.py index 115b89433b..2637097be8 100644 --- a/esphome/components/fingerprint_grow/__init__.py +++ b/esphome/components/fingerprint_grow/__init__.py @@ -261,6 +261,7 @@ async def to_code(config): }, key=CONF_FINGER_ID, ), + synchronous=True, ) async def fingerprint_grow_enroll_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -282,6 +283,7 @@ async def fingerprint_grow_enroll_to_code(config, action_id, template_arg, args) cv.GenerateID(): cv.use_id(FingerprintGrowComponent), } ), + synchronous=True, ) async def fingerprint_grow_cancel_enroll_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -299,6 +301,7 @@ async def fingerprint_grow_cancel_enroll_to_code(config, action_id, template_arg }, key=CONF_FINGER_ID, ), + synchronous=True, ) async def fingerprint_grow_delete_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -317,6 +320,7 @@ async def fingerprint_grow_delete_to_code(config, action_id, template_arg, args) cv.GenerateID(): cv.use_id(FingerprintGrowComponent), } ), + synchronous=True, ) async def fingerprint_grow_delete_all_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -337,6 +341,7 @@ FINGERPRINT_GROW_LED_CONTROL_ACTION_SCHEMA = cv.maybe_simple_value( "fingerprint_grow.led_control", LEDControlAction, FINGERPRINT_GROW_LED_CONTROL_ACTION_SCHEMA, + synchronous=True, ) async def fingerprint_grow_led_control_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -359,6 +364,7 @@ async def fingerprint_grow_led_control_to_code(config, action_id, template_arg, cv.Required(CONF_COUNT): cv.templatable(cv.uint8_t), } ), + synchronous=True, ) async def fingerprint_grow_aura_led_control_to_code( config, action_id, template_arg, args diff --git a/esphome/components/grove_tb6612fng/__init__.py b/esphome/components/grove_tb6612fng/__init__.py index 869c05387f..210e2f7bab 100644 --- a/esphome/components/grove_tb6612fng/__init__.py +++ b/esphome/components/grove_tb6612fng/__init__.py @@ -72,6 +72,7 @@ async def to_code(config): cv.Required(CONF_DIRECTION): cv.enum(DIRECTION_TYPE, upper=True), } ), + synchronous=True, ) async def grove_tb6612fng_run_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -96,6 +97,7 @@ async def grove_tb6612fng_run_to_code(config, action_id, template_arg, args): cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)), } ), + synchronous=True, ) async def grove_tb6612fng_break_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -115,6 +117,7 @@ async def grove_tb6612fng_break_to_code(config, action_id, template_arg, args): cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)), } ), + synchronous=True, ) async def grove_tb6612fng_stop_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -133,6 +136,7 @@ async def grove_tb6612fng_stop_to_code(config, action_id, template_arg, args): cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), } ), + synchronous=True, ) async def grove_tb6612fng_standby_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -149,6 +153,7 @@ async def grove_tb6612fng_standby_to_code(config, action_id, template_arg, args) cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), } ), + synchronous=True, ) async def grove_tb6612fng_no_standby_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -166,6 +171,7 @@ async def grove_tb6612fng_no_standby_to_code(config, action_id, template_arg, ar cv.Required(CONF_ADDRESS): cv.i2c_address, } ), + synchronous=True, ) async def grove_tb6612fng_change_address_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index 6c208f6caa..caaaa18dd6 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -319,10 +319,16 @@ HAIER_HON_BASE_ACTION_SCHEMA = automation.maybe_simple_id( @automation.register_action( - "climate.haier.display_on", DisplayOnAction, HAIER_BASE_ACTION_SCHEMA + "climate.haier.display_on", + DisplayOnAction, + HAIER_BASE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "climate.haier.display_off", DisplayOffAction, HAIER_BASE_ACTION_SCHEMA + "climate.haier.display_off", + DisplayOffAction, + HAIER_BASE_ACTION_SCHEMA, + synchronous=True, ) async def display_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -330,10 +336,16 @@ async def display_action_to_code(config, action_id, template_arg, args): @automation.register_action( - "climate.haier.beeper_on", BeeperOnAction, HAIER_HON_BASE_ACTION_SCHEMA + "climate.haier.beeper_on", + BeeperOnAction, + HAIER_HON_BASE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "climate.haier.beeper_off", BeeperOffAction, HAIER_HON_BASE_ACTION_SCHEMA + "climate.haier.beeper_off", + BeeperOffAction, + HAIER_HON_BASE_ACTION_SCHEMA, + synchronous=True, ) async def beeper_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -345,11 +357,13 @@ async def beeper_action_to_code(config, action_id, template_arg, args): "climate.haier.start_self_cleaning", StartSelfCleaningAction, HAIER_HON_BASE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( "climate.haier.start_steri_cleaning", StartSteriCleaningAction, HAIER_HON_BASE_ACTION_SCHEMA, + synchronous=True, ) async def start_cleaning_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -368,6 +382,7 @@ async def start_cleaning_to_code(config, action_id, template_arg, args): ), } ), + synchronous=True, ) async def haier_set_vertical_airflow_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -391,6 +406,7 @@ async def haier_set_vertical_airflow_to_code(config, action_id, template_arg, ar ), } ), + synchronous=True, ) async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -403,10 +419,16 @@ async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg, @automation.register_action( - "climate.haier.health_on", HealthOnAction, HAIER_BASE_ACTION_SCHEMA + "climate.haier.health_on", + HealthOnAction, + HAIER_BASE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "climate.haier.health_off", HealthOffAction, HAIER_BASE_ACTION_SCHEMA + "climate.haier.health_off", + HealthOffAction, + HAIER_BASE_ACTION_SCHEMA, + synchronous=True, ) async def health_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -414,13 +436,22 @@ async def health_action_to_code(config, action_id, template_arg, args): @automation.register_action( - "climate.haier.power_on", PowerOnAction, HAIER_BASE_ACTION_SCHEMA + "climate.haier.power_on", + PowerOnAction, + HAIER_BASE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "climate.haier.power_off", PowerOffAction, HAIER_BASE_ACTION_SCHEMA + "climate.haier.power_off", + PowerOffAction, + HAIER_BASE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "climate.haier.power_toggle", PowerToggleAction, HAIER_BASE_ACTION_SCHEMA + "climate.haier.power_toggle", + PowerToggleAction, + HAIER_BASE_ACTION_SCHEMA, + synchronous=True, ) async def power_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/hbridge/fan/__init__.py b/esphome/components/hbridge/fan/__init__.py index 31a20a8981..8ea8677ba2 100644 --- a/esphome/components/hbridge/fan/__init__.py +++ b/esphome/components/hbridge/fan/__init__.py @@ -52,6 +52,7 @@ CONFIG_SCHEMA = ( "fan.hbridge.brake", BrakeAction, maybe_simple_id({cv.GenerateID(): cv.use_id(HBridgeFan)}), + synchronous=True, ) async def fan_hbridge_brake_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/hc8/sensor.py b/esphome/components/hc8/sensor.py index 2f39b47f3c..29b428e310 100644 --- a/esphome/components/hc8/sensor.py +++ b/esphome/components/hc8/sensor.py @@ -68,7 +68,10 @@ CALIBRATION_ACTION_SCHEMA = cv.Schema( @automation.register_action( - "hc8.calibrate", HC8CalibrateAction, CALIBRATION_ACTION_SCHEMA + "hc8.calibrate", + HC8CalibrateAction, + CALIBRATION_ACTION_SCHEMA, + synchronous=True, ) async def hc8_calibration_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/hdc302x/sensor.py b/esphome/components/hdc302x/sensor.py index 7215a4cfb7..a6265b9b98 100644 --- a/esphome/components/hdc302x/sensor.py +++ b/esphome/components/hdc302x/sensor.py @@ -114,7 +114,10 @@ HDC302X_HEATER_ON_ACTION_SCHEMA = maybe_simple_id( @automation.register_action( - "hdc302x.heater_on", HeaterOnAction, HDC302X_HEATER_ON_ACTION_SCHEMA + "hdc302x.heater_on", + HeaterOnAction, + HDC302X_HEATER_ON_ACTION_SCHEMA, + synchronous=True, ) async def hdc302x_heater_on_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -127,7 +130,10 @@ async def hdc302x_heater_on_to_code(config, action_id, template_arg, args): @automation.register_action( - "hdc302x.heater_off", HeaterOffAction, HDC302X_ACTION_SCHEMA + "hdc302x.heater_off", + HeaterOffAction, + HDC302X_ACTION_SCHEMA, + synchronous=True, ) async def hdc302x_heater_off_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/hlk_fm22x/__init__.py b/esphome/components/hlk_fm22x/__init__.py index efd64b6513..cb6d5cdfd6 100644 --- a/esphome/components/hlk_fm22x/__init__.py +++ b/esphome/components/hlk_fm22x/__init__.py @@ -170,6 +170,7 @@ async def to_code(config): }, key=CONF_NAME, ), + synchronous=True, ) async def hlk_fm22x_enroll_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -192,6 +193,7 @@ async def hlk_fm22x_enroll_to_code(config, action_id, template_arg, args): }, key=CONF_FACE_ID, ), + synchronous=True, ) async def hlk_fm22x_delete_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -210,6 +212,7 @@ async def hlk_fm22x_delete_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(HlkFm22xComponent), } ), + synchronous=True, ) async def hlk_fm22x_delete_all_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -225,6 +228,7 @@ async def hlk_fm22x_delete_all_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(HlkFm22xComponent), } ), + synchronous=True, ) async def hlk_fm22x_scan_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -240,6 +244,7 @@ async def hlk_fm22x_scan_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(HlkFm22xComponent), } ), + synchronous=True, ) async def hlk_fm22x_reset_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 81337ebdf6..416432cfc4 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -279,13 +279,22 @@ HTTP_REQUEST_SEND_ACTION_SCHEMA = HTTP_REQUEST_ACTION_SCHEMA.extend( @automation.register_action( - "http_request.get", HttpRequestSendAction, HTTP_REQUEST_GET_ACTION_SCHEMA + "http_request.get", + HttpRequestSendAction, + HTTP_REQUEST_GET_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "http_request.post", HttpRequestSendAction, HTTP_REQUEST_POST_ACTION_SCHEMA + "http_request.post", + HttpRequestSendAction, + HTTP_REQUEST_POST_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "http_request.send", HttpRequestSendAction, HTTP_REQUEST_SEND_ACTION_SCHEMA + "http_request.send", + HttpRequestSendAction, + HTTP_REQUEST_SEND_ACTION_SCHEMA, + synchronous=True, ) async def http_request_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/http_request/ota/__init__.py b/esphome/components/http_request/ota/__init__.py index d2c574d8c6..fb59e51943 100644 --- a/esphome/components/http_request/ota/__init__.py +++ b/esphome/components/http_request/ota/__init__.py @@ -70,6 +70,7 @@ OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All( "ota.http_request.flash", OtaHttpRequestComponentFlashAction, OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA, + synchronous=True, ) async def ota_http_request_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/htu21d/sensor.py b/esphome/components/htu21d/sensor.py index a578670e37..92c088a22f 100644 --- a/esphome/components/htu21d/sensor.py +++ b/esphome/components/htu21d/sensor.py @@ -93,6 +93,7 @@ async def to_code(config): }, key=CONF_LEVEL, ), + synchronous=True, ) async def set_heater_level_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -112,6 +113,7 @@ async def set_heater_level_to_code(config, action_id, template_arg, args): }, key=CONF_STATUS, ), + synchronous=True, ) async def set_heater_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/hub75/display.py b/esphome/components/hub75/display.py index f1e6ef4243..4f62ce7a94 100644 --- a/esphome/components/hub75/display.py +++ b/esphome/components/hub75/display.py @@ -652,6 +652,7 @@ async def to_code(config: ConfigType) -> None: }, key=CONF_BRIGHTNESS, ), + synchronous=True, ) async def hub75_set_brightness_to_code( config: ConfigType, diff --git a/esphome/components/integration/sensor.py b/esphome/components/integration/sensor.py index 2676638556..d0aae4201e 100644 --- a/esphome/components/integration/sensor.py +++ b/esphome/components/integration/sensor.py @@ -111,6 +111,7 @@ async def to_code(config): cv.Required(CONF_ID): cv.use_id(IntegrationSensor), } ), + synchronous=True, ) async def sensor_integration_reset_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -127,6 +128,7 @@ async def sensor_integration_reset_to_code(config, action_id, template_arg, args cv.Required(CONF_VALUE): cv.templatable(cv.float_), } ), + synchronous=True, ) async def sensor_integration_set_value_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/key_collector/__init__.py b/esphome/components/key_collector/__init__.py index badb28c32c..1f4519df2d 100644 --- a/esphome/components/key_collector/__init__.py +++ b/esphome/components/key_collector/__init__.py @@ -142,6 +142,7 @@ async def to_code(config): cv.GenerateID(): cv.use_id(KeyCollector), } ), + synchronous=True, ) async def enable_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -157,6 +158,7 @@ async def enable_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(KeyCollector), } ), + synchronous=True, ) async def disable_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/ld2410/__init__.py b/esphome/components/ld2410/__init__.py index b492bbcd14..360e56330a 100644 --- a/esphome/components/ld2410/__init__.py +++ b/esphome/components/ld2410/__init__.py @@ -97,7 +97,10 @@ BLUETOOTH_PASSWORD_SET_SCHEMA = cv.Schema( @automation.register_action( - "bluetooth_password.set", BluetoothPasswordSetAction, BLUETOOTH_PASSWORD_SET_SCHEMA + "bluetooth_password.set", + BluetoothPasswordSetAction, + BLUETOOTH_PASSWORD_SET_SCHEMA, + synchronous=True, ) async def bluetooth_password_set_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/ledc/output.py b/esphome/components/ledc/output.py index 7a45b9dc3f..62ff5ad30a 100644 --- a/esphome/components/ledc/output.py +++ b/esphome/components/ledc/output.py @@ -77,6 +77,7 @@ async def to_code(config): cv.Required(CONF_FREQUENCY): cv.templatable(validate_frequency), } ), + synchronous=True, ) async def ledc_set_frequency_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/libretiny_pwm/output.py b/esphome/components/libretiny_pwm/output.py index 28556514d8..e812b6a8f2 100644 --- a/esphome/components/libretiny_pwm/output.py +++ b/esphome/components/libretiny_pwm/output.py @@ -38,6 +38,7 @@ async def to_code(config): cv.Required(CONF_FREQUENCY): cv.templatable(cv.int_), } ), + synchronous=True, ) async def libretiny_pwm_set_frequency_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 08fd26a937..55273003b9 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -278,7 +278,10 @@ LIGHT_ADDRESSABLE_SET_ACTION_SCHEMA = cv.Schema( @automation.register_action( - "light.addressable_set", AddressableSet, LIGHT_ADDRESSABLE_SET_ACTION_SCHEMA + "light.addressable_set", + AddressableSet, + LIGHT_ADDRESSABLE_SET_ACTION_SCHEMA, + synchronous=True, ) async def light_addressable_set_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/lightwaverf/__init__.py b/esphome/components/lightwaverf/__init__.py index 802b341601..acbbbb4de9 100644 --- a/esphome/components/lightwaverf/__init__.py +++ b/esphome/components/lightwaverf/__init__.py @@ -55,6 +55,7 @@ LIGHTWAVE_SEND_SCHEMA = cv.Any( "lightwaverf.send_raw", LightwaveRawAction, LIGHTWAVE_SEND_SCHEMA, + synchronous=True, ) async def send_raw_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index e37092756f..fe4db23ae3 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -129,9 +129,15 @@ LOCK_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action("lock.unlock", UnlockAction, LOCK_ACTION_SCHEMA) -@automation.register_action("lock.lock", LockAction, LOCK_ACTION_SCHEMA) -@automation.register_action("lock.open", OpenAction, LOCK_ACTION_SCHEMA) +@automation.register_action( + "lock.unlock", UnlockAction, LOCK_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "lock.lock", LockAction, LOCK_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "lock.open", OpenAction, LOCK_ACTION_SCHEMA, synchronous=True +) async def lock_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 026b8aaf24..83a7854165 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -545,6 +545,7 @@ async def logger_log_action_to_code(config, action_id, template_arg, args): }, key=CONF_LEVEL, ), + synchronous=True, ) async def logger_set_level_to_code(config, action_id, template_arg, args): level = LOG_LEVELS[config[CONF_LEVEL]] diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index b589e42f3b..f9adca9c33 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -182,6 +182,7 @@ async def disp_update(disp, config: dict): ), LVGL_SCHEMA, ), + synchronous=True, ) async def obj_invalidate_to_code(config, action_id, template_arg, args): if CONF_LVGL_ID in config: @@ -202,6 +203,7 @@ async def obj_invalidate_to_code(config, action_id, template_arg, args): DISP_BG_SCHEMA.extend(LVGL_SCHEMA).add_extra( cv.has_at_least_one_key(CONF_DISP_BG_COLOR, CONF_DISP_BG_IMAGE) ), + synchronous=True, ) async def lvgl_update_to_code(config, action_id, template_arg, args): widgets = await get_widgets(config, CONF_LVGL_ID) @@ -222,6 +224,7 @@ async def lvgl_update_to_code(config, action_id, template_arg, args): cv.Optional(CONF_SHOW_SNOW, default=False): lv_bool, } ), + synchronous=True, ) async def pause_action_to_code(config, action_id, template_arg, args): lv_comp = await cg.get_variable(config[CONF_LVGL_ID]) @@ -237,6 +240,7 @@ async def pause_action_to_code(config, action_id, template_arg, args): "lvgl.resume", LvglAction, LVGL_SCHEMA, + synchronous=True, ) async def resume_action_to_code(config, action_id, template_arg, args): lv_comp = await cg.get_variable(config[CONF_LVGL_ID]) @@ -247,7 +251,9 @@ async def resume_action_to_code(config, action_id, template_arg, args): return var -@automation.register_action("lvgl.widget.disable", ObjUpdateAction, LIST_ACTION_SCHEMA) +@automation.register_action( + "lvgl.widget.disable", ObjUpdateAction, LIST_ACTION_SCHEMA, synchronous=True +) async def obj_disable_to_code(config, action_id, template_arg, args): async def do_disable(widget: Widget): widget.add_state(LV_STATE.DISABLED) @@ -257,7 +263,9 @@ async def obj_disable_to_code(config, action_id, template_arg, args): ) -@automation.register_action("lvgl.widget.enable", ObjUpdateAction, LIST_ACTION_SCHEMA) +@automation.register_action( + "lvgl.widget.enable", ObjUpdateAction, LIST_ACTION_SCHEMA, synchronous=True +) async def obj_enable_to_code(config, action_id, template_arg, args): async def do_enable(widget: Widget): widget.clear_state(LV_STATE.DISABLED) @@ -267,7 +275,9 @@ async def obj_enable_to_code(config, action_id, template_arg, args): ) -@automation.register_action("lvgl.widget.hide", ObjUpdateAction, LIST_ACTION_SCHEMA) +@automation.register_action( + "lvgl.widget.hide", ObjUpdateAction, LIST_ACTION_SCHEMA, synchronous=True +) async def obj_hide_to_code(config, action_id, template_arg, args): async def do_hide(widget: Widget): widget.add_flag("LV_OBJ_FLAG_HIDDEN") @@ -276,7 +286,9 @@ async def obj_hide_to_code(config, action_id, template_arg, args): return await action_to_code(widgets, do_hide, action_id, template_arg, args) -@automation.register_action("lvgl.widget.show", ObjUpdateAction, LIST_ACTION_SCHEMA) +@automation.register_action( + "lvgl.widget.show", ObjUpdateAction, LIST_ACTION_SCHEMA, synchronous=True +) async def obj_show_to_code(config, action_id, template_arg, args): async def do_show(widget: Widget): widget.clear_flag("LV_OBJ_FLAG_HIDDEN") @@ -318,6 +330,7 @@ def focused_id(value): key=CONF_ID, ), ), + synchronous=True, ) async def widget_focus(config, action_id, template_arg, args): widget = await get_widgets(config) @@ -357,7 +370,10 @@ async def widget_focus(config, action_id, template_arg, args): @automation.register_action( - "lvgl.widget.update", ObjUpdateAction, base_update_schema(lv_obj_base_t, PARTS) + "lvgl.widget.update", + ObjUpdateAction, + base_update_schema(lv_obj_base_t, PARTS), + synchronous=True, ) async def obj_update_to_code(config, action_id, template_arg, args): async def do_update(widget: Widget): @@ -389,6 +405,7 @@ def validate_refresh_config(config): ), validate_refresh_config, ), + synchronous=True, ) async def obj_refresh_to_code(config, action_id, template_arg, args): widget = await get_widgets(config) diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index 3969c9f388..b9801b4133 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -59,6 +59,7 @@ async def styles_to_code(config): cv.Required(CONF_ID): cv.use_id(lv_style_t), } ), + synchronous=True, ) async def style_update_to_code(config, action_id, template_arg, args): await wait_for_widgets() diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 9c92ca7e98..09d40bb7ef 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -164,6 +164,7 @@ class WidgetType: f"lvgl.{self.name}.update", ObjUpdateAction, base_update_schema(self, self.parts).extend(self.modify_schema), + synchronous=True, )(update_to_code) @property diff --git a/esphome/components/lvgl/widgets/animimg.py b/esphome/components/lvgl/widgets/animimg.py index b824d28fb8..8e2db5ff35 100644 --- a/esphome/components/lvgl/widgets/animimg.py +++ b/esphome/components/lvgl/widgets/animimg.py @@ -83,6 +83,7 @@ animimg_spec = AnimimgType() }, key=CONF_ID, ), + synchronous=True, ) async def animimg_start(config, action_id, template_arg, args): widget = await get_widgets(config) @@ -102,6 +103,7 @@ async def animimg_start(config, action_id, template_arg, args): }, key=CONF_ID, ), + synchronous=True, ) async def animimg_stop(config, action_id, template_arg, args): widget = await get_widgets(config) diff --git a/esphome/components/lvgl/widgets/buttonmatrix.py b/esphome/components/lvgl/widgets/buttonmatrix.py index fe421aa477..f94f12b69b 100644 --- a/esphome/components/lvgl/widgets/buttonmatrix.py +++ b/esphome/components/lvgl/widgets/buttonmatrix.py @@ -245,6 +245,7 @@ buttonmatrix_spec = ButtonMatrixType() cv.Optional(CONF_SELECTED): lv_bool, } ), + synchronous=True, ) async def button_update_to_code(config, action_id, template_arg, args): widgets = await get_widgets(config[CONF_ID]) diff --git a/esphome/components/lvgl/widgets/canvas.py b/esphome/components/lvgl/widgets/canvas.py index ead352aa77..50cc8b0af6 100644 --- a/esphome/components/lvgl/widgets/canvas.py +++ b/esphome/components/lvgl/widgets/canvas.py @@ -97,6 +97,7 @@ canvas_spec = CanvasType() cv.Optional(CONF_OPA, default="COVER"): opacity, }, ), + synchronous=True, ) async def canvas_fill(config, action_id, template_arg, args): widget = await get_widgets(config) @@ -120,6 +121,7 @@ async def canvas_fill(config, action_id, template_arg, args): cv.Required(CONF_POINTS): cv.ensure_list(point_schema), }, ), + synchronous=True, ) async def canvas_set_pixel(config, action_id, template_arg, args): widget = await get_widgets(config) @@ -229,6 +231,7 @@ RECT_PROPS = { **{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}, } ), + synchronous=True, ) async def canvas_draw_rect(config, action_id, template_arg, args): width = await pixels.process(config[CONF_WIDTH]) @@ -268,6 +271,7 @@ TEXT_PROPS = { **{cv.Optional(prop): STYLE_PROPS[f"text_{prop}"] for prop in TEXT_PROPS}, }, ), + synchronous=True, ) async def canvas_draw_text(config, action_id, template_arg, args): text = await lv_text.process(config[CONF_TEXT]) @@ -302,6 +306,7 @@ IMG_PROPS = { **{cv.Optional(prop): validator for prop, validator in IMG_PROPS.items()}, } ), + synchronous=True, ) async def canvas_draw_image(config, action_id, template_arg, args): src = await lv_image.process(config[CONF_SRC]) @@ -341,6 +346,7 @@ LINE_PROPS = { **{cv.Optional(prop): validator for prop, validator in LINE_PROPS.items()}, } ), + synchronous=True, ) async def canvas_draw_line(config, action_id, template_arg, args): points = [ @@ -369,6 +375,7 @@ async def canvas_draw_line(config, action_id, template_arg, args): **{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}, }, ), + synchronous=True, ) async def canvas_draw_polygon(config, action_id, template_arg, args): points = [ @@ -408,6 +415,7 @@ ARC_PROPS = { **{cv.Optional(prop): validator for prop, validator in ARC_PROPS.items()}, } ), + synchronous=True, ) async def canvas_draw_arc(config, action_id, template_arg, args): radius = await size.process(config[CONF_RADIUS]) diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index aefda0e71a..b7e3af9a78 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -297,6 +297,7 @@ meter_spec = MeterType() cv.Optional(CONF_OPA): opacity, } ), + synchronous=True, ) async def indicator_update_to_code(config, action_id, template_arg, args): widget = await get_widgets(config) diff --git a/esphome/components/lvgl/widgets/page.py b/esphome/components/lvgl/widgets/page.py index 23c162e010..7e75ab6a2d 100644 --- a/esphome/components/lvgl/widgets/page.py +++ b/esphome/components/lvgl/widgets/page.py @@ -85,6 +85,7 @@ page_spec = PageType() "lvgl.page.next", LvglAction, SHOW_SCHEMA, + synchronous=True, ) async def page_next_to_code(config, action_id, template_arg, args): animation = await LV_ANIM.process(config[CONF_ANIMATION]) @@ -125,6 +126,7 @@ async def page_is_showing_to_code(config, condition_id, template_arg, args): "lvgl.page.previous", LvglAction, SHOW_SCHEMA, + synchronous=True, ) async def page_previous_to_code(config, action_id, template_arg, args): animation = await LV_ANIM.process(config[CONF_ANIMATION]) @@ -148,6 +150,7 @@ async def page_previous_to_code(config, action_id, template_arg, args): ), key=CONF_ID, ), + synchronous=True, ) async def page_show_to_code(config, action_id, template_arg, args): widget = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/lvgl/widgets/spinbox.py b/esphome/components/lvgl/widgets/spinbox.py index c6f25e9587..58e3435c5c 100644 --- a/esphome/components/lvgl/widgets/spinbox.py +++ b/esphome/components/lvgl/widgets/spinbox.py @@ -147,6 +147,7 @@ spinbox_spec = SpinboxType() }, key=CONF_ID, ), + synchronous=True, ) async def spinbox_increment(config, action_id, template_arg, args): widgets = await get_widgets(config) @@ -166,6 +167,7 @@ async def spinbox_increment(config, action_id, template_arg, args): }, key=CONF_ID, ), + synchronous=True, ) async def spinbox_decrement(config, action_id, template_arg, args): widgets = await get_widgets(config) diff --git a/esphome/components/lvgl/widgets/tabview.py b/esphome/components/lvgl/widgets/tabview.py index e8931bab7c..cd7cf7b471 100644 --- a/esphome/components/lvgl/widgets/tabview.py +++ b/esphome/components/lvgl/widgets/tabview.py @@ -109,6 +109,7 @@ tabview_spec = TabviewType() cv.Required(CONF_INDEX): lv_int, }, ).add_extra(cv.has_at_least_one_key(CONF_INDEX, CONF_TAB_ID)), + synchronous=True, ) async def tabview_select(config, action_id, template_arg, args): widget = await get_widgets(config) diff --git a/esphome/components/lvgl/widgets/tileview.py b/esphome/components/lvgl/widgets/tileview.py index 5e3a95f017..430a386d2e 100644 --- a/esphome/components/lvgl/widgets/tileview.py +++ b/esphome/components/lvgl/widgets/tileview.py @@ -112,6 +112,7 @@ def tile_select_validate(config): cv.Optional(CONF_TILE_ID): cv.use_id(lv_tile_t), }, ).add_extra(tile_select_validate), + synchronous=True, ) async def tileview_select(config, action_id, template_arg, args): widgets = await get_widgets(config) diff --git a/esphome/components/max17043/sensor.py b/esphome/components/max17043/sensor.py index 3da0f953b0..ebb045dfce 100644 --- a/esphome/components/max17043/sensor.py +++ b/esphome/components/max17043/sensor.py @@ -71,7 +71,9 @@ MAX17043_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action("max17043.sleep_mode", SleepAction, MAX17043_ACTION_SCHEMA) +@automation.register_action( + "max17043.sleep_mode", SleepAction, MAX17043_ACTION_SCHEMA, synchronous=True +) async def max17043_sleep_mode_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/max6956/__init__.py b/esphome/components/max6956/__init__.py index 0d2ff527c7..be6390fc17 100644 --- a/esphome/components/max6956/__init__.py +++ b/esphome/components/max6956/__init__.py @@ -112,6 +112,7 @@ async def max6956_pin_to_code(config): }, key=CONF_BRIGHTNESS_GLOBAL, ), + synchronous=True, ) async def max6956_set_brightness_global_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -133,6 +134,7 @@ async def max6956_set_brightness_global_to_code(config, action_id, template_arg, }, key=CONF_BRIGHTNESS_MODE, ), + synchronous=True, ) async def max6956_set_brightness_mode_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index a251eaccea..eb751b995d 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -133,10 +133,16 @@ MAX7219_ON_ACTION_SCHEMA = automation.maybe_simple_id( @automation.register_action( - "max7219digit.invert_off", DisplayInvertAction, MAX7219_OFF_ACTION_SCHEMA + "max7219digit.invert_off", + DisplayInvertAction, + MAX7219_OFF_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "max7219digit.invert_on", DisplayInvertAction, MAX7219_ON_ACTION_SCHEMA + "max7219digit.invert_on", + DisplayInvertAction, + MAX7219_ON_ACTION_SCHEMA, + synchronous=True, ) async def max7219digit_invert_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -146,10 +152,16 @@ async def max7219digit_invert_to_code(config, action_id, template_arg, args): @automation.register_action( - "max7219digit.turn_off", DisplayVisibilityAction, MAX7219_OFF_ACTION_SCHEMA + "max7219digit.turn_off", + DisplayVisibilityAction, + MAX7219_OFF_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "max7219digit.turn_on", DisplayVisibilityAction, MAX7219_ON_ACTION_SCHEMA + "max7219digit.turn_on", + DisplayVisibilityAction, + MAX7219_ON_ACTION_SCHEMA, + synchronous=True, ) async def max7219digit_visible_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -159,10 +171,16 @@ async def max7219digit_visible_to_code(config, action_id, template_arg, args): @automation.register_action( - "max7219digit.reverse_off", DisplayReverseAction, MAX7219_OFF_ACTION_SCHEMA + "max7219digit.reverse_off", + DisplayReverseAction, + MAX7219_OFF_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "max7219digit.reverse_on", DisplayReverseAction, MAX7219_ON_ACTION_SCHEMA + "max7219digit.reverse_on", + DisplayReverseAction, + MAX7219_ON_ACTION_SCHEMA, + synchronous=True, ) async def max7219digit_reverse_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -183,7 +201,10 @@ MAX7219_INTENSITY_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "max7219digit.intensity", DisplayIntensityAction, MAX7219_INTENSITY_SCHEMA + "max7219digit.intensity", + DisplayIntensityAction, + MAX7219_INTENSITY_SCHEMA, + synchronous=True, ) async def max7219digit_intensity_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index 051e386eaf..a5baca2994 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -177,6 +177,7 @@ MEDIA_PLAYER_CONDITION_SCHEMA = automation.maybe_simple_id( }, key=CONF_MEDIA_URL, ), + synchronous=True, ) async def media_player_play_media_action(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -206,7 +207,10 @@ def _register_command_actions(): class_name, automation.Action, cg.Parented.template(MediaPlayer) ) automation.register_action( - f"media_player.{action_name}", action_class, MEDIA_PLAYER_ACTION_SCHEMA + f"media_player.{action_name}", + action_class, + MEDIA_PLAYER_ACTION_SCHEMA, + synchronous=True, )(handler) @@ -242,6 +246,7 @@ _register_state_conditions() }, key=CONF_VOLUME, ), + synchronous=True, ) async def media_player_volume_set_action(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py index 2841afde7a..b7d0ad1998 100644 --- a/esphome/components/mhz19/sensor.py +++ b/esphome/components/mhz19/sensor.py @@ -112,13 +112,22 @@ NO_ARGS_ACTION_SCHEMA = maybe_simple_id( @automation.register_action( - "mhz19.calibrate_zero", MHZ19CalibrateZeroAction, NO_ARGS_ACTION_SCHEMA + "mhz19.calibrate_zero", + MHZ19CalibrateZeroAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "mhz19.abc_enable", MHZ19ABCEnableAction, NO_ARGS_ACTION_SCHEMA + "mhz19.abc_enable", + MHZ19ABCEnableAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "mhz19.abc_disable", MHZ19ABCDisableAction, NO_ARGS_ACTION_SCHEMA + "mhz19.abc_disable", + MHZ19ABCDisableAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def mhz19_no_args_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -137,7 +146,10 @@ RANGE_ACTION_SCHEMA = maybe_simple_id( @automation.register_action( - "mhz19.detection_range_set", MHZ19DetectionRangeSetAction, RANGE_ACTION_SCHEMA + "mhz19.detection_range_set", + MHZ19DetectionRangeSetAction, + RANGE_ACTION_SCHEMA, + synchronous=True, ) async def mhz19_detection_range_set_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 74696584da..372eb4c3b0 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -529,8 +529,15 @@ async def to_code(config): MICRO_WAKE_WORD_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(MicroWakeWord)}) -@register_action("micro_wake_word.start", StartAction, MICRO_WAKE_WORD_ACTION_SCHEMA) -@register_action("micro_wake_word.stop", StopAction, MICRO_WAKE_WORD_ACTION_SCHEMA) +@register_action( + "micro_wake_word.start", + StartAction, + MICRO_WAKE_WORD_ACTION_SCHEMA, + synchronous=True, +) +@register_action( + "micro_wake_word.stop", StopAction, MICRO_WAKE_WORD_ACTION_SCHEMA, synchronous=True +) @register_condition( "micro_wake_word.is_running", IsRunningCondition, MICRO_WAKE_WORD_ACTION_SCHEMA ) @@ -551,11 +558,13 @@ MICRO_WAKE_WORLD_MODEL_ACTION_SCHEMA = automation.maybe_simple_id( "micro_wake_word.enable_model", EnableModelAction, MICRO_WAKE_WORLD_MODEL_ACTION_SCHEMA, + synchronous=True, ) @register_action( "micro_wake_word.disable_model", DisableModelAction, MICRO_WAKE_WORLD_MODEL_ACTION_SCHEMA, + synchronous=True, ) @register_condition( "micro_wake_word.model_is_enabled", diff --git a/esphome/components/microphone/__init__.py b/esphome/components/microphone/__init__.py index ce31484413..6b5ee8c3e1 100644 --- a/esphome/components/microphone/__init__.py +++ b/esphome/components/microphone/__init__.py @@ -190,19 +190,25 @@ async def microphone_action(config, action_id, template_arg, args): automation.register_action( - "microphone.capture", CaptureAction, MICROPHONE_ACTION_SCHEMA + "microphone.capture", + CaptureAction, + MICROPHONE_ACTION_SCHEMA, + synchronous=True, )(microphone_action) automation.register_action( - "microphone.stop_capture", StopCaptureAction, MICROPHONE_ACTION_SCHEMA + "microphone.stop_capture", + StopCaptureAction, + MICROPHONE_ACTION_SCHEMA, + synchronous=True, )(microphone_action) -automation.register_action("microphone.mute", MuteAction, MICROPHONE_ACTION_SCHEMA)( - microphone_action -) -automation.register_action("microphone.unmute", UnmuteAction, MICROPHONE_ACTION_SCHEMA)( - microphone_action -) +automation.register_action( + "microphone.mute", MuteAction, MICROPHONE_ACTION_SCHEMA, synchronous=True +)(microphone_action) +automation.register_action( + "microphone.unmute", UnmuteAction, MICROPHONE_ACTION_SCHEMA, synchronous=True +)(microphone_action) automation.register_condition( "microphone.is_capturing", IsCapturingCondition, MICROPHONE_ACTION_SCHEMA diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py index 8a3d4f22ba..c954b45033 100644 --- a/esphome/components/midea/climate.py +++ b/esphome/components/midea/climate.py @@ -53,7 +53,9 @@ def templatize(value): def register_action(name, type_, schema): validator = templatize(schema).extend(MIDEA_ACTION_BASE_SCHEMA) - registerer = automation.register_action(f"midea_ac.{name}", type_, validator) + registerer = automation.register_action( + f"midea_ac.{name}", type_, validator, synchronous=True + ) def decorator(func): async def new_func(config, action_id, template_arg, args): diff --git a/esphome/components/mixer/speaker/__init__.py b/esphome/components/mixer/speaker/__init__.py index a3025d7121..63b419cc98 100644 --- a/esphome/components/mixer/speaker/__init__.py +++ b/esphome/components/mixer/speaker/__init__.py @@ -162,6 +162,7 @@ async def to_code(config): ), } ), + synchronous=True, ) async def ducking_set_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index c25c472038..d110d7c160 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -607,6 +607,7 @@ async def mqtt_connected_to_code(config, condition_id, template_arg, args): cv.GenerateID(): cv.use_id(MQTTClientComponent), } ), + synchronous=True, ) async def mqtt_enable_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -621,6 +622,7 @@ async def mqtt_enable_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(MQTTClientComponent), } ), + synchronous=True, ) async def mqtt_disable_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/nau7802/sensor.py b/esphome/components/nau7802/sensor.py index 9192f48f53..9798c1c297 100644 --- a/esphome/components/nau7802/sensor.py +++ b/esphome/components/nau7802/sensor.py @@ -117,16 +117,19 @@ NAU7802_CALIBRATE_SCHEMA = maybe_simple_id( "nau7802.calibrate_internal_offset", NAU7802CalbrateInternalOffsetAction, NAU7802_CALIBRATE_SCHEMA, + synchronous=True, ) @automation.register_action( "nau7802.calibrate_external_offset", NAU7802CalbrateExternalOffsetAction, NAU7802_CALIBRATE_SCHEMA, + synchronous=True, ) @automation.register_action( "nau7802.calibrate_gain", NAU7802CalbrateGainAction, NAU7802_CALIBRATE_SCHEMA, + synchronous=True, ) async def nau7802_calibrate_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/nextion/binary_sensor/__init__.py b/esphome/components/nextion/binary_sensor/__init__.py index 7ef72c6491..5b5922887c 100644 --- a/esphome/components/nextion/binary_sensor/__init__.py +++ b/esphome/components/nextion/binary_sensor/__init__.py @@ -70,6 +70,7 @@ async def to_code(config): ), } ), + synchronous=True, ) async def sensor_nextion_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index b8fcd5d8cf..5b2dfc488d 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -172,6 +172,7 @@ CONFIG_SCHEMA = cv.All( }, key=CONF_BRIGHTNESS, ), + synchronous=True, ) async def nextion_set_brightness_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/nextion/sensor/__init__.py b/esphome/components/nextion/sensor/__init__.py index 9802762ff3..cab531f1db 100644 --- a/esphome/components/nextion/sensor/__init__.py +++ b/esphome/components/nextion/sensor/__init__.py @@ -110,6 +110,7 @@ async def to_code(config): ), } ), + synchronous=True, ) async def sensor_nextion_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/nextion/switch/__init__.py b/esphome/components/nextion/switch/__init__.py index 1974ff3b9e..81e6721d0f 100644 --- a/esphome/components/nextion/switch/__init__.py +++ b/esphome/components/nextion/switch/__init__.py @@ -52,6 +52,7 @@ async def to_code(config): ), } ), + synchronous=True, ) async def sensor_nextion_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/nextion/text_sensor/__init__.py b/esphome/components/nextion/text_sensor/__init__.py index 8fc0a8ceaf..168a672497 100644 --- a/esphome/components/nextion/text_sensor/__init__.py +++ b/esphome/components/nextion/text_sensor/__init__.py @@ -48,6 +48,7 @@ async def to_code(config): ), } ), + synchronous=True, ) async def sensor_nextion_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index 057244e03d..35a9de3537 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -106,9 +106,14 @@ RELEASE_IMAGE_SCHEMA = automation.maybe_simple_id( ) -@automation.register_action("online_image.set_url", SetUrlAction, SET_URL_SCHEMA) @automation.register_action( - "online_image.release", ReleaseImageAction, RELEASE_IMAGE_SCHEMA + "online_image.set_url", SetUrlAction, SET_URL_SCHEMA, synchronous=True +) +@automation.register_action( + "online_image.release", + ReleaseImageAction, + RELEASE_IMAGE_SCHEMA, + synchronous=True, ) async def online_image_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/output/__init__.py b/esphome/components/output/__init__.py index a4c960927b..a4ce2b2d1a 100644 --- a/esphome/components/output/__init__.py +++ b/esphome/components/output/__init__.py @@ -118,6 +118,7 @@ async def output_set_level_to_code(config, action_id, template_arg, args): cv.Required(CONF_MIN_POWER): cv.templatable(cv.percentage), } ), + synchronous=True, ) async def output_set_min_power_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -136,6 +137,7 @@ async def output_set_min_power_to_code(config, action_id, template_arg, args): cv.Required(CONF_MAX_POWER): cv.templatable(cv.percentage), } ), + synchronous=True, ) async def output_set_max_power_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/pcf85063/time.py b/esphome/components/pcf85063/time.py index f3c0c3230f..8e19178cc9 100644 --- a/esphome/components/pcf85063/time.py +++ b/esphome/components/pcf85063/time.py @@ -29,6 +29,7 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( cv.GenerateID(): cv.use_id(PCF85063Component), } ), + synchronous=True, ) async def pcf85063_write_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -44,6 +45,7 @@ async def pcf85063_write_time_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(PCF85063Component), } ), + synchronous=True, ) async def pcf85063_read_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/pcf8563/time.py b/esphome/components/pcf8563/time.py index e3b3b572aa..0d4de3cb73 100644 --- a/esphome/components/pcf8563/time.py +++ b/esphome/components/pcf8563/time.py @@ -32,6 +32,7 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( cv.GenerateID(): cv.use_id(pcf8563Component), } ), + synchronous=True, ) async def pcf8563_write_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -47,6 +48,7 @@ async def pcf8563_write_time_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(pcf8563Component), } ), + synchronous=True, ) async def pcf8563_read_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py index 5fa3166f9d..0e66b67637 100644 --- a/esphome/components/pid/climate.py +++ b/esphome/components/pid/climate.py @@ -133,6 +133,7 @@ async def to_code(config): cv.Required(CONF_ID): cv.use_id(PIDClimate), } ), + synchronous=True, ) async def pid_reset_integral_term(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -154,6 +155,7 @@ async def pid_reset_integral_term(config, action_id, template_arg, args): ): cv.possibly_negative_percentage, } ), + synchronous=True, ) async def esp8266_set_frequency_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -175,6 +177,7 @@ async def esp8266_set_frequency_to_code(config, action_id, template_arg, args): cv.Optional(CONF_KD, default=0.0): cv.templatable(cv.float_), } ), + synchronous=True, ) async def set_control_parameters(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/pipsolar/output/__init__.py b/esphome/components/pipsolar/output/__init__.py index 829f8f7037..81e99e15a2 100644 --- a/esphome/components/pipsolar/output/__init__.py +++ b/esphome/components/pipsolar/output/__init__.py @@ -98,6 +98,7 @@ async def to_code(config): cv.Required(CONF_VALUE): cv.templatable(cv.positive_float), } ), + synchronous=True, ) async def output_pipsolar_set_level_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/pmwcs3/sensor.py b/esphome/components/pmwcs3/sensor.py index 075b9b00b5..bb40f3e499 100644 --- a/esphome/components/pmwcs3/sensor.py +++ b/esphome/components/pmwcs3/sensor.py @@ -106,11 +106,13 @@ PMWCS3_CALIBRATION_SCHEMA = cv.Schema( "pmwcs3.air_calibration", PMWCS3AirCalibrationAction, PMWCS3_CALIBRATION_SCHEMA, + synchronous=True, ) @automation.register_action( "pmwcs3.water_calibration", PMWCS3WaterCalibrationAction, PMWCS3_CALIBRATION_SCHEMA, + synchronous=True, ) async def pmwcs3_calibration_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) @@ -130,6 +132,7 @@ PMWCS3_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value( "pmwcs3.new_i2c_address", PMWCS3NewI2cAddressAction, PMWCS3_NEW_I2C_ADDRESS_SCHEMA, + synchronous=True, ) async def pmwcs3newi2caddress_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/pn7150/__init__.py b/esphome/components/pn7150/__init__.py index 131b56e30e..6af1412881 100644 --- a/esphome/components/pn7150/__init__.py +++ b/esphome/components/pn7150/__init__.py @@ -119,11 +119,13 @@ PN7150_SCHEMA = cv.Schema( "tag.set_emulation_message", SetEmulationMessageAction, SET_MESSAGE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( "tag.set_write_message", SetWriteMessageAction, SET_MESSAGE_ACTION_SCHEMA, + synchronous=True, ) async def pn7150_set_message_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -138,22 +140,43 @@ async def pn7150_set_message_to_code(config, action_id, template_arg, args): @automation.register_action( - "tag.emulation_off", EmulationOffAction, SIMPLE_ACTION_SCHEMA -) -@automation.register_action("tag.emulation_on", EmulationOnAction, SIMPLE_ACTION_SCHEMA) -@automation.register_action("tag.polling_off", PollingOffAction, SIMPLE_ACTION_SCHEMA) -@automation.register_action("tag.polling_on", PollingOnAction, SIMPLE_ACTION_SCHEMA) -@automation.register_action( - "tag.set_clean_mode", SetCleanModeAction, SIMPLE_ACTION_SCHEMA + "tag.emulation_off", + EmulationOffAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "tag.set_format_mode", SetFormatModeAction, SIMPLE_ACTION_SCHEMA + "tag.emulation_on", EmulationOnAction, SIMPLE_ACTION_SCHEMA, synchronous=True ) @automation.register_action( - "tag.set_read_mode", SetReadModeAction, SIMPLE_ACTION_SCHEMA + "tag.polling_off", PollingOffAction, SIMPLE_ACTION_SCHEMA, synchronous=True ) @automation.register_action( - "tag.set_write_mode", SetWriteModeAction, SIMPLE_ACTION_SCHEMA + "tag.polling_on", PollingOnAction, SIMPLE_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "tag.set_clean_mode", + SetCleanModeAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, +) +@automation.register_action( + "tag.set_format_mode", + SetFormatModeAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, +) +@automation.register_action( + "tag.set_read_mode", + SetReadModeAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, +) +@automation.register_action( + "tag.set_write_mode", + SetWriteModeAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, ) async def pn7150_simple_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/pn7160/__init__.py b/esphome/components/pn7160/__init__.py index 899ecd595e..54e4b74796 100644 --- a/esphome/components/pn7160/__init__.py +++ b/esphome/components/pn7160/__init__.py @@ -123,11 +123,13 @@ PN7160_SCHEMA = cv.Schema( "tag.set_emulation_message", SetEmulationMessageAction, SET_MESSAGE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( "tag.set_write_message", SetWriteMessageAction, SET_MESSAGE_ACTION_SCHEMA, + synchronous=True, ) async def pn7160_set_message_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -142,22 +144,43 @@ async def pn7160_set_message_to_code(config, action_id, template_arg, args): @automation.register_action( - "tag.emulation_off", EmulationOffAction, SIMPLE_ACTION_SCHEMA -) -@automation.register_action("tag.emulation_on", EmulationOnAction, SIMPLE_ACTION_SCHEMA) -@automation.register_action("tag.polling_off", PollingOffAction, SIMPLE_ACTION_SCHEMA) -@automation.register_action("tag.polling_on", PollingOnAction, SIMPLE_ACTION_SCHEMA) -@automation.register_action( - "tag.set_clean_mode", SetCleanModeAction, SIMPLE_ACTION_SCHEMA + "tag.emulation_off", + EmulationOffAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "tag.set_format_mode", SetFormatModeAction, SIMPLE_ACTION_SCHEMA + "tag.emulation_on", EmulationOnAction, SIMPLE_ACTION_SCHEMA, synchronous=True ) @automation.register_action( - "tag.set_read_mode", SetReadModeAction, SIMPLE_ACTION_SCHEMA + "tag.polling_off", PollingOffAction, SIMPLE_ACTION_SCHEMA, synchronous=True ) @automation.register_action( - "tag.set_write_mode", SetWriteModeAction, SIMPLE_ACTION_SCHEMA + "tag.polling_on", PollingOnAction, SIMPLE_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "tag.set_clean_mode", + SetCleanModeAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, +) +@automation.register_action( + "tag.set_format_mode", + SetFormatModeAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, +) +@automation.register_action( + "tag.set_read_mode", + SetReadModeAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, +) +@automation.register_action( + "tag.set_write_mode", + SetWriteModeAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, ) async def pn7160_simple_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index 0124463567..c09d778eda 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -155,6 +155,7 @@ async def to_code(config): cv.Required(CONF_VALUE): cv.templatable(cv.uint32_t), } ), + synchronous=True, ) async def set_total_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/pulse_meter/sensor.py b/esphome/components/pulse_meter/sensor.py index ca026eefa4..499b7309c8 100644 --- a/esphome/components/pulse_meter/sensor.py +++ b/esphome/components/pulse_meter/sensor.py @@ -105,6 +105,7 @@ async def to_code(config): cv.Required(CONF_VALUE): cv.templatable(cv.uint32_t), } ), + synchronous=True, ) async def set_total_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py index 3af73b8695..fa1c3961d0 100644 --- a/esphome/components/pzemac/sensor.py +++ b/esphome/components/pzemac/sensor.py @@ -88,6 +88,7 @@ CONFIG_SCHEMA = ( cv.Required(CONF_ID): cv.use_id(PZEMAC), } ), + synchronous=True, ) async def reset_energy_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/pzemdc/sensor.py b/esphome/components/pzemdc/sensor.py index 383a9dbb2c..3291be4c34 100644 --- a/esphome/components/pzemdc/sensor.py +++ b/esphome/components/pzemdc/sensor.py @@ -72,6 +72,7 @@ CONFIG_SCHEMA = ( cv.GenerateID(CONF_ID): cv.use_id(PZEMDC), } ), + synchronous=True, ) async def reset_energy_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 9d3e655c57..bf17ac27b8 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -163,7 +163,10 @@ BASE_REMOTE_TRANSMITTER_SCHEMA = cv.Schema( def register_action(name, type_, schema): validator = templatize(schema).extend(BASE_REMOTE_TRANSMITTER_SCHEMA) registerer = automation.register_action( - f"remote_transmitter.transmit_{name}", type_, validator + f"remote_transmitter.transmit_{name}", + type_, + validator, + synchronous=True, ) def decorator(func): diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index 371dbb685f..89019e296e 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -120,7 +120,10 @@ DIGITAL_WRITE_ACTION_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "remote_transmitter.digital_write", DigitalWriteAction, DIGITAL_WRITE_ACTION_SCHEMA + "remote_transmitter.digital_write", + DigitalWriteAction, + DIGITAL_WRITE_ACTION_SCHEMA, + synchronous=True, ) async def digital_write_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/rf_bridge/__init__.py b/esphome/components/rf_bridge/__init__.py index b4770726b4..934f24b789 100644 --- a/esphome/components/rf_bridge/__init__.py +++ b/esphome/components/rf_bridge/__init__.py @@ -114,7 +114,10 @@ RFBRIDGE_SEND_CODE_SCHEMA = cv.Schema( @automation.register_action( - "rf_bridge.send_code", RFBridgeSendCodeAction, RFBRIDGE_SEND_CODE_SCHEMA + "rf_bridge.send_code", + RFBridgeSendCodeAction, + RFBRIDGE_SEND_CODE_SCHEMA, + synchronous=True, ) async def rf_bridge_send_code_to_code(config, action_id, template_args, args): paren = await cg.get_variable(config[CONF_ID]) @@ -133,7 +136,9 @@ async def rf_bridge_send_code_to_code(config, action_id, template_args, args): RFBRIDGE_ID_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(RFBridgeComponent)}) -@automation.register_action("rf_bridge.learn", RFBridgeLearnAction, RFBRIDGE_ID_SCHEMA) +@automation.register_action( + "rf_bridge.learn", RFBridgeLearnAction, RFBRIDGE_ID_SCHEMA, synchronous=True +) async def rf_bridge_learnx_to_code(config, action_id, template_args, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_args, paren) @@ -143,6 +148,7 @@ async def rf_bridge_learnx_to_code(config, action_id, template_args, args): "rf_bridge.start_advanced_sniffing", RFBridgeStartAdvancedSniffingAction, RFBRIDGE_ID_SCHEMA, + synchronous=True, ) async def rf_bridge_start_advanced_sniffing_to_code( config, action_id, template_args, args @@ -155,6 +161,7 @@ async def rf_bridge_start_advanced_sniffing_to_code( "rf_bridge.stop_advanced_sniffing", RFBridgeStopAdvancedSniffingAction, RFBRIDGE_ID_SCHEMA, + synchronous=True, ) async def rf_bridge_stop_advanced_sniffing_to_code( config, action_id, template_args, args @@ -167,6 +174,7 @@ async def rf_bridge_stop_advanced_sniffing_to_code( "rf_bridge.start_bucket_sniffing", RFBridgeStartBucketSniffingAction, RFBRIDGE_ID_SCHEMA, + synchronous=True, ) async def rf_bridge_start_bucket_sniffing_to_code( config, action_id, template_args, args @@ -189,6 +197,7 @@ RFBRIDGE_SEND_ADVANCED_CODE_SCHEMA = cv.Schema( "rf_bridge.send_advanced_code", RFBridgeSendAdvancedCodeAction, RFBRIDGE_SEND_ADVANCED_CODE_SCHEMA, + synchronous=True, ) async def rf_bridge_send_advanced_code_to_code(config, action_id, template_args, args): paren = await cg.get_variable(config[CONF_ID]) @@ -211,7 +220,10 @@ RFBRIDGE_SEND_RAW_SCHEMA = cv.Schema( @automation.register_action( - "rf_bridge.send_raw", RFBridgeSendRawAction, RFBRIDGE_SEND_RAW_SCHEMA + "rf_bridge.send_raw", + RFBridgeSendRawAction, + RFBRIDGE_SEND_RAW_SCHEMA, + synchronous=True, ) async def rf_bridge_send_raw_to_code(config, action_id, template_args, args): paren = await cg.get_variable(config[CONF_ID]) @@ -229,7 +241,9 @@ RFBRIDGE_BEEP_SCHEMA = cv.Schema( ) -@automation.register_action("rf_bridge.beep", RFBridgeBeepAction, RFBRIDGE_BEEP_SCHEMA) +@automation.register_action( + "rf_bridge.beep", RFBridgeBeepAction, RFBRIDGE_BEEP_SCHEMA, synchronous=True +) async def rf_bridge_beep_to_code(config, action_id, template_args, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_args, paren) diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index 645b4a81c5..be315db55d 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -139,6 +139,7 @@ async def to_code(config): cv.Required(CONF_VALUE): cv.templatable(cv.int_), } ), + synchronous=True, ) async def sensor_template_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/rp2040_pwm/output.py b/esphome/components/rp2040_pwm/output.py index 441a52de7f..4ea488a6cd 100644 --- a/esphome/components/rp2040_pwm/output.py +++ b/esphome/components/rp2040_pwm/output.py @@ -42,6 +42,7 @@ async def to_code(config): cv.Required(CONF_FREQUENCY): cv.templatable(validate_frequency), } ), + synchronous=True, ) async def rp2040_set_frequency_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py index 19412bb454..3566734200 100644 --- a/esphome/components/rtttl/__init__.py +++ b/esphome/components/rtttl/__init__.py @@ -117,6 +117,7 @@ async def to_code(config): }, key=CONF_RTTTL, ), + synchronous=True, ) async def rtttl_play_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -134,6 +135,7 @@ async def rtttl_play_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(Rtttl), } ), + synchronous=True, ) async def rtttl_stop_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/rx8130/time.py b/esphome/components/rx8130/time.py index cb0402bd32..4f6310358c 100644 --- a/esphome/components/rx8130/time.py +++ b/esphome/components/rx8130/time.py @@ -27,6 +27,7 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( cv.GenerateID(): cv.use_id(RX8130Component), } ), + synchronous=True, ) async def rx8130_write_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -42,6 +43,7 @@ async def rx8130_write_time_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(RX8130Component), } ), + synchronous=True, ) async def rx8130_read_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index f54151b746..e868985054 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -62,6 +62,7 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(): cv.use_id(SafeModeComponent), } ), + synchronous=True, ) async def safe_mode_mark_successful_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index 194df8ec4f..f60e913a0c 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -128,6 +128,7 @@ async def to_code(config): }, key=CONF_VALUE, ), + synchronous=True, ) async def scd30_force_recalibration_with_reference_to_code( config, action_id, template_arg, args diff --git a/esphome/components/scd4x/sensor.py b/esphome/components/scd4x/sensor.py index ec90234ac3..6f14118660 100644 --- a/esphome/components/scd4x/sensor.py +++ b/esphome/components/scd4x/sensor.py @@ -141,6 +141,7 @@ SCD4X_ACTION_SCHEMA = maybe_simple_id( "scd4x.perform_forced_calibration", PerformForcedCalibrationAction, SCD4X_ACTION_SCHEMA, + synchronous=True, ) async def scd4x_frc_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -158,7 +159,10 @@ SCD4X_RESET_ACTION_SCHEMA = maybe_simple_id( @automation.register_action( - "scd4x.factory_reset", FactoryResetAction, SCD4X_RESET_ACTION_SCHEMA + "scd4x.factory_reset", + FactoryResetAction, + SCD4X_RESET_ACTION_SCHEMA, + synchronous=True, ) async def scd4x_reset_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index 369cefad91..51cae695b7 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -221,6 +221,7 @@ async def script_stop_action_to_code(config, action_id, template_arg, args): "script.wait", ScriptWaitAction, maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}), + synchronous=False, ) async def script_wait_action_to_code(config, action_id, template_arg, args): full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID]) diff --git a/esphome/components/sen5x/sensor.py b/esphome/components/sen5x/sensor.py index 538a2f5239..9fe51121f1 100644 --- a/esphome/components/sen5x/sensor.py +++ b/esphome/components/sen5x/sensor.py @@ -267,7 +267,10 @@ SEN5X_ACTION_SCHEMA = maybe_simple_id( @automation.register_action( - "sen5x.start_fan_autoclean", StartFanAction, SEN5X_ACTION_SCHEMA + "sen5x.start_fan_autoclean", + StartFanAction, + SEN5X_ACTION_SCHEMA, + synchronous=True, ) async def sen54_fan_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/senseair/sensor.py b/esphome/components/senseair/sensor.py index 2eb2617e30..c5bef76741 100644 --- a/esphome/components/senseair/sensor.py +++ b/esphome/components/senseair/sensor.py @@ -73,20 +73,31 @@ CALIBRATION_ACTION_SCHEMA = maybe_simple_id( "senseair.background_calibration", SenseAirBackgroundCalibrationAction, CALIBRATION_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( "senseair.background_calibration_result", SenseAirBackgroundCalibrationResultAction, CALIBRATION_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "senseair.abc_enable", SenseAirABCEnableAction, CALIBRATION_ACTION_SCHEMA + "senseair.abc_enable", + SenseAirABCEnableAction, + CALIBRATION_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "senseair.abc_disable", SenseAirABCDisableAction, CALIBRATION_ACTION_SCHEMA + "senseair.abc_disable", + SenseAirABCDisableAction, + CALIBRATION_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "senseair.abc_get_period", SenseAirABCGetPeriodAction, CALIBRATION_ACTION_SCHEMA + "senseair.abc_get_period", + SenseAirABCGetPeriodAction, + CALIBRATION_ACTION_SCHEMA, + synchronous=True, ) async def senseair_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/servo/__init__.py b/esphome/components/servo/__init__.py index 2fee2840a5..a23bb53536 100644 --- a/esphome/components/servo/__init__.py +++ b/esphome/components/servo/__init__.py @@ -62,6 +62,7 @@ async def to_code(config): cv.Required(CONF_LEVEL): cv.templatable(cv.possibly_negative_percentage), } ), + synchronous=True, ) async def servo_write_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -79,6 +80,7 @@ async def servo_write_to_code(config, action_id, template_arg, args): cv.Required(CONF_ID): cv.use_id(Servo), } ), + synchronous=True, ) async def servo_detach_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py index c48a3c63c4..ebb74302a9 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -135,7 +135,10 @@ SIM800L_SEND_SMS_SCHEMA = cv.Schema( @automation.register_action( - "sim800l.send_sms", Sim800LSendSmsAction, SIM800L_SEND_SMS_SCHEMA + "sim800l.send_sms", + Sim800LSendSmsAction, + SIM800L_SEND_SMS_SCHEMA, + synchronous=True, ) async def sim800l_send_sms_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -155,7 +158,9 @@ SIM800L_DIAL_SCHEMA = cv.Schema( ) -@automation.register_action("sim800l.dial", Sim800LDialAction, SIM800L_DIAL_SCHEMA) +@automation.register_action( + "sim800l.dial", Sim800LDialAction, SIM800L_DIAL_SCHEMA, synchronous=True +) async def sim800l_dial_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) @@ -168,6 +173,7 @@ async def sim800l_dial_to_code(config, action_id, template_arg, args): "sim800l.connect", Sim800LConnectAction, cv.Schema({cv.GenerateID(): cv.use_id(Sim800LComponent)}), + synchronous=True, ) async def sim800l_connect_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -183,7 +189,10 @@ SIM800L_SEND_USSD_SCHEMA = cv.Schema( @automation.register_action( - "sim800l.send_ussd", Sim800LSendUssdAction, SIM800L_SEND_USSD_SCHEMA + "sim800l.send_ussd", + Sim800LSendUssdAction, + SIM800L_SEND_USSD_SCHEMA, + synchronous=True, ) async def sim800l_send_ussd_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -197,6 +206,7 @@ async def sim800l_send_ussd_to_code(config, action_id, template_arg, args): "sim800l.disconnect", Sim800LDisconnectAction, cv.Schema({cv.GenerateID(): cv.use_id(Sim800LComponent)}), + synchronous=True, ) async def sim800l_disconnect_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/sound_level/sensor.py b/esphome/components/sound_level/sensor.py index 8ca0feccc0..44f31979b4 100644 --- a/esphome/components/sound_level/sensor.py +++ b/esphome/components/sound_level/sensor.py @@ -89,8 +89,12 @@ SOUND_LEVEL_ACTION_SCHEMA = automation.maybe_simple_id( ) -@automation.register_action("sound_level.start", StartAction, SOUND_LEVEL_ACTION_SCHEMA) -@automation.register_action("sound_level.stop", StopAction, SOUND_LEVEL_ACTION_SCHEMA) +@automation.register_action( + "sound_level.start", StartAction, SOUND_LEVEL_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "sound_level.stop", StopAction, SOUND_LEVEL_ACTION_SCHEMA, synchronous=True +) async def sound_level_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) diff --git a/esphome/components/speaker/__init__.py b/esphome/components/speaker/__init__.py index 10ee6d5212..8480eebcdb 100644 --- a/esphome/components/speaker/__init__.py +++ b/esphome/components/speaker/__init__.py @@ -78,6 +78,7 @@ async def speaker_action(config, action_id, template_arg, args): }, key=CONF_DATA, ), + synchronous=True, ) async def speaker_play_action(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -95,12 +96,12 @@ async def speaker_play_action(config, action_id, template_arg, args): return var -automation.register_action("speaker.stop", StopAction, SPEAKER_AUTOMATION_SCHEMA)( - speaker_action -) -automation.register_action("speaker.finish", FinishAction, SPEAKER_AUTOMATION_SCHEMA)( - speaker_action -) +automation.register_action( + "speaker.stop", StopAction, SPEAKER_AUTOMATION_SCHEMA, synchronous=True +)(speaker_action) +automation.register_action( + "speaker.finish", FinishAction, SPEAKER_AUTOMATION_SCHEMA, synchronous=True +)(speaker_action) automation.register_condition( "speaker.is_playing", IsPlayingCondition, SPEAKER_AUTOMATION_SCHEMA @@ -121,6 +122,7 @@ automation.register_condition( }, key=CONF_VOLUME, ), + synchronous=True, ) async def speaker_volume_set_action(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -131,9 +133,14 @@ async def speaker_volume_set_action(config, action_id, template_arg, args): @automation.register_action( - "speaker.mute_off", MuteOffAction, SPEAKER_AUTOMATION_SCHEMA + "speaker.mute_off", + MuteOffAction, + SPEAKER_AUTOMATION_SCHEMA, + synchronous=True, +) +@automation.register_action( + "speaker.mute_on", MuteOnAction, SPEAKER_AUTOMATION_SCHEMA, synchronous=True ) -@automation.register_action("speaker.mute_on", MuteOnAction, SPEAKER_AUTOMATION_SCHEMA) async def speaker_mute_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 42ca762858..92a178fe95 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -505,6 +505,7 @@ async def to_code(config): }, key=CONF_MEDIA_FILE, ), + synchronous=True, ) async def play_on_device_media_media_action(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py index 50c69f9496..6e2ff4ee2e 100644 --- a/esphome/components/sprinkler/__init__.py +++ b/esphome/components/sprinkler/__init__.py @@ -422,6 +422,7 @@ CONFIG_SCHEMA = cv.All( "sprinkler.set_divider", SetDividerAction, SPRINKLER_ACTION_SET_DIVIDER_SCHEMA, + synchronous=True, ) async def sprinkler_set_divider_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -435,6 +436,7 @@ async def sprinkler_set_divider_to_code(config, action_id, template_arg, args): "sprinkler.set_multiplier", SetMultiplierAction, SPRINKLER_ACTION_SET_MULTIPLIER_SCHEMA, + synchronous=True, ) async def sprinkler_set_multiplier_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -448,6 +450,7 @@ async def sprinkler_set_multiplier_to_code(config, action_id, template_arg, args "sprinkler.queue_valve", QueueValveAction, SPRINKLER_ACTION_QUEUE_VALVE_SCHEMA, + synchronous=True, ) async def sprinkler_set_queued_valve_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -463,6 +466,7 @@ async def sprinkler_set_queued_valve_to_code(config, action_id, template_arg, ar "sprinkler.set_repeat", SetRepeatAction, SPRINKLER_ACTION_REPEAT_SCHEMA, + synchronous=True, ) async def sprinkler_set_repeat_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -476,6 +480,7 @@ async def sprinkler_set_repeat_to_code(config, action_id, template_arg, args): "sprinkler.set_valve_run_duration", SetRunDurationAction, SPRINKLER_ACTION_SET_RUN_DURATION_SCHEMA, + synchronous=True, ) async def sprinkler_set_valve_run_duration_to_code( config, action_id, template_arg, args @@ -490,7 +495,10 @@ async def sprinkler_set_valve_run_duration_to_code( @automation.register_action( - "sprinkler.start_from_queue", StartFromQueueAction, SPRINKLER_ACTION_SCHEMA + "sprinkler.start_from_queue", + StartFromQueueAction, + SPRINKLER_ACTION_SCHEMA, + synchronous=True, ) async def sprinkler_start_from_queue_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -498,7 +506,10 @@ async def sprinkler_start_from_queue_to_code(config, action_id, template_arg, ar @automation.register_action( - "sprinkler.start_full_cycle", StartFullCycleAction, SPRINKLER_ACTION_SCHEMA + "sprinkler.start_full_cycle", + StartFullCycleAction, + SPRINKLER_ACTION_SCHEMA, + synchronous=True, ) async def sprinkler_start_full_cycle_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -509,6 +520,7 @@ async def sprinkler_start_full_cycle_to_code(config, action_id, template_arg, ar "sprinkler.start_single_valve", StartSingleValveAction, SPRINKLER_ACTION_SINGLE_VALVE_SCHEMA, + synchronous=True, ) async def sprinkler_start_single_valve_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -522,21 +534,40 @@ async def sprinkler_start_single_valve_to_code(config, action_id, template_arg, @automation.register_action( - "sprinkler.clear_queued_valves", ClearQueuedValvesAction, SPRINKLER_ACTION_SCHEMA + "sprinkler.clear_queued_valves", + ClearQueuedValvesAction, + SPRINKLER_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sprinkler.next_valve", NextValveAction, SPRINKLER_ACTION_SCHEMA + "sprinkler.next_valve", + NextValveAction, + SPRINKLER_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sprinkler.previous_valve", PreviousValveAction, SPRINKLER_ACTION_SCHEMA -) -@automation.register_action("sprinkler.pause", PauseAction, SPRINKLER_ACTION_SCHEMA) -@automation.register_action("sprinkler.resume", ResumeAction, SPRINKLER_ACTION_SCHEMA) -@automation.register_action( - "sprinkler.resume_or_start_full_cycle", ResumeOrStartAction, SPRINKLER_ACTION_SCHEMA + "sprinkler.previous_valve", + PreviousValveAction, + SPRINKLER_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sprinkler.shutdown", ShutdownAction, SPRINKLER_ACTION_SCHEMA + "sprinkler.pause", PauseAction, SPRINKLER_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "sprinkler.resume", ResumeAction, SPRINKLER_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "sprinkler.resume_or_start_full_cycle", + ResumeOrStartAction, + SPRINKLER_ACTION_SCHEMA, + synchronous=True, +) +@automation.register_action( + "sprinkler.shutdown", + ShutdownAction, + SPRINKLER_ACTION_SCHEMA, + synchronous=True, ) async def sprinkler_simple_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/sps30/sensor.py b/esphome/components/sps30/sensor.py index 3c967fc01b..40557f2cbd 100644 --- a/esphome/components/sps30/sensor.py +++ b/esphome/components/sps30/sensor.py @@ -180,13 +180,22 @@ SPS30_ACTION_SCHEMA = maybe_simple_id( @automation.register_action( - "sps30.start_fan_autoclean", StartFanAction, SPS30_ACTION_SCHEMA + "sps30.start_fan_autoclean", + StartFanAction, + SPS30_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sps30.start_measurement", StartMeasurementAction, SPS30_ACTION_SCHEMA + "sps30.start_measurement", + StartMeasurementAction, + SPS30_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sps30.stop_measurement", StopMeasurementAction, SPS30_ACTION_SCHEMA + "sps30.stop_measurement", + StopMeasurementAction, + SPS30_ACTION_SCHEMA, + synchronous=True, ) async def sps30_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/stepper/__init__.py b/esphome/components/stepper/__init__.py index 62bc71f2d1..27d4fc276d 100644 --- a/esphome/components/stepper/__init__.py +++ b/esphome/components/stepper/__init__.py @@ -97,6 +97,7 @@ async def register_stepper(var, config): cv.Required(CONF_TARGET): cv.templatable(cv.int_), } ), + synchronous=True, ) async def stepper_set_target_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -115,6 +116,7 @@ async def stepper_set_target_to_code(config, action_id, template_arg, args): cv.Required(CONF_POSITION): cv.templatable(cv.int_), } ), + synchronous=True, ) async def stepper_report_position_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -133,6 +135,7 @@ async def stepper_report_position_to_code(config, action_id, template_arg, args) cv.Required(CONF_SPEED): cv.templatable(validate_speed), } ), + synchronous=True, ) async def stepper_set_speed_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -151,6 +154,7 @@ async def stepper_set_speed_to_code(config, action_id, template_arg, args): cv.Required(CONF_ACCELERATION): cv.templatable(validate_acceleration), } ), + synchronous=True, ) async def stepper_set_acceleration_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -169,6 +173,7 @@ async def stepper_set_acceleration_to_code(config, action_id, template_arg, args cv.Required(CONF_DECELERATION): cv.templatable(validate_acceleration), } ), + synchronous=True, ) async def stepper_set_deceleration_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/sx126x/__init__.py b/esphome/components/sx126x/__init__.py index 413eb139d6..08f4c0fb88 100644 --- a/esphome/components/sx126x/__init__.py +++ b/esphome/components/sx126x/__init__.py @@ -290,19 +290,34 @@ NO_ARGS_ACTION_SCHEMA = automation.maybe_simple_id( @automation.register_action( - "sx126x.run_image_cal", RunImageCalAction, NO_ARGS_ACTION_SCHEMA + "sx126x.run_image_cal", + RunImageCalAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sx126x.set_mode_tx", SetModeTxAction, NO_ARGS_ACTION_SCHEMA + "sx126x.set_mode_tx", + SetModeTxAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sx126x.set_mode_rx", SetModeRxAction, NO_ARGS_ACTION_SCHEMA + "sx126x.set_mode_rx", + SetModeRxAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sx126x.set_mode_sleep", SetModeSleepAction, NO_ARGS_ACTION_SCHEMA + "sx126x.set_mode_sleep", + SetModeSleepAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sx126x.set_mode_standby", SetModeStandbyAction, NO_ARGS_ACTION_SCHEMA + "sx126x.set_mode_standby", + SetModeStandbyAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def no_args_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -320,7 +335,10 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "sx126x.send_packet", SendPacketAction, SEND_PACKET_ACTION_SCHEMA + "sx126x.send_packet", + SendPacketAction, + SEND_PACKET_ACTION_SCHEMA, + synchronous=True, ) async def send_packet_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/sx127x/__init__.py b/esphome/components/sx127x/__init__.py index f3a9cca93f..7f554fbf84 100644 --- a/esphome/components/sx127x/__init__.py +++ b/esphome/components/sx127x/__init__.py @@ -283,19 +283,34 @@ NO_ARGS_ACTION_SCHEMA = automation.maybe_simple_id( @automation.register_action( - "sx127x.run_image_cal", RunImageCalAction, NO_ARGS_ACTION_SCHEMA + "sx127x.run_image_cal", + RunImageCalAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sx127x.set_mode_tx", SetModeTxAction, NO_ARGS_ACTION_SCHEMA + "sx127x.set_mode_tx", + SetModeTxAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sx127x.set_mode_rx", SetModeRxAction, NO_ARGS_ACTION_SCHEMA + "sx127x.set_mode_rx", + SetModeRxAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sx127x.set_mode_sleep", SetModeSleepAction, NO_ARGS_ACTION_SCHEMA + "sx127x.set_mode_sleep", + SetModeSleepAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sx127x.set_mode_standby", SetModeStandbyAction, NO_ARGS_ACTION_SCHEMA + "sx127x.set_mode_standby", + SetModeStandbyAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def no_args_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -313,7 +328,10 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "sx127x.send_packet", SendPacketAction, SEND_PACKET_ACTION_SCHEMA + "sx127x.send_packet", + SendPacketAction, + SEND_PACKET_ACTION_SCHEMA, + synchronous=True, ) async def send_packet_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/template/binary_sensor/__init__.py b/esphome/components/template/binary_sensor/__init__.py index 9d4208dcca..e537e1f97c 100644 --- a/esphome/components/template/binary_sensor/__init__.py +++ b/esphome/components/template/binary_sensor/__init__.py @@ -59,6 +59,7 @@ async def to_code(config): cv.Required(CONF_STATE): cv.templatable(cv.boolean), } ), + synchronous=True, ) async def binary_sensor_template_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index a4fb0b7021..cfc19c00cd 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -125,6 +125,7 @@ async def to_code(config): cv.Optional(CONF_TILT): cv.templatable(cv.zero_to_one_float), } ), + synchronous=True, ) async def cover_template_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/template/lock/__init__.py b/esphome/components/template/lock/__init__.py index 4c74a521fa..d8bd9d16c6 100644 --- a/esphome/components/template/lock/__init__.py +++ b/esphome/components/template/lock/__init__.py @@ -90,6 +90,7 @@ async def to_code(config): }, key=CONF_STATE, ), + synchronous=True, ) async def lock_template_publish_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/template/sensor/__init__.py b/esphome/components/template/sensor/__init__.py index 2c325427e9..b0f48ade46 100644 --- a/esphome/components/template/sensor/__init__.py +++ b/esphome/components/template/sensor/__init__.py @@ -44,6 +44,7 @@ async def to_code(config): cv.Required(CONF_STATE): cv.templatable(cv.float_), } ), + synchronous=True, ) async def sensor_template_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/template/switch/__init__.py b/esphome/components/template/switch/__init__.py index 8ae5a07dc3..eb6f0f46de 100644 --- a/esphome/components/template/switch/__init__.py +++ b/esphome/components/template/switch/__init__.py @@ -80,6 +80,7 @@ async def to_code(config): cv.Required(CONF_STATE): cv.templatable(cv.boolean), } ), + synchronous=True, ) async def switch_template_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/template/text_sensor/__init__.py b/esphome/components/template/text_sensor/__init__.py index 550b27356d..ddbdd6dadb 100644 --- a/esphome/components/template/text_sensor/__init__.py +++ b/esphome/components/template/text_sensor/__init__.py @@ -43,6 +43,7 @@ async def to_code(config): cv.Required(CONF_STATE): cv.templatable(cv.string_strict), } ), + synchronous=True, ) async def text_sensor_template_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/template/valve/__init__.py b/esphome/components/template/valve/__init__.py index 526751564d..3e8fd81603 100644 --- a/esphome/components/template/valve/__init__.py +++ b/esphome/components/template/valve/__init__.py @@ -112,6 +112,7 @@ async def to_code(config): ), } ), + synchronous=True, ) async def valve_template_publish_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/template/water_heater/__init__.py b/esphome/components/template/water_heater/__init__.py index cb5f2dbe56..814aa40193 100644 --- a/esphome/components/template/water_heater/__init__.py +++ b/esphome/components/template/water_heater/__init__.py @@ -134,6 +134,7 @@ async def to_code(config: ConfigType) -> None: cv.Optional(CONF_IS_ON): cv.templatable(cv.boolean), } ), + synchronous=True, ) async def water_heater_template_publish_to_code( config: ConfigType, diff --git a/esphome/components/tm1651/__init__.py b/esphome/components/tm1651/__init__.py index 49796d9b42..fb35eb21b5 100644 --- a/esphome/components/tm1651/__init__.py +++ b/esphome/components/tm1651/__init__.py @@ -73,6 +73,7 @@ BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id( }, key=CONF_BRIGHTNESS, ), + synchronous=True, ) async def tm1651_set_brightness_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -92,6 +93,7 @@ async def tm1651_set_brightness_to_code(config, action_id, template_arg, args): }, key=CONF_LEVEL, ), + synchronous=True, ) async def tm1651_set_level_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -111,6 +113,7 @@ async def tm1651_set_level_to_code(config, action_id, template_arg, args): }, key=CONF_LEVEL_PERCENT, ), + synchronous=True, ) async def tm1651_set_level_percent_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -121,7 +124,10 @@ async def tm1651_set_level_percent_to_code(config, action_id, template_arg, args @automation.register_action( - "tm1651.turn_off", TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA + "tm1651.turn_off", + TurnOffAction, + BINARY_OUTPUT_ACTION_SCHEMA, + synchronous=True, ) async def output_turn_off_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -129,7 +135,9 @@ async def output_turn_off_to_code(config, action_id, template_arg, args): return var -@automation.register_action("tm1651.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA) +@automation.register_action( + "tm1651.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA, synchronous=True +) async def output_turn_on_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 2cb6eac050..83649cc209 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -500,6 +500,7 @@ async def register_uart_device(var, config): }, key=CONF_DATA, ), + synchronous=True, ) async def uart_write_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index 37dd871a6c..17bbf19c9e 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -171,6 +171,7 @@ def validate_raw_data(value): }, key=CONF_DATA, ), + synchronous=True, ) async def udp_write_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/ufire_ec/sensor.py b/esphome/components/ufire_ec/sensor.py index 9edf0f89ff..10b4ece614 100644 --- a/esphome/components/ufire_ec/sensor.py +++ b/esphome/components/ufire_ec/sensor.py @@ -97,6 +97,7 @@ UFIRE_EC_CALIBRATE_PROBE_SCHEMA = cv.Schema( "ufire_ec.calibrate_probe", UFireECCalibrateProbeAction, UFIRE_EC_CALIBRATE_PROBE_SCHEMA, + synchronous=True, ) async def ufire_ec_calibrate_probe_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -119,6 +120,7 @@ UFIRE_EC_RESET_SCHEMA = cv.Schema( "ufire_ec.reset", UFireECResetAction, UFIRE_EC_RESET_SCHEMA, + synchronous=True, ) async def ufire_ec_reset_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/ufire_ise/sensor.py b/esphome/components/ufire_ise/sensor.py index 8009cdaa6a..a116012d05 100644 --- a/esphome/components/ufire_ise/sensor.py +++ b/esphome/components/ufire_ise/sensor.py @@ -91,6 +91,7 @@ UFIRE_ISE_CALIBRATE_PROBE_SCHEMA = cv.Schema( "ufire_ise.calibrate_probe_low", UFireISECalibrateProbeLowAction, UFIRE_ISE_CALIBRATE_PROBE_SCHEMA, + synchronous=True, ) async def ufire_ise_calibrate_probe_low_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -104,6 +105,7 @@ async def ufire_ise_calibrate_probe_low_to_code(config, action_id, template_arg, "ufire_ise.calibrate_probe_high", UFireISECalibrateProbeHighAction, UFIRE_ISE_CALIBRATE_PROBE_SCHEMA, + synchronous=True, ) async def ufire_ise_calibrate_probe_high_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -120,6 +122,7 @@ UFIRE_ISE_RESET_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(UFireISEComponent "ufire_ise.reset", UFireISEResetAction, UFIRE_ISE_RESET_SCHEMA, + synchronous=True, ) async def ufire_ise_reset_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index c36a4ab769..db6c1445e3 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -138,6 +138,7 @@ async def to_code(config): cv.Optional(CONF_FORCE_UPDATE, default=False): cv.templatable(cv.boolean), } ), + synchronous=True, ) async def update_perform_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -156,6 +157,7 @@ async def update_perform_action_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(UpdateEntity), } ), + synchronous=True, ) async def update_check_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index 22cd01988d..0319ff50e7 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -180,25 +180,33 @@ VALVE_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action("valve.open", OpenAction, VALVE_ACTION_SCHEMA) +@automation.register_action( + "valve.open", OpenAction, VALVE_ACTION_SCHEMA, synchronous=True +) async def valve_open_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("valve.close", CloseAction, VALVE_ACTION_SCHEMA) +@automation.register_action( + "valve.close", CloseAction, VALVE_ACTION_SCHEMA, synchronous=True +) async def valve_close_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("valve.stop", StopAction, VALVE_ACTION_SCHEMA) +@automation.register_action( + "valve.stop", StopAction, VALVE_ACTION_SCHEMA, synchronous=True +) async def valve_stop_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("valve.toggle", ToggleAction, VALVE_ACTION_SCHEMA) +@automation.register_action( + "valve.toggle", ToggleAction, VALVE_ACTION_SCHEMA, synchronous=True +) async def valve_toggle_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @@ -214,7 +222,9 @@ VALVE_CONTROL_ACTION_SCHEMA = cv.Schema( ) -@automation.register_action("valve.control", ControlAction, VALVE_CONTROL_ACTION_SCHEMA) +@automation.register_action( + "valve.control", ControlAction, VALVE_CONTROL_ACTION_SCHEMA, synchronous=True +) async def valve_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 8b7dcb4f21..d970df2a44 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -393,6 +393,7 @@ VOICE_ASSISTANT_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(VoiceAssis "voice_assistant.start_continuous", StartContinuousAction, VOICE_ASSISTANT_ACTION_SCHEMA, + synchronous=True, ) @register_action( "voice_assistant.start", @@ -403,6 +404,7 @@ VOICE_ASSISTANT_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(VoiceAssis cv.Optional(CONF_WAKE_WORD): cv.templatable(cv.string), } ), + synchronous=True, ) async def voice_assistant_listen_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -415,7 +417,9 @@ async def voice_assistant_listen_to_code(config, action_id, template_arg, args): return var -@register_action("voice_assistant.stop", StopAction, VOICE_ASSISTANT_ACTION_SCHEMA) +@register_action( + "voice_assistant.stop", StopAction, VOICE_ASSISTANT_ACTION_SCHEMA, synchronous=True +) async def voice_assistant_stop_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 0f86ec059e..2808d31311 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -670,12 +670,16 @@ async def wifi_ap_active_to_code(config, condition_id, template_arg, args): return cg.new_Pvariable(condition_id, template_arg) -@automation.register_action("wifi.enable", WiFiEnableAction, cv.Schema({})) +@automation.register_action( + "wifi.enable", WiFiEnableAction, cv.Schema({}), synchronous=True +) async def wifi_enable_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg) -@automation.register_action("wifi.disable", WiFiDisableAction, cv.Schema({})) +@automation.register_action( + "wifi.disable", WiFiDisableAction, cv.Schema({}), synchronous=True +) async def wifi_disable_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg) @@ -781,6 +785,7 @@ async def final_step(): cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True), } ), + synchronous=False, ) async def wifi_set_sta_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/wireguard/__init__.py b/esphome/components/wireguard/__init__.py index 124d9a8c32..e2ea61a542 100644 --- a/esphome/components/wireguard/__init__.py +++ b/esphome/components/wireguard/__init__.py @@ -168,6 +168,7 @@ async def wireguard_enabled_to_code(config, condition_id, template_arg, args): "wireguard.enable", WireguardEnableAction, cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)}), + synchronous=True, ) async def wireguard_enable_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -179,6 +180,7 @@ async def wireguard_enable_to_code(config, action_id, template_arg, args): "wireguard.disable", WireguardDisableAction, cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)}), + synchronous=True, ) async def wireguard_disable_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index a327cc2988..280ff6b50c 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -195,6 +195,7 @@ FactoryResetAction = zigbee_ns.class_( "zigbee.factory_reset", FactoryResetAction, ZIGBEE_ACTION_SCHEMA, + synchronous=True, ) async def reset_zigbee_to_code( config: ConfigType, From 4d2ef09a296c4fd3e1b5e723024770ab8b8c3339 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 09:12:10 -1000 Subject: [PATCH 514/609] [log] Detect early log calls before logger init and optimize hot path (#14538) --- esphome/components/logger/__init__.py | 5 +- esphome/core/application.h | 2 + esphome/core/log.cpp | 60 +++++++++++++------ esphome/core/log.h | 12 +++- script/cpp_unit_test.py | 1 + tests/component_tests/logger/__init__.py | 0 tests/component_tests/logger/test_logger.py | 50 ++++++++++++++++ tests/component_tests/logger/test_logger.yaml | 14 +++++ tests/components/main.cpp | 8 +++ tests/integration/conftest.py | 1 + 10 files changed, 134 insertions(+), 19 deletions(-) create mode 100644 tests/component_tests/logger/__init__.py create mode 100644 tests/component_tests/logger/test_logger.py create mode 100644 tests/component_tests/logger/test_logger.yaml diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 83a7854165..e370f4215d 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -337,6 +337,10 @@ async def to_code(config): ) if CORE.is_esp32: cg.add(log.create_pthread_key()) + # pre_setup() must be called before init_log_buffer() because + # init_log_buffer() calls disable_loop() which may log at VV level, + # and global_logger must be set before any logging occurs. + cg.add(log.pre_setup()) if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] if task_log_buffer_size > 0: @@ -356,7 +360,6 @@ async def to_code(config): HARDWARE_UART_TO_UART_SELECTION[config[CONF_HARDWARE_UART]] ) ) - cg.add(log.pre_setup()) # Enable runtime tag levels if logs are configured or explicitly enabled logs_config = config[CONF_LOGS] diff --git a/esphome/core/application.h b/esphome/core/application.h index f357c6b1a3..23bb209eaf 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -142,6 +142,7 @@ static constexpr uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for qu class Application { public: #ifdef ESPHOME_NAME_ADD_MAC_SUFFIX + // Called before Logger::pre_setup() — must not log (global_logger is not yet set). /// Pre-setup with MAC suffix: overwrites placeholder in mutable static buffers with actual MAC. void pre_setup(char *name, size_t name_len, char *friendly_name, size_t friendly_name_len) { arch_init(); @@ -163,6 +164,7 @@ class Application { this->friendly_name_ = StringRef(friendly_name, friendly_name_len); } #else + // Called before Logger::pre_setup() — must not log (global_logger is not yet set). /// Pre-setup without MAC suffix: StringRef points directly at const string literals in flash. void pre_setup(const char *name, size_t name_len, const char *friendly_name, size_t friendly_name_len) { arch_init(); diff --git a/esphome/core/log.cpp b/esphome/core/log.cpp index 8338efbb33..0da457adec 100644 --- a/esphome/core/log.cpp +++ b/esphome/core/log.cpp @@ -1,6 +1,7 @@ #include "log.h" #include "defines.h" #include "helpers.h" +#include <cstdio> #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -8,40 +9,63 @@ namespace esphome { +#ifdef ESPHOME_DEBUG +static void early_log_printf_(const char *tag, int line, const char *format, va_list args) { + fprintf(stderr, "LOG BEFORE LOGGER INIT [%s:%d]: ", tag, line); + vfprintf(stderr, format, args); + fputc('\n', stderr); + assert(false && "log called before Logger::pre_setup()"); // NOLINT +} +#endif + void HOT esp_log_printf_(int level, const char *tag, int line, const char *format, ...) { // NOLINT +#ifdef USE_LOGGER +#ifdef ESPHOME_DEBUG + if (logger::global_logger == nullptr) { + va_list arg; + va_start(arg, format); + early_log_printf_(tag, line, format, arg); + va_end(arg); + return; + } +#endif va_list arg; va_start(arg, format); - esp_log_vprintf_(level, tag, line, format, arg); + logger::global_logger->log_vprintf_(static_cast<uint8_t>(level), tag, line, format, arg); va_end(arg); +#endif } + #ifdef USE_STORE_LOG_STR_IN_FLASH void HOT esp_log_printf_(int level, const char *tag, int line, const __FlashStringHelper *format, ...) { +#ifdef USE_LOGGER + ESPHOME_DEBUG_ASSERT(logger::global_logger != nullptr); va_list arg; va_start(arg, format); - esp_log_vprintf_(level, tag, line, format, arg); + logger::global_logger->log_vprintf_(static_cast<uint8_t>(level), tag, line, format, arg); va_end(arg); +#endif } #endif void HOT esp_log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT #ifdef USE_LOGGER - auto *log = logger::global_logger; - if (log == nullptr) +#ifdef ESPHOME_DEBUG + if (logger::global_logger == nullptr) { + early_log_printf_(tag, line, format, args); return; - - log->log_vprintf_(static_cast<uint8_t>(level), tag, line, format, args); + } +#endif + logger::global_logger->log_vprintf_(static_cast<uint8_t>(level), tag, line, format, args); #endif } #ifdef USE_STORE_LOG_STR_IN_FLASH -void HOT esp_log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, - va_list args) { // NOLINT +// Remove before 2026.9.0 +void HOT esp_log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args) { #ifdef USE_LOGGER - auto *log = logger::global_logger; - if (log == nullptr) - return; - - log->log_vprintf_(static_cast<uint8_t>(level), tag, line, format, args); + ESPHOME_DEBUG_ASSERT(logger::global_logger != nullptr); + logger::global_logger->log_vprintf_(static_cast<uint8_t>(level), tag, line, format, args); #endif } #endif @@ -49,11 +73,13 @@ void HOT esp_log_vprintf_(int level, const char *tag, int line, const __FlashStr #ifdef USE_ESP32 int HOT esp_idf_log_vprintf_(const char *format, va_list args) { // NOLINT #ifdef USE_LOGGER - auto *log = logger::global_logger; - if (log == nullptr) +#ifdef ESPHOME_DEBUG + if (logger::global_logger == nullptr) { + early_log_printf_("esp-idf", 0, format, args); return 0; - - log->log_vprintf_(ESPHOME_LOG_LEVEL, "esp-idf", 0, format, args); + } +#endif + logger::global_logger->log_vprintf_(ESPHOME_LOG_LEVEL, "esp-idf", 0, format, args); #endif return 0; } diff --git a/esphome/core/log.h b/esphome/core/log.h index a2c4b35c6e..ff39633142 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -4,6 +4,14 @@ #include <cassert> #include <cstdarg> + +// Debug assert that only fires when ESPHOME_DEBUG is defined (e.g. in CI/test builds). +// Zero cost in production firmware. +#ifdef ESPHOME_DEBUG +#define ESPHOME_DEBUG_ASSERT(expr) assert(expr) // NOLINT +#else +#define ESPHOME_DEBUG_ASSERT(expr) ((void) 0) +#endif // for PRIu32 and friends #include <cinttypes> #include <string> @@ -61,7 +69,9 @@ void esp_log_printf_(int level, const char *tag, int line, const __FlashStringHe #endif void esp_log_vprintf_(int level, const char *tag, int line, const char *format, va_list args); // NOLINT #ifdef USE_STORE_LOG_STR_IN_FLASH -void esp_log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args); +// Remove before 2026.9.0 +__attribute__((deprecated("Use esp_log_printf_() instead. Removed in 2026.9.0."))) void esp_log_vprintf_( + int level, const char *tag, int line, const __FlashStringHelper *format, va_list args); #endif #if defined(USE_ESP32) int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT diff --git a/script/cpp_unit_test.py b/script/cpp_unit_test.py index e11687dc16..c917458472 100755 --- a/script/cpp_unit_test.py +++ b/script/cpp_unit_test.py @@ -78,6 +78,7 @@ def create_test_config(config_name: str, includes: list[str]) -> dict: "build_flags": [ "-Og", # optimize for debug "-DUSE_TIME_TIMEZONE", # enable timezone code paths for testing + "-DESPHOME_DEBUG", # enable debug assertions ], "debug_build_flags": [ # only for debug builds "-g3", # max debug info diff --git a/tests/component_tests/logger/__init__.py b/tests/component_tests/logger/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/logger/test_logger.py b/tests/component_tests/logger/test_logger.py new file mode 100644 index 0000000000..98aa741964 --- /dev/null +++ b/tests/component_tests/logger/test_logger.py @@ -0,0 +1,50 @@ +"""Tests for the logger component.""" + +import re + + +def test_logger_pre_setup_before_other_components(generate_main): + """Logger::pre_setup() must be called before any other component is created. + + Log functions call global_logger->log_vprintf_() without a null check, + so global_logger must be set before anything can log. + """ + main_cpp = generate_main("tests/component_tests/logger/test_logger.yaml") + + # Find the logger's pre_setup() call specifically + logger_pre_setup = re.search(r"logger_logger->pre_setup\(\)", main_cpp) + if logger_pre_setup is None: + # Fall back to finding any logger-related pre_setup + logger_pre_setup = re.search(r"logger\w*->pre_setup\(\)", main_cpp) + assert logger_pre_setup is not None, ( + "Logger pre_setup() not found in generated code" + ) + + # Find all "new " allocations (component creation) + new_allocations = list(re.finditer(r"\bnew [\w:]+", main_cpp)) + assert len(new_allocations) > 0, "No component allocations found" + + # Separate logger and non-logger allocations + logger_allocs = [a for a in new_allocations if "logger" in a.group().lower()] + non_logger_allocs = [ + a + for a in new_allocations + if "logger" not in a.group().lower() + # Skip placement new for App + and "(&App)" not in main_cpp[max(0, a.start() - 5) : a.start()] + ] + + assert len(logger_allocs) > 0, ( + f"Logger allocation not found in: {[a.group() for a in new_allocations]}" + ) + assert len(non_logger_allocs) > 0, ( + "No non-logger component allocations found — " + "add a component to test_logger.yaml so the ordering check is meaningful" + ) + + # All non-logger allocations must appear after logger pre_setup() + for alloc in non_logger_allocs: + assert alloc.start() > logger_pre_setup.start(), ( + f"Component allocation '{alloc.group()}' at position {alloc.start()} " + f"appears before logger pre_setup() at position {logger_pre_setup.start()}" + ) diff --git a/tests/component_tests/logger/test_logger.yaml b/tests/component_tests/logger/test_logger.yaml new file mode 100644 index 0000000000..f43e99d94f --- /dev/null +++ b/tests/component_tests/logger/test_logger.yaml @@ -0,0 +1,14 @@ +--- +esphome: + name: test + +esp8266: + board: d1_mini_lite + +logger: + level: DEBUG + +# Need at least one non-logger component so the ordering test +# can verify that logger pre_setup() comes before other allocations. +preferences: + flash_write_interval: 1min diff --git a/tests/components/main.cpp b/tests/components/main.cpp index 928f0e6059..373fde7151 100644 --- a/tests/components/main.cpp +++ b/tests/components/main.cpp @@ -1,5 +1,7 @@ #include <gtest/gtest.h> +#include "esphome/components/logger/logger.h" + /* This special main.cpp replaces the default one. It will run all the Google Tests found in all compiled cpp files and then exit with the result @@ -18,6 +20,12 @@ void original_setup() { } void setup() { + // Log functions call global_logger->log_vprintf_() without a null check, + // so we must set up a Logger before any test that triggers logging. + static esphome::logger::Logger test_logger(0); + test_logger.set_log_level(ESPHOME_LOG_LEVEL); + test_logger.pre_setup(); + ::testing::InitGoogleTest(); int exit_code = RUN_ALL_TESTS(); exit(exit_code); diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index b7f7fc60b3..b652b4174c 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -193,6 +193,7 @@ async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> s " platformio_options:\n" " build_flags:\n" ' - "-DDEBUG" # Enable assert() statements\n' + ' - "-DESPHOME_DEBUG" # Enable ESPHOME_DEBUG_ASSERT checks\n' ' - "-DESPHOME_DEBUG_API" # Enable API protocol asserts\n' ' - "-g" # Add debug symbols', ) From 9404eadaf8fe50fab155bae12252adad31530336 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 09:12:28 -1000 Subject: [PATCH 515/609] [rp2040_ble] Add BLE component for RP2040/RP2350 (#14603) --- CODEOWNERS | 1 + esphome/components/rp2040_ble/__init__.py | 31 +++++ esphome/components/rp2040_ble/rp2040_ble.cpp | 124 ++++++++++++++++++ esphome/components/rp2040_ble/rp2040_ble.h | 51 +++++++ esphome/core/defines.h | 1 + tests/components/rp2040_ble/common.yaml | 1 + .../test-disable-on-boot.rp2040-ard.yaml | 2 + .../rp2040_ble/test.rp2040-ard.yaml | 1 + 8 files changed, 212 insertions(+) create mode 100644 esphome/components/rp2040_ble/__init__.py create mode 100644 esphome/components/rp2040_ble/rp2040_ble.cpp create mode 100644 esphome/components/rp2040_ble/rp2040_ble.h create mode 100644 tests/components/rp2040_ble/common.yaml create mode 100644 tests/components/rp2040_ble/test-disable-on-boot.rp2040-ard.yaml create mode 100644 tests/components/rp2040_ble/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index cb415bb625..a95e100cbf 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -410,6 +410,7 @@ esphome/components/restart/* @esphome/core esphome/components/rf_bridge/* @jesserockz esphome/components/rgbct/* @jesserockz esphome/components/rp2040/* @jesserockz +esphome/components/rp2040_ble/* @bdraco esphome/components/rp2040_pio_led_strip/* @Papa-DMan esphome/components/rp2040_pwm/* @jesserockz esphome/components/rpi_dpi_rgb/* @clydebarrow diff --git a/esphome/components/rp2040_ble/__init__.py b/esphome/components/rp2040_ble/__init__.py new file mode 100644 index 0000000000..648f22691c --- /dev/null +++ b/esphome/components/rp2040_ble/__init__.py @@ -0,0 +1,31 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ID +from esphome.types import ConfigType + +DEPENDENCIES = ["rp2040"] +CODEOWNERS = ["@bdraco"] + +rp2040_ble_ns = cg.esphome_ns.namespace("rp2040_ble") +RP2040BLE = rp2040_ble_ns.class_("RP2040BLE", cg.Component) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(RP2040BLE), + cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) + + # Enable Bluetooth in the arduino-pico build + # This switches linking from liblwip.a to liblwip-bt.a and defines + # ENABLE_CLASSIC, ENABLE_BLE, CYW43_ENABLE_BLUETOOTH + cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH") + + cg.add_define("USE_RP2040_BLE") diff --git a/esphome/components/rp2040_ble/rp2040_ble.cpp b/esphome/components/rp2040_ble/rp2040_ble.cpp new file mode 100644 index 0000000000..4125da7ec0 --- /dev/null +++ b/esphome/components/rp2040_ble/rp2040_ble.cpp @@ -0,0 +1,124 @@ +#include "rp2040_ble.h" + +#ifdef USE_RP2040_BLE + +#include "esphome/core/log.h" + +namespace esphome::rp2040_ble { + +static const char *const TAG = "rp2040_ble"; + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +RP2040BLE *global_ble = nullptr; + +void RP2040BLE::setup() { + global_ble = this; + + if (this->enable_on_boot_) { + this->enable(); + } else { + this->state_ = BLEComponentState::DISABLED; + } +} + +void RP2040BLE::enable() { + if (this->state_ == BLEComponentState::ACTIVE || this->state_ == BLEComponentState::ENABLING) { + return; + } + + ESP_LOGD(TAG, "Enabling BLE..."); + this->state_ = BLEComponentState::ENABLING; + this->active_logged_ = false; + + if (!this->btstack_initialized_) { + // BTstack init functions are not idempotent — only call once + l2cap_init(); + sm_init(); + + this->hci_event_callback_registration_.callback = &RP2040BLE::packet_handler_; + hci_add_event_handler(&this->hci_event_callback_registration_); + + this->sm_event_callback_registration_.callback = &RP2040BLE::packet_handler_; + sm_add_event_handler(&this->sm_event_callback_registration_); + + this->btstack_initialized_ = true; + } + + hci_power_control(HCI_POWER_ON); +} + +void RP2040BLE::disable() { + if (this->state_ == BLEComponentState::DISABLED || this->state_ == BLEComponentState::OFF) { + return; + } + + ESP_LOGD(TAG, "Disabling BLE..."); + this->state_ = BLEComponentState::DISABLING; + + hci_power_control(HCI_POWER_OFF); + + this->state_ = BLEComponentState::DISABLED; + ESP_LOGD(TAG, "BLE disabled"); +} + +void RP2040BLE::loop() { + if (this->state_ == BLEComponentState::ACTIVE && !this->active_logged_) { + this->active_logged_ = true; + ESP_LOGI(TAG, "BLE active"); + } +} + +static const char *state_to_str(BLEComponentState state) { + switch (state) { + case BLEComponentState::OFF: + return "OFF"; + case BLEComponentState::ENABLING: + return "ENABLING"; + case BLEComponentState::ACTIVE: + return "ACTIVE"; + case BLEComponentState::DISABLING: + return "DISABLING"; + case BLEComponentState::DISABLED: + return "DISABLED"; + default: + return "UNKNOWN"; + } +} + +void RP2040BLE::dump_config() { + ESP_LOGCONFIG(TAG, + "RP2040 BLE:\n" + " Enable on boot: %s\n" + " State: %s", + YESNO(this->enable_on_boot_), state_to_str(this->state_)); +} + +float RP2040BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; } + +void RP2040BLE::packet_handler_(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size) { + if (global_ble == nullptr) { + return; + } + + if (type != HCI_EVENT_PACKET) { + return; + } + + uint8_t event_type = hci_event_packet_get_type(packet); + + switch (event_type) { + case BTSTACK_EVENT_STATE: { + uint8_t state = btstack_event_state_get_state(packet); + if (state == HCI_STATE_WORKING && global_ble->state_ == BLEComponentState::ENABLING) { + global_ble->state_ = BLEComponentState::ACTIVE; + } + break; + } + default: + break; + } +} + +} // namespace esphome::rp2040_ble + +#endif // USE_RP2040_BLE diff --git a/esphome/components/rp2040_ble/rp2040_ble.h b/esphome/components/rp2040_ble/rp2040_ble.h new file mode 100644 index 0000000000..24b3860cc1 --- /dev/null +++ b/esphome/components/rp2040_ble/rp2040_ble.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/defines.h" // Must be included before conditional includes + +#ifdef USE_RP2040_BLE + +#include "esphome/core/component.h" + +#include <btstack.h> + +namespace esphome::rp2040_ble { + +enum class BLEComponentState : uint8_t { + OFF = 0, + ENABLING, + ACTIVE, + DISABLING, + DISABLED, +}; + +class RP2040BLE : public Component { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override; + + void enable(); + void disable(); + bool is_active() const { return this->state_ == BLEComponentState::ACTIVE; } + + void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } + + protected: + static void packet_handler_(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size); + + btstack_packet_callback_registration_t hci_event_callback_registration_{}; + btstack_packet_callback_registration_t sm_event_callback_registration_{}; + + BLEComponentState state_{BLEComponentState::OFF}; + bool enable_on_boot_{true}; + bool btstack_initialized_{false}; + bool active_logged_{false}; +}; + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern RP2040BLE *global_ble; + +} // namespace esphome::rp2040_ble + +#endif // USE_RP2040_BLE diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 51f474d80e..44918fe00c 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -339,6 +339,7 @@ #define USE_I2C #define USE_LOGGER_USB_CDC #define USE_SOCKET_IMPL_LWIP_TCP +#define USE_RP2040_BLE #define USE_SPI #endif diff --git a/tests/components/rp2040_ble/common.yaml b/tests/components/rp2040_ble/common.yaml new file mode 100644 index 0000000000..f6205a4724 --- /dev/null +++ b/tests/components/rp2040_ble/common.yaml @@ -0,0 +1 @@ +rp2040_ble: diff --git a/tests/components/rp2040_ble/test-disable-on-boot.rp2040-ard.yaml b/tests/components/rp2040_ble/test-disable-on-boot.rp2040-ard.yaml new file mode 100644 index 0000000000..2154536111 --- /dev/null +++ b/tests/components/rp2040_ble/test-disable-on-boot.rp2040-ard.yaml @@ -0,0 +1,2 @@ +rp2040_ble: + enable_on_boot: false diff --git a/tests/components/rp2040_ble/test.rp2040-ard.yaml b/tests/components/rp2040_ble/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/rp2040_ble/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 04d80cfb75ffc19eaa14fcadce805c4c57369919 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:17:30 -0400 Subject: [PATCH 516/609] [esp32_hosted] Bump esp_wifi_remote and esp_hosted versions (#14680) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/esp32_hosted/__init__.py | 4 ++-- esphome/idf_component.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index 720d81acc4..6d49053d6d 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -103,9 +103,9 @@ async def to_code(config): framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}" if framework_ver >= cv.Version(5, 5, 0): - esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.3.2") + esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.4.0") esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4") - esp32.add_idf_component(name="espressif/esp_hosted", ref="2.11.5") + esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.0") else: esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0") esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 550e7b9af7..acd7f7a479 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -12,7 +12,7 @@ dependencies: espressif/mdns: version: 1.10.0 espressif/esp_wifi_remote: - version: 1.3.2 + version: 1.4.0 rules: - if: "target in [esp32h2, esp32p4]" espressif/eppp_link: @@ -20,7 +20,7 @@ dependencies: rules: - if: "target in [esp32h2, esp32p4]" espressif/esp_hosted: - version: 2.11.5 + version: 2.12.0 rules: - if: "target in [esp32h2, esp32p4]" zorxx/multipart-parser: From 780e009bf46854cabb42fb937659569eadc23d83 Mon Sep 17 00:00:00 2001 From: mahumpula <90641469+mahumpula@users.noreply.github.com> Date: Tue, 10 Mar 2026 21:23:49 +0100 Subject: [PATCH 517/609] [runtime_image] Add support for 8bit BMPs and fix existing issues (#10733) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .../components/runtime_image/bmp_decoder.cpp | 109 ++++++++++++++---- .../components/runtime_image/bmp_decoder.h | 4 + tests/components/online_image/common.yaml | 4 + 3 files changed, 95 insertions(+), 22 deletions(-) diff --git a/esphome/components/runtime_image/bmp_decoder.cpp b/esphome/components/runtime_image/bmp_decoder.cpp index 7003f4da2f..174f924b28 100644 --- a/esphome/components/runtime_image/bmp_decoder.cpp +++ b/esphome/components/runtime_image/bmp_decoder.cpp @@ -12,7 +12,10 @@ static const char *const TAG = "image_decoder.bmp"; int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { size_t index = 0; - if (this->current_index_ == 0 && index == 0 && size > 14) { + if (this->current_index_ == 0) { + if (size <= 14) { + return 0; // Need more data for file header + } /** * BMP file format: * 0-1: Signature (BM) @@ -39,7 +42,10 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { this->current_index_ = 14; index = 14; } - if (this->current_index_ == 14 && index == 14 && size > this->data_offset_) { + if (this->current_index_ == 14) { + if (size <= this->data_offset_) { + return 0; // Need more data for DIB header and color table + } /** * BMP DIB header: * 14-17: DIB header size @@ -66,6 +72,28 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { this->width_bytes_ = (this->width_ + 7) / 8; this->padding_bytes_ = (4 - (this->width_bytes_ % 4)) % 4; break; + case 8: { + this->width_bytes_ = this->width_; + if (this->color_table_entries_ == 0) { + this->color_table_entries_ = 256; + } else if (this->color_table_entries_ > 256) { + ESP_LOGE(TAG, "Too many color table entries: %" PRIu32, this->color_table_entries_); + return DECODE_ERROR_UNSUPPORTED_FORMAT; + } + size_t header_size = encode_uint32(buffer[17], buffer[16], buffer[15], buffer[14]); + size_t offset = 14 + header_size; + + this->color_table_ = std::make_unique<uint32_t[]>(this->color_table_entries_); + + for (size_t i = 0; i < this->color_table_entries_; i++) { + this->color_table_[i] = encode_uint32(buffer[offset + i * 4 + 3], buffer[offset + i * 4 + 2], + buffer[offset + i * 4 + 1], buffer[offset + i * 4]); + } + + this->padding_bytes_ = (4 - (this->width_bytes_ % 4)) % 4; + + break; + } case 24: this->width_bytes_ = this->width_ * 3; if (this->width_bytes_ % 4 != 0) { @@ -91,21 +119,24 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { } switch (this->bits_per_pixel_) { case 1: { + size_t width = static_cast<size_t>(this->width_); while (index < size) { - uint8_t current_byte = buffer[index]; - bool end_of_row = false; - for (uint8_t i = 0; i < 8; i++) { - size_t x = this->paint_index_ % static_cast<size_t>(this->width_); - size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / static_cast<size_t>(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_++; - // End of pixel row: skip remaining bits in this byte - if (x + 1 >= static_cast<size_t>(this->width_)) { - end_of_row = true; - break; - } + size_t x = this->paint_index_ % width; + size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / width); + size_t remaining_in_row = width - x; + uint8_t pixels_in_byte = std::min<size_t>(remaining_in_row, 8); + bool end_of_row = remaining_in_row <= 8; + size_t needed = 1 + (end_of_row ? this->padding_bytes_ : 0); + if (index + needed > size) { + this->decoded_bytes_ += index; + return index; } + uint8_t current_byte = buffer[index]; + for (uint8_t i = 0; i < pixels_in_byte; i++) { + Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF; + this->draw(x + i, y, 1, 1, c); + } + this->paint_index_ += pixels_in_byte; this->current_index_++; index++; // End of pixel row: skip row padding bytes (4-byte alignment) @@ -116,23 +147,57 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { } break; } - case 24: { + case 8: { + size_t width = static_cast<size_t>(this->width_); + size_t last_col = width - 1; while (index < size) { - if (index + 2 >= size) { + size_t x = this->paint_index_ % width; + size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / width); + size_t needed = 1 + ((x == last_col) ? this->padding_bytes_ : 0); + if (index + needed > size) { + this->decoded_bytes_ += index; + return index; + } + + uint8_t color_index = buffer[index]; + if (color_index >= this->color_table_entries_) { + ESP_LOGE(TAG, "Invalid color index: %u", color_index); + return DECODE_ERROR_UNSUPPORTED_FORMAT; + } + + uint32_t rgb = this->color_table_[color_index]; + uint8_t b = rgb & 0xff; + uint8_t g = (rgb >> 8) & 0xff; + uint8_t r = (rgb >> 16) & 0xff; + this->draw(x, y, 1, 1, Color(r, g, b)); + this->paint_index_++; + this->current_index_++; + index++; + if (x == last_col && this->padding_bytes_ > 0) { + index += this->padding_bytes_; + this->current_index_ += this->padding_bytes_; + } + } + break; + } + case 24: { + size_t width = static_cast<size_t>(this->width_); + size_t last_col = width - 1; + while (index < size) { + size_t x = this->paint_index_ % width; + size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / width); + size_t needed = 3 + ((x == last_col) ? this->padding_bytes_ : 0); + if (index + needed > 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_ % static_cast<size_t>(this->width_); - size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / static_cast<size_t>(this->width_)); - Color c = Color(r, g, b); - this->draw(x, y, 1, 1, c); + this->draw(x, y, 1, 1, Color(r, g, b)); this->paint_index_++; this->current_index_ += 3; index += 3; - size_t last_col = static_cast<size_t>(this->width_) - 1; if (x == last_col && this->padding_bytes_ > 0) { index += this->padding_bytes_; this->current_index_ += this->padding_bytes_; diff --git a/esphome/components/runtime_image/bmp_decoder.h b/esphome/components/runtime_image/bmp_decoder.h index 37db6b4940..73e54f5430 100644 --- a/esphome/components/runtime_image/bmp_decoder.h +++ b/esphome/components/runtime_image/bmp_decoder.h @@ -3,6 +3,9 @@ #include "esphome/core/defines.h" #ifdef USE_RUNTIME_IMAGE_BMP +#include <algorithm> +#include <memory> + #include "image_decoder.h" #include "runtime_image.h" @@ -36,6 +39,7 @@ class BmpDecoder : public ImageDecoder { uint32_t compression_method_{0}; uint32_t image_data_size_{0}; uint32_t color_table_entries_{0}; + std::unique_ptr<uint32_t[]> color_table_; size_t width_bytes_{0}; size_t data_offset_{0}; uint8_t padding_bytes_{0}; diff --git a/tests/components/online_image/common.yaml b/tests/components/online_image/common.yaml index 422a24b540..fc3cc94217 100644 --- a/tests/components/online_image/common.yaml +++ b/tests/components/online_image/common.yaml @@ -40,6 +40,10 @@ online_image: url: https://samples-files.com/samples/images/bmp/480-360-sample.bmp format: BMP type: BINARY + - id: online_rgb_bmp_8bit + url: https://samples-files.com/samples/images/bmp/480-360-sample.bmp + format: BMP + type: RGB - id: online_jpeg_image url: http://www.faqs.org/images/library.jpg format: JPEG From 8ca6ee4349acb28878d4c15ef32d67999b96f139 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Tue, 10 Mar 2026 15:25:26 -0500 Subject: [PATCH 518/609] [speaker_source] Add new media player (#14649) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- CODEOWNERS | 1 + esphome/components/speaker_source/__init__.py | 0 .../components/speaker_source/media_player.py | 212 +++++++ .../speaker_source_media_player.cpp | 546 ++++++++++++++++++ .../speaker_source_media_player.h | 217 +++++++ tests/components/speaker_source/common.yaml | 43 ++ .../speaker_source/test.esp32-idf.yaml | 9 + tests/components/speaker_source/test.wav | Bin 0 -> 46 bytes 8 files changed, 1028 insertions(+) create mode 100644 esphome/components/speaker_source/__init__.py create mode 100644 esphome/components/speaker_source/media_player.py create mode 100644 esphome/components/speaker_source/speaker_source_media_player.cpp create mode 100644 esphome/components/speaker_source/speaker_source_media_player.h create mode 100644 tests/components/speaker_source/common.yaml create mode 100644 tests/components/speaker_source/test.esp32-idf.yaml create mode 100644 tests/components/speaker_source/test.wav diff --git a/CODEOWNERS b/CODEOWNERS index a95e100cbf..12aff01e73 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -459,6 +459,7 @@ esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/sound_level/* @kahrendt esphome/components/speaker/* @jesserockz @kahrendt esphome/components/speaker/media_player/* @kahrendt @synesthesiam +esphome/components/speaker_source/* @kahrendt esphome/components/spi/* @clydebarrow @esphome/core esphome/components/spi_device/* @clydebarrow esphome/components/spi_led_strip/* @clydebarrow diff --git a/esphome/components/speaker_source/__init__.py b/esphome/components/speaker_source/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/speaker_source/media_player.py b/esphome/components/speaker_source/media_player.py new file mode 100644 index 0000000000..a44cdcbf01 --- /dev/null +++ b/esphome/components/speaker_source/media_player.py @@ -0,0 +1,212 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import audio, media_player, media_source, speaker +import esphome.config_validation as cv +from esphome.const import ( + CONF_FORMAT, + CONF_ID, + CONF_NUM_CHANNELS, + CONF_SAMPLE_RATE, + CONF_SPEAKER, +) +from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType + +AUTO_LOAD = ["audio"] +DEPENDENCIES = ["media_source", "speaker"] + +CODEOWNERS = ["@kahrendt"] + +CONF_MEDIA_PIPELINE = "media_pipeline" +CONF_ON_MUTE = "on_mute" +CONF_ON_UNMUTE = "on_unmute" +CONF_ON_VOLUME = "on_volume" +CONF_SOURCES = "sources" +CONF_VOLUME_INCREMENT = "volume_increment" +CONF_VOLUME_INITIAL = "volume_initial" +CONF_VOLUME_MAX = "volume_max" +CONF_VOLUME_MIN = "volume_min" + +speaker_source_ns = cg.esphome_ns.namespace("speaker_source") + +SpeakerSourceMediaPlayer = speaker_source_ns.class_( + "SpeakerSourceMediaPlayer", cg.Component, media_player.MediaPlayer +) + +PipelineContext = speaker_source_ns.struct("PipelineContext") + +Pipeline = speaker_source_ns.enum("Pipeline") + + +FORMAT_MAPPING = { + "FLAC": "flac", + "MP3": "mp3", + "OPUS": "opus", + "WAV": "wav", +} + + +# Returns a media_player.MediaPlayerSupportedFormat struct with the configured +# format, sample rate, number of channels, purpose, and bytes per sample +def _get_supported_format_struct(pipeline: ConfigType): + args = [ + media_player.MediaPlayerSupportedFormat, + ] + + args.append(("format", FORMAT_MAPPING[pipeline[CONF_FORMAT]])) + + args.append(("sample_rate", pipeline[CONF_SAMPLE_RATE])) + args.append(("num_channels", pipeline[CONF_NUM_CHANNELS])) + args.append(("purpose", media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["default"])) + + # Omit sample_bytes for MP3: ffmpeg transcoding in Home Assistant fails + # if the number of bytes per sample is specified for MP3. + if pipeline[CONF_FORMAT] != "MP3": + args.append(("sample_bytes", 2)) + + return cg.StructInitializer(*args) + + +def _validate_pipeline(config: ConfigType) -> ConfigType: + # Inherit settings from speaker if not manually set + inherit_property_from(CONF_NUM_CHANNELS, CONF_SPEAKER)(config) + inherit_property_from(CONF_SAMPLE_RATE, CONF_SPEAKER)(config) + + # Opus only supports 48 kHz + if config.get(CONF_FORMAT) == "OPUS" and config.get(CONF_SAMPLE_RATE) != 48000: + raise cv.Invalid("Opus only supports a sample rate of 48000 Hz") + + audio.final_validate_audio_schema( + "speaker_source media_player", + audio_device=CONF_SPEAKER, + bits_per_sample=16, + channels=config.get(CONF_NUM_CHANNELS), + sample_rate=config.get(CONF_SAMPLE_RATE), + )(config) + + return config + + +PIPELINE_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id( + PipelineContext + ), # Needed to inherit audio settings from the speaker + cv.Required(CONF_SPEAKER): cv.use_id(speaker.Speaker), + cv.Required(CONF_SOURCES): cv.All( + cv.ensure_list(cv.use_id(media_source.MediaSource)), + cv.Length(min=1), + ), + cv.Optional(CONF_FORMAT, default="FLAC"): cv.enum(audio.AUDIO_FILE_TYPE_ENUM), + cv.Optional(CONF_SAMPLE_RATE): cv.int_range(min=1), + cv.Optional(CONF_NUM_CHANNELS): cv.int_range(1, 2), + } +) + + +def _validate_volume_settings(config: ConfigType) -> ConfigType: + # CONF_VOLUME_INITIAL is in the scaled volume domain (0.0-1.0) and doesn't need to be validated + if config[CONF_VOLUME_MIN] > config[CONF_VOLUME_MAX]: + raise cv.Invalid( + f"{CONF_VOLUME_MIN} ({config[CONF_VOLUME_MIN]}) must be less than or equal to {CONF_VOLUME_MAX} ({config[CONF_VOLUME_MAX]})" + ) + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage, + cv.Optional(CONF_VOLUME_INITIAL, default=0.5): cv.percentage, + cv.Optional(CONF_VOLUME_MAX, default=1.0): cv.percentage, + cv.Optional(CONF_VOLUME_MIN, default=0.0): cv.percentage, + cv.Required(CONF_MEDIA_PIPELINE): PIPELINE_SCHEMA, + cv.Optional(CONF_ON_MUTE): automation.validate_automation(single=True), + cv.Optional(CONF_ON_UNMUTE): automation.validate_automation(single=True), + cv.Optional(CONF_ON_VOLUME): automation.validate_automation(single=True), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(media_player.media_player_schema(SpeakerSourceMediaPlayer)), + cv.only_on_esp32, + _validate_volume_settings, +) + + +def _final_validate_codecs(config: ConfigType) -> ConfigType: + pipeline = config[CONF_MEDIA_PIPELINE] + fmt = pipeline[CONF_FORMAT] + if fmt == "NONE": + audio.request_flac_support() + audio.request_mp3_support() + audio.request_opus_support() + elif fmt == "FLAC": + audio.request_flac_support() + elif fmt == "MP3": + audio.request_mp3_support() + elif fmt == "OPUS": + audio.request_opus_support() + + return config + + +FINAL_VALIDATE_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_MEDIA_PIPELINE): _validate_pipeline, + }, + extra=cv.ALLOW_EXTRA, + ), + _final_validate_codecs, +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await media_player.register_media_player(var, config) + + cg.add(var.set_volume_increment(config[CONF_VOLUME_INCREMENT])) + cg.add(var.set_volume_initial(config[CONF_VOLUME_INITIAL])) + cg.add(var.set_volume_max(config[CONF_VOLUME_MAX])) + cg.add(var.set_volume_min(config[CONF_VOLUME_MIN])) + + pipeline_config = config[CONF_MEDIA_PIPELINE] + pipeline_enum = Pipeline.MEDIA_PIPELINE + + for source in pipeline_config[CONF_SOURCES]: + src = await cg.get_variable(source) + cg.add(var.add_media_source(pipeline_enum, src)) + + cg.add( + var.set_speaker( + pipeline_enum, + await cg.get_variable(pipeline_config[CONF_SPEAKER]), + ) + ) + if pipeline_config[CONF_FORMAT] != "NONE": + cg.add( + var.set_format( + pipeline_enum, + _get_supported_format_struct(pipeline_config), + ) + ) + + if on_mute := config.get(CONF_ON_MUTE): + await automation.build_automation( + var.get_mute_trigger(), + [], + on_mute, + ) + if on_unmute := config.get(CONF_ON_UNMUTE): + await automation.build_automation( + var.get_unmute_trigger(), + [], + on_unmute, + ) + if on_volume := config.get(CONF_ON_VOLUME): + await automation.build_automation( + var.get_volume_trigger(), + [(cg.float_, "x")], + on_volume, + ) diff --git a/esphome/components/speaker_source/speaker_source_media_player.cpp b/esphome/components/speaker_source/speaker_source_media_player.cpp new file mode 100644 index 0000000000..a3679891d2 --- /dev/null +++ b/esphome/components/speaker_source/speaker_source_media_player.cpp @@ -0,0 +1,546 @@ +#include "speaker_source_media_player.h" + +#ifdef USE_ESP32 + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::speaker_source { + +static constexpr uint32_t MEDIA_CONTROLS_QUEUE_LENGTH = 20; + +static const char *const TAG = "speaker_source_media_player"; + +// SourceBinding method implementations (defined here because SpeakerSourceMediaPlayer is forward-declared in the +// header) + +// THREAD CONTEXT: Called from media source decode task thread +size_t SourceBinding::write_audio(const uint8_t *data, size_t length, uint32_t timeout_ms, + const audio::AudioStreamInfo &stream_info) { + return this->player->handle_media_output_(this->pipeline, this->source, data, length, timeout_ms, stream_info); +} + +// THREAD CONTEXT: Called from main loop (media source's loop() calls set_state_ which calls report_state) +void SourceBinding::report_state(media_source::MediaSourceState state) { + this->player->handle_media_state_changed_(this->pipeline, this->source, state); +} + +// THREAD CONTEXT: Called from media source task thread; uses defer() to marshal to main loop +void SourceBinding::request_volume(float volume) { + this->player->defer([this, volume]() { this->player->handle_volume_request_(volume); }); +} + +// THREAD CONTEXT: Called from media source task thread; uses defer() to marshal to main loop +void SourceBinding::request_mute(bool is_muted) { + this->player->defer([this, is_muted]() { this->player->handle_mute_request_(is_muted); }); +} + +// THREAD CONTEXT: Called from media source task thread; uses defer() to marshal to main loop +void SourceBinding::request_play_uri(const std::string &uri) { + this->player->defer([this, uri]() { this->player->handle_play_uri_request_(this->pipeline, uri); }); +} + +// THREAD CONTEXT: Called during code generation setup (main loop) +void SpeakerSourceMediaPlayer::add_media_source(uint8_t pipeline, media_source::MediaSource *media_source) { + auto &binding = + this->pipelines_[pipeline].sources.emplace_back(std::make_unique<SourceBinding>(this, media_source, pipeline)); + media_source->set_listener(binding.get()); +} + +void SpeakerSourceMediaPlayer::dump_config() { + ESP_LOGCONFIG(TAG, + "Speaker Source Media Player:\n" + " Volume Increment: %.2f\n" + " Volume Min: %.2f\n" + " Volume Max: %.2f", + this->volume_increment_, this->volume_min_, this->volume_max_); +} + +void SpeakerSourceMediaPlayer::setup() { + this->state = media_player::MEDIA_PLAYER_STATE_IDLE; + + this->media_control_command_queue_ = xQueueCreate(MEDIA_CONTROLS_QUEUE_LENGTH, sizeof(MediaPlayerControlCommand)); + + this->pref_ = this->make_entity_preference<VolumeRestoreState>(); + + VolumeRestoreState volume_restore_state; + if (this->pref_.load(&volume_restore_state)) { + this->set_volume_(volume_restore_state.volume); + this->set_mute_state_(volume_restore_state.is_muted); + } else { + this->set_volume_(this->volume_initial_); + this->set_mute_state_(false); + } + + // Register callbacks to receive playback notifications from speakers + for (size_t i = 0; i < this->pipelines_.size(); i++) { + if (this->pipelines_[i].is_configured()) { + this->pipelines_[i].speaker->add_audio_output_callback([this, i](uint32_t frames, int64_t timestamp) { + this->handle_speaker_playback_callback_(frames, timestamp, i); + }); + } + } +} + +// THREAD CONTEXT: Called from the speaker's playback callback task (not main loop) +void SpeakerSourceMediaPlayer::handle_speaker_playback_callback_(uint32_t frames, int64_t timestamp, uint8_t pipeline) { + PipelineContext &ps = this->pipelines_[pipeline]; + + // Load once so the null check and use below are consistent + media_source::MediaSource *active_source = ps.active_source.load(std::memory_order_relaxed); + if (active_source == nullptr) { + return; + } + + // CAS loop to safely subtract frames without underflow. If pending_frames is reset to 0 (new source + // starting) between the load and the subtract, compare_exchange_weak will fail and reload the current value. + uint32_t current = ps.pending_frames.load(std::memory_order_relaxed); + uint32_t source_frames; + do { + source_frames = std::min(frames, current); + } while (source_frames > 0 && + !ps.pending_frames.compare_exchange_weak(current, current - source_frames, std::memory_order_relaxed)); + + if (source_frames > 0) { + // Notify the source about the played audio + active_source->notify_audio_played(source_frames, timestamp); + } +} + +// THREAD CONTEXT: Called from main loop via defer() +void SpeakerSourceMediaPlayer::handle_volume_request_(float volume) { + // Update the media player's volume + this->set_volume_(volume); + this->publish_state(); +} + +// THREAD CONTEXT: Called from main loop via defer() +void SpeakerSourceMediaPlayer::handle_mute_request_(bool is_muted) { + // Update the media player's mute state + this->set_mute_state_(is_muted); + this->publish_state(); +} + +// THREAD CONTEXT: Called from main loop via defer() +void SpeakerSourceMediaPlayer::handle_play_uri_request_(uint8_t pipeline, const std::string &uri) { + // Smart source is requesting the player to play a different URI + auto call = this->make_call(); + call.set_media_url(uri); + call.perform(); +} + +// THREAD CONTEXT: Called from main loop (media source's loop() calls set_state_ which calls report_state) +void SpeakerSourceMediaPlayer::handle_media_state_changed_(uint8_t pipeline, media_source::MediaSource *source, + media_source::MediaSourceState state) { + PipelineContext &ps = this->pipelines_[pipeline]; + + if (state == media_source::MediaSourceState::IDLE) { + // Source went idle - clear stopping flag if this was the source we asked to stop + if (ps.stopping_source == source) { + ps.stopping_source = nullptr; + } + + // Clear pending flag if this was the source we asked to play + if (ps.pending_source == source) { + ps.pending_source = nullptr; + } + + // Source went idle - clear it if it's the active source + if (ps.active_source == source) { + ps.last_source = ps.active_source; + ps.active_source = nullptr; + + // Finish the speaker to ensure it's ready for the next playback + ps.speaker->finish(); + } + } else if (state == media_source::MediaSourceState::PLAYING) { + // Source started playing - make it the active source if no one else is active + if (ps.active_source == nullptr) { + ps.active_source = source; + ps.last_source = nullptr; + + // Clear pending flag now that the source is active + if (ps.pending_source == source) { + ps.pending_source = nullptr; + } + } + } +} + +// THREAD CONTEXT: Called from media source decode task thread (not main loop). +// Reads ps.active_source (atomic), writes ps.pending_frames (atomic), and calls +// ps.speaker methods (speaker pointer is immutable after setup). +size_t SpeakerSourceMediaPlayer::handle_media_output_(uint8_t pipeline, media_source::MediaSource *source, + const uint8_t *data, size_t length, uint32_t timeout_ms, + const audio::AudioStreamInfo &stream_info) { + PipelineContext &ps = this->pipelines_[pipeline]; + + // Single read; the if-body only uses ps.speaker (immutable after setup) and the source parameter. + if (ps.active_source == source) { + // This source is active - play the audio + if (ps.speaker->get_audio_stream_info() != stream_info) { + // Setup the speaker to play this stream + ps.speaker->set_audio_stream_info(stream_info); + vTaskDelay(pdMS_TO_TICKS(timeout_ms)); + return 0; + } + size_t bytes_written = ps.speaker->play(data, length, pdMS_TO_TICKS(timeout_ms)); + if (bytes_written > 0) { + // Track frames sent to speaker for this source + ps.pending_frames.fetch_add(stream_info.bytes_to_frames(bytes_written), std::memory_order_relaxed); + } + return bytes_written; + } + + // Not the active source - wait for state callback to set us as active when we transition to PLAYING + vTaskDelay(pdMS_TO_TICKS(timeout_ms)); + return 0; +} + +media_player::MediaPlayerState SpeakerSourceMediaPlayer::get_media_pipeline_state_( + media_source::MediaSource *source) const { + if (source != nullptr) { + switch (source->get_state()) { + case media_source::MediaSourceState::PLAYING: + return media_player::MEDIA_PLAYER_STATE_PLAYING; + case media_source::MediaSourceState::PAUSED: + return media_player::MEDIA_PLAYER_STATE_PAUSED; + case media_source::MediaSourceState::ERROR: + ESP_LOGE(TAG, "Source error"); + return media_player::MEDIA_PLAYER_STATE_IDLE; + case media_source::MediaSourceState::IDLE: + default: + return media_player::MEDIA_PLAYER_STATE_IDLE; + } + } + + return media_player::MEDIA_PLAYER_STATE_IDLE; +} + +void SpeakerSourceMediaPlayer::loop() { + // Process queued control commands + MediaPlayerControlCommand control_command; + + // Use peek to check command without removing it + if (xQueuePeek(this->media_control_command_queue_, &control_command, 0) == pdTRUE) { + bool command_executed = false; + uint8_t pipeline = control_command.pipeline; + + switch (control_command.type) { + case MediaPlayerControlCommand::PLAY_URI: { + command_executed = this->try_execute_play_uri_(*control_command.data.uri, pipeline); + break; + } + + case MediaPlayerControlCommand::SEND_COMMAND: { + PipelineContext &ps = this->pipelines_[pipeline]; + + // Determine target source: prefer active, fall back to last + media_source::MediaSource *target_source = nullptr; + if (ps.active_source != nullptr) { + target_source = ps.active_source; + } else if (ps.last_source != nullptr) { + target_source = ps.last_source; + } + + media_player::MediaPlayerCommand player_command = control_command.data.command; + switch (player_command) { + case media_player::MEDIA_PLAYER_COMMAND_TOGGLE: { + media_source::MediaSource *active_source = ps.active_source; + if ((active_source != nullptr) && (active_source->get_state() == media_source::MediaSourceState::PLAYING)) { + if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::PAUSE); + } + } else { + if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::PLAY); + } + } + break; + } + + case media_player::MEDIA_PLAYER_COMMAND_PLAY: { + if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::PLAY); + } + break; + } + + case media_player::MEDIA_PLAYER_COMMAND_PAUSE: { + if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::PAUSE); + } + break; + } + + case media_player::MEDIA_PLAYER_COMMAND_STOP: { + if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::STOP); + } + break; + } + + default: + break; + } + + command_executed = true; + break; + } + } + + // Only remove from queue if successfully executed + if (command_executed) { + xQueueReceive(this->media_control_command_queue_, &control_command, 0); + + // Delete the allocated string for PLAY_URI commands + if (control_command.type == MediaPlayerControlCommand::PLAY_URI) { + delete control_command.data.uri; + } + } + } + + // Update state based on active sources + media_player::MediaPlayerState old_state = this->state; + + PipelineContext &media_ps = this->pipelines_[MEDIA_PIPELINE]; + this->state = this->get_media_pipeline_state_(media_ps.active_source); + + if (this->state != old_state) { + this->publish_state(); + ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state)); + } +} + +media_source::MediaSource *SpeakerSourceMediaPlayer::find_source_for_uri_(const std::string &uri, uint8_t pipeline) { + PipelineContext &ps = this->pipelines_[pipeline]; + media_source::MediaSource *first_match = nullptr; + for (auto &binding : ps.sources) { + if (binding->source->can_handle(uri)) { + // Prefer an idle source; otherwise remember the first match (will be stopped by try_execute_play_uri_) + if (binding->source->get_state() == media_source::MediaSourceState::IDLE) { + return binding->source; + } + if (first_match == nullptr) { + first_match = binding->source; + } + } + } + return first_match; +} + +bool SpeakerSourceMediaPlayer::try_execute_play_uri_(const std::string &uri, uint8_t pipeline) { + // Find target source + media_source::MediaSource *target_source = this->find_source_for_uri_(uri, pipeline); + if (target_source == nullptr) { + ESP_LOGW(TAG, "No source for URI"); + ESP_LOGV(TAG, "URI: %s", uri.c_str()); + return true; // Remove from queue (unrecoverable) + } + + PipelineContext &ps = this->pipelines_[pipeline]; + + media_source::MediaSource *active_source = ps.active_source; + + // If active source exists and is not IDLE, stop it and wait + if (active_source != nullptr) { + media_source::MediaSourceState active_state = active_source->get_state(); + if (active_state != media_source::MediaSourceState::IDLE) { + // Only send END command once per source - check if we've already asked this source to stop + if (ps.stopping_source != active_source) { + ESP_LOGV(TAG, "Pipeline %u: stopping active source", pipeline); + active_source->handle_command(media_source::MediaSourceCommand::STOP); + ps.speaker->stop(); + ps.stopping_source = active_source; + } + return false; // Leave in queue, retry next loop + } + } + + // Also check target source directly - handles case where source errored before PLAYING state + media_source::MediaSourceState target_state = target_source->get_state(); + if (target_state != media_source::MediaSourceState::IDLE) { + // Only send STOP command once per source + if (ps.stopping_source != target_source) { + ESP_LOGV(TAG, "Pipeline %u: target source busy, stopping", pipeline); + target_source->handle_command(media_source::MediaSourceCommand::STOP); + ps.speaker->stop(); + ps.stopping_source = target_source; + } + return false; // Leave in queue, retry next loop + } + + // Clear stopping flag since we're past the stopping phase + ps.stopping_source = nullptr; + + // Check if speaker is ready + if (!ps.speaker->is_stopped()) { + return false; // Speaker not ready yet, retry later + } + + // Set pending source so handle_media_state_changed_ can recognize it when the source transitions to PLAYING + ps.pending_source = target_source; + + // Speaker is ready, try to play + if (!target_source->play_uri(uri)) { + ESP_LOGE(TAG, "Pipeline %u: Failed to play URI: %s", pipeline, uri.c_str()); + ps.pending_source = nullptr; + } + + // Reset pending frame counter for this pipeline since we're starting a new source + ps.pending_frames.store(0, std::memory_order_relaxed); + + return true; // Remove from queue +} + +// THREAD CONTEXT: Called from main loop only. Entry points: +// - HA/automation commands (direct) +// - handle_play_uri_request_() via make_call().perform() (deferred from source tasks) +void SpeakerSourceMediaPlayer::control(const media_player::MediaPlayerCall &call) { + if (!this->is_ready()) { + return; + } + + MediaPlayerControlCommand control_command; + control_command.pipeline = MEDIA_PIPELINE; + + auto media_url = call.get_media_url(); + if (media_url.has_value()) { + control_command.type = MediaPlayerControlCommand::PLAY_URI; + // Heap allocation is unavoidable: URIs from Home Assistant are arbitrary-length (media URLs with tokens + // can easily exceed 500 bytes). Deleted after the command is consumed. FreeRTOS queues require items to be + // copyable, so we store a pointer to the string in the queue rather than the string itself. + control_command.data.uri = new std::string(media_url.value()); + if (xQueueSend(this->media_control_command_queue_, &control_command, 0) != pdTRUE) { + delete control_command.data.uri; + ESP_LOGE(TAG, "Queue full, URI dropped"); + } + return; + } + + auto volume = call.get_volume(); + if (volume.has_value()) { + this->set_volume_(volume.value()); + this->publish_state(); + return; + } + + auto cmd = call.get_command(); + if (cmd.has_value()) { + switch (cmd.value()) { + case media_player::MEDIA_PLAYER_COMMAND_MUTE: + this->set_mute_state_(true); + break; + case media_player::MEDIA_PLAYER_COMMAND_UNMUTE: + this->set_mute_state_(false); + break; + case media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP: + this->set_volume_(std::min(1.0f, this->volume + this->volume_increment_)); + break; + case media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN: + this->set_volume_(std::max(0.0f, this->volume - this->volume_increment_)); + break; + default: + // Queue command for processing in loop() + control_command.type = MediaPlayerControlCommand::SEND_COMMAND; + control_command.data.command = cmd.value(); + if (xQueueSend(this->media_control_command_queue_, &control_command, 0) != pdTRUE) { + ESP_LOGE(TAG, "Queue full, command dropped"); + } + return; + } + this->publish_state(); + } +} + +media_player::MediaPlayerTraits SpeakerSourceMediaPlayer::get_traits() { + auto traits = media_player::MediaPlayerTraits(); + traits.set_supports_pause(true); + + for (const auto &ps : this->pipelines_) { + if (ps.format.has_value()) { + traits.get_supported_formats().push_back(ps.format.value()); + } + } + + return traits; +} + +void SpeakerSourceMediaPlayer::save_volume_restore_state_() { + VolumeRestoreState volume_restore_state; + volume_restore_state.volume = this->volume; + volume_restore_state.is_muted = this->is_muted_; + this->pref_.save(&volume_restore_state); +} + +void SpeakerSourceMediaPlayer::set_mute_state_(bool mute_state, bool publish) { + if (this->is_muted_ == mute_state) { + return; + } + + for (auto &ps : this->pipelines_) { + if (ps.is_configured()) { + ps.speaker->set_mute_state(mute_state); + } + } + + this->is_muted_ = mute_state; + + if (publish) { + this->save_volume_restore_state_(); + } + + // Notify all media sources about the mute state change + for (auto &ps : this->pipelines_) { + for (auto &binding : ps.sources) { + binding->source->notify_mute_changed(mute_state); + } + } + + if (mute_state) { + this->defer([this]() { this->mute_trigger_.trigger(); }); + } else { + this->defer([this]() { this->unmute_trigger_.trigger(); }); + } +} + +void SpeakerSourceMediaPlayer::set_volume_(float volume, bool publish) { + // Remap the volume to fit within the configured limits + float bounded_volume = remap<float, float>(volume, 0.0f, 1.0f, this->volume_min_, this->volume_max_); + + for (auto &ps : this->pipelines_) { + if (ps.is_configured()) { + ps.speaker->set_volume(bounded_volume); + } + } + + if (publish) { + this->volume = volume; + } + + // Notify all media sources about the volume change + for (auto &ps : this->pipelines_) { + for (auto &binding : ps.sources) { + binding->source->notify_volume_changed(volume); + } + } + + // Turn on the mute state if the volume is effectively zero, off otherwise. + // Pass publish=false to avoid saving twice. + if (volume < 0.001) { + this->set_mute_state_(true, false); + } else { + this->set_mute_state_(false, false); + } + + // Save after mute mutation so the restored state has the correct is_muted_ value + if (publish) { + this->save_volume_restore_state_(); + } + + this->defer([this, volume]() { this->volume_trigger_.trigger(volume); }); +} + +} // namespace esphome::speaker_source + +#endif // USE_ESP32 diff --git a/esphome/components/speaker_source/speaker_source_media_player.h b/esphome/components/speaker_source/speaker_source_media_player.h new file mode 100644 index 0000000000..7896fef295 --- /dev/null +++ b/esphome/components/speaker_source/speaker_source_media_player.h @@ -0,0 +1,217 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_ESP32 + +#include "esphome/components/audio/audio.h" +#include "esphome/components/media_source/media_source.h" +#include "esphome/components/media_player/media_player.h" +#include "esphome/components/speaker/speaker.h" + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +#include <array> +#include <atomic> +#include <memory> +#include <vector> +#include <freertos/FreeRTOS.h> +#include <freertos/queue.h> + +namespace esphome::speaker_source { + +// THREADING MODEL: +// This component coordinates media sources that run their own decode tasks with speakers +// that have their own playback callback tasks. Three thread contexts exist: +// +// - Main loop task: setup(), loop(), dump_config(), handle_media_state_changed_(), +// handle_volume_request_(), handle_mute_request_(), handle_play_uri_request_(), +// set_volume_(), set_mute_state_(), control(), get_media_pipeline_state_(), +// find_source_for_uri_(), try_execute_play_uri_(), save_volume_restore_state_() +// +// - Media source task(s): handle_media_output_() via SourceBinding::write_audio(). +// Called from each source's decode task thread when streaming audio data. +// Reads ps.active_source (atomic), writes ps.pending_frames (atomic), and calls +// ps.speaker methods (speaker pointer is immutable after setup). +// +// - Speaker callback task: handle_speaker_playback_callback_() via speaker's +// add_audio_output_callback(). Called when the speaker finishes writing frames to the DAC. +// Reads ps.active_source (atomic), writes ps.pending_frames (atomic), and calls +// active_source->notify_audio_played(). +// +// control() is only called from the main loop (HA/automation commands). +// Source tasks use defer() for all requests (volume, mute, play_uri). +// +// Thread-safe communication: +// - FreeRTOS queue (media_control_command_queue_): control() -> loop() for play/command dispatch +// - defer(): SourceBinding::request_volume/request_mute/request_play_uri -> main loop +// - Atomic fields (active_source, pending_frames): shared between all three thread contexts +// +// Non-atomic pipeline fields (last_source, stopping_source, pending_source) are only accessed +// from the main loop thread. + +enum Pipeline : uint8_t { + MEDIA_PIPELINE = 0, +}; + +// Forward declaration +class SpeakerSourceMediaPlayer; + +/// @brief Per-source listener binding that captures the source pointer at registration time. +/// Each binding implements MediaSourceListener and forwards callbacks to the player with the source identified. +/// Defined before PipelineContext so pipelines can own their bindings directly. +struct SourceBinding : public media_source::MediaSourceListener { + SourceBinding(SpeakerSourceMediaPlayer *player, media_source::MediaSource *source, uint8_t pipeline) + : player(player), source(source), pipeline(pipeline) {} + SpeakerSourceMediaPlayer *player; + media_source::MediaSource *source; + uint8_t pipeline; + + // Implementations are in the .cpp file because SpeakerSourceMediaPlayer is only forward-declared here + size_t write_audio(const uint8_t *data, size_t length, uint32_t timeout_ms, + const audio::AudioStreamInfo &stream_info) override; + void report_state(media_source::MediaSourceState state) override; + void request_volume(float volume) override; + void request_mute(bool is_muted) override; + void request_play_uri(const std::string &uri) override; +}; + +struct PipelineContext { + speaker::Speaker *speaker{nullptr}; + optional<media_player::MediaPlayerSupportedFormat> format; + + std::atomic<media_source::MediaSource *> active_source{nullptr}; + media_source::MediaSource *last_source{nullptr}; + media_source::MediaSource *stopping_source{nullptr}; // Source we've asked to stop, awaiting IDLE + media_source::MediaSource *pending_source{nullptr}; // Source we've asked to play, awaiting PLAYING + + // Each SourceBinding pairs a MediaSource* with its listener implementation. + // Uses unique_ptr so binding addresses are stable and set_listener() can be called in add_media_source(). + // Uses std::vector because the count varies across instances (multiple speaker_source media players may exist). + std::vector<std::unique_ptr<SourceBinding>> sources; + + // Track frames sent to speaker to correlate with playback callbacks. + // Atomic because it is written from the main loop/source tasks and read/decremented from the speaker playback + // callback. + std::atomic<uint32_t> pending_frames{0}; + + /// @brief Check if this pipeline is configured (has a speaker assigned) + bool is_configured() const { return this->speaker != nullptr; } +}; + +struct MediaPlayerControlCommand { + enum Type : uint8_t { + PLAY_URI, // Find a source that can handle this URI and play it + SEND_COMMAND, // Send command to active source + }; + Type type; + uint8_t pipeline; + + union { + std::string *uri; // Owned pointer, must delete after xQueueReceive (for PLAY_URI) + media_player::MediaPlayerCommand command; + } data; +}; + +struct VolumeRestoreState { + float volume; + bool is_muted; +}; + +class SpeakerSourceMediaPlayer : public Component, public media_player::MediaPlayer { + friend struct SourceBinding; + + public: + float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; } + void setup() override; + void loop() override; + void dump_config() override; + + // MediaPlayer implementations + media_player::MediaPlayerTraits get_traits() override; + bool is_muted() const override { return this->is_muted_; } + + // Percentage to increase or decrease the volume for volume up or volume down commands + void set_volume_increment(float volume_increment) { this->volume_increment_ = volume_increment; } + + // Volume used initially on first boot when no volume had been previously saved + void set_volume_initial(float volume_initial) { this->volume_initial_ = volume_initial; } + + void set_volume_max(float volume_max) { this->volume_max_ = volume_max; } + void set_volume_min(float volume_min) { this->volume_min_ = volume_min; } + + /// @brief Adds a media source to a pipeline and registers this player as its listener + void add_media_source(uint8_t pipeline, media_source::MediaSource *media_source); + + void set_speaker(uint8_t pipeline, speaker::Speaker *speaker) { this->pipelines_[pipeline].speaker = speaker; } + void set_format(uint8_t pipeline, const media_player::MediaPlayerSupportedFormat &format) { + this->pipelines_[pipeline].format = format; + } + + Trigger<> *get_mute_trigger() { return &this->mute_trigger_; } + Trigger<> *get_unmute_trigger() { return &this->unmute_trigger_; } + Trigger<float> *get_volume_trigger() { return &this->volume_trigger_; } + + protected: + // Callbacks from source bindings (pipeline index is captured at binding creation time) + size_t handle_media_output_(uint8_t pipeline, media_source::MediaSource *source, const uint8_t *data, size_t length, + uint32_t timeout_ms, const audio::AudioStreamInfo &stream_info); + void handle_media_state_changed_(uint8_t pipeline, media_source::MediaSource *source, + media_source::MediaSourceState state); + void handle_volume_request_(float volume); + void handle_mute_request_(bool is_muted); + void handle_play_uri_request_(uint8_t pipeline, const std::string &uri); + + void handle_speaker_playback_callback_(uint32_t frames, int64_t timestamp, uint8_t pipeline); + + // Receives commands from HA or from the voice assistant component + // Sends commands to the media_control_command_queue_ + void control(const media_player::MediaPlayerCall &call) override; + + /// @brief Updates this->volume and saves volume/mute state to flash for restoration if publish is true. + void set_volume_(float volume, bool publish = true); + + /// @brief Sets the mute state. + /// @param mute_state If true, audio will be muted. If false, audio will be unmuted + /// @param publish If true, saves volume/mute state to flash for restoration + void set_mute_state_(bool mute_state, bool publish = true); + + /// @brief Saves the current volume and mute state to the flash for restoration. + void save_volume_restore_state_(); + + /// @brief Determine media player state from the media pipeline's active source + /// @param media_source Active source for the media pipeline (may be nullptr) + /// @return The appropriate MediaPlayerState + media_player::MediaPlayerState get_media_pipeline_state_(media_source::MediaSource *media_source) const; + + bool try_execute_play_uri_(const std::string &uri, uint8_t pipeline); + media_source::MediaSource *find_source_for_uri_(const std::string &uri, uint8_t pipeline); + QueueHandle_t media_control_command_queue_; + + // Pipeline context for media pipeline. See THREADING MODEL at top of namespace for access rules. + std::array<PipelineContext, 1> pipelines_; + + // Used to save volume/mute state for restoration on reboot + ESPPreferenceObject pref_; + + Trigger<> mute_trigger_; + Trigger<> unmute_trigger_; + Trigger<float> volume_trigger_; + + // The amount to change the volume on volume up/down commands + float volume_increment_; + + // The initial volume used by Setup when no previous volume was saved + float volume_initial_; + + float volume_max_; + float volume_min_; + + bool is_muted_{false}; +}; + +} // namespace esphome::speaker_source + +#endif // USE_ESP32 diff --git a/tests/components/speaker_source/common.yaml b/tests/components/speaker_source/common.yaml new file mode 100644 index 0000000000..cfcb065f57 --- /dev/null +++ b/tests/components/speaker_source/common.yaml @@ -0,0 +1,43 @@ +i2s_audio: + i2s_lrclk_pin: ${i2s_bclk_pin} + i2s_bclk_pin: ${i2s_lrclk_pin} + i2s_mclk_pin: ${i2s_mclk_pin} + +speaker: + - platform: i2s_audio + id: speaker_id + dac_type: external + i2s_dout_pin: ${i2s_dout_pin} + sample_rate: 48000 + num_channels: 2 + +audio_file: + - id: test_audio + file: + type: local + path: $component_dir/test.wav + +media_source: + - platform: audio_file + id: audio_file_source + +media_player: + - platform: speaker_source + id: media_player_id + name: Media Player + volume_increment: 0.02 + volume_initial: 0.75 + volume_max: 0.95 + volume_min: 0.0 + media_pipeline: + speaker: speaker_id + format: FLAC + num_channels: 1 + sources: + - audio_file_source + on_mute: + - media_player.pause: + id: media_player_id + on_unmute: + - media_player.play: + id: media_player_id diff --git a/tests/components/speaker_source/test.esp32-idf.yaml b/tests/components/speaker_source/test.esp32-idf.yaml new file mode 100644 index 0000000000..e2439ebdf2 --- /dev/null +++ b/tests/components/speaker_source/test.esp32-idf.yaml @@ -0,0 +1,9 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + i2s_bclk_pin: GPIO27 + i2s_lrclk_pin: GPIO26 + i2s_mclk_pin: GPIO25 + i2s_dout_pin: GPIO23 + +<<: !include common.yaml diff --git a/tests/components/speaker_source/test.wav b/tests/components/speaker_source/test.wav new file mode 100644 index 0000000000000000000000000000000000000000..f9d07ef2238eb2fcb355055466d3789ee1a1fe0b GIT binary patch literal 46 ycmWIYbaPW<U|<M$40BD(Em06)U|?WmU}R{pV_;yYWnf@p5MW42EJ<Wy0098Ji3Z~U literal 0 HcmV?d00001 From 8d988723cdcd18dbe5a464f3065017a3cf64cbcf Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:38:50 -0400 Subject: [PATCH 519/609] [config] Allow !extend/!remove on components without id in schema (#14682) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/voluptuous_schema.py | 6 ++ .../external_components/common.yaml | 6 +- .../external_components/test.esp32-idf.yaml | 4 + tests/unit_tests/test_voluptuous_schema.py | 86 +++++++++++++++++++ 4 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 tests/unit_tests/test_voluptuous_schema.py diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py index 7220fb307f..0703c54a7a 100644 --- a/esphome/voluptuous_schema.py +++ b/esphome/voluptuous_schema.py @@ -175,6 +175,12 @@ class _Schema(vol.Schema): else: if self.extra == vol.ALLOW_EXTRA: out[key] = value + elif key == "id": + # Silently drop 'id' on any dict so that + # !extend / !remove work on every list-based + # config without requiring each component to + # declare an id in its schema. + pass elif self.extra != vol.REMOVE_EXTRA: if isinstance(key, str) and key_names: matches = difflib.get_close_matches(key, key_names) diff --git a/tests/components/external_components/common.yaml b/tests/components/external_components/common.yaml index 2b51267ec6..c55129094c 100644 --- a/tests/components/external_components/common.yaml +++ b/tests/components/external_components/common.yaml @@ -1,6 +1,8 @@ external_components: - - source: github://esphome/esphome@dev + - id: my_ext + source: github://esphome/esphome@dev refresh: 1d components: [bh1750] - - source: ../../../esphome/components + - id: my_local + source: ../../../esphome/components components: [sntp] diff --git a/tests/components/external_components/test.esp32-idf.yaml b/tests/components/external_components/test.esp32-idf.yaml index dade44d145..afe2bd5d6a 100644 --- a/tests/components/external_components/test.esp32-idf.yaml +++ b/tests/components/external_components/test.esp32-idf.yaml @@ -1 +1,5 @@ +# WARNING: Using !extend or !remove prevents automatic component grouping in CI, making builds slower. <<: !include common.yaml + +external_components: + - id: !remove my_local diff --git a/tests/unit_tests/test_voluptuous_schema.py b/tests/unit_tests/test_voluptuous_schema.py new file mode 100644 index 0000000000..21c7decede --- /dev/null +++ b/tests/unit_tests/test_voluptuous_schema.py @@ -0,0 +1,86 @@ +"""Tests for voluptuous_schema.py.""" + +import pytest +import voluptuous as vol + +from esphome.voluptuous_schema import _Schema + + +class TestIdKeyDropping: + """Test that 'id' keys are silently dropped in PREVENT_EXTRA schemas.""" + + def test_id_key_silently_dropped(self): + """Schema without 'id' should accept and drop 'id' key from input.""" + schema = _Schema( + { + vol.Required("name"): str, + vol.Optional("value", default=0): int, + } + ) + result = schema({"name": "test", "value": 42, "id": "my_id"}) + assert result == {"name": "test", "value": 42} + assert "id" not in result + + def test_id_key_dropped_with_only_required(self): + """Schema with only required keys should still drop 'id'.""" + schema = _Schema( + { + vol.Required("source"): str, + } + ) + result = schema({"source": "github://test", "id": "my_component"}) + assert result == {"source": "github://test"} + + def test_other_extra_keys_still_rejected(self): + """Non-'id' extra keys should still raise errors.""" + schema = _Schema( + { + vol.Required("name"): str, + } + ) + with pytest.raises(vol.MultipleInvalid, match="extra keys not allowed"): + schema({"name": "test", "unknown_key": "value"}) + + def test_id_key_not_dropped_when_in_schema(self): + """When 'id' is declared in the schema, it should be validated normally.""" + schema = _Schema( + { + vol.Required("id"): str, + vol.Required("name"): str, + } + ) + result = schema({"id": "my_id", "name": "test"}) + assert result == {"id": "my_id", "name": "test"} + + def test_id_key_not_dropped_with_allow_extra(self): + """With ALLOW_EXTRA, 'id' should be kept (not dropped).""" + schema = _Schema( + { + vol.Required("name"): str, + }, + extra=vol.ALLOW_EXTRA, + ) + result = schema({"name": "test", "id": "my_id"}) + assert result == {"name": "test", "id": "my_id"} + + def test_id_key_dropped_with_remove_extra(self): + """With REMOVE_EXTRA, 'id' should be removed along with other extras.""" + schema = _Schema( + { + vol.Required("name"): str, + }, + extra=vol.REMOVE_EXTRA, + ) + result = schema({"name": "test", "id": "my_id", "other": "value"}) + assert result == {"name": "test"} + + def test_without_id_no_extra_keys(self): + """Normal validation without 'id' key should work as before.""" + schema = _Schema( + { + vol.Required("name"): str, + vol.Optional("value", default=0): int, + } + ) + result = schema({"name": "test"}) + assert result == {"name": "test", "value": 0} From 6356e3def9dce68f3e815d5bd58d2c42ef4ccb7f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 10:42:38 -1000 Subject: [PATCH 520/609] [core] Warn on crystal frequency mismatch during serial upload (#14582) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/__main__.py | 58 ++++++++++- esphome/util.py | 45 +++++++-- tests/unit_tests/test_main.py | 124 ++++++++++++++++++++++++ tests/unit_tests/test_util.py | 175 ++++++++++++++++++++++++++++++++++ 4 files changed, 391 insertions(+), 11 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index f33e7f4b42..3f0da85a69 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1,6 +1,7 @@ # PYTHON_ARGCOMPLETE_OK import argparse from collections.abc import Callable +from contextlib import suppress from datetime import datetime import functools import getpass @@ -687,6 +688,47 @@ def _check_and_emit_build_info() -> None: ) +def _get_configured_xtal_freq() -> int | None: + """Read the configured crystal frequency from the sdkconfig file.""" + sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}") + if not sdkconfig_path.is_file(): + return None + with suppress(OSError, ValueError): + content = sdkconfig_path.read_text() + for line in content.splitlines(): + if line.startswith("CONFIG_XTAL_FREQ="): + return int(line.split("=", 1)[1]) + return None + + +def _make_crystal_freq_callback( + configured_freq: int, +) -> Callable[[str], str | None]: + """Create a callback that checks esptool crystal frequency output.""" + crystal_re = re.compile(r"Crystal frequency:\s+(\d+)\s*MHz") + + def check_crystal_line(line: str) -> str | None: + if not (match := crystal_re.search(line)): + return None + detected = int(match.group(1)) + if detected == configured_freq: + return None + return ( + f"\n\033[33mWARNING: Crystal frequency mismatch! " + f"Device reports {detected}MHz but firmware is configured " + f"for {configured_freq}MHz.\n" + f"UART logging and other clock-dependent features will not " + f"work correctly.\n" + f"Set the correct crystal frequency with sdkconfig_options:\n" + f" esp32:\n" + f" framework:\n" + f" sdkconfig_options:\n" + f" CONFIG_XTAL_FREQ_{detected}: 'y'\033[0m\n\n" + ) + + return check_crystal_line + + def upload_using_esptool( config: ConfigType, port: str, file: str, speed: int ) -> str | int: @@ -715,6 +757,14 @@ def upload_using_esptool( mcu = get_esp32_variant().lower() + line_callbacks: list[Callable[[str], str | None]] = [] + if ( + CORE.is_esp32 + and file is None + and (configured_freq := _get_configured_xtal_freq()) is not None + ): + line_callbacks.append(_make_crystal_freq_callback(configured_freq)) + def run_esptool(baud_rate): cmd = [ "esptool", @@ -739,9 +789,13 @@ def upload_using_esptool( if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: import esptool - return run_external_command(esptool.main, *cmd) # pylint: disable=no-member + return run_external_command( + esptool.main, # pylint: disable=no-member + *cmd, + line_callbacks=line_callbacks, + ) - return run_external_process(*cmd) + return run_external_process(*cmd, line_callbacks=line_callbacks) rc = run_esptool(first_baudrate) if rc == 0 or first_baudrate == 115200: diff --git a/esphome/util.py b/esphome/util.py index 6a21b4f627..73cc3aa5ab 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -125,7 +125,12 @@ ANSI_ESCAPE = re.compile(r"\033[@-_][0-?]*[ -/]*[@-~]") class RedirectText: - def __init__(self, out, filter_lines=None): + def __init__( + self, + out, + filter_lines: list[str] | None = None, + line_callbacks: list[Callable[[str], str | None]] | None = None, + ) -> None: self._out = out if filter_lines is None: self._filter_pattern = None @@ -133,6 +138,7 @@ class RedirectText: pattern = r"|".join(r"(?:" + pattern + r")" for pattern in filter_lines) self._filter_pattern = re.compile(pattern) self._line_buffer = "" + self._line_callbacks = line_callbacks or [] def __getattr__(self, item): return getattr(self._out, item) @@ -157,7 +163,7 @@ class RedirectText: if not isinstance(s, str): s = s.decode() - if self._filter_pattern is not None: + if self._filter_pattern is not None or self._line_callbacks: self._line_buffer += s lines = self._line_buffer.splitlines(True) for line in lines: @@ -169,7 +175,10 @@ class RedirectText: line_without_ansi = ANSI_ESCAPE.sub("", line) line_without_end = line_without_ansi.rstrip() - if self._filter_pattern.match(line_without_end) is not None: + if ( + self._filter_pattern is not None + and self._filter_pattern.match(line_without_end) is not None + ): # Filter pattern matched, ignore the line continue @@ -181,6 +190,9 @@ class RedirectText: and (help_msg := get_esp32_arduino_flash_error_help()) ): self._write_color_replace(help_msg) + for callback in self._line_callbacks: + if msg := callback(line_without_end): + self._write_color_replace(msg) else: self._write_color_replace(s) @@ -194,7 +206,11 @@ class RedirectText: def run_external_command( - func, *cmd, capture_stdout: bool = False, filter_lines: str = None + func, + *cmd, + capture_stdout: bool = False, + filter_lines: list[str] | None = None, + line_callbacks: list[Callable[[str], str | None]] | None = None, ) -> int | str: """ Run a function from an external package that acts like a main method. @@ -204,7 +220,9 @@ def run_external_command( :param func: Function to execute :param cmd: Command to run as (eg first element of sys.argv) :param capture_stdout: Capture text from stdout and return that. - :param filter_lines: Regular expression used to filter captured output. + Note: line_callbacks are not invoked when capture_stdout is True. + :param filter_lines: Regular expressions used to filter captured output. + :param line_callbacks: Callbacks invoked per line; non-None returns are written to output. :return: str if `capture_stdout` is set else int exit code. """ @@ -218,9 +236,13 @@ def run_external_command( _LOGGER.debug("Running: %s", full_cmd) orig_stdout = sys.stdout - sys.stdout = RedirectText(sys.stdout, filter_lines=filter_lines) + sys.stdout = RedirectText( + sys.stdout, filter_lines=filter_lines, line_callbacks=line_callbacks + ) orig_stderr = sys.stderr - sys.stderr = RedirectText(sys.stderr, filter_lines=filter_lines) + sys.stderr = RedirectText( + sys.stderr, filter_lines=filter_lines, line_callbacks=line_callbacks + ) if capture_stdout: cap_stdout = sys.stdout = io.StringIO() @@ -254,14 +276,19 @@ def run_external_process(*cmd: str, **kwargs: Any) -> int | str: full_cmd = " ".join(shlex_quote(x) for x in cmd) _LOGGER.debug("Running: %s", full_cmd) filter_lines = kwargs.get("filter_lines") + line_callbacks = kwargs.get("line_callbacks") capture_stdout = kwargs.get("capture_stdout", False) if capture_stdout: sub_stdout = subprocess.PIPE else: - sub_stdout = RedirectText(sys.stdout, filter_lines=filter_lines) + sub_stdout = RedirectText( + sys.stdout, filter_lines=filter_lines, line_callbacks=line_callbacks + ) - sub_stderr = RedirectText(sys.stderr, filter_lines=filter_lines) + sub_stderr = RedirectText( + sys.stderr, filter_lines=filter_lines, line_callbacks=line_callbacks + ) try: proc = subprocess.run( diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index b6f1a28086..b853461151 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -6,6 +6,7 @@ from collections.abc import Generator from dataclasses import dataclass import json import logging +import os from pathlib import Path import re import sys @@ -19,6 +20,8 @@ from pytest import CaptureFixture from esphome import platformio_api from esphome.__main__ import ( Purpose, + _get_configured_xtal_freq, + _make_crystal_freq_callback, choose_upload_log_host, command_analyze_memory, command_clean_all, @@ -3717,3 +3720,124 @@ esp32: clean_output.split("SUMMARY")[1] if "SUMMARY" in clean_output else "" ) assert "secrets.yaml" not in summary_section + + +def test_get_configured_xtal_freq_reads_sdkconfig(tmp_path: Path) -> None: + """Test reading XTAL_FREQ from sdkconfig.""" + CORE.name = "test-device" + CORE.build_path = tmp_path + sdkconfig = tmp_path / "sdkconfig.test-device" + sdkconfig.write_text( + "CONFIG_SOC_XTAL_SUPPORT_26M=y\nCONFIG_XTAL_FREQ=26\nCONFIG_XTAL_FREQ_26=y\n" + ) + assert _get_configured_xtal_freq() == 26 + + +def test_get_configured_xtal_freq_default_40(tmp_path: Path) -> None: + """Test reading default 40MHz XTAL_FREQ from sdkconfig.""" + CORE.name = "test-device" + CORE.build_path = tmp_path + sdkconfig = tmp_path / "sdkconfig.test-device" + sdkconfig.write_text("CONFIG_XTAL_FREQ=40\nCONFIG_XTAL_FREQ_40=y\n") + assert _get_configured_xtal_freq() == 40 + + +def test_get_configured_xtal_freq_missing_file(tmp_path: Path) -> None: + """Test that missing sdkconfig returns None.""" + CORE.name = "test-device" + CORE.build_path = tmp_path + assert _get_configured_xtal_freq() is None + + +def test_get_configured_xtal_freq_no_xtal_line(tmp_path: Path) -> None: + """Test that sdkconfig without XTAL_FREQ returns None.""" + CORE.name = "test-device" + CORE.build_path = tmp_path + sdkconfig = tmp_path / "sdkconfig.test-device" + sdkconfig.write_text("CONFIG_OTHER=123\n") + assert _get_configured_xtal_freq() is None + + +def test_crystal_freq_callback_mismatch() -> None: + """Test callback returns warning on crystal frequency mismatch.""" + callback = _make_crystal_freq_callback(40) + result = callback("Crystal frequency: 26MHz") + assert result is not None + assert "26MHz" in result + assert "40MHz" in result + assert "CONFIG_XTAL_FREQ_26" in result + + +def test_crystal_freq_callback_match() -> None: + """Test callback returns None when frequencies match.""" + callback = _make_crystal_freq_callback(40) + result = callback("Crystal frequency: 40MHz") + assert result is None + + +def test_crystal_freq_callback_no_crystal_line() -> None: + """Test callback returns None for unrelated lines.""" + callback = _make_crystal_freq_callback(40) + assert callback("Chip type: ESP8684H") is None + assert callback("MAC: a0:b7:65:8b:16:d4") is None + assert callback("") is None + + +def test_upload_using_esptool_passes_crystal_callback( + tmp_path: Path, + mock_run_external_command_main: Mock, + mock_get_idedata: Mock, +) -> None: + """Test that upload_using_esptool passes crystal freq callback for ESP32.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test") + CORE.data[KEY_ESP32] = {KEY_VARIANT: VARIANT_ESP32} + + # Create sdkconfig with XTAL_FREQ + build_dir = Path(CORE.build_path) + build_dir.mkdir(parents=True, exist_ok=True) + sdkconfig = build_dir / "sdkconfig.test" + sdkconfig.write_text("CONFIG_XTAL_FREQ=40\n") + + mock_idedata = MagicMock(spec=platformio_api.IDEData) + mock_idedata.firmware_bin_path = tmp_path / "firmware.bin" + mock_idedata.extra_flash_images = [] + mock_get_idedata.return_value = mock_idedata + (tmp_path / "firmware.bin").touch() + + config = {CONF_ESPHOME: {"platformio_options": {}}} + upload_using_esptool(config, "/dev/ttyUSB0", None, None) + + # Verify line_callbacks was passed with the crystal callback + call_kwargs = mock_run_external_command_main.call_args[1] + assert "line_callbacks" in call_kwargs + assert len(call_kwargs["line_callbacks"]) == 1 + + +def test_upload_using_esptool_subprocess_passes_crystal_callback( + mock_run_external_process: Mock, + mock_get_idedata: Mock, + tmp_path: Path, +) -> None: + """Test that crystal freq callback is passed via run_external_process.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test") + CORE.data[KEY_ESP32] = {KEY_VARIANT: VARIANT_ESP32} + + # Create sdkconfig with XTAL_FREQ + build_dir = Path(CORE.build_path) + build_dir.mkdir(parents=True, exist_ok=True) + sdkconfig = build_dir / "sdkconfig.test" + sdkconfig.write_text("CONFIG_XTAL_FREQ=40\n") + + mock_idedata = MagicMock(spec=platformio_api.IDEData) + mock_idedata.firmware_bin_path = tmp_path / "firmware.bin" + mock_idedata.extra_flash_images = [] + mock_get_idedata.return_value = mock_idedata + (tmp_path / "firmware.bin").touch() + + config = {CONF_ESPHOME: {"platformio_options": {}}} + with patch.dict(os.environ, {"ESPHOME_USE_SUBPROCESS": "1"}): + upload_using_esptool(config, "/dev/ttyUSB0", None, None) + + call_kwargs = mock_run_external_process.call_args[1] + assert "line_callbacks" in call_kwargs + assert len(call_kwargs["line_callbacks"]) == 1 diff --git a/tests/unit_tests/test_util.py b/tests/unit_tests/test_util.py index ca3fd9b78a..ff58fb1394 100644 --- a/tests/unit_tests/test_util.py +++ b/tests/unit_tests/test_util.py @@ -2,9 +2,12 @@ from __future__ import annotations +from collections.abc import Callable +import io from pathlib import Path import subprocess import sys +from typing import Any from unittest.mock import MagicMock, patch import pytest @@ -407,6 +410,178 @@ def test_shlex_quote_edge_cases() -> None: assert util.shlex_quote(" ") == "' '" +def _make_redirect( + line_callbacks: list[Callable[[str], str | None]] | None = None, + filter_lines: list[str] | None = None, +) -> tuple[util.RedirectText, io.StringIO]: + """Create a RedirectText that writes to a StringIO buffer.""" + buf = io.StringIO() + redirect = util.RedirectText( + buf, filter_lines=filter_lines, line_callbacks=line_callbacks + ) + return redirect, buf + + +def test_redirect_text_callback_called_on_matching_line() -> None: + """Test that a line callback is called and its output is written.""" + results: list[str] = [] + + def callback(line: str) -> str | None: + results.append(line) + if "target" in line: + return "CALLBACK OUTPUT\n" + return None + + redirect, buf = _make_redirect(line_callbacks=[callback]) + redirect.write("some target line\n") + + assert "some target line" in buf.getvalue() + assert "CALLBACK OUTPUT" in buf.getvalue() + assert len(results) == 1 + + +def test_redirect_text_callback_not_triggered_on_non_matching_line() -> None: + """Test that callback returns None for non-matching lines.""" + + def callback(line: str) -> str | None: + if "target" in line: + return "FOUND\n" + return None + + redirect, buf = _make_redirect(line_callbacks=[callback]) + redirect.write("no match here\n") + + assert "no match here" in buf.getvalue() + assert "FOUND" not in buf.getvalue() + + +def test_redirect_text_callback_works_without_filter_pattern() -> None: + """Test that callbacks fire even when no filter_lines is set.""" + + def callback(line: str) -> str | None: + if "Crystal" in line: + return "WARNING: mismatch\n" + return None + + redirect, buf = _make_redirect(line_callbacks=[callback]) + redirect.write("Crystal frequency: 26MHz\n") + + assert "Crystal frequency: 26MHz" in buf.getvalue() + assert "WARNING: mismatch" in buf.getvalue() + + +def test_redirect_text_callback_works_with_filter_pattern() -> None: + """Test that callbacks fire alongside filter patterns.""" + + def callback(line: str) -> str | None: + if "important" in line: + return "NOTED\n" + return None + + redirect, buf = _make_redirect( + line_callbacks=[callback], + filter_lines=[r"^skip this.*"], + ) + redirect.write("skip this line\n") + redirect.write("important line\n") + + assert "skip this" not in buf.getvalue() + assert "important line" in buf.getvalue() + assert "NOTED" in buf.getvalue() + + +def test_redirect_text_multiple_callbacks() -> None: + """Test that multiple callbacks are all invoked.""" + + def callback_a(line: str) -> str | None: + if "test" in line: + return "FROM A\n" + return None + + def callback_b(line: str) -> str | None: + if "test" in line: + return "FROM B\n" + return None + + redirect, buf = _make_redirect(line_callbacks=[callback_a, callback_b]) + redirect.write("test line\n") + + output = buf.getvalue() + assert "FROM A" in output + assert "FROM B" in output + + +def test_redirect_text_incomplete_line_buffered() -> None: + """Test that incomplete lines are buffered until newline.""" + results: list[str] = [] + + def callback(line: str) -> str | None: + results.append(line) + return None + + redirect, buf = _make_redirect(line_callbacks=[callback]) + redirect.write("partial") + assert len(results) == 0 + + redirect.write(" line\n") + assert len(results) == 1 + assert results[0] == "partial line" + + +def test_run_external_command_line_callbacks(capsys: pytest.CaptureFixture) -> None: + """Test that run_external_command passes line_callbacks to RedirectText.""" + results: list[str] = [] + + def callback(line: str) -> str | None: + results.append(line) + if "hello" in line: + return "CALLBACK FIRED\n" + return None + + def fake_main() -> int: + print("hello world") + return 0 + + rc = util.run_external_command(fake_main, "fake", line_callbacks=[callback]) + + assert rc == 0 + assert len(results) == 1 + assert "hello world" in results[0] + captured = capsys.readouterr() + assert "CALLBACK FIRED" in captured.out + + +def test_run_external_process_line_callbacks() -> None: + """Test that run_external_process passes line_callbacks to RedirectText.""" + results: list[str] = [] + + def callback(line: str) -> str | None: + results.append(line) + if "from subprocess" in line: + return "PROCESS CALLBACK\n" + return None + + with patch("esphome.util.subprocess.run") as mock_run: + + def run_side_effect(*args: Any, **kwargs: Any) -> MagicMock: + # Simulate subprocess writing to the stdout RedirectText + stdout = kwargs.get("stdout") + if stdout is not None and isinstance(stdout, util.RedirectText): + stdout.write("from subprocess\n") + return MagicMock(returncode=0) + + mock_run.side_effect = run_side_effect + + rc = util.run_external_process( + "echo", + "test", + line_callbacks=[callback], + ) + + assert rc == 0 + assert any("from subprocess" in r for r in results) + + def test_get_picotool_path_found(tmp_path: Path) -> None: """Test picotool path derivation from cc_path.""" # Create the expected directory structure From 9513edc46875904c9638133c9feb612de9301c62 Mon Sep 17 00:00:00 2001 From: CFlix <38142312+CFlix@users.noreply.github.com> Date: Tue, 10 Mar 2026 22:17:13 +0100 Subject: [PATCH 521/609] [dew_point] Add dew_point sensor component (#14441) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/dew_point/__init__.py | 1 + esphome/components/dew_point/dew_point.cpp | 82 +++++++++++++++++++ esphome/components/dew_point/dew_point.h | 26 ++++++ esphome/components/dew_point/sensor.py | 46 +++++++++++ tests/components/dew_point/common.yaml | 19 +++++ .../components/dew_point/test.esp32-idf.yaml | 1 + .../dew_point/test.esp8266-ard.yaml | 1 + .../components/dew_point/test.rp2040-ard.yaml | 1 + 9 files changed, 178 insertions(+) create mode 100644 esphome/components/dew_point/__init__.py create mode 100644 esphome/components/dew_point/dew_point.cpp create mode 100644 esphome/components/dew_point/dew_point.h create mode 100644 esphome/components/dew_point/sensor.py create mode 100644 tests/components/dew_point/common.yaml create mode 100644 tests/components/dew_point/test.esp32-idf.yaml create mode 100644 tests/components/dew_point/test.esp8266-ard.yaml create mode 100644 tests/components/dew_point/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 12aff01e73..e72b164761 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -132,6 +132,7 @@ esphome/components/dashboard_import/* @esphome/core esphome/components/datetime/* @jesserockz @rfdarter esphome/components/debug/* @esphome/core esphome/components/delonghi/* @grob6000 +esphome/components/dew_point/* @CFlix esphome/components/dfplayer/* @glmnet esphome/components/dfrobot_sen0395/* @niklasweber esphome/components/dht/* @OttoWinter diff --git a/esphome/components/dew_point/__init__.py b/esphome/components/dew_point/__init__.py new file mode 100644 index 0000000000..3b852436c3 --- /dev/null +++ b/esphome/components/dew_point/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@CFlix"] diff --git a/esphome/components/dew_point/dew_point.cpp b/esphome/components/dew_point/dew_point.cpp new file mode 100644 index 0000000000..04ac305e3d --- /dev/null +++ b/esphome/components/dew_point/dew_point.cpp @@ -0,0 +1,82 @@ + +#include "dew_point.h" + +namespace esphome::dew_point { + +static const char *const TAG = "dew_point.sensor"; + +void DewPointComponent::setup() { + // Register callbacks for sensor updates + if (this->temperature_sensor_ != nullptr) { + this->temperature_sensor_->add_on_state_callback([this](float state) { + this->temperature_value_ = state; + this->enable_loop(); + }); + // Get initial value + if (this->temperature_sensor_->has_state()) { + this->temperature_value_ = this->temperature_sensor_->get_state(); + } + } + + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->humidity_value_ = state; + this->enable_loop(); + }); + // Get initial value + if (this->humidity_sensor_->has_state()) { + this->humidity_value_ = this->humidity_sensor_->get_state(); + } + } +} + +void DewPointComponent::dump_config() { + LOG_SENSOR("", "Dew Point", this); + ESP_LOGCONFIG(TAG, + "Sources\n" + " Temperature: '%s'\n" + " Humidity: '%s'", + this->temperature_sensor_->get_name().c_str(), this->humidity_sensor_->get_name().c_str()); +} + +float DewPointComponent::get_setup_priority() const { return setup_priority::DATA; } + +void DewPointComponent::loop() { + // Only run once + this->disable_loop(); + + // Check if we have valid values for both sensors + if (std::isnan(this->temperature_value_) || std::isnan(this->humidity_value_)) { + ESP_LOGW(TAG, "Temperature or humidity value is NaN, skipping calculation"); + this->publish_state(NAN); + return; + } + + // Check for valid humidity range + if (this->humidity_value_ <= 0.0f || this->humidity_value_ > 100.0f) { + ESP_LOGW(TAG, "Humidity value out of range (0-100): %.2f", this->humidity_value_); + this->publish_state(NAN); + return; + } + + // Magnus formula constants + const float a{17.625f}; + const float b{243.04f}; + + // Calculate dew point using Magnus formula + // Td = (b * alpha) / (a - alpha) + // where alpha = ln(RH/100) + (a * T) / (b + T) + + const float alpha{std::log(this->humidity_value_ / 100.0f) + + (a * this->temperature_value_) / (b + this->temperature_value_)}; + + const float dew_point{(b * alpha) / (a - alpha)}; + + // Publish the calculated dew point + this->publish_state(dew_point); + + ESP_LOGD(TAG, "'%s' >> %.1f°C (T: %.1f°C, RH: %.1f%%)", this->get_name().c_str(), dew_point, this->temperature_value_, + this->humidity_value_); +} + +} // namespace esphome::dew_point diff --git a/esphome/components/dew_point/dew_point.h b/esphome/components/dew_point/dew_point.h new file mode 100644 index 0000000000..833c50fba2 --- /dev/null +++ b/esphome/components/dew_point/dew_point.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome::dew_point { + +class DewPointComponent : public Component, public sensor::Sensor { + public: + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } + + void setup() override; + void dump_config() override; + void loop() override; + + float get_setup_priority() const override; + + protected: + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + float temperature_value_{NAN}; + float humidity_value_{NAN}; +}; + +} // namespace esphome::dew_point diff --git a/esphome/components/dew_point/sensor.py b/esphome/components/dew_point/sensor.py new file mode 100644 index 0000000000..4fee095602 --- /dev/null +++ b/esphome/components/dew_point/sensor.py @@ -0,0 +1,46 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_HUMIDITY, + CONF_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +DEPENDENCIES = ["sensor"] + +dew_point_ns = cg.esphome_ns.namespace("dew_point") +DewPointComponent = dew_point_ns.class_( + "DewPointComponent", cg.Component, sensor.Sensor +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + DewPointComponent, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:weather-rainy", + ) + .extend( + { + cv.Required(CONF_TEMPERATURE): cv.use_id(sensor.Sensor), + cv.Required(CONF_HUMIDITY): cv.use_id(sensor.Sensor), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + + temperature_sensor = await cg.get_variable(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(temperature_sensor)) + + humidity_sensor = await cg.get_variable(config[CONF_HUMIDITY]) + cg.add(var.set_humidity_sensor(humidity_sensor)) diff --git a/tests/components/dew_point/common.yaml b/tests/components/dew_point/common.yaml new file mode 100644 index 0000000000..527eeb2f84 --- /dev/null +++ b/tests/components/dew_point/common.yaml @@ -0,0 +1,19 @@ +sensor: + - platform: dew_point + name: Dew Point + temperature: template_temperature + humidity: template_humidity + - platform: template + id: template_humidity + lambda: |- + if (millis() > 10000) { + return 0.6; + } + return 0.0; + - platform: template + id: template_temperature + lambda: |- + if (millis() > 10000) { + return 42.0; + } + return 0.0; diff --git a/tests/components/dew_point/test.esp32-idf.yaml b/tests/components/dew_point/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/dew_point/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/dew_point/test.esp8266-ard.yaml b/tests/components/dew_point/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/dew_point/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/dew_point/test.rp2040-ard.yaml b/tests/components/dew_point/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/dew_point/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 30c8c6870383f5c5e6d3f47328b3b685fc8a1518 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 11:22:23 -1000 Subject: [PATCH 522/609] [socket] Fix RP2040 TCP race condition between lwip callbacks and main loop (#14679) --- esphome/components/rp2040/helpers.cpp | 16 +++++- .../components/socket/lwip_raw_tcp_impl.cpp | 52 +++++++++++++++++++ esphome/components/socket/lwip_raw_tcp_impl.h | 6 +++ 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/esphome/components/rp2040/helpers.cpp b/esphome/components/rp2040/helpers.cpp index 30b40a723a..4191c2164a 100644 --- a/esphome/components/rp2040/helpers.cpp +++ b/esphome/components/rp2040/helpers.cpp @@ -7,6 +7,7 @@ #if defined(USE_WIFI) #include <WiFi.h> +#include <pico/cyw43_arch.h> // For cyw43_arch_lwip_begin/end (LwIPLock) #endif #include <hardware/structs/rosc.h> #include <hardware/sync.h> @@ -44,9 +45,22 @@ void Mutex::unlock() {} IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); } IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); } -// RP2040 doesn't support lwIP core locking, so this is a no-op +// On RP2040 (Pico W), arduino-pico sets PICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1. +// This means lwip callbacks run from a low-priority user IRQ context, not the +// main loop (see low_priority_irq_handler() in pico-sdk +// async_context_threadsafe_background.c). cyw43_arch_lwip_begin/end acquires the +// async_context recursive mutex to prevent IRQ callbacks from firing during +// critical sections. See esphome#10681. +// +// When CYW43 is not available (non-WiFi RP2040 boards), this is a no-op since +// there's no network stack and no lwip callbacks to race with. +#if defined(USE_WIFI) +LwIPLock::LwIPLock() { cyw43_arch_lwip_begin(); } +LwIPLock::~LwIPLock() { cyw43_arch_lwip_end(); } +#else LwIPLock::LwIPLock() {} LwIPLock::~LwIPLock() {} +#endif void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) #ifdef USE_WIFI diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 445a57809d..d7fa6a2694 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -111,6 +111,24 @@ void socket_wake() { } #endif +// ---- LWIP thread safety ---- +// +// On RP2040 (Pico W), arduino-pico sets PICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1. +// This means lwip callbacks (recv_fn, accept_fn, err_fn) run from a low-priority +// user IRQ context, not the main loop (see low_priority_irq_handler() in pico-sdk +// async_context_threadsafe_background.c). They can preempt main-loop code at any point. +// +// Without locking, this causes race conditions between recv_fn and read() on the +// shared rx_buf_ pbuf chain — recv_fn calls pbuf_cat() while read() is freeing +// nodes, leading to use-after-free and infinite-loop crashes. See esphome#10681. +// +// On ESP8266, lwip callbacks run from the SYS context which cooperates with user +// code (CONT context) — they never preempt each other, so no locking is needed. +// +// esphome::LwIPLock is the platform-provided RAII guard (see helpers.h/helpers.cpp). +// On RP2040, it acquires cyw43_arch_lwip_begin/end. On ESP8266, it's a no-op. +#define LWIP_LOCK() esphome::LwIPLock lwip_lock_guard // NOLINT + static const char *const TAG = "socket.lwip"; // set to 1 to enable verbose lwip logging @@ -123,6 +141,7 @@ static const char *const TAG = "socket.lwip"; // ---- LWIPRawCommon methods ---- LWIPRawCommon::~LWIPRawCommon() { + LWIP_LOCK(); if (this->pcb_ != nullptr) { LWIP_LOG("tcp_abort(%p)", this->pcb_); tcp_abort(this->pcb_); @@ -131,6 +150,7 @@ LWIPRawCommon::~LWIPRawCommon() { } int LWIPRawCommon::bind(const struct sockaddr *name, socklen_t addrlen) { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = EBADF; return -1; @@ -196,6 +216,7 @@ int LWIPRawCommon::bind(const struct sockaddr *name, socklen_t addrlen) { } int LWIPRawCommon::close() { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -214,6 +235,7 @@ int LWIPRawCommon::close() { } int LWIPRawCommon::shutdown(int how) { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -240,6 +262,7 @@ int LWIPRawCommon::shutdown(int how) { } int LWIPRawCommon::getpeername(struct sockaddr *name, socklen_t *addrlen) { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -252,6 +275,7 @@ int LWIPRawCommon::getpeername(struct sockaddr *name, socklen_t *addrlen) { } int LWIPRawCommon::getsockname(struct sockaddr *name, socklen_t *addrlen) { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -284,6 +308,7 @@ size_t LWIPRawCommon::getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf) { } int LWIPRawCommon::getsockopt(int level, int optname, void *optval, socklen_t *optlen) { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -318,6 +343,7 @@ int LWIPRawCommon::getsockopt(int level, int optname, void *optval, socklen_t *o } int LWIPRawCommon::setsockopt(int level, int optname, const void *optval, socklen_t optlen) { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -388,6 +414,7 @@ int LWIPRawCommon::ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *n // ---- LWIPRawImpl methods ---- LWIPRawImpl::~LWIPRawImpl() { + LWIP_LOCK(); // Free any received pbufs that LWIP transferred ownership of via recv_fn. // tcp_abort() in the base destructor won't free these since LWIP considers // ownership transferred once the recv callback accepts them. @@ -399,6 +426,7 @@ LWIPRawImpl::~LWIPRawImpl() { } void LWIPRawImpl::init() { + LWIP_LOCK(); LWIP_LOG("init(%p)", this->pcb_); tcp_arg(this->pcb_, this); tcp_recv(this->pcb_, LWIPRawImpl::s_recv_fn); @@ -406,6 +434,9 @@ void LWIPRawImpl::init() { } void LWIPRawImpl::s_err_fn(void *arg, err_t err) { + // Called by lwip core which already holds the async_context lock on RP2040. + // No LWIP_LOCK() needed — acquiring it would be redundant (recursive mutex). + // // "If a connection is aborted because of an error, the application is alerted of this event by // the err callback." // pcb is already freed when this callback is called @@ -422,6 +453,7 @@ err_t LWIPRawImpl::s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, er } err_t LWIPRawImpl::recv_fn(struct pbuf *pb, err_t err) { + // Called by lwip core which already holds the async_context lock on RP2040. LWIP_LOG("recv(pb=%p err=%d)", pb, err); if (err != 0) { // "An error code if there has been an error receiving Only return ERR_ABRT if you have @@ -448,6 +480,7 @@ err_t LWIPRawImpl::recv_fn(struct pbuf *pb, err_t err) { } ssize_t LWIPRawImpl::read(void *buf, size_t len) { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -507,6 +540,7 @@ ssize_t LWIPRawImpl::read(void *buf, size_t len) { } ssize_t LWIPRawImpl::readv(const struct iovec *iov, int iovcnt) { + LWIP_LOCK(); // Hold for entire scatter-gather operation ssize_t ret = 0; for (int i = 0; i < iovcnt; i++) { ssize_t err = this->read(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len); @@ -525,6 +559,7 @@ ssize_t LWIPRawImpl::readv(const struct iovec *iov, int iovcnt) { } ssize_t LWIPRawImpl::internal_write_(const void *buf, size_t len) { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -557,6 +592,11 @@ ssize_t LWIPRawImpl::internal_write_(const void *buf, size_t len) { } int LWIPRawImpl::internal_output_() { + LWIP_LOCK(); + if (this->pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } LWIP_LOG("tcp_output(%p)", this->pcb_); err_t err = tcp_output(this->pcb_); if (err == ERR_ABRT) { @@ -576,6 +616,7 @@ int LWIPRawImpl::internal_output_() { } ssize_t LWIPRawImpl::write(const void *buf, size_t len) { + LWIP_LOCK(); // Hold for write + optional output ssize_t written = this->internal_write_(buf, len); if (written == -1) return -1; @@ -592,6 +633,7 @@ ssize_t LWIPRawImpl::write(const void *buf, size_t len) { } ssize_t LWIPRawImpl::writev(const struct iovec *iov, int iovcnt) { + LWIP_LOCK(); // Hold for entire scatter-gather operation ssize_t written = 0; for (int i = 0; i < iovcnt; i++) { ssize_t err = this->internal_write_(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len); @@ -621,6 +663,7 @@ ssize_t LWIPRawImpl::writev(const struct iovec *iov, int iovcnt) { // ---- LWIPRawListenImpl methods ---- LWIPRawListenImpl::~LWIPRawListenImpl() { + LWIP_LOCK(); // Listen PCBs must use tcp_close(), not tcp_abort(). // tcp_abandon() asserts pcb->state != LISTEN and would access // fields that don't exist in the smaller tcp_pcb_listen struct. @@ -632,6 +675,7 @@ LWIPRawListenImpl::~LWIPRawListenImpl() { } void LWIPRawListenImpl::init() { + LWIP_LOCK(); LWIP_LOG("init(%p)", this->pcb_); tcp_arg(this->pcb_, this); tcp_accept(this->pcb_, LWIPRawListenImpl::s_accept_fn); @@ -639,6 +683,7 @@ void LWIPRawListenImpl::init() { } void LWIPRawListenImpl::s_err_fn(void *arg, err_t err) { + // Called by lwip core which already holds the async_context lock on RP2040. auto *arg_this = reinterpret_cast<LWIPRawListenImpl *>(arg); ESP_LOGVV(TAG, "socket %p: err(err=%d)", arg_this, err); arg_this->pcb_ = nullptr; @@ -650,6 +695,7 @@ err_t LWIPRawListenImpl::s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t er } std::unique_ptr<LWIPRawImpl> LWIPRawListenImpl::accept(struct sockaddr *addr, socklen_t *addrlen) { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = EBADF; return nullptr; @@ -674,6 +720,7 @@ std::unique_ptr<LWIPRawImpl> LWIPRawListenImpl::accept(struct sockaddr *addr, so } int LWIPRawListenImpl::listen(int backlog) { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = EBADF; return -1; @@ -699,6 +746,7 @@ int LWIPRawListenImpl::listen(int backlog) { } err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) { + // Called by lwip core which already holds the async_context lock on RP2040. LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err); if (err != ERR_OK || newpcb == nullptr) { // "An error code if there has been an error accepting. Only return ERR_ABRT if you have @@ -734,6 +782,7 @@ std::unique_ptr<Socket> socket(int domain, int type, int protocol) { errno = EPROTOTYPE; return nullptr; } + LWIP_LOCK(); auto *pcb = tcp_new(); if (pcb == nullptr) return nullptr; @@ -753,6 +802,7 @@ std::unique_ptr<ListenSocket> socket_listen(int domain, int type, int protocol) errno = EPROTOTYPE; return nullptr; } + LWIP_LOCK(); auto *pcb = tcp_new(); if (pcb == nullptr) return nullptr; @@ -766,6 +816,8 @@ std::unique_ptr<ListenSocket> socket_listen_loop_monitored(int domain, int type, return socket_listen(domain, type, protocol); } +#undef LWIP_LOCK + } // namespace esphome::socket #endif // USE_SOCKET_IMPL_LWIP_TCP diff --git a/esphome/components/socket/lwip_raw_tcp_impl.h b/esphome/components/socket/lwip_raw_tcp_impl.h index c171e0537f..5b2c11cfe2 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.h +++ b/esphome/components/socket/lwip_raw_tcp_impl.h @@ -95,8 +95,13 @@ class LWIPRawImpl : public LWIPRawCommon { errno = ENOSYS; return -1; } + // Intentionally unlocked — this is a polling check called every loop iteration. + // A stale read at worst delays processing by one loop tick; the actual I/O in + // read() holds the lwip lock and re-checks properly. See esphome#10681. bool ready() const { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; } + // No lock needed — only called during setup before callbacks are registered. + // A stale pcb_ read is benign (returns ECONNRESET, which the caller handles). int setblocking(bool blocking) { if (this->pcb_ == nullptr) { errno = ECONNRESET; @@ -134,6 +139,7 @@ class LWIPRawListenImpl : public LWIPRawCommon { void init(); + // Intentionally unlocked — polling check, see LWIPRawImpl::ready() comment. bool ready() const { return this->accepted_socket_count_ > 0; } std::unique_ptr<LWIPRawImpl> accept(struct sockaddr *addr, socklen_t *addrlen); From dcbf3c8728a1a0040e0f2899a6bfc899888f0e9e Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht <rupprecht.thomas@gmail.com> Date: Tue, 10 Mar 2026 23:18:35 +0100 Subject: [PATCH 523/609] [esp32] gpio type improvements (#14517) --- esphome/components/esp32/gpio.py | 4 ++-- esphome/components/esp32/gpio_esp32.py | 5 +++-- esphome/components/esp32/gpio_esp32_c2.py | 5 +++-- esphome/components/esp32/gpio_esp32_c3.py | 5 +++-- esphome/components/esp32/gpio_esp32_c5.py | 5 +++-- esphome/components/esp32/gpio_esp32_c6.py | 5 +++-- esphome/components/esp32/gpio_esp32_c61.py | 5 +++-- esphome/components/esp32/gpio_esp32_h2.py | 5 +++-- esphome/components/esp32/gpio_esp32_p4.py | 5 +++-- esphome/components/esp32/gpio_esp32_s2.py | 5 +++-- esphome/components/esp32/gpio_esp32_s3.py | 5 +++-- 11 files changed, 32 insertions(+), 22 deletions(-) diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index c0803f40a8..a7180cbcd7 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -88,8 +88,8 @@ def _translate_pin(value): @dataclass class ESP32ValidationFunctions: - pin_validation: Callable[[Any], Any] - usage_validation: Callable[[Any], Any] + pin_validation: Callable[[int], int] + usage_validation: Callable[[dict[str, Any]], dict[str, Any]] _esp32_validations = { diff --git a/esphome/components/esp32/gpio_esp32.py b/esphome/components/esp32/gpio_esp32.py index 973d2dc0ef..b3166cf822 100644 --- a/esphome/components/esp32/gpio_esp32.py +++ b/esphome/components/esp32/gpio_esp32.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import ( @@ -22,7 +23,7 @@ _ESP32_STRAPPING_PINS = {0, 2, 5, 12, 15} _LOGGER = logging.getLogger(__name__) -def esp32_validate_gpio_pin(value): +def esp32_validate_gpio_pin(value: int) -> int: if value < 0 or value > 39: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)") if value in _ESP_SDIO_PINS: @@ -41,7 +42,7 @@ def esp32_validate_gpio_pin(value): return value -def esp32_validate_supports(value): +def esp32_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_c2.py b/esphome/components/esp32/gpio_esp32_c2.py index 32a24050ca..2d6e3a4a4e 100644 --- a/esphome/components/esp32/gpio_esp32_c2.py +++ b/esphome/components/esp32/gpio_esp32_c2.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER @@ -9,14 +10,14 @@ _ESP32C2_STRAPPING_PINS = {8, 9} _LOGGER = logging.getLogger(__name__) -def esp32_c2_validate_gpio_pin(value): +def esp32_c2_validate_gpio_pin(value: int) -> int: if value < 0 or value > 20: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-20)") return value -def esp32_c2_validate_supports(value): +def esp32_c2_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_c3.py b/esphome/components/esp32/gpio_esp32_c3.py index c1427cc02a..93e0b97093 100644 --- a/esphome/components/esp32/gpio_esp32_c3.py +++ b/esphome/components/esp32/gpio_esp32_c3.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER @@ -18,7 +19,7 @@ _ESP32C3_STRAPPING_PINS = {2, 8, 9} _LOGGER = logging.getLogger(__name__) -def esp32_c3_validate_gpio_pin(value): +def esp32_c3_validate_gpio_pin(value: int) -> int: if value < 0 or value > 21: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-21)") if value in _ESP32C3_SPI_PSRAM_PINS: @@ -29,7 +30,7 @@ def esp32_c3_validate_gpio_pin(value): return value -def esp32_c3_validate_supports(value): +def esp32_c3_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_c5.py b/esphome/components/esp32/gpio_esp32_c5.py index fa2ce1a689..639ed64c9e 100644 --- a/esphome/components/esp32/gpio_esp32_c5.py +++ b/esphome/components/esp32/gpio_esp32_c5.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA @@ -22,7 +23,7 @@ _ESP32C5_STRAPPING_PINS = {2, 7, 27, 28} _LOGGER = logging.getLogger(__name__) -def esp32_c5_validate_gpio_pin(value): +def esp32_c5_validate_gpio_pin(value: int) -> int: if value < 0 or value > 28: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-28)") if value in _ESP32C5_SPI_PSRAM_PINS: @@ -33,7 +34,7 @@ def esp32_c5_validate_gpio_pin(value): return value -def esp32_c5_validate_supports(value): +def esp32_c5_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_c6.py b/esphome/components/esp32/gpio_esp32_c6.py index 5d679dede2..cfd3bca833 100644 --- a/esphome/components/esp32/gpio_esp32_c6.py +++ b/esphome/components/esp32/gpio_esp32_c6.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA @@ -22,7 +23,7 @@ _ESP32C6_STRAPPING_PINS = {8, 9, 15} _LOGGER = logging.getLogger(__name__) -def esp32_c6_validate_gpio_pin(value): +def esp32_c6_validate_gpio_pin(value: int) -> int: if value < 0 or value > 23: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-23)") if value in _ESP32C6_SPI_PSRAM_PINS: @@ -33,7 +34,7 @@ def esp32_c6_validate_gpio_pin(value): return value -def esp32_c6_validate_supports(value): +def esp32_c6_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_c61.py b/esphome/components/esp32/gpio_esp32_c61.py index 77be42db3e..2f3abe6a0f 100644 --- a/esphome/components/esp32/gpio_esp32_c61.py +++ b/esphome/components/esp32/gpio_esp32_c61.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER @@ -20,7 +21,7 @@ _ESP32C61_STRAPPING_PINS = {8, 9} _LOGGER = logging.getLogger(__name__) -def esp32_c61_validate_gpio_pin(value): +def esp32_c61_validate_gpio_pin(value: int) -> int: if value < 0 or value > 29: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-29)") if value in _ESP32C61_SPI_PSRAM_PINS: @@ -31,7 +32,7 @@ def esp32_c61_validate_gpio_pin(value): return value -def esp32_c61_validate_supports(value): +def esp32_c61_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_h2.py b/esphome/components/esp32/gpio_esp32_h2.py index f37297764b..5e7a6158f9 100644 --- a/esphome/components/esp32/gpio_esp32_h2.py +++ b/esphome/components/esp32/gpio_esp32_h2.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER @@ -13,7 +14,7 @@ _ESP32H2_STRAPPING_PINS = {2, 3, 8, 9, 25} _LOGGER = logging.getLogger(__name__) -def esp32_h2_validate_gpio_pin(value): +def esp32_h2_validate_gpio_pin(value: int) -> int: if value < 0 or value > 27: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-27)") if value in _ESP32H2_SPI_FLASH_PINS: @@ -33,7 +34,7 @@ def esp32_h2_validate_gpio_pin(value): return value -def esp32_h2_validate_supports(value): +def esp32_h2_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_p4.py b/esphome/components/esp32/gpio_esp32_p4.py index 2726c5932f..865db92652 100644 --- a/esphome/components/esp32/gpio_esp32_p4.py +++ b/esphome/components/esp32/gpio_esp32_p4.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA @@ -14,7 +15,7 @@ _ESP32P4_STRAPPING_PINS = {34, 35, 36, 37, 38} _LOGGER = logging.getLogger(__name__) -def esp32_p4_validate_gpio_pin(value): +def esp32_p4_validate_gpio_pin(value: int) -> int: if value < 0 or value > 54: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)") if value in _ESP32P4_USB_JTAG_PINS: @@ -27,7 +28,7 @@ def esp32_p4_validate_gpio_pin(value): return value -def esp32_p4_validate_supports(value): +def esp32_p4_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_s2.py b/esphome/components/esp32/gpio_esp32_s2.py index 331aeb9d94..4978f48a1c 100644 --- a/esphome/components/esp32/gpio_esp32_s2.py +++ b/esphome/components/esp32/gpio_esp32_s2.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import ( @@ -26,7 +27,7 @@ _ESP32S2_STRAPPING_PINS = {0, 45, 46} _LOGGER = logging.getLogger(__name__) -def esp32_s2_validate_gpio_pin(value): +def esp32_s2_validate_gpio_pin(value: int) -> int: if value < 0 or value > 46: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-46)") @@ -43,7 +44,7 @@ def esp32_s2_validate_gpio_pin(value): return value -def esp32_s2_validate_supports(value): +def esp32_s2_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_s3.py b/esphome/components/esp32/gpio_esp32_s3.py index aea378f499..cb0eb8178c 100644 --- a/esphome/components/esp32/gpio_esp32_s3.py +++ b/esphome/components/esp32/gpio_esp32_s3.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER @@ -27,7 +28,7 @@ _ESP_32S3_STRAPPING_PINS = {0, 3, 45, 46} _LOGGER = logging.getLogger(__name__) -def esp32_s3_validate_gpio_pin(value): +def esp32_s3_validate_gpio_pin(value: int) -> int: if value < 0 or value > 48: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-48)") @@ -49,7 +50,7 @@ def esp32_s3_validate_gpio_pin(value): return value -def esp32_s3_validate_supports(value): +def esp32_s3_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] From b84d773becad306de294beadb0a3efa73c9a854d Mon Sep 17 00:00:00 2001 From: CFlix <38142312+CFlix@users.noreply.github.com> Date: Wed, 11 Mar 2026 01:24:46 +0100 Subject: [PATCH 524/609] [bme280] Change communication error message to include "no response" hint. (#14686) --- esphome/components/bme280_base/bme280_base.cpp | 2 +- esphome/components/bmp280_base/bmp280_base.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp index addbfe618d..f31940df10 100644 --- a/esphome/components/bme280_base/bme280_base.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -7,7 +7,7 @@ #include <esphome/components/sensor/sensor.h> #include <esphome/core/component.h> -#define BME280_ERROR_WRONG_CHIP_ID "Wrong chip ID" +#define BME280_ERROR_WRONG_CHIP_ID "Wrong chip ID or no response" namespace esphome { namespace bme280_base { diff --git a/esphome/components/bmp280_base/bmp280_base.cpp b/esphome/components/bmp280_base/bmp280_base.cpp index de685e7c27..603966a2b5 100644 --- a/esphome/components/bmp280_base/bmp280_base.cpp +++ b/esphome/components/bmp280_base/bmp280_base.cpp @@ -2,7 +2,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -#define BMP280_ERROR_WRONG_CHIP_ID "Wrong chip ID" +#define BMP280_ERROR_WRONG_CHIP_ID "Wrong chip ID or no response" namespace esphome { namespace bmp280_base { From 794098de99f083105440f8a36f2f070d1e6f9a04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 16:40:45 -1000 Subject: [PATCH 525/609] [rp2040] Add HardFault crash handler with backtrace (#14685) --- esphome/components/logger/logger_rp2040.cpp | 2 + esphome/components/rp2040/__init__.py | 47 ++++ esphome/components/rp2040/core.cpp | 2 + esphome/components/rp2040/crash_handler.cpp | 227 ++++++++++++++++++++ esphome/components/rp2040/crash_handler.h | 17 ++ 5 files changed, 295 insertions(+) create mode 100644 esphome/components/rp2040/crash_handler.cpp create mode 100644 esphome/components/rp2040/crash_handler.h diff --git a/esphome/components/logger/logger_rp2040.cpp b/esphome/components/logger/logger_rp2040.cpp index 1f435031f6..f76b823a8f 100644 --- a/esphome/components/logger/logger_rp2040.cpp +++ b/esphome/components/logger/logger_rp2040.cpp @@ -1,5 +1,6 @@ #ifdef USE_RP2040 #include "logger.h" +#include "esphome/components/rp2040/crash_handler.h" #include "esphome/core/log.h" namespace esphome::logger { @@ -25,6 +26,7 @@ void Logger::pre_setup() { } global_logger = this; ESP_LOGI(TAG, "Log initialized"); + rp2040::crash_handler_log(); } void HOT Logger::write_msg_(const char *msg, uint16_t len) { diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 359337adfb..b15811241c 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -1,6 +1,8 @@ import logging from pathlib import Path +import re from string import ascii_letters, digits +import subprocess import esphome.codegen as cg import esphome.config_validation as cv @@ -264,3 +266,48 @@ def copy_files(): path = CORE.relative_src_path("esphome.h") content = read_file(path).rstrip("\n") write_file_if_changed(path, content + '\n#include "pio_includes.h"\n') + + +# RP2040 crash handler stacktrace decoding +# Matches output from esphome/components/rp2040/crash_handler.cpp +_CRASH_RE = re.compile(r"CRASH DETECTED ON PREVIOUS BOOT") +_CRASH_ADDR_RE = re.compile( + r"(?:PC|LR|BT\d):\s+(0x[0-9a-fA-F]{8})\s+\((?:fault location|return address|stack backtrace)\)" +) + + +def _addr2line(tool: str, elf: Path, addr: str) -> str: + try: + result = subprocess.run( + [tool, "-pfiaC", "-e", str(elf), addr], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + except (OSError, subprocess.CalledProcessError): + return f"{addr} (decode failed)" + + +def process_stacktrace(config, line: str, backtrace_state: bool) -> bool: + """Decode RP2040 crash handler output using addr2line.""" + if _CRASH_RE.search(line): + _LOGGER.error("RP2040 crash detected - decoding addresses") + return True + + if backtrace_state: + if match := _CRASH_ADDR_RE.search(line): + from esphome.platformio_api import get_idedata + + idedata = get_idedata(config) + if idedata.addr2line_path: + elf = idedata.firmware_elf_path + if elf.exists(): + decoded = _addr2line(idedata.addr2line_path, elf, match.group(1)) + _LOGGER.error(" %s => %s", match.group(1), decoded) + + # Stop backtrace state after addr2line hint (last line of crash dump) + if "addr2line" in line: + return False + + return backtrace_state diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index 63b154d80d..5e5a96c78b 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -1,6 +1,7 @@ #ifdef USE_RP2040 #include "core.h" +#include "crash_handler.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -24,6 +25,7 @@ void arch_restart() { } void arch_init() { + rp2040::crash_handler_read_and_clear(); #if USE_RP2040_WATCHDOG_TIMEOUT > 0 watchdog_enable(USE_RP2040_WATCHDOG_TIMEOUT, false); #endif diff --git a/esphome/components/rp2040/crash_handler.cpp b/esphome/components/rp2040/crash_handler.cpp new file mode 100644 index 0000000000..6ab46da444 --- /dev/null +++ b/esphome/components/rp2040/crash_handler.cpp @@ -0,0 +1,227 @@ +#ifdef USE_RP2040 + +#include "crash_handler.h" +#include "esphome/core/log.h" + +#include <cinttypes> +#include <hardware/regs/addressmap.h> +#include <hardware/structs/watchdog.h> +#include <hardware/watchdog.h> + +// Cortex-M0+ exception frame offsets (words) +// When a fault occurs, the CPU pushes: R0, R1, R2, R3, R12, LR, PC, xPSR +static constexpr uint32_t EF_LR = 5; +static constexpr uint32_t EF_PC = 6; + +static constexpr uint32_t CRASH_MAGIC = 0xDEADBEEF; + +// We only have 8 scratch registers (32 bytes) that survive watchdog reboot. +// Use them for the most important data, then scan the stack for code addresses. +// +// Scratch register layout: +// [0] = magic (CRASH_MAGIC) +// [1] = PC (program counter at fault) +// [2] = LR (link register from exception frame) +// [3] = SP (stack pointer at fault) +// [4..7] = up to 4 additional code addresses found by scanning the stack +// (return addresses from callers, giving a deeper backtrace) + +// Flash is mapped at XIP_BASE (0x10000000). We use a conservative upper bound +// to keep false positives low during stack scanning. Wider ranges would match +// more stale data on the stack that happens to look like code addresses. +#if defined(PICO_RP2350) +static constexpr uint32_t FLASH_SCAN_END = XIP_BASE + 0x400000; // 4MB — RP2350 typical max +#else +static constexpr uint32_t FLASH_SCAN_END = XIP_BASE + 0x200000; // 2MB — RP2040 typical max +#endif + +static inline bool is_code_addr(uint32_t val) { + uint32_t cleared = val & ~1u; // Clear Thumb bit + return cleared >= XIP_BASE && cleared < FLASH_SCAN_END; +} + +static constexpr size_t MAX_BACKTRACE = 4; + +namespace esphome::rp2040 { + +static const char *const TAG = "rp2040.crash"; + +// Placed in .noinit so BSS zero-init cannot race with crash_handler_read_and_clear(). +// The valid field is explicitly cleared in crash_handler_read_and_clear() instead. +static struct { + bool valid; + uint32_t pc; + uint32_t lr; + uint32_t sp; + uint32_t backtrace[MAX_BACKTRACE]; + uint8_t backtrace_count; +} __attribute__((section(".noinit"))) s_crash_data; + +void crash_handler_read_and_clear() { + s_crash_data.valid = false; + if (watchdog_hw->scratch[0] == CRASH_MAGIC) { + s_crash_data.valid = true; + s_crash_data.pc = watchdog_hw->scratch[1]; + s_crash_data.lr = watchdog_hw->scratch[2]; + s_crash_data.sp = watchdog_hw->scratch[3]; + s_crash_data.backtrace_count = 0; + for (size_t i = 0; i < MAX_BACKTRACE; i++) { + uint32_t addr = watchdog_hw->scratch[4 + i]; + if (addr == 0) + break; + s_crash_data.backtrace[i] = addr; + s_crash_data.backtrace_count++; + } + } + // Clear scratch registers regardless + for (int i = 0; i < 8; i++) { + watchdog_hw->scratch[i] = 0; + } +} + +// Intentionally uses separate ESP_LOGE calls per line instead of combining into +// one multi-line log message. This ensures each address appears as its own line +// on the serial console (miniterm), making it possible to see partial output if +// the device crashes again during boot, and allowing the CLI's process_stacktrace +// to match and decode each address individually. +void crash_handler_log() { + if (!s_crash_data.valid) + return; + + ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***"); + ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_crash_data.pc); + ESP_LOGE(TAG, " LR: 0x%08" PRIX32 " (return address)", s_crash_data.lr); + ESP_LOGE(TAG, " SP: 0x%08" PRIX32, s_crash_data.sp); + for (uint8_t i = 0; i < s_crash_data.backtrace_count; i++) { + ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (stack backtrace)", i, s_crash_data.backtrace[i]); + } + // Build addr2line hint with all captured addresses for easy copy-paste + char hint[160]; + int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e firmware.elf 0x%08" PRIX32 " 0x%08" PRIX32, + s_crash_data.pc, s_crash_data.lr); + for (uint8_t i = 0; i < s_crash_data.backtrace_count && pos < (int) sizeof(hint) - 12; i++) { + pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, s_crash_data.backtrace[i]); + } + ESP_LOGE(TAG, "%s", hint); +} + +} // namespace esphome::rp2040 + +// --- HardFault handler --- +// Overrides the weak isr_hardfault from arduino-pico's crt0.S. +// On Cortex-M0+, the CPU pushes {R0,R1,R2,R3,R12,LR,PC,xPSR} onto the +// active stack (MSP or PSP). We determine which stack was active, +// extract key registers, store them in watchdog scratch registers +// (which survive watchdog reboot), then trigger a reboot. + +// Check if a pointer falls within SRAM (valid for stack access). +// SRAM_BASE and SRAM_END are chip-specific SDK defines: +// RP2040: 0x20000000 - 0x20042000 (264KB) +// RP2350: 0x20000000 - 0x20082000 (520KB) +static inline bool is_valid_sram_ptr(const uint32_t *ptr) { + auto addr = reinterpret_cast<uintptr_t>(ptr); + // Exception frame is 8 words (32 bytes), so frame+7 must also be in SRAM. + // Check alignment (must be word-aligned) and that the full frame fits. + return (addr % 4 == 0) && addr >= SRAM_BASE && (addr + 32) <= SRAM_END; +} + +// C handler called from the asm wrapper with the exception frame pointer. +static void __attribute__((used, noreturn)) hard_fault_handler_c(uint32_t *frame, uint32_t /*exc_return*/) { + // watchdog_reboot() overwrites scratch[4]-[7], so we must call it first + // then write ALL our data after. The 10ms timeout gives us plenty of time. + watchdog_reboot(0, 0, 10); + + // Validate frame pointer before dereferencing. If the HardFault was caused + // by a stacking error or corrupted SP, frame may be invalid. Write a minimal + // crash marker so we at least know a crash occurred. + if (!is_valid_sram_ptr(frame)) { + watchdog_hw->scratch[0] = CRASH_MAGIC; + watchdog_hw->scratch[1] = 0; // PC unknown + watchdog_hw->scratch[2] = 0; // LR unknown + watchdog_hw->scratch[3] = reinterpret_cast<uintptr_t>(frame); // Record the bad SP for diagnosis + for (uint32_t i = 0; i < MAX_BACKTRACE; i++) { + watchdog_hw->scratch[4 + i] = 0; + } + while (true) { + __asm volatile("nop"); + } + } + + // Pre-fault SP: the exception frame is 8 words pushed onto the stack, + // so the SP before the fault was frame + 8 words. If xPSR bit 9 is set, + // the hardware pushed an extra alignment word to maintain 8-byte stack + // alignment (ARMv6-M/ARMv7-M spec), so add 1 more word. + static constexpr uint32_t EF_XPSR = 7; + uint32_t extra_align = (frame[EF_XPSR] & (1u << 9)) ? 1 : 0; + uint32_t *post_frame = frame + 8 + extra_align; + uint32_t pre_fault_sp = reinterpret_cast<uintptr_t>(post_frame); + + // Write key registers + watchdog_hw->scratch[0] = CRASH_MAGIC; + watchdog_hw->scratch[1] = frame[EF_PC]; + watchdog_hw->scratch[2] = frame[EF_LR]; + watchdog_hw->scratch[3] = pre_fault_sp; + + // Scan stack for code addresses to build a deeper backtrace. + // The exception frame is 8 words (32 bytes) at 'frame', plus an optional + // alignment word. Walk up to 64 words looking for return addresses. + uint32_t *scan_start = post_frame; + // SRAM_END is chip-specific: 0x20042000 (RP2040) or 0x20082000 (RP2350) + uint32_t *stack_top = reinterpret_cast<uint32_t *>(SRAM_END); + // Scan up to 64 words (256 bytes) — covers typical nested call frames + // without scanning too much stale stack data that could produce false positives. + uint32_t bt_count = 0; + + for (uint32_t *p = scan_start; p < stack_top && p < scan_start + 64 && bt_count < MAX_BACKTRACE; p++) { + uint32_t val = *p; + // Check if this looks like a code address in flash + // Skip if it's the same as PC or LR we already saved + if (is_code_addr(val) && val != frame[EF_PC] && val != frame[EF_LR]) { + watchdog_hw->scratch[4 + bt_count] = val; + bt_count++; + } + } + // Zero remaining slots + for (uint32_t i = bt_count; i < MAX_BACKTRACE; i++) { + watchdog_hw->scratch[4 + i] = 0; + } + + while (true) { + __asm volatile("nop"); + } +} + +// Naked asm wrapper - Cortex-M0+ compatible (no ITE/conditional execution). +// Determines active stack pointer and branches to C handler. +// Uses literal pool (.word) for addresses since M0+ has limited immediate encoding. +// +// Based on the standard Cortex-M0+ HardFault handler pattern described in: +// - ARM Application Note AN209: "Using Cortex-M3/M4/M7 Fault Exceptions" +// (adapted for M0+ which lacks conditional execution instructions) +// - Memfault: "How to debug a HardFault on an ARM Cortex-M MCU" +// https://interrupt.memfault.com/blog/cortex-m-hardfault-debug +// - Raspberry Pi Forums: "Cortex-M0+ Hard Fault handler porting" +// https://www.eevblog.com/forum/microcontrollers/cortex-m0-hard-fault-handler-porting/ +// +// The key M0+ adaptation: replaces ITE/MRSEQ/MRSNE (Cortex-M3+) with +// MOVS+TST+BEQ branch sequence, and uses a literal pool for the C handler address. +extern "C" void __attribute__((naked, used)) isr_hardfault() { + __asm volatile("movs r0, #4 \n" // Prepare bit 2 mask + "mov r1, lr \n" // r1 = EXC_RETURN + "tst r1, r0 \n" // Test bit 2 + "beq 1f \n" // If 0, was using MSP + "mrs r0, psp \n" // Bit 2 set = PSP was active + "b 2f \n" + "1: \n" + "mrs r0, msp \n" // Bit 2 clear = MSP was active + "2: \n" + // r0 = exception frame pointer, r1 = EXC_RETURN (still in r1) + "ldr r2, 3f \n" // Load C handler address from literal pool + "bx r2 \n" // Branch to handler (r0=frame, r1=exc_return) + ".align 2 \n" + "3: .word %c0 \n" // Literal pool: address of C handler + : + : "i"(hard_fault_handler_c)); +} + +#endif // USE_RP2040 diff --git a/esphome/components/rp2040/crash_handler.h b/esphome/components/rp2040/crash_handler.h new file mode 100644 index 0000000000..f10db47c23 --- /dev/null +++ b/esphome/components/rp2040/crash_handler.h @@ -0,0 +1,17 @@ +#pragma once + +#ifdef USE_RP2040 + +#include <cstdint> + +namespace esphome::rp2040 { + +/// Read crash data from watchdog scratch registers and clear them. +void crash_handler_read_and_clear(); + +/// Log crash data if a crash was detected on previous boot. +void crash_handler_log(); + +} // namespace esphome::rp2040 + +#endif // USE_RP2040 From 6561c9bc95500915584afbe13a8cf3da6f3da051 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Tue, 10 Mar 2026 22:32:29 -0500 Subject: [PATCH 526/609] [core] Fix waiting for port indefinitely (#14688) --- esphome/__main__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 3f0da85a69..58b995e8df 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -919,9 +919,11 @@ def _wait_for_serial_port( """ def _port_found() -> bool: - ports = get_serial_ports() if port is not None: - return any(p.path == port for p in ports) + if os.name == "posix": + return os.path.exists(port) + return any(p.path == port for p in get_serial_ports()) + ports = get_serial_ports() if known_ports is not None: return any(p.path not in known_ports for p in ports) return bool(ports) From d0f37ae69460ac90e2f95a39216ec09090373df6 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Tue, 10 Mar 2026 23:31:27 -0500 Subject: [PATCH 527/609] [logger] Fix UART selection not applied before `pre_setup()` (#14690) --- esphome/components/logger/__init__.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index e370f4215d..675f9a2ca4 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -337,6 +337,16 @@ async def to_code(config): ) if CORE.is_esp32: cg.add(log.create_pthread_key()) + # set_uart_selection() must be called before pre_setup() because + # pre_setup() switches on uart_ to decide which hardware to initialize + # (e.g. UART0 vs USB_SERIAL_JTAG). Without this, uart_ is still the + # default UART_SELECTION_UART0 and the wrong hardware gets initialized. + if CONF_HARDWARE_UART in config: + cg.add( + log.set_uart_selection( + HARDWARE_UART_TO_UART_SELECTION[config[CONF_HARDWARE_UART]] + ) + ) # pre_setup() must be called before init_log_buffer() because # init_log_buffer() calls disable_loop() which may log at VV level, # and global_logger must be set before any logging occurs. @@ -354,12 +364,6 @@ async def to_code(config): cg.add(log.init_log_buffer(64)) # Fixed 64 slots for host cg.add(log.set_log_level(initial_level)) - if CONF_HARDWARE_UART in config: - cg.add( - log.set_uart_selection( - HARDWARE_UART_TO_UART_SELECTION[config[CONF_HARDWARE_UART]] - ) - ) # Enable runtime tag levels if logs are configured or explicitly enabled logs_config = config[CONF_LOGS] From 4df3d3554e5767edf6469977972409d3b882cd2e Mon Sep 17 00:00:00 2001 From: Adam DeMuri <adam.demuri@gmail.com> Date: Tue, 10 Mar 2026 23:44:05 -0600 Subject: [PATCH 528/609] Enable the address and behavior sanitizers for C++ component unit tests (#13490) --- script/cpp_unit_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/script/cpp_unit_test.py b/script/cpp_unit_test.py index c917458472..c6cfd8270f 100755 --- a/script/cpp_unit_test.py +++ b/script/cpp_unit_test.py @@ -79,6 +79,10 @@ def create_test_config(config_name: str, includes: list[str]) -> dict: "-Og", # optimize for debug "-DUSE_TIME_TIMEZONE", # enable timezone code paths for testing "-DESPHOME_DEBUG", # enable debug assertions + # Enable the address and undefined behavior sanitizers + "-fsanitize=address", + "-fsanitize=undefined", + "-fno-omit-frame-pointer", ], "debug_build_flags": [ # only for debug builds "-g3", # max debug info From e8e700a683f2ad210892393e6f67260855e493d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 20:51:54 -1000 Subject: [PATCH 529/609] [socket] Fix RP2040 heap corruption from malloc in lwip accept callback (#14687) --- .../components/socket/lwip_raw_tcp_impl.cpp | 142 ++++++++++++++---- esphome/components/socket/lwip_raw_tcp_impl.h | 41 ++--- 2 files changed, 138 insertions(+), 45 deletions(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index d7fa6a2694..fd1b8a9554 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -425,20 +425,24 @@ LWIPRawImpl::~LWIPRawImpl() { // Base class destructor handles pcb_ cleanup via tcp_abort } -void LWIPRawImpl::init() { +void LWIPRawImpl::init(struct pbuf *initial_rx, bool initial_rx_closed) { LWIP_LOCK(); LWIP_LOG("init(%p)", this->pcb_); tcp_arg(this->pcb_, this); tcp_recv(this->pcb_, LWIPRawImpl::s_recv_fn); tcp_err(this->pcb_, LWIPRawImpl::s_err_fn); + if (initial_rx != nullptr) { + this->rx_buf_ = initial_rx; + this->rx_buf_offset_ = 0; + } + this->rx_closed_ = initial_rx_closed; } void LWIPRawImpl::s_err_fn(void *arg, err_t err) { - // Called by lwip core which already holds the async_context lock on RP2040. - // No LWIP_LOCK() needed — acquiring it would be redundant (recursive mutex). + // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ). + // No heap allocation allowed — malloc is not IRQ-safe (see #14687). + // No LWIP_LOCK() needed — lwip core already holds the async_context lock. // - // "If a connection is aborted because of an error, the application is alerted of this event by - // the err callback." // pcb is already freed when this callback is called // ERR_RST: connection was reset by remote host // ERR_ABRT: aborted through tcp_abort or TCP timer @@ -453,11 +457,15 @@ err_t LWIPRawImpl::s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, er } err_t LWIPRawImpl::recv_fn(struct pbuf *pb, err_t err) { - // Called by lwip core which already holds the async_context lock on RP2040. + // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ). + // No heap allocation allowed — malloc is not IRQ-safe (see #14687). LWIP_LOG("recv(pb=%p err=%d)", pb, err); if (err != 0) { // "An error code if there has been an error receiving Only return ERR_ABRT if you have // called tcp_abort from within the callback function!" + if (pb != nullptr) { + pbuf_free(pb); + } this->rx_closed_ = true; return ERR_OK; } @@ -664,6 +672,22 @@ ssize_t LWIPRawImpl::writev(const struct iovec *iov, int iovcnt) { LWIPRawListenImpl::~LWIPRawListenImpl() { LWIP_LOCK(); + // Abort any queued PCBs that were never accepted by the main loop. + // Clear the error callback first — tcp_abort triggers it, and we don't + // want s_queued_err_fn writing to slots during destruction. + for (uint8_t i = 0; i < this->accepted_socket_count_; i++) { + auto &entry = this->accepted_pcbs_[i]; + if (entry.pcb != nullptr) { + tcp_err(entry.pcb, nullptr); + tcp_abort(entry.pcb); + entry.pcb = nullptr; + } + if (entry.rx_buf != nullptr) { + pbuf_free(entry.rx_buf); + entry.rx_buf = nullptr; + } + } + this->accepted_socket_count_ = 0; // Listen PCBs must use tcp_close(), not tcp_abort(). // tcp_abandon() asserts pcb->state != LISTEN and would access // fields that don't exist in the smaller tcp_pcb_listen struct. @@ -683,12 +707,49 @@ void LWIPRawListenImpl::init() { } void LWIPRawListenImpl::s_err_fn(void *arg, err_t err) { - // Called by lwip core which already holds the async_context lock on RP2040. + // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ). + // No heap allocation allowed — malloc is not IRQ-safe (see #14687). auto *arg_this = reinterpret_cast<LWIPRawListenImpl *>(arg); ESP_LOGVV(TAG, "socket %p: err(err=%d)", arg_this, err); arg_this->pcb_ = nullptr; } +void LWIPRawListenImpl::s_queued_err_fn(void *arg, err_t err) { + // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ). + // No heap allocation allowed — malloc is not IRQ-safe (see #14687). + // Called when a queued (not yet accepted) PCB errors — e.g., remote sent RST. + // The PCB is already freed by lwip. Null our pointer so accept() skips it. + (void) err; + auto *entry = reinterpret_cast<QueuedPcb *>(arg); + entry->pcb = nullptr; + // Don't free rx_buf here — accept() will clean it up when it sees pcb==nullptr +} + +err_t LWIPRawListenImpl::s_queued_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err) { + // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ). + // No heap allocation allowed — malloc is not IRQ-safe (see #14687). + // Temporary recv callback for PCBs queued between accept_fn_ and accept(). + // Without this, lwip's default tcp_recv_null handler would ACK and drop the data, + // causing the API handshake to silently fail (client sends Hello, server never sees it). + (void) pcb; + auto *entry = reinterpret_cast<QueuedPcb *>(arg); + if (pb == nullptr || err != ERR_OK) { + // Remote closed or error + if (pb != nullptr) { + pbuf_free(pb); + } + entry->rx_closed = true; + return ERR_OK; + } + // Buffer the data — tcp_recved() is deferred to read() after accept() creates the socket. + if (entry->rx_buf == nullptr) { + entry->rx_buf = pb; + } else { + pbuf_cat(entry->rx_buf, pb); + } + return ERR_OK; +} + err_t LWIPRawListenImpl::s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) { auto *arg_this = reinterpret_cast<LWIPRawListenImpl *>(arg); return arg_this->accept_fn_(newpcb, err); @@ -700,23 +761,40 @@ std::unique_ptr<LWIPRawImpl> LWIPRawListenImpl::accept(struct sockaddr *addr, so errno = EBADF; return nullptr; } - if (this->accepted_socket_count_ == 0) { - errno = EWOULDBLOCK; - return nullptr; + // Dequeue front entry, skipping any null entries (PCBs freed by lwip while queued). + // The error callback nulled their pcb pointers; clean up buffered data and discard. + while (this->accepted_socket_count_ > 0) { + QueuedPcb entry = this->accepted_pcbs_[0]; + // Shift remaining entries forward, updating tcp_arg pointers as we go. + // Safe because we hold LWIP_LOCK, so err/recv callbacks can't fire during the update. + for (uint8_t i = 1; i < this->accepted_socket_count_; i++) { + this->accepted_pcbs_[i - 1] = this->accepted_pcbs_[i]; + if (this->accepted_pcbs_[i - 1].pcb != nullptr) { + tcp_arg(this->accepted_pcbs_[i - 1].pcb, &this->accepted_pcbs_[i - 1]); + } + } + this->accepted_pcbs_[this->accepted_socket_count_ - 1] = {}; + this->accepted_socket_count_--; + if (entry.pcb == nullptr) { + // PCB was freed by lwip (RST/timeout) while queued — discard and try next + if (entry.rx_buf != nullptr) { + pbuf_free(entry.rx_buf); + } + continue; + } + LWIP_LOG("Connection accepted by application, queue size: %d", this->accepted_socket_count_); + // Create socket wrapper on the main loop (not in accept callback) to avoid + // heap allocation in IRQ context on RP2040. Transfer any data received while queued. + auto sock = make_unique<LWIPRawImpl>(this->family_, entry.pcb); + sock->init(entry.rx_buf, entry.rx_closed); + if (addr != nullptr) { + sock->getpeername(addr, addrlen); + } + LWIP_LOG("accept(%p)", sock.get()); + return sock; } - // Take from front for FIFO ordering - std::unique_ptr<LWIPRawImpl> sock = std::move(this->accepted_sockets_[0]); - // Shift remaining sockets forward - for (uint8_t i = 1; i < this->accepted_socket_count_; i++) { - this->accepted_sockets_[i - 1] = std::move(this->accepted_sockets_[i]); - } - this->accepted_socket_count_--; - LWIP_LOG("Connection accepted by application, queue size: %d", this->accepted_socket_count_); - if (addr != nullptr) { - sock->getpeername(addr, addrlen); - } - LWIP_LOG("accept(%p)", sock.get()); - return sock; + errno = EWOULDBLOCK; + return nullptr; } int LWIPRawListenImpl::listen(int backlog) { @@ -746,7 +824,8 @@ int LWIPRawListenImpl::listen(int backlog) { } err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) { - // Called by lwip core which already holds the async_context lock on RP2040. + // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ). + // No heap allocation allowed — malloc is not IRQ-safe (see #14687). LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err); if (err != ERR_OK || newpcb == nullptr) { // "An error code if there has been an error accepting. Only return ERR_ABRT if you have @@ -763,9 +842,18 @@ err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) { // Must return ERR_ABRT since we called tcp_abort() return ERR_ABRT; } - auto sock = make_unique<LWIPRawImpl>(this->family_, newpcb); - sock->init(); - this->accepted_sockets_[this->accepted_socket_count_++] = std::move(sock); + // Store the raw PCB — LWIPRawImpl creation is deferred to the main-loop accept(). + // This avoids heap allocation in this callback, which is unsafe from IRQ context on RP2040. + uint8_t idx = this->accepted_socket_count_++; + this->accepted_pcbs_[idx] = {newpcb, nullptr, false}; + // Register temporary callbacks so that while the PCB is queued: + // - err: nulls our pointer if the connection errors (RST, timeout) + // - recv: buffers any data that arrives before accept() creates the LWIPRawImpl + // (without this, lwip's default tcp_recv_null would ACK and drop the data) + // tcp_arg points to our queue entry; accept() updates these pointers after shifting. + tcp_arg(newpcb, &this->accepted_pcbs_[idx]); + tcp_err(newpcb, LWIPRawListenImpl::s_queued_err_fn); + tcp_recv(newpcb, LWIPRawListenImpl::s_queued_recv_fn); LWIP_LOG("Accepted connection, queue size: %d", this->accepted_socket_count_); #if (defined(USE_ESP8266) || defined(USE_RP2040)) // Wake the main loop immediately so it can accept the new connection. diff --git a/esphome/components/socket/lwip_raw_tcp_impl.h b/esphome/components/socket/lwip_raw_tcp_impl.h index 5b2c11cfe2..95931afcf3 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.h +++ b/esphome/components/socket/lwip_raw_tcp_impl.h @@ -66,7 +66,7 @@ class LWIPRawImpl : public LWIPRawCommon { using LWIPRawCommon::LWIPRawCommon; ~LWIPRawImpl(); - void init(); + void init(struct pbuf *initial_rx = nullptr, bool initial_rx_closed = false); // Non-listening sockets return error std::unique_ptr<LWIPRawImpl> accept(struct sockaddr *, socklen_t *) { @@ -182,23 +182,28 @@ class LWIPRawListenImpl : public LWIPRawCommon { err_t accept_fn_(struct tcp_pcb *newpcb, err_t err); static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err); - // Accept queue - holds incoming connections briefly until the event loop calls accept() - // This is NOT a connection pool - just a temporary queue between LWIP callbacks and the main loop - // 3 slots is plenty since connections are pulled out quickly by the event loop - // - // Memory analysis: std::array<3> vs original std::queue implementation: - // - std::queue uses std::deque internally which on 32-bit systems needs: - // 24 bytes (deque object) + 32+ bytes (map array) + heap allocations - // Total: ~56+ bytes minimum, plus heap fragmentation - // - std::array<3>: 12 bytes fixed (3 pointers × 4 bytes) - // Saves ~44+ bytes RAM per listening socket + avoids ALL heap allocations - // Used on ESP8266 and RP2040 (platforms using LWIP_TCP implementation) - // - // By using a separate listening socket class, regular connected sockets save - // 16 bytes (12 bytes array + 1 byte count + 3 bytes padding) of memory overhead on 32-bit systems - static constexpr size_t MAX_ACCEPTED_SOCKETS = 3; - std::array<std::unique_ptr<LWIPRawImpl>, MAX_ACCEPTED_SOCKETS> accepted_sockets_; - uint8_t accepted_socket_count_ = 0; // Number of sockets currently in queue + // Temporary callbacks for queued PCBs (between accept_fn_ and accept()) + static void s_queued_err_fn(void *arg, err_t err); + static err_t s_queued_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err); + + // Accept queue entry — stores a raw tcp_pcb and any data received while queued. + // lwip's default tcp_recv_null handler drops data and ACKs it, so we must register + // a temporary recv callback to buffer any data that arrives between accept_fn_ + // (which stores the PCB) and accept() (which creates the LWIPRawImpl). + struct QueuedPcb { + struct tcp_pcb *pcb{nullptr}; + struct pbuf *rx_buf{nullptr}; // Data received while queued (before accept() picks it up) + bool rx_closed{false}; // Remote sent FIN while queued + }; + + // Accept queue — stores raw tcp_pcb entries instead of heap-allocated LWIPRawImpl objects. + // LWIPRawImpl creation is deferred to the main-loop accept() call. This avoids: + // - Heap allocation in the accept callback (unsafe from IRQ context on RP2040) + // - Dangling LWIPRawImpl if the connection errors before accept() picks it up + // 2 slots is plenty since the main loop drains the queue every iteration. + static constexpr size_t MAX_ACCEPTED_SOCKETS = 2; + std::array<QueuedPcb, MAX_ACCEPTED_SOCKETS> accepted_pcbs_{}; + uint8_t accepted_socket_count_ = 0; // Number of entries currently in queue }; } // namespace esphome::socket From 236f6b1935aec81d6350213e3049c6b0f4fb0794 Mon Sep 17 00:00:00 2001 From: Robert Resch <robert@resch.dev> Date: Wed, 11 Mar 2026 07:52:43 +0100 Subject: [PATCH 530/609] [micronova] Add command queue (#12268) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: edenhaus <edenhaus@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick+github@koston.org> --- esphome/components/micronova/__init__.py | 54 +++- .../components/micronova/button/__init__.py | 2 + .../micronova/button/micronova_button.cpp | 10 +- .../micronova/button/micronova_button.h | 11 +- esphome/components/micronova/micronova.cpp | 256 +++++++++++------- esphome/components/micronova/micronova.h | 93 ++++--- .../components/micronova/number/__init__.py | 7 +- .../micronova/number/micronova_number.cpp | 10 +- .../micronova/number/micronova_number.h | 9 +- .../micronova/sensor/micronova_sensor.cpp | 7 + .../micronova/sensor/micronova_sensor.h | 8 +- .../components/micronova/switch/__init__.py | 2 + .../micronova/switch/micronova_switch.cpp | 21 +- .../micronova/switch/micronova_switch.h | 8 +- .../text_sensor/micronova_text_sensor.cpp | 7 + .../text_sensor/micronova_text_sensor.h | 8 +- esphome/core/defines.h | 2 + esphome/core/helpers.h | 75 +++++ 18 files changed, 399 insertions(+), 191 deletions(-) diff --git a/esphome/components/micronova/__init__.py b/esphome/components/micronova/__init__.py index d6ef93cf30..b462352229 100644 --- a/esphome/components/micronova/__init__.py +++ b/esphome/components/micronova/__init__.py @@ -1,14 +1,34 @@ +from dataclasses import dataclass + from esphome import pins import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv from esphome.const import CONF_ID +from esphome.core import CORE, coroutine_with_priority +from esphome.coroutine import CoroPriority CODEOWNERS = ["@jorre05", "@edenhaus"] DEPENDENCIES = ["uart"] DOMAIN = "micronova" + + +@dataclass +class MicronovaData: + """Track micronova component state during code generation.""" + + listener_count: int = 0 + has_writer: bool = False + + +def _get_data() -> MicronovaData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = MicronovaData() + return CORE.data[DOMAIN] + + CONF_MICRONOVA_ID = f"{DOMAIN}_id" CONF_ENABLE_RX_PIN = "enable_rx_pin" CONF_MEMORY_LOCATION = "memory_location" @@ -66,16 +86,42 @@ def MICRONOVA_ADDRESS_SCHEMA( return schema +def register_micronova_writer() -> None: + """Register a component that can write to the stove (button, switch, number).""" + _get_data().has_writer = True + + async def to_code_micronova_listener(mv, var, config): + _get_data().listener_count += 1 await cg.register_component(var, config) - cg.add(mv.register_micronova_listener(var)) cg.add(var.set_memory_location(config[CONF_MEMORY_LOCATION])) cg.add(var.set_memory_address(config[CONF_MEMORY_ADDRESS])) + # Register listener as last step as we need all properties set before registering + cg.add(mv.register_micronova_listener(var)) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + enable_rx_pin = await cg.gpio_pin_expression(config[CONF_ENABLE_RX_PIN]) + var = cg.new_Pvariable(config[CONF_ID], enable_rx_pin) await cg.register_component(var, config) await uart.register_uart_device(var, config) - enable_rx_pin = await cg.gpio_pin_expression(config[CONF_ENABLE_RX_PIN]) - cg.add(var.set_enable_rx_pin(enable_rx_pin)) + CORE.add_job(_final_step) + + +@coroutine_with_priority(CoroPriority.FINAL) +async def _final_step() -> None: + """Add defines for listener and writer counts after all are registered.""" + data = _get_data() + if data.listener_count == 0 and not data.has_writer: + raise cv.Invalid( + "No micronova entities configured. Add at least one micronova entity." + ) + if data.listener_count > 255: + raise cv.Invalid( + f"Too many micronova reading entities ({data.listener_count}). Maximum is 255." + ) + if data.listener_count > 0: + cg.add_define("MICRONOVA_LISTENER_COUNT", data.listener_count) + + if data.has_writer: + cg.add_define("USE_MICRONOVA_WRITER") diff --git a/esphome/components/micronova/button/__init__.py b/esphome/components/micronova/button/__init__.py index 6adf8d96fe..1ef359ea6c 100644 --- a/esphome/components/micronova/button/__init__.py +++ b/esphome/components/micronova/button/__init__.py @@ -9,6 +9,7 @@ from .. import ( MICRONOVA_ADDRESS_SCHEMA, MicroNova, micronova_ns, + register_micronova_writer, ) MicroNovaButton = micronova_ns.class_("MicroNovaButton", button.Button, cg.Component) @@ -36,6 +37,7 @@ async def to_code(config): mv = await cg.get_variable(config[CONF_MICRONOVA_ID]) if custom_button_config := config.get(CONF_CUSTOM_BUTTON): + register_micronova_writer() bt = await button.new_button(custom_button_config, mv) cg.add(bt.set_memory_location(custom_button_config[CONF_MEMORY_LOCATION])) cg.add(bt.set_memory_address(custom_button_config[CONF_MEMORY_ADDRESS])) diff --git a/esphome/components/micronova/button/micronova_button.cpp b/esphome/components/micronova/button/micronova_button.cpp index 3f49d4b5b3..13c0e1f117 100644 --- a/esphome/components/micronova/button/micronova_button.cpp +++ b/esphome/components/micronova/button/micronova_button.cpp @@ -2,9 +2,15 @@ namespace esphome::micronova { +static const char *const TAG = "micronova.button"; + +void MicroNovaButton::dump_config() { + LOG_BUTTON("", "Micronova button", this); + this->dump_base_config(); +} + void MicroNovaButton::press_action() { - this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_); - this->micronova_->request_update_listeners(); + this->micronova_->queue_write_command(this->memory_location_, this->memory_address_, this->memory_data_); } } // namespace esphome::micronova diff --git a/esphome/components/micronova/button/micronova_button.h b/esphome/components/micronova/button/micronova_button.h index 951ae8bba3..0258dbb53c 100644 --- a/esphome/components/micronova/button/micronova_button.h +++ b/esphome/components/micronova/button/micronova_button.h @@ -6,19 +6,18 @@ namespace esphome::micronova { -class MicroNovaButton : public Component, public button::Button, public MicroNovaButtonListener { +class MicroNovaButton : public Component, public button::Button, public MicroNovaBaseListener { public: - MicroNovaButton(MicroNova *m) : MicroNovaButtonListener(m) {} - void dump_config() override { - LOG_BUTTON("", "Micronova button", this); - this->dump_base_config(); - } + MicroNovaButton(MicroNova *m) : MicroNovaBaseListener(m) {} + void dump_config() override; void set_memory_data(uint8_t f) { this->memory_data_ = f; } uint8_t get_memory_data() { return this->memory_data_; } protected: void press_action() override; + + uint8_t memory_data_ = 0; }; } // namespace esphome::micronova diff --git a/esphome/components/micronova/micronova.cpp b/esphome/components/micronova/micronova.cpp index 22daef4fe6..c2e3ef8640 100644 --- a/esphome/components/micronova/micronova.cpp +++ b/esphome/components/micronova/micronova.cpp @@ -3,8 +3,12 @@ namespace esphome::micronova { -static const int STOVE_REPLY_DELAY = 60; -static const uint8_t WRITE_BIT = 1 << 7; // 0x80 +static const char *const TAG = "micronova"; +static constexpr uint8_t STOVE_REPLY_SIZE = 2; +static constexpr uint32_t STOVE_REPLY_TIMEOUT = 200; // ms +static constexpr uint8_t WRITE_BIT = 1 << 7; // 0x80 + +bool MicroNovaCommand::is_write() const { return this->memory_location & WRITE_BIT; } void MicroNovaBaseListener::dump_base_config() { ESP_LOGCONFIG(TAG, @@ -18,139 +22,193 @@ void MicroNovaListener::dump_base_config() { LOG_UPDATE_INTERVAL(this); } +void MicroNovaListener::request_value_from_stove_() { + this->micronova_->queue_read_request(this->memory_location_, this->memory_address_); +} + void MicroNova::setup() { - if (this->enable_rx_pin_ != nullptr) { - this->enable_rx_pin_->setup(); - this->enable_rx_pin_->pin_mode(gpio::FLAG_OUTPUT); - this->enable_rx_pin_->digital_write(false); - } - this->current_transmission_.request_transmission_time = millis(); - this->current_transmission_.memory_location = 0; - this->current_transmission_.memory_address = 0; - this->current_transmission_.reply_pending = false; - this->current_transmission_.initiating_listener = nullptr; + this->enable_rx_pin_->setup(); + this->enable_rx_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->enable_rx_pin_->digital_write(false); } void MicroNova::dump_config() { ESP_LOGCONFIG(TAG, "MicroNova:"); - if (this->enable_rx_pin_ != nullptr) { - LOG_PIN(" Enable RX Pin: ", this->enable_rx_pin_); - } + LOG_PIN(" Enable RX Pin: ", this->enable_rx_pin_); } -void MicroNova::request_update_listeners() { - ESP_LOGD(TAG, "Schedule listener update"); - for (auto &mv_listener : this->micronova_listeners_) { - mv_listener->set_needs_update(true); +#ifdef MICRONOVA_LISTENER_COUNT +void MicroNova::register_micronova_listener(MicroNovaListener *listener) { + this->listeners_.push_back(listener); + // Request initial value + this->queue_read_request(listener->get_memory_location(), listener->get_memory_address()); +} + +void MicroNova::request_update_listeners_() { + ESP_LOGD(TAG, "Requesting update from all listeners"); + for (auto *listener : this->listeners_) { + this->queue_read_request(listener->get_memory_location(), listener->get_memory_address()); } } +#endif void MicroNova::loop() { - // Only read one sensor that needs update per loop - // If STOVE_REPLY_DELAY time has passed since last loop() - // check for a reply from the stove - if ((this->current_transmission_.reply_pending) && - (millis() - this->current_transmission_.request_transmission_time > STOVE_REPLY_DELAY)) { - int stove_reply_value = this->read_stove_reply(); - if (this->current_transmission_.initiating_listener != nullptr) { - this->current_transmission_.initiating_listener->process_value_from_stove(stove_reply_value); - this->current_transmission_.initiating_listener = nullptr; - } - this->current_transmission_.reply_pending = false; - return; - } else if (!this->current_transmission_.reply_pending) { - for (auto &mv_listener : this->micronova_listeners_) { - if (mv_listener->get_needs_update()) { - mv_listener->set_needs_update(false); - this->current_transmission_.initiating_listener = mv_listener; - mv_listener->request_value_from_stove(); - return; + // Check if we're processing a command and waiting for reply + if (this->reply_pending_) { + // Check if all reply bytes have arrived + if (this->available() >= STOVE_REPLY_SIZE) { +#ifdef MICRONOVA_LISTENER_COUNT + int stove_reply_value = this->read_stove_reply_(); + if (this->current_command_.is_write()) { + if (stove_reply_value == -1) { + ESP_LOGW(TAG, "Write to [0x%02X:0x%02X] may have failed (checksum mismatch in reply)", + this->current_command_.memory_location & ~WRITE_BIT, this->current_command_.memory_address); + } + } else { + // For READ commands, notify all listeners registered for this address + uint8_t loc = this->current_command_.memory_location; + uint8_t addr = this->current_command_.memory_address; + for (auto *listener : this->listeners_) { + if (listener->get_memory_location() == loc && listener->get_memory_address() == addr) { + listener->process_value_from_stove(stove_reply_value); + } + } } +#else + this->read_stove_reply_(); +#endif + this->reply_pending_ = false; + } else if (millis() - this->transmission_time_ > STOVE_REPLY_TIMEOUT) { + // Timeout - no reply received (buffer cleared before next command) + ESP_LOGW(TAG, "Timeout waiting for reply from [0x%02X:0x%02X], available: %d", + this->current_command_.memory_location, this->current_command_.memory_address, this->available()); + this->reply_pending_ = false; } + return; } + + // No reply pending - process next command (writes have priority over reads) +#ifdef USE_MICRONOVA_WRITER + if (!this->write_queue_.empty()) { + this->current_command_ = this->write_queue_.front(); + this->write_queue_.pop(); + this->send_current_command_(); + return; + } +#endif +#ifdef MICRONOVA_LISTENER_COUNT + if (!this->read_queue_.empty()) { + this->current_command_ = this->read_queue_.front(); + this->read_queue_.pop(); + this->send_current_command_(); + } +#endif } -void MicroNova::request_address(uint8_t location, uint8_t address, MicroNovaListener *listener) { - uint8_t write_data[2] = {0, 0}; +#ifdef MICRONOVA_LISTENER_COUNT +void MicroNova::queue_read_request(uint8_t location, uint8_t address) { + // Check if this read is already queued + for (const auto &queued : this->read_queue_) { + if (queued.memory_location == location && queued.memory_address == address) { + ESP_LOGV(TAG, "Read [%02X,%02X] already queued, skipping", location, address); + return; + } + } + + MicroNovaCommand cmd; + cmd.memory_location = location; + cmd.memory_address = address; + cmd.data = 0; + + if (!this->read_queue_.push(cmd)) { + ESP_LOGW(TAG, "Read queue full, dropping read [%02X,%02X]", location, address); + return; + } + ESP_LOGV(TAG, "Queued read [%02X,%02X] (queue size: %u)", location, address, this->read_queue_.size()); +} +#endif + +void MicroNova::send_current_command_() { uint8_t trash_rx; - if (this->reply_pending_mutex_.try_lock()) { - // clear rx buffer. - // Stove hickups may cause late replies in the rx - while (this->available()) { - this->read_byte(&trash_rx); - ESP_LOGW(TAG, "Reading excess byte 0x%02X", trash_rx); - } - - write_data[0] = location; - write_data[1] = address; - ESP_LOGV(TAG, "Request from stove [%02X,%02X]", write_data[0], write_data[1]); - - this->enable_rx_pin_->digital_write(true); - this->write_array(write_data, 2); - this->flush(); - this->enable_rx_pin_->digital_write(false); - - this->current_transmission_.request_transmission_time = millis(); - this->current_transmission_.memory_location = location; - this->current_transmission_.memory_address = address; - this->current_transmission_.reply_pending = true; - this->current_transmission_.initiating_listener = listener; - } else { - ESP_LOGE(TAG, "Reply is pending, skipping read request"); + // Clear rx buffer - stove hiccups may cause late replies in the rx + while (this->available()) { + this->read_byte(&trash_rx); + ESP_LOGW(TAG, "Reading excess byte 0x%02X", trash_rx); } + + uint8_t write_data[4] = {this->current_command_.memory_location, this->current_command_.memory_address, 0, 0}; + size_t write_len; + + if (this->current_command_.is_write()) { + write_len = 4; + write_data[2] = this->current_command_.data; + // calculate checksum + write_data[3] = write_data[0] + write_data[1] + write_data[2]; + ESP_LOGV(TAG, "Sending write request [%02X,%02X,%02X,%02X]", write_data[0], write_data[1], write_data[2], + write_data[3]); + } else { + write_len = 2; + ESP_LOGV(TAG, "Sending read request [%02X,%02X]", write_data[0], write_data[1]); + } + + this->enable_rx_pin_->digital_write(true); + this->write_array(write_data, write_len); + this->flush(); + this->enable_rx_pin_->digital_write(false); + + this->transmission_time_ = millis(); + this->reply_pending_ = true; } -int MicroNova::read_stove_reply() { +int MicroNova::read_stove_reply_() { uint8_t reply_data[2] = {0, 0}; - uint8_t checksum = 0; - // assert enable_rx_pin is false this->read_array(reply_data, 2); - this->reply_pending_mutex_.unlock(); ESP_LOGV(TAG, "Reply from stove [%02X,%02X]", reply_data[0], reply_data[1]); - checksum = ((uint16_t) this->current_transmission_.memory_location + - (uint16_t) this->current_transmission_.memory_address + (uint16_t) reply_data[1]) & - 0xFF; + uint8_t checksum = this->current_command_.memory_location + this->current_command_.memory_address + reply_data[1]; if (reply_data[0] != checksum) { - ESP_LOGE(TAG, "Checksum missmatch! From [0x%02X:0x%02X] received [0x%02X,0x%02X]. Expected 0x%02X, got 0x%02X", - this->current_transmission_.memory_location, this->current_transmission_.memory_address, reply_data[0], + ESP_LOGE(TAG, "Checksum mismatch! From [0x%02X:0x%02X] received [0x%02X,0x%02X]. Expected 0x%02X, got 0x%02X", + this->current_command_.memory_location, this->current_command_.memory_address, reply_data[0], reply_data[1], checksum, reply_data[0]); return -1; } return ((int) reply_data[1]); } -void MicroNova::write_address(uint8_t location, uint8_t address, uint8_t data) { - uint8_t write_data[4] = {0, 0, 0, 0}; - uint16_t checksum = 0; +#ifdef USE_MICRONOVA_WRITER +bool MicroNova::queue_write_command(uint8_t location, uint8_t address, uint8_t data) { + MicroNovaCommand cmd; + cmd.memory_location = location | WRITE_BIT; + cmd.memory_address = address; + cmd.data = data; - if (this->reply_pending_mutex_.try_lock()) { - uint8_t write_location = location | WRITE_BIT; - write_data[0] = write_location; - write_data[1] = address; - write_data[2] = data; - - checksum = ((uint16_t) write_data[0] + (uint16_t) write_data[1] + (uint16_t) write_data[2]) & 0xFF; - write_data[3] = checksum; - - ESP_LOGV(TAG, "Write 4 bytes [%02X,%02X,%02X,%02X]", write_data[0], write_data[1], write_data[2], write_data[3]); - - this->enable_rx_pin_->digital_write(true); - this->write_array(write_data, 4); - this->flush(); - this->enable_rx_pin_->digital_write(false); - - this->current_transmission_.request_transmission_time = millis(); - this->current_transmission_.memory_location = write_location; - this->current_transmission_.memory_address = address; - this->current_transmission_.reply_pending = true; - this->current_transmission_.initiating_listener = nullptr; - } else { - ESP_LOGE(TAG, "Reply is pending, skipping write"); + // Check if a write to the same address is already queued - update data in-place + for (auto &queued : this->write_queue_) { + if (queued.memory_location == cmd.memory_location && queued.memory_address == cmd.memory_address) { + if (queued.data != cmd.data) { + ESP_LOGD(TAG, "Updating queued write [%02X,%02X] data 0x%02X -> 0x%02X", location, address, queued.data, data); + queued.data = cmd.data; + } else { + ESP_LOGV(TAG, "Write [%02X,%02X] with data 0x%02X already queued, skipping", location, address, data); + } + return true; + } } + + if (!this->write_queue_.push(cmd)) { + ESP_LOGW(TAG, "Write queue full, dropping command"); + return false; + } + ESP_LOGD(TAG, "Queued write [%02X,%02X] (queue size: %u)", location, address, this->write_queue_.size()); +#ifdef MICRONOVA_LISTENER_COUNT + // Automatically queue sensor updates after write commands + this->request_update_listeners_(); +#endif + return true; } +#endif } // namespace esphome::micronova diff --git a/esphome/components/micronova/micronova.h b/esphome/components/micronova/micronova.h index a70f355ead..58cca30b83 100644 --- a/esphome/components/micronova/micronova.h +++ b/esphome/components/micronova/micronova.h @@ -6,11 +6,19 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include <vector> - namespace esphome::micronova { -static const char *const TAG = "micronova"; +static constexpr uint8_t WRITE_QUEUE_SIZE = 10; + +/// Represents a command to be sent to the stove +/// Write commands have the high bit (0x80) set in memory_location +struct MicroNovaCommand { + uint8_t memory_location; + uint8_t memory_address; + uint8_t data; ///< Only used for write commands + + bool is_write() const; +}; class MicroNova; @@ -18,11 +26,8 @@ class MicroNova; // Interface classes. class MicroNovaBaseListener { public: - MicroNovaBaseListener() {} MicroNovaBaseListener(MicroNova *m) { this->micronova_ = m; } - void set_micronova_object(MicroNova *m) { this->micronova_ = m; } - void set_memory_location(uint8_t l) { this->memory_location_ = l; } uint8_t get_memory_location() { return this->memory_location_; } @@ -32,70 +37,76 @@ class MicroNovaBaseListener { void dump_base_config(); protected: - MicroNova *micronova_{nullptr}; + MicroNova *micronova_; uint8_t memory_location_ = 0; uint8_t memory_address_ = 0; }; class MicroNovaListener : public MicroNovaBaseListener, public PollingComponent { public: - MicroNovaListener() {} MicroNovaListener(MicroNova *m) : MicroNovaBaseListener(m) {} - virtual void request_value_from_stove() = 0; + + void update() override { this->request_value_from_stove_(); } + virtual void process_value_from_stove(int value_from_stove) = 0; - void set_needs_update(bool u) { this->needs_update_ = u; } - bool get_needs_update() { return this->needs_update_; } - - void update() override { this->set_needs_update(true); } - void dump_base_config(); protected: - bool needs_update_ = false; -}; - -class MicroNovaButtonListener : public MicroNovaBaseListener { - public: - MicroNovaButtonListener(MicroNova *m) : MicroNovaBaseListener(m) {} - - protected: - uint8_t memory_data_ = 0; + void request_value_from_stove_(); }; ///////////////////////////////////////////////////////////////////// // Main component class class MicroNova : public Component, public uart::UARTDevice { public: - MicroNova() {} + MicroNova(GPIOPin *enable_rx_pin) : enable_rx_pin_(enable_rx_pin) {} void setup() override; void loop() override; void dump_config() override; - void register_micronova_listener(MicroNovaListener *l) { this->micronova_listeners_.push_back(l); } - void request_update_listeners(); - void request_address(uint8_t location, uint8_t address, MicroNovaListener *listener); - void write_address(uint8_t location, uint8_t address, uint8_t data); - int read_stove_reply(); +#ifdef MICRONOVA_LISTENER_COUNT + void register_micronova_listener(MicroNovaListener *listener); - void set_enable_rx_pin(GPIOPin *enable_rx_pin) { this->enable_rx_pin_ = enable_rx_pin; } + /// Queue a read request to the stove (low priority - added at back) + /// All listeners registered for this address will be notified with the result + /// @param location Memory location on the stove + /// @param address Memory address on the stove + void queue_read_request(uint8_t location, uint8_t address); +#endif + +#ifdef USE_MICRONOVA_WRITER + /// Queue a write command to the stove (processed before reads) + /// @param location Memory location on the stove + /// @param address Memory address on the stove + /// @param data Data to write + /// @return true if command was queued, false if queue was full + bool queue_write_command(uint8_t location, uint8_t address, uint8_t data); +#endif protected: - GPIOPin *enable_rx_pin_{nullptr}; + void send_current_command_(); + int read_stove_reply_(); +#ifdef MICRONOVA_LISTENER_COUNT + void request_update_listeners_(); +#endif - struct MicroNovaSerialTransmission { - uint32_t request_transmission_time; - uint8_t memory_location; - uint8_t memory_address; - bool reply_pending; - MicroNovaListener *initiating_listener; - }; + GPIOPin *enable_rx_pin_; - Mutex reply_pending_mutex_; - MicroNovaSerialTransmission current_transmission_; +#ifdef USE_MICRONOVA_WRITER + StaticRingBuffer<MicroNovaCommand, WRITE_QUEUE_SIZE> write_queue_; +#endif +#ifdef MICRONOVA_LISTENER_COUNT + StaticRingBuffer<MicroNovaCommand, MICRONOVA_LISTENER_COUNT> read_queue_; +#endif + MicroNovaCommand current_command_{}; + uint32_t transmission_time_{0}; ///< Time when current command was sent + bool reply_pending_{false}; ///< True if we are waiting for a reply from the stove - std::vector<MicroNovaListener *> micronova_listeners_{}; +#ifdef MICRONOVA_LISTENER_COUNT + StaticVector<MicroNovaListener *, MICRONOVA_LISTENER_COUNT> listeners_; +#endif }; } // namespace esphome::micronova diff --git a/esphome/components/micronova/number/__init__.py b/esphome/components/micronova/number/__init__.py index ef6cc0f7d7..bcc972c5a9 100644 --- a/esphome/components/micronova/number/__init__.py +++ b/esphome/components/micronova/number/__init__.py @@ -9,6 +9,7 @@ from .. import ( MicroNova, MicroNovaListener, micronova_ns, + register_micronova_writer, to_code_micronova_listener, ) @@ -59,22 +60,24 @@ async def to_code(config): mv = await cg.get_variable(config[CONF_MICRONOVA_ID]) if thermostat_temperature_config := config.get(CONF_THERMOSTAT_TEMPERATURE): + register_micronova_writer() numb = await number.new_number( thermostat_temperature_config, + mv, min_value=0, max_value=40, step=thermostat_temperature_config.get(CONF_STEP), ) await to_code_micronova_listener(mv, numb, thermostat_temperature_config) - cg.add(numb.set_micronova_object(mv)) cg.add(numb.set_use_step_scaling(True)) if power_level_config := config.get(CONF_POWER_LEVEL): + register_micronova_writer() numb = await number.new_number( power_level_config, + mv, min_value=1, max_value=5, step=1, ) await to_code_micronova_listener(mv, numb, power_level_config) - cg.add(numb.set_micronova_object(mv)) diff --git a/esphome/components/micronova/number/micronova_number.cpp b/esphome/components/micronova/number/micronova_number.cpp index 8027947468..02a6de1c84 100644 --- a/esphome/components/micronova/number/micronova_number.cpp +++ b/esphome/components/micronova/number/micronova_number.cpp @@ -2,6 +2,13 @@ namespace esphome::micronova { +static const char *const TAG = "micronova.number"; + +void MicroNovaNumber::dump_config() { + LOG_NUMBER("", "Micronova number", this); + this->dump_base_config(); +} + void MicroNovaNumber::process_value_from_stove(int value_from_stove) { if (value_from_stove == -1) { this->publish_state(NAN); @@ -22,8 +29,7 @@ void MicroNovaNumber::control(float value) { } else { new_number = static_cast<uint8_t>(value); } - this->micronova_->write_address(this->memory_location_, this->memory_address_, new_number); - this->micronova_->request_update_listeners(); + this->micronova_->queue_write_command(this->memory_location_, this->memory_address_, new_number); } } // namespace esphome::micronova diff --git a/esphome/components/micronova/number/micronova_number.h b/esphome/components/micronova/number/micronova_number.h index 3fc5838a4f..73666b632b 100644 --- a/esphome/components/micronova/number/micronova_number.h +++ b/esphome/components/micronova/number/micronova_number.h @@ -7,16 +7,9 @@ namespace esphome::micronova { class MicroNovaNumber : public number::Number, public MicroNovaListener { public: - MicroNovaNumber() {} MicroNovaNumber(MicroNova *m) : MicroNovaListener(m) {} - void dump_config() override { - LOG_NUMBER("", "Micronova number", this); - this->dump_base_config(); - } + void dump_config() override; void control(float value) override; - void request_value_from_stove() override { - this->micronova_->request_address(this->memory_location_, this->memory_address_, this); - } void process_value_from_stove(int value_from_stove) override; void set_use_step_scaling(bool v) { this->use_step_scaling_ = v; } diff --git a/esphome/components/micronova/sensor/micronova_sensor.cpp b/esphome/components/micronova/sensor/micronova_sensor.cpp index d845e0ab3c..8d528145cd 100644 --- a/esphome/components/micronova/sensor/micronova_sensor.cpp +++ b/esphome/components/micronova/sensor/micronova_sensor.cpp @@ -2,6 +2,13 @@ namespace esphome::micronova { +static const char *const TAG = "micronova.sensor"; + +void MicroNovaSensor::dump_config() { + LOG_SENSOR("", "Micronova sensor", this); + this->dump_base_config(); +} + void MicroNovaSensor::process_value_from_stove(int value_from_stove) { if (value_from_stove == -1) { this->publish_state(NAN); diff --git a/esphome/components/micronova/sensor/micronova_sensor.h b/esphome/components/micronova/sensor/micronova_sensor.h index a2f232c7dc..f3b06d140e 100644 --- a/esphome/components/micronova/sensor/micronova_sensor.h +++ b/esphome/components/micronova/sensor/micronova_sensor.h @@ -8,14 +8,8 @@ namespace esphome::micronova { class MicroNovaSensor : public sensor::Sensor, public MicroNovaListener { public: MicroNovaSensor(MicroNova *m) : MicroNovaListener(m) {} - void dump_config() override { - LOG_SENSOR("", "Micronova sensor", this); - this->dump_base_config(); - } + void dump_config() override; - void request_value_from_stove() override { - this->micronova_->request_address(this->memory_location_, this->memory_address_, this); - } void process_value_from_stove(int value_from_stove) override; void set_divisor(uint8_t d) { this->divisor_ = d; } diff --git a/esphome/components/micronova/switch/__init__.py b/esphome/components/micronova/switch/__init__.py index c937a4cac9..d9722b5d48 100644 --- a/esphome/components/micronova/switch/__init__.py +++ b/esphome/components/micronova/switch/__init__.py @@ -9,6 +9,7 @@ from .. import ( MicroNova, MicroNovaListener, micronova_ns, + register_micronova_writer, to_code_micronova_listener, ) @@ -48,6 +49,7 @@ async def to_code(config): mv = await cg.get_variable(config[CONF_MICRONOVA_ID]) if stove_config := config.get(CONF_STOVE): + register_micronova_writer() sw = await switch.new_switch(stove_config, mv) await to_code_micronova_listener(mv, sw, stove_config) cg.add(sw.set_memory_data_on(stove_config[CONF_MEMORY_DATA_ON])) diff --git a/esphome/components/micronova/switch/micronova_switch.cpp b/esphome/components/micronova/switch/micronova_switch.cpp index 9b9ad61018..d01cc57254 100644 --- a/esphome/components/micronova/switch/micronova_switch.cpp +++ b/esphome/components/micronova/switch/micronova_switch.cpp @@ -2,33 +2,42 @@ namespace esphome::micronova { +static const char *const TAG = "micronova.switch"; + +void MicroNovaSwitch::dump_config() { + LOG_SWITCH("", "Micronova switch", this); + this->dump_base_config(); +} + void MicroNovaSwitch::write_state(bool state) { if (state) { // Only send power-on when current state is Off if (this->raw_state_ == 0) { - this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_on_); - this->publish_state(true); + if (this->micronova_->queue_write_command(this->memory_location_, this->memory_address_, this->memory_data_on_)) { + this->publish_state(true); + } } else { ESP_LOGW(TAG, "Unable to turn stove on, invalid state: %d", this->raw_state_); } } else { // don't send power-off when status is Off or Final cleaning if (this->raw_state_ != 0 && this->raw_state_ != 6) { - this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_off_); - this->publish_state(false); + if (this->micronova_->queue_write_command(this->memory_location_, this->memory_address_, + this->memory_data_off_)) { + this->publish_state(false); + } } else { ESP_LOGW(TAG, "Unable to turn stove off, invalid state: %d", this->raw_state_); } } - this->set_needs_update(true); } void MicroNovaSwitch::process_value_from_stove(int value_from_stove) { - this->raw_state_ = value_from_stove; if (value_from_stove == -1) { ESP_LOGE(TAG, "Error reading stove state"); return; } + this->raw_state_ = value_from_stove; // set the stove switch to on for any value but 0 bool state = value_from_stove != 0; diff --git a/esphome/components/micronova/switch/micronova_switch.h b/esphome/components/micronova/switch/micronova_switch.h index 96c2c14e9e..fee3c73976 100644 --- a/esphome/components/micronova/switch/micronova_switch.h +++ b/esphome/components/micronova/switch/micronova_switch.h @@ -9,13 +9,7 @@ namespace esphome::micronova { class MicroNovaSwitch : public switch_::Switch, public MicroNovaListener { public: MicroNovaSwitch(MicroNova *m) : MicroNovaListener(m) {} - void dump_config() override { - LOG_SWITCH("", "Micronova switch", this); - this->dump_base_config(); - } - void request_value_from_stove() override { - this->micronova_->request_address(this->memory_location_, this->memory_address_, this); - } + void dump_config() override; void process_value_from_stove(int value_from_stove) override; void set_memory_data_on(uint8_t f) { this->memory_data_on_ = f; } diff --git a/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp b/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp index 2217ed6d6f..50a0d34b3a 100644 --- a/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp +++ b/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp @@ -2,6 +2,13 @@ namespace esphome::micronova { +static const char *const TAG = "micronova.text_sensor"; + +void MicroNovaTextSensor::dump_config() { + LOG_TEXT_SENSOR("", "Micronova text sensor", this); + this->dump_base_config(); +} + void MicroNovaTextSensor::process_value_from_stove(int value_from_stove) { if (value_from_stove == -1) { this->publish_state("unknown"); diff --git a/esphome/components/micronova/text_sensor/micronova_text_sensor.h b/esphome/components/micronova/text_sensor/micronova_text_sensor.h index 290f0ca45a..6918a372e8 100644 --- a/esphome/components/micronova/text_sensor/micronova_text_sensor.h +++ b/esphome/components/micronova/text_sensor/micronova_text_sensor.h @@ -20,13 +20,7 @@ static const char *const STOVE_STATES[11] = {"Off", class MicroNovaTextSensor : public text_sensor::TextSensor, public MicroNovaListener { public: MicroNovaTextSensor(MicroNova *m) : MicroNovaListener(m) {} - void dump_config() override { - LOG_TEXT_SENSOR("", "Micronova text sensor", this); - this->dump_base_config(); - } - void request_value_from_stove() override { - this->micronova_->request_address(this->memory_location_, this->memory_address_, this); - } + void dump_config() override; void process_value_from_stove(int value_from_stove) override; }; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 44918fe00c..cec77fe2e2 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -107,6 +107,8 @@ #define MDNS_SERVICE_COUNT 3 #define USE_MDNS_DYNAMIC_TXT #define MDNS_DYNAMIC_TXT_COUNT 2 +#define MICRONOVA_LISTENER_COUNT 1 +#define USE_MICRONOVA_WRITER #define SERIAL_PROXY_COUNT 2 #define SNTP_SERVER_COUNT 3 #define USE_MEDIA_PLAYER diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 11e0afe526..70ac1574f0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -293,6 +293,81 @@ template<typename T, size_t N> class StaticVector { operator std::span<const T>() const { return std::span<const T>(data_.data(), count_); } }; +/// Fixed-size circular buffer with FIFO semantics and iteration support. +/// +/// A tiny ring buffer that avoids dynamic allocations from std::deque/std::queue +/// (which can be wasteful on MCUs), while supporting iteration over queued elements. +/// +/// Not thread-safe. All access (push/pop/iteration) must occur from a single +/// context, or the caller must provide external synchronization. +template<typename T, size_t N> class StaticRingBuffer { + using index_type = std::conditional_t<(N <= 255), uint8_t, uint16_t>; + + public: + class Iterator { + public: + Iterator(StaticRingBuffer *buf, index_type pos) : buf_(buf), pos_(pos) {} + T &operator*() { return buf_->data_[(buf_->head_ + pos_) % N]; } + Iterator &operator++() { + ++pos_; + return *this; + } + bool operator!=(const Iterator &other) const { return pos_ != other.pos_; } + + private: + StaticRingBuffer *buf_; + index_type pos_; + }; + + class ConstIterator { + public: + ConstIterator(const StaticRingBuffer *buf, index_type pos) : buf_(buf), pos_(pos) {} + const T &operator*() const { return buf_->data_[(buf_->head_ + pos_) % N]; } + ConstIterator &operator++() { + ++pos_; + return *this; + } + bool operator!=(const ConstIterator &other) const { return pos_ != other.pos_; } + + private: + const StaticRingBuffer *buf_; + index_type pos_; + }; + + bool push(const T &value) { + if (this->count_ >= N) { + return false; + } + this->data_[this->tail_] = value; + this->tail_ = (this->tail_ + 1) % N; + ++this->count_; + return true; + } + + void pop() { + if (this->count_ > 0) { + this->head_ = (this->head_ + 1) % N; + --this->count_; + } + } + + T &front() { return this->data_[this->head_]; } + const T &front() const { return this->data_[this->head_]; } + index_type size() const { return this->count_; } + bool empty() const { return this->count_ == 0; } + + Iterator begin() { return Iterator(this, 0); } + Iterator end() { return Iterator(this, this->count_); } + ConstIterator begin() const { return ConstIterator(this, 0); } + ConstIterator end() const { return ConstIterator(this, this->count_); } + + protected: + T data_[N]; + index_type head_{0}; + index_type tail_{0}; + index_type count_{0}; +}; + /// Fixed-capacity vector - allocates once at runtime, never reallocates /// This avoids std::vector template overhead (_M_realloc_insert, _M_default_append) /// when size is known at initialization but not at compile time From c52a48ed38923eccf0d5c38a70650d34e17a4f22 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 11 Mar 2026 03:11:46 -0400 Subject: [PATCH 531/609] [multiple] Convert static function locals to member variables (#14689) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/daikin_arc/daikin_arc.cpp | 5 ++--- esphome/components/daikin_arc/daikin_arc.h | 1 + esphome/components/haier/hon_climate.cpp | 5 ++--- esphome/components/haier/hon_climate.h | 1 + .../components/ina2xx_base/ina2xx_base.cpp | 5 ++--- esphome/components/ltr_als_ps/ltr_als_ps.cpp | 21 +++++++++---------- esphome/components/ltr_als_ps/ltr_als_ps.h | 5 ++++- .../matrix_keypad/matrix_keypad.cpp | 12 +++++------ .../components/matrix_keypad/matrix_keypad.h | 2 ++ .../components/mqtt/mqtt_backend_esp32.cpp | 9 ++++---- esphome/components/mqtt/mqtt_backend_esp32.h | 1 + esphome/components/sgp4x/sgp4x.cpp | 5 +++-- esphome/components/sgp4x/sgp4x.h | 3 ++- 13 files changed, 39 insertions(+), 36 deletions(-) diff --git a/esphome/components/daikin_arc/daikin_arc.cpp b/esphome/components/daikin_arc/daikin_arc.cpp index adb7b9fec7..18f12dbfc6 100644 --- a/esphome/components/daikin_arc/daikin_arc.cpp +++ b/esphome/components/daikin_arc/daikin_arc.cpp @@ -91,11 +91,10 @@ void DaikinArcClimate::transmit_state() { remote_state[5] = this->operation_mode_() | 0x08; remote_state[6] = this->temperature_(); remote_state[7] = this->humidity_(); - static uint8_t last_humidity = 0x66; - if (remote_state[7] != last_humidity && this->mode != climate::CLIMATE_MODE_OFF) { + if (remote_state[7] != this->last_humidity_ && this->mode != climate::CLIMATE_MODE_OFF) { ESP_LOGD(TAG, "Set Humditiy: %d, %d\n", (int) this->target_humidity, (int) remote_state[7]); remote_header[9] |= 0x10; - last_humidity = remote_state[7]; + this->last_humidity_ = remote_state[7]; } uint16_t fan_speed = this->fan_speed_(); remote_state[8] = fan_speed >> 8; diff --git a/esphome/components/daikin_arc/daikin_arc.h b/esphome/components/daikin_arc/daikin_arc.h index 6cfffd4725..2b4d4375aa 100644 --- a/esphome/components/daikin_arc/daikin_arc.h +++ b/esphome/components/daikin_arc/daikin_arc.h @@ -70,6 +70,7 @@ class DaikinArcClimate : public climate_ir::ClimateIR { // Handle received IR Buffer bool on_receive(remote_base::RemoteReceiveData data) override; bool parse_state_frame_(const uint8_t frame[]); + uint8_t last_humidity_{0x66}; }; } // namespace daikin_arc diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index be5035caa1..b8889ef2bd 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -1375,9 +1375,8 @@ void HonClimate::process_protocol_reset() { bool HonClimate::should_get_big_data_() { if (this->big_data_sensors_ > 0) { - static uint8_t counter = 0; - counter = (counter + 1) % 3; - return counter == 1; + this->big_data_counter_ = (this->big_data_counter_ + 1) % 3; + return this->big_data_counter_ == 1; } return false; } diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index 4565ed2981..9bddac3f92 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -188,6 +188,7 @@ class HonClimate : public HaierClimateBase { float active_alarm_count_{NAN}; std::chrono::steady_clock::time_point last_alarm_request_; int big_data_sensors_{0}; + uint8_t big_data_counter_{0}; esphome::optional<hon_protocol::VerticalSwingMode> current_vertical_swing_{}; esphome::optional<hon_protocol::HorizontalSwingMode> current_horizontal_swing_{}; HonSettings settings_{}; diff --git a/esphome/components/ina2xx_base/ina2xx_base.cpp b/esphome/components/ina2xx_base/ina2xx_base.cpp index 9f510eef74..2d08562e54 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.cpp +++ b/esphome/components/ina2xx_base/ina2xx_base.cpp @@ -572,9 +572,8 @@ bool INA2XX::write_unsigned_16_(uint8_t reg, uint16_t val) { } bool INA2XX::read_unsigned_(uint8_t reg, uint8_t reg_size, uint64_t &data_out) { - static uint8_t rx_buf[5] = {0}; // max buffer size - - if (reg_size > 5) { + uint8_t rx_buf[5]{}; + if (reg_size > sizeof(rx_buf)) { return false; } diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.cpp b/esphome/components/ltr_als_ps/ltr_als_ps.cpp index f9c1474c85..ff335fe34c 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.cpp +++ b/esphome/components/ltr_als_ps/ltr_als_ps.cpp @@ -137,7 +137,6 @@ void LTRAlsPsComponent::update() { void LTRAlsPsComponent::loop() { ErrorCode err = i2c::ERROR_OK; - static uint8_t tries{0}; switch (this->state_) { case State::DELAYED_SETUP: @@ -166,20 +165,20 @@ void LTRAlsPsComponent::loop() { case State::WAITING_FOR_DATA: if (this->is_als_data_ready_(this->als_readings_) == LtrDataAvail::LTR_DATA_OK) { - tries = 0; + this->read_data_tries_ = 0; ESP_LOGV(TAG, "Reading sensor data having gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain), get_itime_ms(this->als_readings_.integration_time)); this->read_sensor_data_(this->als_readings_); this->state_ = State::DATA_COLLECTED; this->apply_lux_calculation_(this->als_readings_); - } else if (tries >= MAX_TRIES) { + } else if (this->read_data_tries_ >= MAX_TRIES) { ESP_LOGW(TAG, "Can't get data after several tries."); - tries = 0; + this->read_data_tries_ = 0; this->status_set_warning(); this->state_ = State::IDLE; return; } else { - tries++; + this->read_data_tries_++; } break; @@ -221,21 +220,21 @@ void LTRAlsPsComponent::loop() { } void LTRAlsPsComponent::check_and_trigger_ps_() { - static uint32_t last_high_trigger_time{0}; - static uint32_t last_low_trigger_time{0}; uint16_t ps_data = this->read_ps_data_(); uint32_t now = millis(); if (ps_data != this->ps_readings_) { this->ps_readings_ = ps_data; // Higher values - object is closer to sensor - if (ps_data > this->ps_threshold_high_ && now - last_high_trigger_time >= this->ps_cooldown_time_s_ * 1000) { - last_high_trigger_time = now; + if (ps_data > this->ps_threshold_high_ && + now - this->last_ps_high_trigger_time_ >= this->ps_cooldown_time_s_ * 1000) { + this->last_ps_high_trigger_time_ = now; ESP_LOGV(TAG, "Proximity high threshold triggered. Value = %d, Trigger level = %d", ps_data, this->ps_threshold_high_); this->on_ps_high_trigger_callback_.call(); - } else if (ps_data < this->ps_threshold_low_ && now - last_low_trigger_time >= this->ps_cooldown_time_s_ * 1000) { - last_low_trigger_time = now; + } else if (ps_data < this->ps_threshold_low_ && + now - this->last_ps_low_trigger_time_ >= this->ps_cooldown_time_s_ * 1000) { + this->last_ps_low_trigger_time_ = now; ESP_LOGV(TAG, "Proximity low threshold triggered. Value = %d, Trigger level = %d", ps_data, this->ps_threshold_low_); this->on_ps_low_trigger_callback_.call(); diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.h b/esphome/components/ltr_als_ps/ltr_als_ps.h index c6052300de..3ab2cea074 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.h +++ b/esphome/components/ltr_als_ps/ltr_als_ps.h @@ -126,10 +126,13 @@ class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice { MeasurementRepeatRate repeat_rate_{MeasurementRepeatRate::REPEAT_RATE_500MS}; float glass_attenuation_factor_{1.0}; + uint32_t last_ps_high_trigger_time_{0}; + uint32_t last_ps_low_trigger_time_{0}; uint16_t ps_cooldown_time_s_{5}; - PsGain ps_gain_{PsGain::PS_GAIN_16}; uint16_t ps_threshold_high_{0xffff}; uint16_t ps_threshold_low_{0x0000}; + uint8_t read_data_tries_{0}; + PsGain ps_gain_{PsGain::PS_GAIN_16}; // // Sensors for publishing data diff --git a/esphome/components/matrix_keypad/matrix_keypad.cpp b/esphome/components/matrix_keypad/matrix_keypad.cpp index febbe794e4..cc46ba98d6 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.cpp +++ b/esphome/components/matrix_keypad/matrix_keypad.cpp @@ -27,8 +27,6 @@ void MatrixKeypad::setup() { } void MatrixKeypad::loop() { - static uint32_t active_start = 0; - static int active_key = -1; uint32_t now = App.get_loop_component_start_time(); int key = -1; bool error = false; @@ -54,8 +52,8 @@ void MatrixKeypad::loop() { if (error) return; - if (key != active_key) { - if ((active_key != -1) && (this->pressed_key_ == active_key)) { + if (key != this->active_key_) { + if ((this->active_key_ != -1) && (this->pressed_key_ == this->active_key_)) { row = this->pressed_key_ / this->columns_.size(); col = this->pressed_key_ % this->columns_.size(); ESP_LOGD(TAG, "key @ row %d, col %d released", row, col); @@ -70,13 +68,13 @@ void MatrixKeypad::loop() { this->pressed_key_ = -1; } - active_key = key; + this->active_key_ = key; if (key == -1) return; - active_start = now; + this->active_start_ = now; } - if ((this->pressed_key_ == key) || (now - active_start < this->debounce_time_)) + if ((this->pressed_key_ == key) || (now - this->active_start_ < this->debounce_time_)) return; row = key / this->columns_.size(); diff --git a/esphome/components/matrix_keypad/matrix_keypad.h b/esphome/components/matrix_keypad/matrix_keypad.h index 258ab4fadc..8963612d0c 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.h +++ b/esphome/components/matrix_keypad/matrix_keypad.h @@ -44,6 +44,8 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component { bool has_diodes_{false}; bool has_pulldowns_{false}; int pressed_key_ = -1; + uint32_t active_start_{0}; + int active_key_{-1}; std::vector<MatrixKeypadListener *> listeners_{}; std::vector<MatrixKeyTrigger *> key_triggers_; diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index 8a7fb965e9..5642fd5f7b 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -150,17 +150,16 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) { this->on_publish_.call((int) event.msg_id); break; case MQTT_EVENT_DATA: { - static std::string topic; if (!event.topic.empty()) { // When a single message arrives as multiple chunks, the topic will be empty // on any but the first message, leading to event.topic being an empty string. // To ensure handlers get the correct topic, cache the last seen topic to // simulate always receiving the topic from underlying library - topic = event.topic; + this->cached_topic_ = event.topic; } - ESP_LOGV(TAG, "MQTT_EVENT_DATA %s", topic.c_str()); - this->on_message_.call(topic.c_str(), event.data.data(), event.data.size(), event.current_data_offset, - event.total_data_len); + ESP_LOGV(TAG, "MQTT_EVENT_DATA %s", this->cached_topic_.c_str()); + this->on_message_.call(this->cached_topic_.c_str(), event.data.data(), event.data.size(), + event.current_data_offset, event.total_data_len); } break; case MQTT_EVENT_ERROR: ESP_LOGE(TAG, "MQTT_EVENT_ERROR"); diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index ccc4c4026c..5c4dc413bd 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -265,6 +265,7 @@ class MQTTBackendESP32 final : public MQTTBackend { CallbackManager<on_unsubscribe_callback_t> on_unsubscribe_; CallbackManager<on_message_callback_t> on_message_; CallbackManager<on_publish_user_callback_t> on_publish_; + std::string cached_topic_; std::queue<Event> mqtt_events_; #if defined(USE_MQTT_IDF_ENQUEUE) diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index cb41e374f8..bbf1ffd4c0 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -124,6 +124,7 @@ void SGP4xComponent::self_test_() { } this->self_test_complete_ = true; + this->nox_conditioning_start_ = millis(); ESP_LOGD(TAG, "Self-test complete"); }); } @@ -161,7 +162,6 @@ void SGP4xComponent::update_gas_indices_() { void SGP4xComponent::measure_raw_() { float humidity = NAN; - static uint32_t nox_conditioning_start = millis(); if (!this->self_test_complete_) { ESP_LOGW(TAG, "Self-test incomplete"); @@ -191,10 +191,11 @@ void SGP4xComponent::measure_raw_() { response_words = 1; } else { // SGP41 sensor must use NOx conditioning command for the first 10 seconds - if (millis() - nox_conditioning_start < 10000) { + if (this->nox_conditioning_start_.has_value() && millis() - *this->nox_conditioning_start_ < 10000) { command = SGP41_CMD_NOX_CONDITIONING; response_words = 1; } else { + this->nox_conditioning_start_.reset(); command = SGP41_CMD_MEASURE_RAW; response_words = 2; } diff --git a/esphome/components/sgp4x/sgp4x.h b/esphome/components/sgp4x/sgp4x.h index 89fa627c61..6b8b598aff 100644 --- a/esphome/components/sgp4x/sgp4x.h +++ b/esphome/components/sgp4x/sgp4x.h @@ -127,8 +127,9 @@ class SGP4xComponent : public PollingComponent, public sensor::Sensor, public se uint16_t measure_time_; uint8_t samples_read_ = 0; uint8_t samples_to_stabilize_ = static_cast<int8_t>(GasIndexAlgorithm_INITIAL_BLACKOUT) * 2; - bool store_baseline_; + + optional<uint32_t> nox_conditioning_start_{}; ESPPreferenceObject pref_; uint32_t seconds_since_last_store_; SGP4xBaselines voc_baselines_storage_; From 4e16f270a3928738460139426748c3d96dafdddd Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Wed, 11 Mar 2026 12:47:58 -0500 Subject: [PATCH 532/609] [speaker_source] Add playlist management (#14652) --- .../components/speaker_source/automation.h | 29 ++ .../components/speaker_source/media_player.py | 43 ++ .../speaker_source_media_player.cpp | 401 ++++++++++++++---- .../speaker_source_media_player.h | 46 +- tests/components/speaker_source/common.yaml | 5 + 5 files changed, 429 insertions(+), 95 deletions(-) create mode 100644 esphome/components/speaker_source/automation.h diff --git a/esphome/components/speaker_source/automation.h b/esphome/components/speaker_source/automation.h new file mode 100644 index 0000000000..b436149a03 --- /dev/null +++ b/esphome/components/speaker_source/automation.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_ESP32 + +#include "esphome/core/automation.h" +#include "speaker_source_media_player.h" + +namespace esphome::speaker_source { + +template<typename... Ts> class SetPlaylistDelayAction : public Action<Ts...> { + public: + explicit SetPlaylistDelayAction(SpeakerSourceMediaPlayer *parent) : parent_(parent) {} + + TEMPLATABLE_VALUE(uint8_t, pipeline) + TEMPLATABLE_VALUE(uint32_t, delay) + + void play(const Ts &...x) override { + this->parent_->set_playlist_delay_ms(this->pipeline_.value(x...), this->delay_.value(x...)); + } + + protected: + SpeakerSourceMediaPlayer *parent_; +}; + +} // namespace esphome::speaker_source + +#endif // USE_ESP32 diff --git a/esphome/components/speaker_source/media_player.py b/esphome/components/speaker_source/media_player.py index a44cdcbf01..9080bebcae 100644 --- a/esphome/components/speaker_source/media_player.py +++ b/esphome/components/speaker_source/media_player.py @@ -3,13 +3,16 @@ import esphome.codegen as cg from esphome.components import audio, media_player, media_source, speaker import esphome.config_validation as cv from esphome.const import ( + CONF_DELAY, CONF_FORMAT, CONF_ID, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE, CONF_SPEAKER, ) +from esphome.core import ID from esphome.core.entity_helpers import inherit_property_from +from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType AUTO_LOAD = ["audio"] @@ -19,6 +22,7 @@ CODEOWNERS = ["@kahrendt"] CONF_MEDIA_PIPELINE = "media_pipeline" CONF_ON_MUTE = "on_mute" +CONF_PIPELINE = "pipeline" CONF_ON_UNMUTE = "on_unmute" CONF_ON_VOLUME = "on_volume" CONF_SOURCES = "sources" @@ -36,6 +40,13 @@ SpeakerSourceMediaPlayer = speaker_source_ns.class_( PipelineContext = speaker_source_ns.struct("PipelineContext") Pipeline = speaker_source_ns.enum("Pipeline") +PIPELINE_ENUM = { + "media": Pipeline.MEDIA_PIPELINE, +} + +SetPlaylistDelayAction = speaker_source_ns.class_( + "SetPlaylistDelayAction", automation.Action +) FORMAT_MAPPING = { @@ -210,3 +221,35 @@ async def to_code(config: ConfigType) -> None: [(cg.float_, "x")], on_volume, ) + + +SET_PLAYLIST_DELAY_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(SpeakerSourceMediaPlayer), + cv.Required(CONF_PIPELINE): cv.enum(PIPELINE_ENUM, lower=True), + cv.Required(CONF_DELAY): cv.templatable(cv.positive_time_period_milliseconds), + } +) + + +@automation.register_action( + "speaker_source.set_playlist_delay", + SetPlaylistDelayAction, + SET_PLAYLIST_DELAY_ACTION_SCHEMA, + synchronous=True, +) +async def set_playlist_delay_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) + + cg.add(var.set_pipeline(config[CONF_PIPELINE])) + + template_ = await cg.templatable(config[CONF_DELAY], args, cg.uint32) + cg.add(var.set_delay(template_)) + + return var diff --git a/esphome/components/speaker_source/speaker_source_media_player.cpp b/esphome/components/speaker_source/speaker_source_media_player.cpp index a3679891d2..3724e20667 100644 --- a/esphome/components/speaker_source/speaker_source_media_player.cpp +++ b/esphome/components/speaker_source/speaker_source_media_player.cpp @@ -135,8 +135,12 @@ void SpeakerSourceMediaPlayer::handle_media_state_changed_(uint8_t pipeline, med PipelineContext &ps = this->pipelines_[pipeline]; if (state == media_source::MediaSourceState::IDLE) { + // Track whether this IDLE was from an orchestrator-initiated stop (e.g., NEXT/PREV/PLAY_URI) + // so we can suppress spurious PLAYLIST_ADVANCE below + bool was_stopping = (ps.stopping_source == source); + // Source went idle - clear stopping flag if this was the source we asked to stop - if (ps.stopping_source == source) { + if (was_stopping) { ps.stopping_source = nullptr; } @@ -152,6 +156,11 @@ void SpeakerSourceMediaPlayer::handle_media_state_changed_(uint8_t pipeline, med // Finish the speaker to ensure it's ready for the next playback ps.speaker->finish(); + + // Only advance the playlist if the track finished naturally (not stopped by the orchestrator) + if (!was_stopping) { + this->queue_command_(MediaPlayerControlCommand::PLAYLIST_ADVANCE, pipeline); + } } } else if (state == media_source::MediaSourceState::PLAYING) { // Source started playing - make it the active source if no one else is active @@ -197,8 +206,9 @@ size_t SpeakerSourceMediaPlayer::handle_media_output_(uint8_t pipeline, media_so return 0; } +// THREAD CONTEXT: Called from main loop (loop) media_player::MediaPlayerState SpeakerSourceMediaPlayer::get_media_pipeline_state_( - media_source::MediaSource *source) const { + media_source::MediaSource *source, bool playlist_active, media_player::MediaPlayerState old_state) const { if (source != nullptr) { switch (source->get_state()) { case media_source::MediaSourceState::PLAYING: @@ -214,97 +224,27 @@ media_player::MediaPlayerState SpeakerSourceMediaPlayer::get_media_pipeline_stat } } + // No active source. Stay PLAYING during playlist transitions + if (playlist_active && old_state == media_player::MEDIA_PLAYER_STATE_PLAYING) { + return media_player::MEDIA_PLAYER_STATE_PLAYING; + } return media_player::MEDIA_PLAYER_STATE_IDLE; } void SpeakerSourceMediaPlayer::loop() { // Process queued control commands - MediaPlayerControlCommand control_command; - - // Use peek to check command without removing it - if (xQueuePeek(this->media_control_command_queue_, &control_command, 0) == pdTRUE) { - bool command_executed = false; - uint8_t pipeline = control_command.pipeline; - - switch (control_command.type) { - case MediaPlayerControlCommand::PLAY_URI: { - command_executed = this->try_execute_play_uri_(*control_command.data.uri, pipeline); - break; - } - - case MediaPlayerControlCommand::SEND_COMMAND: { - PipelineContext &ps = this->pipelines_[pipeline]; - - // Determine target source: prefer active, fall back to last - media_source::MediaSource *target_source = nullptr; - if (ps.active_source != nullptr) { - target_source = ps.active_source; - } else if (ps.last_source != nullptr) { - target_source = ps.last_source; - } - - media_player::MediaPlayerCommand player_command = control_command.data.command; - switch (player_command) { - case media_player::MEDIA_PLAYER_COMMAND_TOGGLE: { - media_source::MediaSource *active_source = ps.active_source; - if ((active_source != nullptr) && (active_source->get_state() == media_source::MediaSourceState::PLAYING)) { - if (target_source != nullptr) { - target_source->handle_command(media_source::MediaSourceCommand::PAUSE); - } - } else { - if (target_source != nullptr) { - target_source->handle_command(media_source::MediaSourceCommand::PLAY); - } - } - break; - } - - case media_player::MEDIA_PLAYER_COMMAND_PLAY: { - if (target_source != nullptr) { - target_source->handle_command(media_source::MediaSourceCommand::PLAY); - } - break; - } - - case media_player::MEDIA_PLAYER_COMMAND_PAUSE: { - if (target_source != nullptr) { - target_source->handle_command(media_source::MediaSourceCommand::PAUSE); - } - break; - } - - case media_player::MEDIA_PLAYER_COMMAND_STOP: { - if (target_source != nullptr) { - target_source->handle_command(media_source::MediaSourceCommand::STOP); - } - break; - } - - default: - break; - } - - command_executed = true; - break; - } - } - - // Only remove from queue if successfully executed - if (command_executed) { - xQueueReceive(this->media_control_command_queue_, &control_command, 0); - - // Delete the allocated string for PLAY_URI commands - if (control_command.type == MediaPlayerControlCommand::PLAY_URI) { - delete control_command.data.uri; - } - } - } + this->process_control_queue_(); // Update state based on active sources media_player::MediaPlayerState old_state = this->state; PipelineContext &media_ps = this->pipelines_[MEDIA_PIPELINE]; - this->state = this->get_media_pipeline_state_(media_ps.active_source); + + // Check playlist state to detect transitions between items + bool media_playlist_active = (media_ps.playlist_index < media_ps.playlist.size()) || + (media_ps.repeat_mode != REPEAT_OFF && !media_ps.playlist.empty()); + + this->state = this->get_media_pipeline_state_(media_ps.active_source, media_playlist_active, old_state); if (this->state != old_state) { this->publish_state(); @@ -349,9 +289,9 @@ bool SpeakerSourceMediaPlayer::try_execute_play_uri_(const std::string &uri, uin // Only send END command once per source - check if we've already asked this source to stop if (ps.stopping_source != active_source) { ESP_LOGV(TAG, "Pipeline %u: stopping active source", pipeline); + ps.stopping_source = active_source; active_source->handle_command(media_source::MediaSourceCommand::STOP); ps.speaker->stop(); - ps.stopping_source = active_source; } return false; // Leave in queue, retry next loop } @@ -363,9 +303,9 @@ bool SpeakerSourceMediaPlayer::try_execute_play_uri_(const std::string &uri, uin // Only send STOP command once per source if (ps.stopping_source != target_source) { ESP_LOGV(TAG, "Pipeline %u: target source busy, stopping", pipeline); + ps.stopping_source = target_source; target_source->handle_command(media_source::MediaSourceCommand::STOP); ps.speaker->stop(); - ps.stopping_source = target_source; } return false; // Leave in queue, retry next loop } @@ -385,6 +325,7 @@ bool SpeakerSourceMediaPlayer::try_execute_play_uri_(const std::string &uri, uin if (!target_source->play_uri(uri)) { ESP_LOGE(TAG, "Pipeline %u: Failed to play URI: %s", pipeline, uri.c_str()); ps.pending_source = nullptr; + this->queue_command_(MediaPlayerControlCommand::PLAYLIST_ADVANCE, pipeline); } // Reset pending frame counter for this pipeline since we're starting a new source @@ -393,6 +334,280 @@ bool SpeakerSourceMediaPlayer::try_execute_play_uri_(const std::string &uri, uin return true; // Remove from queue } +// THREAD CONTEXT: Called from main loop (process_control_queue_, queue_play_current_, handle_media_state_changed_) +void SpeakerSourceMediaPlayer::queue_command_(MediaPlayerControlCommand::Type type, uint8_t pipeline) { + MediaPlayerControlCommand cmd{}; + cmd.type = type; + cmd.pipeline = pipeline; + if (xQueueSend(this->media_control_command_queue_, &cmd, 0) != pdTRUE) { + ESP_LOGE(TAG, "Queue full, command dropped"); + } +} + +// THREAD CONTEXT: Called from main loop via automation commands (direct) +void SpeakerSourceMediaPlayer::set_playlist_delay_ms(uint8_t pipeline, uint32_t delay_ms) { + if (pipeline < this->pipelines_.size()) { + this->pipelines_[pipeline].playlist_delay_ms = delay_ms; + } +} + +// THREAD CONTEXT: Called from main loop (process_control_queue_). +// The timeout callback also runs on the main loop. +void SpeakerSourceMediaPlayer::queue_play_current_(uint8_t pipeline, uint32_t delay_ms) { + if (delay_ms > 0) { + this->set_timeout(PipelineContext::TIMEOUT_IDS[pipeline], delay_ms, + [this, pipeline]() { this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); }); + } else { + this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); + } +} + +// THREAD CONTEXT: Called from main loop (loop) +void SpeakerSourceMediaPlayer::process_control_queue_() { + MediaPlayerControlCommand control_command; + + // Use peek to check command without removing it + if (xQueuePeek(this->media_control_command_queue_, &control_command, 0) != pdTRUE) { + return; + } + + bool command_executed = false; + uint8_t pipeline = control_command.pipeline; + + // Get pipeline state + PipelineContext &ps = this->pipelines_[pipeline]; + media_source::MediaSource *active_source = ps.active_source; + + switch (control_command.type) { + case MediaPlayerControlCommand::PLAY_URI: { + // Always use our local playlist to start playback + this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); + ps.playlist.clear(); + ps.playlist_index = 0; // Reset index + ps.playlist.push_back(*control_command.data.uri); + + // Queue PLAY_CURRENT to initiate playback + this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); + command_executed = true; + break; + } + + case MediaPlayerControlCommand::ENQUEUE_URI: { + // Always add to our local playlist + ps.playlist.push_back(*control_command.data.uri); + + // If nothing is playing and no upcoming items are queued, start the new item. + bool nothing_playing = + (active_source == nullptr) || (active_source->get_state() == media_source::MediaSourceState::IDLE); + if (nothing_playing && ps.playlist_index >= ps.playlist.size() - 1) { + ps.playlist_index = ps.playlist.size() - 1; // Point to newly added item + this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); + } + command_executed = true; + break; + } + + case MediaPlayerControlCommand::PLAYLIST_ADVANCE: { + // Internal message: a track finished, advance to next + if (ps.repeat_mode != REPEAT_ONE) { + ps.playlist_index++; + } + + // Check if we should continue playback + if (ps.playlist_index < ps.playlist.size()) { + this->queue_play_current_(pipeline, ps.playlist_delay_ms); + } else if (ps.repeat_mode == REPEAT_ALL && !ps.playlist.empty()) { + ps.playlist_index = 0; + this->queue_play_current_(pipeline, ps.playlist_delay_ms); + } + command_executed = true; + break; + } + + case MediaPlayerControlCommand::PLAY_CURRENT: { + // Play the item at current playlist index + if (ps.playlist_index < ps.playlist.size()) { + command_executed = this->try_execute_play_uri_(ps.playlist[ps.playlist_index], pipeline); + } else { + command_executed = true; // Index out of bounds or empty playlist + } + break; + } + + case MediaPlayerControlCommand::SEND_COMMAND: { + this->handle_player_command_(control_command.data.command, pipeline); + command_executed = true; + break; + } + } + + // Only remove from queue if successfully executed + if (command_executed) { + xQueueReceive(this->media_control_command_queue_, &control_command, 0); + + // Delete the allocated string for PLAY_URI and ENQUEUE_URI commands + if (control_command.type == MediaPlayerControlCommand::PLAY_URI || + control_command.type == MediaPlayerControlCommand::ENQUEUE_URI) { + delete control_command.data.uri; + } + } +} + +// THREAD CONTEXT: Called from main loop only (via process_control_queue_) +void SpeakerSourceMediaPlayer::handle_player_command_(media_player::MediaPlayerCommand player_command, + uint8_t pipeline) { + PipelineContext &ps = this->pipelines_[pipeline]; + media_source::MediaSource *active_source = ps.active_source; + bool has_internal_playlist = (active_source != nullptr) && active_source->has_internal_playlist(); + + // Determine target source: prefer active, fall back to last + media_source::MediaSource *target_source = nullptr; + if (active_source != nullptr) { + target_source = active_source; + } else if (ps.last_source != nullptr) { + target_source = ps.last_source; + } + + switch (player_command) { + case media_player::MEDIA_PLAYER_COMMAND_TOGGLE: { + // Convert TOGGLE to PLAY or PAUSE based on current state + if ((active_source != nullptr) && (active_source->get_state() == media_source::MediaSourceState::PLAYING)) { + if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::PAUSE); + } + } else if (!has_internal_playlist && active_source == nullptr && !ps.playlist.empty()) { + bool last_has_internal_playlist = (ps.last_source != nullptr) && ps.last_source->has_internal_playlist(); + if (last_has_internal_playlist) { + ps.last_source->handle_command(media_source::MediaSourceCommand::PLAY); + } else { + if (ps.playlist_index >= ps.playlist.size()) { + ps.playlist_index = 0; + } + this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); + } + } else { + if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::PLAY); + } + } + break; + } + + case media_player::MEDIA_PLAYER_COMMAND_PLAY: { + if (!has_internal_playlist && active_source == nullptr && !ps.playlist.empty()) { + bool last_has_internal_playlist = (ps.last_source != nullptr) && ps.last_source->has_internal_playlist(); + if (last_has_internal_playlist) { + ps.last_source->handle_command(media_source::MediaSourceCommand::PLAY); + } else { + if (ps.playlist_index >= ps.playlist.size()) { + ps.playlist_index = 0; + } + this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); + } + } else if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::PLAY); + } + break; + } + + case media_player::MEDIA_PLAYER_COMMAND_PAUSE: { + if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::PAUSE); + } + break; + } + + case media_player::MEDIA_PLAYER_COMMAND_STOP: { + if (!has_internal_playlist) { + this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); + ps.playlist.clear(); + ps.playlist_index = 0; + } + if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::STOP); + } + break; + } + + case media_player::MEDIA_PLAYER_COMMAND_NEXT: { + if (!has_internal_playlist) { + this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); + if (ps.playlist_index + 1 < ps.playlist.size()) { + ps.playlist_index++; + this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); + } else if (ps.repeat_mode == REPEAT_ALL && !ps.playlist.empty()) { + ps.playlist_index = 0; + this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); + } + } else if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::NEXT); + } + break; + } + + case media_player::MEDIA_PLAYER_COMMAND_PREVIOUS: { + if (!has_internal_playlist) { + this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); + if (ps.playlist_index > 0) { + ps.playlist_index--; + this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); + } else if (ps.repeat_mode == REPEAT_ALL && !ps.playlist.empty()) { + ps.playlist_index = ps.playlist.size() - 1; + this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); + } + } else if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::PREVIOUS); + } + break; + } + + case media_player::MEDIA_PLAYER_COMMAND_REPEAT_ONE: + if (!has_internal_playlist) { + ps.repeat_mode = REPEAT_ONE; + } else if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::REPEAT_ONE); + } + break; + + case media_player::MEDIA_PLAYER_COMMAND_REPEAT_OFF: + if (!has_internal_playlist) { + ps.repeat_mode = REPEAT_OFF; + } else if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::REPEAT_OFF); + } + break; + + case media_player::MEDIA_PLAYER_COMMAND_REPEAT_ALL: + if (!has_internal_playlist) { + ps.repeat_mode = REPEAT_ALL; + } else if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::REPEAT_ALL); + } + break; + + case media_player::MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST: { + if (!has_internal_playlist) { + this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); + if (ps.playlist_index < ps.playlist.size()) { + ps.playlist[0] = std::move(ps.playlist[ps.playlist_index]); + ps.playlist.resize(1); + ps.playlist_index = 0; + } else { + ps.playlist.clear(); + ps.playlist_index = 0; + } + } else if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::CLEAR_PLAYLIST); + } + break; + } + + default: + // TURN_ON, TURN_OFF, ENQUEUE (handled separately with URL), SHUFFLE/UNSHUFFLE (PR3) are no-ops + break; + } +} + // THREAD CONTEXT: Called from main loop only. Entry points: // - HA/automation commands (direct) // - handle_play_uri_request_() via make_call().perform() (deferred from source tasks) @@ -406,10 +621,17 @@ void SpeakerSourceMediaPlayer::control(const media_player::MediaPlayerCall &call auto media_url = call.get_media_url(); if (media_url.has_value()) { - control_command.type = MediaPlayerControlCommand::PLAY_URI; + auto command = call.get_command(); + bool enqueue = command.has_value() && command.value() == media_player::MEDIA_PLAYER_COMMAND_ENQUEUE; + + if (enqueue) { + control_command.type = MediaPlayerControlCommand::ENQUEUE_URI; + } else { + control_command.type = MediaPlayerControlCommand::PLAY_URI; + } // Heap allocation is unavoidable: URIs from Home Assistant are arbitrary-length (media URLs with tokens - // can easily exceed 500 bytes). Deleted after the command is consumed. FreeRTOS queues require items to be - // copyable, so we store a pointer to the string in the queue rather than the string itself. + // can easily exceed 500 bytes). Deleted in process_control_queue_() after the command is consumed. FreeRTOS queues + // require items to be copyable, so we store a pointer to the string in the queue rather than the string itself. control_command.data.uri = new std::string(media_url.value()); if (xQueueSend(this->media_control_command_queue_, &control_command, 0) != pdTRUE) { delete control_command.data.uri; @@ -454,6 +676,9 @@ void SpeakerSourceMediaPlayer::control(const media_player::MediaPlayerCall &call } media_player::MediaPlayerTraits SpeakerSourceMediaPlayer::get_traits() { + // This media player supports more traits like playlists, repeat, and shuffle, but the ESPHome API currently (March + // 2026) doesn't support those commands, so we only report pause support for now since that's used by the frontend and + // supported by our player. auto traits = media_player::MediaPlayerTraits(); traits.set_supports_pause(true); diff --git a/esphome/components/speaker_source/speaker_source_media_player.h b/esphome/components/speaker_source/speaker_source_media_player.h index 7896fef295..0967084504 100644 --- a/esphome/components/speaker_source/speaker_source_media_player.h +++ b/esphome/components/speaker_source/speaker_source_media_player.h @@ -29,7 +29,8 @@ namespace esphome::speaker_source { // - Main loop task: setup(), loop(), dump_config(), handle_media_state_changed_(), // handle_volume_request_(), handle_mute_request_(), handle_play_uri_request_(), // set_volume_(), set_mute_state_(), control(), get_media_pipeline_state_(), -// find_source_for_uri_(), try_execute_play_uri_(), save_volume_restore_state_() +// find_source_for_uri_(), try_execute_play_uri_(), save_volume_restore_state_(), +// process_control_queue_(), handle_player_command_(), queue_command_(), queue_play_current_() // // - Media source task(s): handle_media_output_() via SourceBinding::write_audio(). // Called from each source's decode task thread when streaming audio data. @@ -49,13 +50,19 @@ namespace esphome::speaker_source { // - defer(): SourceBinding::request_volume/request_mute/request_play_uri -> main loop // - Atomic fields (active_source, pending_frames): shared between all three thread contexts // -// Non-atomic pipeline fields (last_source, stopping_source, pending_source) are only accessed -// from the main loop thread. +// Non-atomic pipeline fields (last_source, stopping_source, pending_source, playlist, +// playlist_index, repeat_mode) are only accessed from the main loop thread. enum Pipeline : uint8_t { MEDIA_PIPELINE = 0, }; +enum RepeatMode : uint8_t { + REPEAT_OFF = 0, + REPEAT_ONE = 1, + REPEAT_ALL = 2, +}; + // Forward declaration class SpeakerSourceMediaPlayer; @@ -79,6 +86,9 @@ struct SourceBinding : public media_source::MediaSourceListener { }; struct PipelineContext { + /// @brief Timeout IDs for playlist delay, indexed by Pipeline enum + static constexpr const char *const TIMEOUT_IDS[] = {"next_media"}; + speaker::Speaker *speaker{nullptr}; optional<media_player::MediaPlayerSupportedFormat> format; @@ -92,6 +102,14 @@ struct PipelineContext { // Uses std::vector because the count varies across instances (multiple speaker_source media players may exist). std::vector<std::unique_ptr<SourceBinding>> sources; + // Dynamic allocation is unavoidable here: URIs from Home Assistant are arbitrary-length strings + // (media URLs with tokens can easily exceed 500 bytes), and playlist size is unbounded. + // Pre-allocating fixed buffers would waste significant RAM when idle without covering worst cases. + std::vector<std::string> playlist; + size_t playlist_index{0}; + RepeatMode repeat_mode{REPEAT_OFF}; + uint32_t playlist_delay_ms{0}; + // Track frames sent to speaker to correlate with playback callbacks. // Atomic because it is written from the main loop/source tasks and read/decremented from the speaker playback // callback. @@ -103,14 +121,17 @@ struct PipelineContext { struct MediaPlayerControlCommand { enum Type : uint8_t { - PLAY_URI, // Find a source that can handle this URI and play it - SEND_COMMAND, // Send command to active source + PLAY_URI, // Clear playlist, reset index, add URI, queue PLAY_CURRENT + ENQUEUE_URI, // Add URI to playlist, queue PLAY_CURRENT if idle + PLAYLIST_ADVANCE, // Advance index (or wrap for repeat_all), queue PLAY_CURRENT if more items + PLAY_CURRENT, // Play item at current playlist index (can retry if speaker not ready) + SEND_COMMAND, // Send command to active source }; Type type; uint8_t pipeline; union { - std::string *uri; // Owned pointer, must delete after xQueueReceive (for PLAY_URI) + std::string *uri; // Owned pointer, must delete after xQueueReceive (for PLAY_URI and ENQUEUE_URI) media_player::MediaPlayerCommand command; } data; }; @@ -154,6 +175,8 @@ class SpeakerSourceMediaPlayer : public Component, public media_player::MediaPla Trigger<> *get_unmute_trigger() { return &this->unmute_trigger_; } Trigger<float> *get_volume_trigger() { return &this->volume_trigger_; } + void set_playlist_delay_ms(uint8_t pipeline, uint32_t delay_ms); + protected: // Callbacks from source bindings (pipeline index is captured at binding creation time) size_t handle_media_output_(uint8_t pipeline, media_source::MediaSource *source, const uint8_t *data, size_t length, @@ -183,11 +206,20 @@ class SpeakerSourceMediaPlayer : public Component, public media_player::MediaPla /// @brief Determine media player state from the media pipeline's active source /// @param media_source Active source for the media pipeline (may be nullptr) + /// @param playlist_active Whether the media pipeline's playlist is in progress + /// @param old_state Previous media player state (used for transition smoothing) /// @return The appropriate MediaPlayerState - media_player::MediaPlayerState get_media_pipeline_state_(media_source::MediaSource *media_source) const; + media_player::MediaPlayerState get_media_pipeline_state_(media_source::MediaSource *media_source, + bool playlist_active, + media_player::MediaPlayerState old_state) const; + void process_control_queue_(); + void handle_player_command_(media_player::MediaPlayerCommand player_command, uint8_t pipeline); bool try_execute_play_uri_(const std::string &uri, uint8_t pipeline); media_source::MediaSource *find_source_for_uri_(const std::string &uri, uint8_t pipeline); + void queue_command_(MediaPlayerControlCommand::Type type, uint8_t pipeline); + void queue_play_current_(uint8_t pipeline, uint32_t delay_ms = 0); + QueueHandle_t media_control_command_queue_; // Pipeline context for media pipeline. See THREADING MODEL at top of namespace for access rules. diff --git a/tests/components/speaker_source/common.yaml b/tests/components/speaker_source/common.yaml index cfcb065f57..9e4c309c06 100644 --- a/tests/components/speaker_source/common.yaml +++ b/tests/components/speaker_source/common.yaml @@ -41,3 +41,8 @@ media_player: on_unmute: - media_player.play: id: media_player_id + on_volume: + - speaker_source.set_playlist_delay: + id: media_player_id + pipeline: media + delay: 500ms From 3d4ebe74ce2dc7948d2163cdf11f7f89be3c6dec Mon Sep 17 00:00:00 2001 From: Big Mike <mikelawrence@users.noreply.github.com> Date: Wed, 11 Mar 2026 13:00:42 -0500 Subject: [PATCH 533/609] [sensirion_common] Use SmallBufferWithHeapFallback helper (#14270) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- .../sensirion_common/i2c_sensirion.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/esphome/components/sensirion_common/i2c_sensirion.cpp b/esphome/components/sensirion_common/i2c_sensirion.cpp index 0279e08b9f..6e244faf59 100644 --- a/esphome/components/sensirion_common/i2c_sensirion.cpp +++ b/esphome/components/sensirion_common/i2c_sensirion.cpp @@ -12,24 +12,25 @@ static const char *const TAG = "sensirion_i2c"; static const size_t BUFFER_STACK_SIZE = 16; bool SensirionI2CDevice::read_data(uint16_t *data, const uint8_t len) { - const size_t num_bytes = len * 3; - uint8_t buf[num_bytes]; + const size_t required_buffer_len = len * 3; + SmallBufferWithHeapFallback<BUFFER_STACK_SIZE> buffer(required_buffer_len); + uint8_t *temp = buffer.get(); - this->last_error_ = this->read(buf, num_bytes); + this->last_error_ = this->read(temp, required_buffer_len); if (this->last_error_ != i2c::ERROR_OK) { return false; } - for (uint8_t i = 0; i < len; i++) { - const uint8_t j = 3 * i; + for (size_t i = 0; i < len; i++) { + const size_t j = i * 3; // Use MSB first since Sensirion devices use CRC-8 with MSB first - uint8_t crc = crc8(&buf[j], 2, 0xFF, CRC_POLYNOMIAL, true); - if (crc != buf[j + 2]) { - ESP_LOGE(TAG, "CRC invalid @ %d! 0x%02X != 0x%02X", i, buf[j + 2], crc); + uint8_t crc = crc8(&temp[j], 2, 0xFF, CRC_POLYNOMIAL, true); + if (crc != temp[j + 2]) { + ESP_LOGE(TAG, "CRC invalid @ %zu! 0x%02X != 0x%02X", i, temp[j + 2], crc); this->last_error_ = i2c::ERROR_CRC; return false; } - data[i] = encode_uint16(buf[j], buf[j + 1]); + data[i] = encode_uint16(temp[j], temp[j + 1]); } return true; } From b27165a8427cb45044f7795cb46a3d6d2835cb52 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Wed, 11 Mar 2026 13:11:00 -0500 Subject: [PATCH 534/609] [speaker_source] Add shuffle support (#14653) --- .../speaker_source_media_player.cpp | 90 +++++++++++++++++-- .../speaker_source_media_player.h | 13 +++ tests/components/speaker_source/common.yaml | 4 +- 3 files changed, 100 insertions(+), 7 deletions(-) diff --git a/esphome/components/speaker_source/speaker_source_media_player.cpp b/esphome/components/speaker_source/speaker_source_media_player.cpp index 3724e20667..cf7169df6a 100644 --- a/esphome/components/speaker_source/speaker_source_media_player.cpp +++ b/esphome/components/speaker_source/speaker_source_media_player.cpp @@ -5,6 +5,8 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include <algorithm> + namespace esphome::speaker_source { static constexpr uint32_t MEDIA_CONTROLS_QUEUE_LENGTH = 20; @@ -383,7 +385,8 @@ void SpeakerSourceMediaPlayer::process_control_queue_() { // Always use our local playlist to start playback this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); ps.playlist.clear(); - ps.playlist_index = 0; // Reset index + ps.shuffle_indices.clear(); // Clear shuffle when starting fresh playlist + ps.playlist_index = 0; // Reset index ps.playlist.push_back(*control_command.data.uri); // Queue PLAY_CURRENT to initiate playback @@ -396,6 +399,11 @@ void SpeakerSourceMediaPlayer::process_control_queue_() { // Always add to our local playlist ps.playlist.push_back(*control_command.data.uri); + // If shuffle is active, add the new item to the end of the shuffle order + if (!ps.shuffle_indices.empty()) { + ps.shuffle_indices.push_back(ps.playlist.size() - 1); + } + // If nothing is playing and no upcoming items are queued, start the new item. bool nothing_playing = (active_source == nullptr) || (active_source->get_state() == media_source::MediaSourceState::IDLE); @@ -425,9 +433,10 @@ void SpeakerSourceMediaPlayer::process_control_queue_() { } case MediaPlayerControlCommand::PLAY_CURRENT: { - // Play the item at current playlist index + // Play the item at current playlist index (mapped through shuffle if active) if (ps.playlist_index < ps.playlist.size()) { - command_executed = this->try_execute_play_uri_(ps.playlist[ps.playlist_index], pipeline); + size_t actual_position = this->get_playlist_position_(pipeline); + command_executed = this->try_execute_play_uri_(ps.playlist[actual_position], pipeline); } else { command_executed = true; // Index out of bounds or empty playlist } @@ -521,6 +530,7 @@ void SpeakerSourceMediaPlayer::handle_player_command_(media_player::MediaPlayerC if (!has_internal_playlist) { this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); ps.playlist.clear(); + ps.shuffle_indices.clear(); ps.playlist_index = 0; } if (target_source != nullptr) { @@ -589,21 +599,39 @@ void SpeakerSourceMediaPlayer::handle_player_command_(media_player::MediaPlayerC if (!has_internal_playlist) { this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); if (ps.playlist_index < ps.playlist.size()) { - ps.playlist[0] = std::move(ps.playlist[ps.playlist_index]); + size_t actual_position = this->get_playlist_position_(pipeline); + ps.playlist[0] = std::move(ps.playlist[actual_position]); ps.playlist.resize(1); ps.playlist_index = 0; } else { ps.playlist.clear(); ps.playlist_index = 0; } + ps.shuffle_indices.clear(); } else if (target_source != nullptr) { target_source->handle_command(media_source::MediaSourceCommand::CLEAR_PLAYLIST); } break; } + case media_player::MEDIA_PLAYER_COMMAND_SHUFFLE: + if (!has_internal_playlist) { + this->shuffle_playlist_(pipeline); + } else if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::SHUFFLE); + } + break; + + case media_player::MEDIA_PLAYER_COMMAND_UNSHUFFLE: + if (!has_internal_playlist) { + this->unshuffle_playlist_(pipeline); + } else if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::UNSHUFFLE); + } + break; + default: - // TURN_ON, TURN_OFF, ENQUEUE (handled separately with URL), SHUFFLE/UNSHUFFLE (PR3) are no-ops + // TURN_ON, TURN_OFF, ENQUEUE (handled separately with URL) are no-ops break; } } @@ -766,6 +794,58 @@ void SpeakerSourceMediaPlayer::set_volume_(float volume, bool publish) { this->defer([this, volume]() { this->volume_trigger_.trigger(volume); }); } +size_t SpeakerSourceMediaPlayer::get_playlist_position_(uint8_t pipeline) const { + const PipelineContext &ps = this->pipelines_[pipeline]; + + if (ps.shuffle_indices.empty() || ps.playlist_index >= ps.shuffle_indices.size()) { + return ps.playlist_index; + } + return ps.shuffle_indices[ps.playlist_index]; +} + +void SpeakerSourceMediaPlayer::shuffle_playlist_(uint8_t pipeline) { + PipelineContext &ps = this->pipelines_[pipeline]; + + if (ps.playlist.size() <= 1) { + ps.shuffle_indices.clear(); + return; + } + + // Capture current actual position BEFORE modifying shuffle_indices + size_t current_actual = this->get_playlist_position_(pipeline); + + // Build indices vector + ps.shuffle_indices.resize(ps.playlist.size()); + for (size_t i = 0; i < ps.playlist.size(); i++) { + ps.shuffle_indices[i] = i; + } + + // Fisher-Yates shuffle using ESPHome's random helper + for (size_t i = ps.shuffle_indices.size() - 1; i > 0; i--) { + size_t j = random_uint32() % (i + 1); + std::swap(ps.shuffle_indices[i], ps.shuffle_indices[j]); + } + + // Move current track to current position (so playback continues seamlessly) + if (ps.playlist_index < ps.shuffle_indices.size()) { + for (size_t i = 0; i < ps.shuffle_indices.size(); i++) { + if (ps.shuffle_indices[i] == current_actual) { + std::swap(ps.shuffle_indices[i], ps.shuffle_indices[ps.playlist_index]); + break; + } + } + } +} + +void SpeakerSourceMediaPlayer::unshuffle_playlist_(uint8_t pipeline) { + PipelineContext &ps = this->pipelines_[pipeline]; + + if (!ps.shuffle_indices.empty() && ps.playlist_index < ps.shuffle_indices.size()) { + ps.playlist_index = ps.shuffle_indices[ps.playlist_index]; + } + ps.shuffle_indices.clear(); +} + } // namespace esphome::speaker_source #endif // USE_ESP32 diff --git a/esphome/components/speaker_source/speaker_source_media_player.h b/esphome/components/speaker_source/speaker_source_media_player.h index 0967084504..4fbb534110 100644 --- a/esphome/components/speaker_source/speaker_source_media_player.h +++ b/esphome/components/speaker_source/speaker_source_media_player.h @@ -110,6 +110,10 @@ struct PipelineContext { RepeatMode repeat_mode{REPEAT_OFF}; uint32_t playlist_delay_ms{0}; + // When non-empty, playlist_index indexes into these vectors + // which contain the actual playlist indices in shuffled order + std::vector<size_t> shuffle_indices; + // Track frames sent to speaker to correlate with playback callbacks. // Atomic because it is written from the main loop/source tasks and read/decremented from the speaker playback // callback. @@ -220,6 +224,15 @@ class SpeakerSourceMediaPlayer : public Component, public media_player::MediaPla void queue_command_(MediaPlayerControlCommand::Type type, uint8_t pipeline); void queue_play_current_(uint8_t pipeline, uint32_t delay_ms = 0); + /// @brief Maps playlist_index through shuffle indices if shuffle is active + size_t get_playlist_position_(uint8_t pipeline) const; + + /// @brief Generates shuffled indices for the playlist, keeping current track at current position + void shuffle_playlist_(uint8_t pipeline); + + /// @brief Clears shuffle indices and adjusts playlist_index to maintain current track + void unshuffle_playlist_(uint8_t pipeline); + QueueHandle_t media_control_command_queue_; // Pipeline context for media pipeline. See THREADING MODEL at top of namespace for access rules. diff --git a/tests/components/speaker_source/common.yaml b/tests/components/speaker_source/common.yaml index 9e4c309c06..05b5181dfd 100644 --- a/tests/components/speaker_source/common.yaml +++ b/tests/components/speaker_source/common.yaml @@ -36,10 +36,10 @@ media_player: sources: - audio_file_source on_mute: - - media_player.pause: + - media_player.shuffle: id: media_player_id on_unmute: - - media_player.play: + - media_player.unshuffle: id: media_player_id on_volume: - speaker_source.set_playlist_delay: From 03c0ce704b0c89a87f1fe2b67dbbcb6a74eb9688 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:22:48 -0400 Subject: [PATCH 535/609] Bump pyupgrade to v3.21.2 for Python 3.14 compatibility (#14699) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b036da6ef1..2d8f698395 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/asottile/pyupgrade - rev: v3.20.0 + rev: v3.21.2 hooks: - id: pyupgrade args: [--py311-plus] From 04bcd9f56ba24d40199f8f4cadf7761429bc942c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:25:36 -0400 Subject: [PATCH 536/609] [dashboard] Use sys.executable for dashboard subprocess commands (#14698) Co-authored-by: Jonathan Swoboda <swoboda1337@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/__main__.py | 13 +++++++------ esphome/dashboard/const.py | 5 ++++- esphome/dashboard/web_server.py | 6 +++--- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 58b995e8df..4b0fc2cec7 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -74,6 +74,8 @@ from esphome.util import ( _LOGGER = logging.getLogger(__name__) +ESPHOME_COMMAND = [sys.executable, "-m", "esphome"] + # Maximum buffer size for serial log reading to prevent unbounded memory growth SERIAL_BUFFER_MAX_SIZE = 65536 @@ -1307,9 +1309,8 @@ def command_update_all(args: ArgsProtocol) -> int | None: files = list_yaml_files(args.configuration) def build_command(f): - if CORE.dashboard: - return ["esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"] - return ["esphome", "run", f, "--no-logs", "--device", "OTA"] + dashboard = ["--dashboard"] if CORE.dashboard else [] + return [*ESPHOME_COMMAND, *dashboard, "run", f, "--no-logs", "--device", "OTA"] return run_multiple_configs(files, build_command) @@ -1458,7 +1459,7 @@ def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None: new_path.write_text(new_raw, encoding="utf-8") - rc = run_external_process("esphome", "config", str(new_path)) + rc = run_external_process(*ESPHOME_COMMAND, "config", str(new_path)) if rc != 0: print(color(AnsiFore.BOLD_RED, "Rename failed. Reverting changes.")) new_path.unlink() @@ -1476,7 +1477,7 @@ def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None: cli_args.insert(0, "--dashboard") try: - rc = run_external_process("esphome", *cli_args) + rc = run_external_process(*ESPHOME_COMMAND, *cli_args) except KeyboardInterrupt: rc = 1 if rc != 0: @@ -1873,7 +1874,7 @@ def run_esphome(argv): # argv[0] is the program path, skip it since we prefix with "esphome" def build_command(f): return ( - ["esphome"] + [*ESPHOME_COMMAND] + [arg for arg in argv[1:] if arg not in args.configuration] + [str(f)] ) diff --git a/esphome/dashboard/const.py b/esphome/dashboard/const.py index ada5575d0e..9cadc442ef 100644 --- a/esphome/dashboard/const.py +++ b/esphome/dashboard/const.py @@ -1,5 +1,7 @@ from __future__ import annotations +import sys + from esphome.enum import StrEnum @@ -26,4 +28,5 @@ MAX_EXECUTOR_WORKERS = 48 SENTINEL = object() -DASHBOARD_COMMAND = ["esphome", "--dashboard"] +ESPHOME_COMMAND = [sys.executable, "-m", "esphome"] +DASHBOARD_COMMAND = [*ESPHOME_COMMAND, "--dashboard"] diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 92cab929ef..b8e17244e5 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -52,7 +52,7 @@ from esphome.util import get_serial_ports, shlex_quote from esphome.yaml_util import FastestAvailableSafeLoader from ..helpers import write_file -from .const import DASHBOARD_COMMAND, DashboardEvent +from .const import DASHBOARD_COMMAND, ESPHOME_COMMAND, DashboardEvent from .core import DASHBOARD, ESPHomeDashboard, Event from .entries import UNKNOWN_STATE, DashboardEntry, entry_state_to_bool from .models import build_device_list_response @@ -1079,7 +1079,7 @@ class DownloadBinaryRequestHandler(BaseHandler): return if not path.is_file(): - args = ["esphome", "idedata", settings.rel_path(configuration)] + args = [*ESPHOME_COMMAND, "idedata", settings.rel_path(configuration)] rc, stdout, _ = await async_run_system_command(args) if rc != 0: @@ -1462,7 +1462,7 @@ class JsonConfigRequestHandler(BaseHandler): self.send_error(404) return - args = ["esphome", "config", str(filename), "--show-secrets"] + args = [*ESPHOME_COMMAND, "config", str(filename), "--show-secrets"] rc, stdout, stderr = await async_run_system_command(args) From bef5e4de9c0a89f319c9f7124814cf7390fb922e Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Wed, 11 Mar 2026 13:29:17 -0500 Subject: [PATCH 537/609] [speaker_source] Add announcement pipeline (#14654) --- .../components/speaker_source/media_player.py | 125 +++++++++++++----- .../speaker_source_media_player.cpp | 74 +++++++++-- .../speaker_source_media_player.h | 27 ++-- tests/components/speaker_source/common.yaml | 21 ++- 4 files changed, 186 insertions(+), 61 deletions(-) diff --git a/esphome/components/speaker_source/media_player.py b/esphome/components/speaker_source/media_player.py index 9080bebcae..fe50536e8f 100644 --- a/esphome/components/speaker_source/media_player.py +++ b/esphome/components/speaker_source/media_player.py @@ -20,6 +20,7 @@ DEPENDENCIES = ["media_source", "speaker"] CODEOWNERS = ["@kahrendt"] +CONF_ANNOUNCEMENT_PIPELINE = "announcement_pipeline" CONF_MEDIA_PIPELINE = "media_pipeline" CONF_ON_MUTE = "on_mute" CONF_PIPELINE = "pipeline" @@ -42,6 +43,19 @@ PipelineContext = speaker_source_ns.struct("PipelineContext") Pipeline = speaker_source_ns.enum("Pipeline") PIPELINE_ENUM = { "media": Pipeline.MEDIA_PIPELINE, + "announcement": Pipeline.ANNOUNCEMENT_PIPELINE, +} + +# Maps config key -> (C++ Pipeline enum value, format purpose) +_PIPELINE_INFO = { + CONF_MEDIA_PIPELINE: ( + Pipeline.MEDIA_PIPELINE, + media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["default"], + ), + CONF_ANNOUNCEMENT_PIPELINE: ( + Pipeline.ANNOUNCEMENT_PIPELINE, + media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["announcement"], + ), } SetPlaylistDelayAction = speaker_source_ns.class_( @@ -59,7 +73,7 @@ FORMAT_MAPPING = { # Returns a media_player.MediaPlayerSupportedFormat struct with the configured # format, sample rate, number of channels, purpose, and bytes per sample -def _get_supported_format_struct(pipeline: ConfigType): +def _get_supported_format_struct(pipeline: ConfigType, purpose: MockObj): args = [ media_player.MediaPlayerSupportedFormat, ] @@ -68,7 +82,7 @@ def _get_supported_format_struct(pipeline: ConfigType): args.append(("sample_rate", pipeline[CONF_SAMPLE_RATE])) args.append(("num_channels", pipeline[CONF_NUM_CHANNELS])) - args.append(("purpose", media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["default"])) + args.append(("purpose", purpose)) # Omit sample_bytes for MP3: ffmpeg transcoding in Home Assistant fails # if the number of bytes per sample is specified for MP3. @@ -115,6 +129,40 @@ PIPELINE_SCHEMA = cv.Schema( ) +def _validate_no_shared_resources(config: ConfigType) -> ConfigType: + announcement_config = config.get(CONF_ANNOUNCEMENT_PIPELINE) + media_config = config.get(CONF_MEDIA_PIPELINE) + + # Check for duplicates within each pipeline + for pipeline_key in (CONF_ANNOUNCEMENT_PIPELINE, CONF_MEDIA_PIPELINE): + if pipeline_config := config.get(pipeline_key): + source_ids = [s.id for s in pipeline_config[CONF_SOURCES]] + if len(source_ids) != len(set(source_ids)): + raise cv.Invalid( + f"Duplicate media sources in {pipeline_key}. " + "Each media source can only appear once per pipeline." + ) + + # Check for sources shared between pipelines + if announcement_config and media_config: + if announcement_config[CONF_SPEAKER] == media_config[CONF_SPEAKER]: + raise cv.Invalid( + "The announcement and media pipelines cannot use the same speaker. " + "Use the `mixer` speaker component to create two source speakers." + ) + + announcement_source_ids = {s.id for s in announcement_config[CONF_SOURCES]} + media_source_ids = {s.id for s in media_config[CONF_SOURCES]} + shared = announcement_source_ids & media_source_ids + if shared: + raise cv.Invalid( + f"Media sources cannot be shared between pipelines: {', '.join(shared)}. " + "Create separate media source instances for each pipeline." + ) + + return config + + def _validate_volume_settings(config: ConfigType) -> ConfigType: # CONF_VOLUME_INITIAL is in the scaled volume domain (0.0-1.0) and doesn't need to be validated if config[CONF_VOLUME_MIN] > config[CONF_VOLUME_MAX]: @@ -131,7 +179,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_VOLUME_INITIAL, default=0.5): cv.percentage, cv.Optional(CONF_VOLUME_MAX, default=1.0): cv.percentage, cv.Optional(CONF_VOLUME_MIN, default=0.0): cv.percentage, - cv.Required(CONF_MEDIA_PIPELINE): PIPELINE_SCHEMA, + cv.Optional(CONF_ANNOUNCEMENT_PIPELINE): PIPELINE_SCHEMA, + cv.Optional(CONF_MEDIA_PIPELINE): PIPELINE_SCHEMA, cv.Optional(CONF_ON_MUTE): automation.validate_automation(single=True), cv.Optional(CONF_ON_UNMUTE): automation.validate_automation(single=True), cv.Optional(CONF_ON_VOLUME): automation.validate_automation(single=True), @@ -140,23 +189,37 @@ CONFIG_SCHEMA = cv.All( .extend(cv.COMPONENT_SCHEMA) .extend(media_player.media_player_schema(SpeakerSourceMediaPlayer)), cv.only_on_esp32, + cv.has_at_least_one_key(CONF_ANNOUNCEMENT_PIPELINE, CONF_MEDIA_PIPELINE), + _validate_no_shared_resources, _validate_volume_settings, ) def _final_validate_codecs(config: ConfigType) -> ConfigType: - pipeline = config[CONF_MEDIA_PIPELINE] - fmt = pipeline[CONF_FORMAT] - if fmt == "NONE": + # "NONE" means the pipeline accepts any format at runtime, so all optional codecs must be available. + # When a specific format is set, only that codec is requested. + needed_formats: set[str] = set() + need_all = False + + for pipeline_key in (CONF_ANNOUNCEMENT_PIPELINE, CONF_MEDIA_PIPELINE): + if pipeline := config.get(pipeline_key): + fmt = pipeline[CONF_FORMAT] + if fmt == "NONE": + need_all = True + else: + needed_formats.add(fmt) + + if need_all: audio.request_flac_support() audio.request_mp3_support() audio.request_opus_support() - elif fmt == "FLAC": - audio.request_flac_support() - elif fmt == "MP3": - audio.request_mp3_support() - elif fmt == "OPUS": - audio.request_opus_support() + else: + if "FLAC" in needed_formats: + audio.request_flac_support() + if "MP3" in needed_formats: + audio.request_mp3_support() + if "OPUS" in needed_formats: + audio.request_opus_support() return config @@ -164,7 +227,8 @@ def _final_validate_codecs(config: ConfigType) -> ConfigType: FINAL_VALIDATE_SCHEMA = cv.All( cv.Schema( { - cv.Required(CONF_MEDIA_PIPELINE): _validate_pipeline, + cv.Optional(CONF_ANNOUNCEMENT_PIPELINE): _validate_pipeline, + cv.Optional(CONF_MEDIA_PIPELINE): _validate_pipeline, }, extra=cv.ALLOW_EXTRA, ), @@ -182,26 +246,25 @@ async def to_code(config: ConfigType) -> None: cg.add(var.set_volume_max(config[CONF_VOLUME_MAX])) cg.add(var.set_volume_min(config[CONF_VOLUME_MIN])) - pipeline_config = config[CONF_MEDIA_PIPELINE] - pipeline_enum = Pipeline.MEDIA_PIPELINE + for pipeline_key, (pipeline_enum, purpose) in _PIPELINE_INFO.items(): + if pipeline_config := config.get(pipeline_key): + for source in pipeline_config[CONF_SOURCES]: + src = await cg.get_variable(source) + cg.add(var.add_media_source(pipeline_enum, src)) - for source in pipeline_config[CONF_SOURCES]: - src = await cg.get_variable(source) - cg.add(var.add_media_source(pipeline_enum, src)) - - cg.add( - var.set_speaker( - pipeline_enum, - await cg.get_variable(pipeline_config[CONF_SPEAKER]), - ) - ) - if pipeline_config[CONF_FORMAT] != "NONE": - cg.add( - var.set_format( - pipeline_enum, - _get_supported_format_struct(pipeline_config), + cg.add( + var.set_speaker( + pipeline_enum, + await cg.get_variable(pipeline_config[CONF_SPEAKER]), + ) ) - ) + if pipeline_config[CONF_FORMAT] != "NONE": + cg.add( + var.set_format( + pipeline_enum, + _get_supported_format_struct(pipeline_config, purpose), + ) + ) if on_mute := config.get(CONF_ON_MUTE): await automation.build_automation( diff --git a/esphome/components/speaker_source/speaker_source_media_player.cpp b/esphome/components/speaker_source/speaker_source_media_player.cpp index cf7169df6a..9d02ac9c74 100644 --- a/esphome/components/speaker_source/speaker_source_media_player.cpp +++ b/esphome/components/speaker_source/speaker_source_media_player.cpp @@ -128,6 +128,7 @@ void SpeakerSourceMediaPlayer::handle_play_uri_request_(uint8_t pipeline, const // Smart source is requesting the player to play a different URI auto call = this->make_call(); call.set_media_url(uri); + call.set_announcement(pipeline == ANNOUNCEMENT_PIPELINE); call.perform(); } @@ -209,7 +210,7 @@ size_t SpeakerSourceMediaPlayer::handle_media_output_(uint8_t pipeline, media_so } // THREAD CONTEXT: Called from main loop (loop) -media_player::MediaPlayerState SpeakerSourceMediaPlayer::get_media_pipeline_state_( +media_player::MediaPlayerState SpeakerSourceMediaPlayer::get_source_state_( media_source::MediaSource *source, bool playlist_active, media_player::MediaPlayerState old_state) const { if (source != nullptr) { switch (source->get_state()) { @@ -218,7 +219,7 @@ media_player::MediaPlayerState SpeakerSourceMediaPlayer::get_media_pipeline_stat case media_source::MediaSourceState::PAUSED: return media_player::MEDIA_PLAYER_STATE_PAUSED; case media_source::MediaSourceState::ERROR: - ESP_LOGE(TAG, "Source error"); + ESP_LOGE(TAG, "Media source error"); return media_player::MEDIA_PLAYER_STATE_IDLE; case media_source::MediaSourceState::IDLE: default: @@ -237,16 +238,47 @@ void SpeakerSourceMediaPlayer::loop() { // Process queued control commands this->process_control_queue_(); - // Update state based on active sources + // Update state based on active sources - announcement pipeline takes priority media_player::MediaPlayerState old_state = this->state; + PipelineContext &ann_ps = this->pipelines_[ANNOUNCEMENT_PIPELINE]; PipelineContext &media_ps = this->pipelines_[MEDIA_PIPELINE]; // Check playlist state to detect transitions between items + bool announcement_playlist_active = (ann_ps.playlist_index < ann_ps.playlist.size()) || + (ann_ps.repeat_mode != REPEAT_OFF && !ann_ps.playlist.empty()); bool media_playlist_active = (media_ps.playlist_index < media_ps.playlist.size()) || (media_ps.repeat_mode != REPEAT_OFF && !media_ps.playlist.empty()); - this->state = this->get_media_pipeline_state_(media_ps.active_source, media_playlist_active, old_state); + // Check announcement pipeline first + media_source::MediaSource *announcement_source = ann_ps.active_source; + if (announcement_source != nullptr) { + media_source::MediaSourceState announcement_state = announcement_source->get_state(); + if (announcement_state != media_source::MediaSourceState::IDLE) { + // Announcement is active - announcements take priority and never report PAUSED + switch (announcement_state) { + case media_source::MediaSourceState::PLAYING: + case media_source::MediaSourceState::PAUSED: // Treat paused announcements as announcing + this->state = media_player::MEDIA_PLAYER_STATE_ANNOUNCING; + break; + case media_source::MediaSourceState::ERROR: + ESP_LOGE(TAG, "Announcement source error"); + // Fall through to media pipeline state + this->state = this->get_source_state_(media_ps.active_source, media_playlist_active, old_state); + break; + default: + break; + } + } else { + // Announcement source is idle, fall through to media pipeline + this->state = this->get_source_state_(media_ps.active_source, media_playlist_active, old_state); + } + } else if (announcement_playlist_active && old_state == media_player::MEDIA_PLAYER_STATE_ANNOUNCING) { + this->state = media_player::MEDIA_PLAYER_STATE_ANNOUNCING; + } else { + // No active announcement, check media pipeline + this->state = this->get_source_state_(media_ps.active_source, media_playlist_active, old_state); + } if (this->state != old_state) { this->publish_state(); @@ -357,7 +389,7 @@ void SpeakerSourceMediaPlayer::set_playlist_delay_ms(uint8_t pipeline, uint32_t // The timeout callback also runs on the main loop. void SpeakerSourceMediaPlayer::queue_play_current_(uint8_t pipeline, uint32_t delay_ms) { if (delay_ms > 0) { - this->set_timeout(PipelineContext::TIMEOUT_IDS[pipeline], delay_ms, + this->set_timeout(PIPELINE_TIMEOUT_IDS[pipeline], delay_ms, [this, pipeline]() { this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); }); } else { this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); @@ -366,7 +398,7 @@ void SpeakerSourceMediaPlayer::queue_play_current_(uint8_t pipeline, uint32_t de // THREAD CONTEXT: Called from main loop (loop) void SpeakerSourceMediaPlayer::process_control_queue_() { - MediaPlayerControlCommand control_command; + MediaPlayerControlCommand control_command{}; // Use peek to check command without removing it if (xQueuePeek(this->media_control_command_queue_, &control_command, 0) != pdTRUE) { @@ -383,7 +415,7 @@ void SpeakerSourceMediaPlayer::process_control_queue_() { switch (control_command.type) { case MediaPlayerControlCommand::PLAY_URI: { // Always use our local playlist to start playback - this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); + this->cancel_timeout(PIPELINE_TIMEOUT_IDS[pipeline]); ps.playlist.clear(); ps.shuffle_indices.clear(); // Clear shuffle when starting fresh playlist ps.playlist_index = 0; // Reset index @@ -528,7 +560,7 @@ void SpeakerSourceMediaPlayer::handle_player_command_(media_player::MediaPlayerC case media_player::MEDIA_PLAYER_COMMAND_STOP: { if (!has_internal_playlist) { - this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); + this->cancel_timeout(PIPELINE_TIMEOUT_IDS[pipeline]); ps.playlist.clear(); ps.shuffle_indices.clear(); ps.playlist_index = 0; @@ -541,7 +573,7 @@ void SpeakerSourceMediaPlayer::handle_player_command_(media_player::MediaPlayerC case media_player::MEDIA_PLAYER_COMMAND_NEXT: { if (!has_internal_playlist) { - this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); + this->cancel_timeout(PIPELINE_TIMEOUT_IDS[pipeline]); if (ps.playlist_index + 1 < ps.playlist.size()) { ps.playlist_index++; this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); @@ -557,7 +589,7 @@ void SpeakerSourceMediaPlayer::handle_player_command_(media_player::MediaPlayerC case media_player::MEDIA_PLAYER_COMMAND_PREVIOUS: { if (!has_internal_playlist) { - this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); + this->cancel_timeout(PIPELINE_TIMEOUT_IDS[pipeline]); if (ps.playlist_index > 0) { ps.playlist_index--; this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); @@ -597,7 +629,7 @@ void SpeakerSourceMediaPlayer::handle_player_command_(media_player::MediaPlayerC case media_player::MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST: { if (!has_internal_playlist) { - this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); + this->cancel_timeout(PIPELINE_TIMEOUT_IDS[pipeline]); if (ps.playlist_index < ps.playlist.size()) { size_t actual_position = this->get_playlist_position_(pipeline); ps.playlist[0] = std::move(ps.playlist[actual_position]); @@ -644,8 +676,24 @@ void SpeakerSourceMediaPlayer::control(const media_player::MediaPlayerCall &call return; } - MediaPlayerControlCommand control_command; - control_command.pipeline = MEDIA_PIPELINE; + MediaPlayerControlCommand control_command{}; + + // Determine which pipeline to use based on announcement flag, falling back if the preferred pipeline + // is not configured + auto announcement = call.get_announcement(); + if (announcement.has_value() && announcement.value()) { + if (this->pipelines_[ANNOUNCEMENT_PIPELINE].is_configured()) { + control_command.pipeline = ANNOUNCEMENT_PIPELINE; + } else { + control_command.pipeline = MEDIA_PIPELINE; + } + } else { + if (this->pipelines_[MEDIA_PIPELINE].is_configured()) { + control_command.pipeline = MEDIA_PIPELINE; + } else { + control_command.pipeline = ANNOUNCEMENT_PIPELINE; + } + } auto media_url = call.get_media_url(); if (media_url.has_value()) { diff --git a/esphome/components/speaker_source/speaker_source_media_player.h b/esphome/components/speaker_source/speaker_source_media_player.h index 4fbb534110..652390edd2 100644 --- a/esphome/components/speaker_source/speaker_source_media_player.h +++ b/esphome/components/speaker_source/speaker_source_media_player.h @@ -28,7 +28,7 @@ namespace esphome::speaker_source { // // - Main loop task: setup(), loop(), dump_config(), handle_media_state_changed_(), // handle_volume_request_(), handle_mute_request_(), handle_play_uri_request_(), -// set_volume_(), set_mute_state_(), control(), get_media_pipeline_state_(), +// set_volume_(), set_mute_state_(), control(), get_source_state_(), // find_source_for_uri_(), try_execute_play_uri_(), save_volume_restore_state_(), // process_control_queue_(), handle_player_command_(), queue_command_(), queue_play_current_() // @@ -55,6 +55,7 @@ namespace esphome::speaker_source { enum Pipeline : uint8_t { MEDIA_PIPELINE = 0, + ANNOUNCEMENT_PIPELINE = 1, }; enum RepeatMode : uint8_t { @@ -85,10 +86,10 @@ struct SourceBinding : public media_source::MediaSourceListener { void request_play_uri(const std::string &uri) override; }; -struct PipelineContext { - /// @brief Timeout IDs for playlist delay, indexed by Pipeline enum - static constexpr const char *const TIMEOUT_IDS[] = {"next_media"}; +/// @brief Timeout IDs for playlist delay, indexed by Pipeline enum +static constexpr uint32_t PIPELINE_TIMEOUT_IDS[] = {1, 2}; +struct PipelineContext { speaker::Speaker *speaker{nullptr}; optional<media_player::MediaPlayerSupportedFormat> format; @@ -132,7 +133,7 @@ struct MediaPlayerControlCommand { SEND_COMMAND, // Send command to active source }; Type type; - uint8_t pipeline; + uint8_t pipeline; // MEDIA_PIPELINE or ANNOUNCEMENT_PIPELINE union { std::string *uri; // Owned pointer, must delete after xQueueReceive (for PLAY_URI and ENQUEUE_URI) @@ -208,14 +209,13 @@ class SpeakerSourceMediaPlayer : public Component, public media_player::MediaPla /// @brief Saves the current volume and mute state to the flash for restoration. void save_volume_restore_state_(); - /// @brief Determine media player state from the media pipeline's active source - /// @param media_source Active source for the media pipeline (may be nullptr) - /// @param playlist_active Whether the media pipeline's playlist is in progress + /// @brief Determine media player state from a pipeline's active source + /// @param media_source Active source (may be nullptr) + /// @param playlist_active Whether the pipeline's playlist is in progress /// @param old_state Previous media player state (used for transition smoothing) /// @return The appropriate MediaPlayerState - media_player::MediaPlayerState get_media_pipeline_state_(media_source::MediaSource *media_source, - bool playlist_active, - media_player::MediaPlayerState old_state) const; + media_player::MediaPlayerState get_source_state_(media_source::MediaSource *media_source, bool playlist_active, + media_player::MediaPlayerState old_state) const; void process_control_queue_(); void handle_player_command_(media_player::MediaPlayerCommand player_command, uint8_t pipeline); @@ -235,8 +235,9 @@ class SpeakerSourceMediaPlayer : public Component, public media_player::MediaPla QueueHandle_t media_control_command_queue_; - // Pipeline context for media pipeline. See THREADING MODEL at top of namespace for access rules. - std::array<PipelineContext, 1> pipelines_; + // Pipeline context for media (index 0) and announcement (index 1) pipelines. + // See THREADING MODEL at top of namespace for access rules. + std::array<PipelineContext, 2> pipelines_; // Used to save volume/mute state for restoration on reboot ESPPreferenceObject pref_; diff --git a/tests/components/speaker_source/common.yaml b/tests/components/speaker_source/common.yaml index 05b5181dfd..7d663b802c 100644 --- a/tests/components/speaker_source/common.yaml +++ b/tests/components/speaker_source/common.yaml @@ -10,6 +10,11 @@ speaker: i2s_dout_pin: ${i2s_dout_pin} sample_rate: 48000 num_channels: 2 + - platform: mixer + output_speaker: speaker_id + source_speakers: + - id: announcement_mixer_speaker_id + - id: media_mixer_speaker_id audio_file: - id: test_audio @@ -19,7 +24,9 @@ audio_file: media_source: - platform: audio_file - id: audio_file_source + id: announcement_audio_file_source + - platform: audio_file + id: media_audio_file_source media_player: - platform: speaker_source @@ -29,12 +36,18 @@ media_player: volume_initial: 0.75 volume_max: 0.95 volume_min: 0.0 - media_pipeline: - speaker: speaker_id + announcement_pipeline: + speaker: announcement_mixer_speaker_id format: FLAC num_channels: 1 sources: - - audio_file_source + - announcement_audio_file_source + media_pipeline: + speaker: media_mixer_speaker_id + format: FLAC + num_channels: 1 + sources: + - media_audio_file_source on_mute: - media_player.shuffle: id: media_player_id From 02f7aee680bb0a7b7a7a54e1b2cd759f8876d77c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 12 Mar 2026 07:34:53 +1300 Subject: [PATCH 538/609] Bump version to 2026.3.0b1 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 1a9e0b4e10..9f5cd0a2ce 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.3.0-dev +PROJECT_NUMBER = 2026.3.0b1 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index d409514f3c..eb49c9a1d7 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.3.0-dev" +__version__ = "2026.3.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 4219d6d3673d8a1e4d8f36ac6ce45b162c898292 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 09:28:19 -1000 Subject: [PATCH 539/609] Bump tornado from 6.5.4 to 6.5.5 (#14704) 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 3da2d52b44..e634bcb104 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ PyYAML==6.0.3 paho-mqtt==1.6.1 colorama==0.4.6 icmplib==3.0.4 -tornado==6.5.4 +tornado==6.5.5 tzlocal==5.3.1 # from time tzdata>=2021.1 # from time pyserial==3.5 From 2627490a11952d55ef282c5b6b57522e6467123e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:30:44 -0400 Subject: [PATCH 540/609] [esp32_hosted] Bump esp_hosted to 2.12.1 (#14708) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/esp32_hosted/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index 6d49053d6d..a51ae2cd66 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -105,7 +105,7 @@ async def to_code(config): if framework_ver >= cv.Version(5, 5, 0): esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.4.0") esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4") - esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.0") + esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.1") else: esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0") esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index acd7f7a479..f7fd3e67bc 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -20,7 +20,7 @@ dependencies: rules: - if: "target in [esp32h2, esp32p4]" espressif/esp_hosted: - version: 2.12.0 + version: 2.12.1 rules: - if: "target in [esp32h2, esp32p4]" zorxx/multipart-parser: From 064bd13ebb6ba91576c7e6c78c23a2986ad5e5d5 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Wed, 11 Mar 2026 19:56:25 -0500 Subject: [PATCH 541/609] [ethernet] ESP32-P4 Ethernet compilation fix (#14714) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .../components/ethernet/ethernet_component.cpp | 18 +----------------- .../components/ethernet/ethernet_component.h | 2 ++ esphome/components/ethernet/ethernet_helpers.c | 8 ++++++++ .../components/ethernet/test.esp32-p4-idf.yaml | 1 + 4 files changed, 12 insertions(+), 17 deletions(-) create mode 100644 esphome/components/ethernet/ethernet_helpers.c create mode 100644 tests/components/ethernet/test.esp32-p4-idf.yaml diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index d6b0d40cd9..e0788e1149 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -21,22 +21,6 @@ namespace esphome::ethernet { -#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) -// work around IDF compile issue on P4 https://github.com/espressif/esp-idf/pull/15637 -#ifdef USE_ESP32_VARIANT_ESP32P4 -#undef ETH_ESP32_EMAC_DEFAULT_CONFIG -#define ETH_ESP32_EMAC_DEFAULT_CONFIG() \ - { \ - .smi_gpio = {.mdc_num = 31, .mdio_num = 52}, .interface = EMAC_DATA_INTERFACE_RMII, \ - .clock_config = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = (emac_rmii_clock_gpio_t) 50}}, \ - .dma_burst_len = ETH_DMA_BURST_LEN_32, .intr_priority = 0, \ - .emac_dataif_gpio = \ - {.rmii = {.tx_en_num = 49, .txd0_num = 34, .txd1_num = 35, .crs_dv_num = 28, .rxd0_num = 29, .rxd1_num = 30}}, \ - .clock_config_out_in = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = (emac_rmii_clock_gpio_t) -1}}, \ - } -#endif -#endif - static const char *const TAG = "ethernet"; // PHY register size for hex logging @@ -162,7 +146,7 @@ void EthernetComponent::setup() { phy_config.phy_addr = this->phy_addr_; phy_config.reset_gpio_num = this->power_pin_; - eth_esp32_emac_config_t esp32_emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG(); + eth_esp32_emac_config_t esp32_emac_config = eth_esp32_emac_default_config(); #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) esp32_emac_config.smi_gpio.mdc_num = this->mdc_pin_; esp32_emac_config.smi_gpio.mdio_num = this->mdio_pin_; diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index d9f05be9de..c464e20b84 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -15,6 +15,8 @@ #include "esp_mac.h" #include "esp_idf_version.h" +extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void); + namespace esphome::ethernet { #ifdef USE_ETHERNET_IP_STATE_LISTENERS diff --git a/esphome/components/ethernet/ethernet_helpers.c b/esphome/components/ethernet/ethernet_helpers.c new file mode 100644 index 0000000000..96faccad24 --- /dev/null +++ b/esphome/components/ethernet/ethernet_helpers.c @@ -0,0 +1,8 @@ +#include "esp_eth_mac_esp.h" + +// ETH_ESP32_EMAC_DEFAULT_CONFIG() uses out-of-order designated initializers +// which are valid in C but not in C++. This wrapper allows C++ code to get +// the default config without replicating the macro's contents. +eth_esp32_emac_config_t eth_esp32_emac_default_config(void) { + return (eth_esp32_emac_config_t) ETH_ESP32_EMAC_DEFAULT_CONFIG(); +} diff --git a/tests/components/ethernet/test.esp32-p4-idf.yaml b/tests/components/ethernet/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..e52329d7ea --- /dev/null +++ b/tests/components/ethernet/test.esp32-p4-idf.yaml @@ -0,0 +1 @@ +<<: !include common-ip101.yaml From 2b0c471ed7b08e422fe48fd7153c22cfd8f1a23a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 11 Mar 2026 15:00:20 -1000 Subject: [PATCH 542/609] [esp32] Add crash handler to capture and report backtrace across reboots (#14709) --- esphome/components/api/api_connection.h | 6 + esphome/components/api/client.py | 21 ++ esphome/components/esp32/__init__.py | 5 + esphome/components/esp32/core.cpp | 6 + esphome/components/esp32/crash_handler.cpp | 355 +++++++++++++++++++++ esphome/components/esp32/crash_handler.h | 18 ++ esphome/components/logger/logger_esp32.cpp | 4 + esphome/core/defines.h | 1 + esphome/platformio_api.py | 7 + tests/unit_tests/test_platformio_api.py | 28 ++ 10 files changed, 451 insertions(+) create mode 100644 esphome/components/esp32/crash_handler.cpp create mode 100644 esphome/components/esp32/crash_handler.h diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 3356511684..60cc3e91b1 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -14,6 +14,9 @@ #include "api_server.h" #include "esphome/core/application.h" #include "esphome/core/component.h" +#ifdef USE_ESP32_CRASH_HANDLER +#include "esphome/components/esp32/crash_handler.h" +#endif #include "esphome/core/entity_base.h" #include "esphome/core/string_ref.h" @@ -235,6 +238,9 @@ class APIConnection final : public APIServerConnectionBase { this->flags_.log_subscription = msg.level; if (msg.dump_config) App.schedule_dump_config(); +#ifdef USE_ESP32_CRASH_HANDLER + esp32::crash_handler_log(); +#endif } #ifdef USE_API_HOMEASSISTANT_SERVICES void on_subscribe_homeassistant_services_request() override { this->flags_.service_call_subscription = true; } diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 200d0938bd..0e71ad8fcb 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio from datetime import datetime +import importlib import logging from typing import TYPE_CHECKING, Any import warnings @@ -18,6 +19,7 @@ import contextlib from esphome.const import CONF_KEY, CONF_PORT, __version__ from esphome.core import CORE +from esphome.platformio_api import process_stacktrace from . import CONF_ENCRYPTION @@ -55,9 +57,19 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: addresses=addresses, # Pass all addresses for automatic retry ) dashboard = CORE.dashboard + backtrace_state = False + + # Try platform-specific stacktrace handler first, fall back to generic + platform_process_stacktrace = None + try: + module = importlib.import_module("esphome.components." + CORE.target_platform) + platform_process_stacktrace = getattr(module, "process_stacktrace") + except (AttributeError, ImportError): + pass def on_log(msg: SubscribeLogsResponse) -> None: """Handle a new log message.""" + nonlocal backtrace_state time_ = datetime.now() message: bytes = msg.message text = message.decode("utf8", "backslashreplace") @@ -67,6 +79,15 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: ) for parsed_msg in parse_log_message(text, timestamp): print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg) + for raw_line in text.splitlines(): + if platform_process_stacktrace: + backtrace_state = platform_process_stacktrace( + config, raw_line, backtrace_state + ) + else: + backtrace_state = process_stacktrace( + config, raw_line, backtrace_state=backtrace_state + ) stop = await async_run(cli, on_log, name=name) try: diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 52e70501dc..475de6aa3e 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1442,6 +1442,11 @@ async def to_code(config): cg.add_build_flag("-DUSE_ESP32") cg.add_define("USE_NATIVE_64BIT_TIME") cg.add_build_flag("-Wl,-z,noexecstack") + # Arduino already wraps esp_panic_handler for its own backtrace handler, + # so only add our wrap when using ESP-IDF framework to avoid linker conflicts. + if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: + cg.add_build_flag("-Wl,--wrap=esp_panic_handler") + cg.add_define("USE_ESP32_CRASH_HANDLER") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) variant = config[CONF_VARIANT] cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{variant}") diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 46c000562e..cba25bca2b 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -1,6 +1,7 @@ #ifdef USE_ESP32 #include "esphome/core/defines.h" +#include "crash_handler.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -36,6 +37,11 @@ void arch_restart() { } void arch_init() { +#ifdef USE_ESP32_CRASH_HANDLER + // Read crash data from previous boot before anything else + esp32::crash_handler_read_and_clear(); +#endif + // Enable the task watchdog only on the loop task (from which we're currently running) esp_task_wdt_add(nullptr); diff --git a/esphome/components/esp32/crash_handler.cpp b/esphome/components/esp32/crash_handler.cpp new file mode 100644 index 0000000000..ecf30d7878 --- /dev/null +++ b/esphome/components/esp32/crash_handler.cpp @@ -0,0 +1,355 @@ +#ifdef USE_ESP32 + +#include "esphome/core/defines.h" +#ifdef USE_ESP32_CRASH_HANDLER + +#include "crash_handler.h" +#include "esphome/core/log.h" + +#include <cinttypes> +#include <cstring> +#include <esp_attr.h> +#include <esp_private/panic_internal.h> +#include <soc/soc.h> + +#if CONFIG_IDF_TARGET_ARCH_XTENSA +#include <esp_cpu_utils.h> +#include <esp_debug_helpers.h> +#include <xtensa_context.h> +#elif CONFIG_IDF_TARGET_ARCH_RISCV +#include <riscv/rvruntime-frames.h> +#endif + +static constexpr uint32_t CRASH_MAGIC = 0xDEADBEEF; +static constexpr size_t MAX_BACKTRACE = 16; + +// Check if an address looks like code (flash-mapped or IRAM). +// Must be safe to call from panic context (no flash access needed). +static inline bool IRAM_ATTR is_code_addr(uint32_t addr) { + return (addr >= SOC_IROM_LOW && addr < SOC_IROM_HIGH) || (addr >= SOC_IRAM_LOW && addr < SOC_IRAM_HIGH); +} + +#if CONFIG_IDF_TARGET_ARCH_RISCV +// Check if a code address is a real return address by verifying the preceding +// instruction is a JAL or JALR with rd=ra (x1). Called at log time (not during +// panic) so flash cache is available and both IRAM and IROM are safely readable. +static inline bool is_return_addr(uint32_t addr) { + if (!is_code_addr(addr) || addr < 4) + return false; + // A return address on the stack points to the instruction after a call. + // Check for 4-byte JAL/JALR call instruction before this address. + // Use memcpy for alignment safety — RISC-V C extension means code addresses + // are only 2-byte aligned, so addr-4 may not be 4-byte aligned. + uint32_t inst; + memcpy(&inst, (const void *) (addr - 4), sizeof(inst)); + // RISC-V instruction encoding: bits [6:0] = opcode, bits [11:7] = rd + uint32_t opcode = inst & 0x7f; // Extract 7-bit opcode + uint32_t rd = inst & 0xf80; // Extract rd field (bits 11:7) + // Match JAL (0x6f) or JALR (0x67) with rd=ra (x1, encoded as 0x80 = 1<<7) + if ((opcode == 0x6f || opcode == 0x67) && rd == 0x80) + return true; + // Check for 2-byte compressed c.jalr before this address (C extension). + // c.jalr saves to ra implicitly: funct4=1001, rs1!=0, rs2=0, op=10 + if (addr >= 2) { + uint16_t c_inst = *(uint16_t *) (addr - 2); + if ((c_inst & 0xf07f) == 0x9002 && (c_inst & 0x0f80) != 0) + return true; + } + return false; +} +#endif + +// Raw crash data written by the panic handler wrapper. +// Lives in .noinit so it survives software reset but contains garbage after power cycle. +// Validated by magic marker. Static linkage since it's only used within this file. +// Version field is first so future firmware can always identify the struct layout. +// Magic is second to validate the data. Remaining fields can change between versions. +// Version is uint32_t because it would be padded to 4 bytes anyway before the next +// uint32_t field, so we use the full width rather than wasting 3 bytes of padding. +static constexpr uint32_t CRASH_DATA_VERSION = 1; +struct RawCrashData { + uint32_t version; + uint32_t magic; + uint32_t pc; + uint8_t backtrace_count; + uint8_t reg_frame_count; // Number of entries from registers (not stack-scanned) + uint8_t exception; // panic_exception_t enum (FAULT/ABORT/IWDT/TWDT/DEBUG) + uint8_t pseudo_excause; // Whether cause is a pseudo exception (Xtensa SoC-level panic) + uint32_t backtrace[MAX_BACKTRACE]; + uint32_t cause; // Architecture-specific: exccause (Xtensa) or mcause (RISC-V) +}; +static RawCrashData __attribute__((section(".noinit"))) +s_raw_crash_data; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +// Whether crash data was found and validated this boot. +static bool s_crash_data_valid = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +namespace esphome::esp32 { + +static const char *const TAG = "esp32.crash"; + +void crash_handler_read_and_clear() { + if (s_raw_crash_data.magic == CRASH_MAGIC && s_raw_crash_data.version == CRASH_DATA_VERSION) { + s_crash_data_valid = true; + // Clamp counts to prevent out-of-bounds reads from corrupt .noinit data + if (s_raw_crash_data.backtrace_count > MAX_BACKTRACE) + s_raw_crash_data.backtrace_count = MAX_BACKTRACE; + if (s_raw_crash_data.reg_frame_count > s_raw_crash_data.backtrace_count) + s_raw_crash_data.reg_frame_count = s_raw_crash_data.backtrace_count; + if (s_raw_crash_data.exception > 4) // panic_exception_t max value + s_raw_crash_data.exception = 4; // Default to PANIC_EXCEPTION_FAULT + if (s_raw_crash_data.pseudo_excause > 1) + s_raw_crash_data.pseudo_excause = 0; + } + // Clear magic regardless so we don't re-report on next normal reboot + s_raw_crash_data.magic = 0; +} + +bool crash_handler_has_data() { return s_crash_data_valid; } + +// Look up the exception cause as a human-readable string. +// Tables mirror ESP-IDF's panic_arch_fill_info() which uses local static arrays +// not exposed via any public API. +static const char *get_exception_reason() { +#if CONFIG_IDF_TARGET_ARCH_XTENSA + if (s_raw_crash_data.pseudo_excause) { + // SoC-level panic: watchdog, cache error, etc. + // Keep in sync with ESP-IDF's PANIC_RSN_* defines + static const char *const PSEUDO_REASON[] = { + "Unknown reason", // 0 + "Unhandled debug exception", // 1 + "Double exception", // 2 + "Unhandled kernel exception", // 3 + "Coprocessor exception", // 4 + "Interrupt wdt timeout on CPU0", // 5 + "Interrupt wdt timeout on CPU1", // 6 + "Cache error", // 7 + }; + uint32_t cause = s_raw_crash_data.cause; + if (cause < sizeof(PSEUDO_REASON) / sizeof(PSEUDO_REASON[0])) + return PSEUDO_REASON[cause]; + return PSEUDO_REASON[0]; + } + // Real Xtensa exception + static const char *const REASON[] = { + "IllegalInstruction", + "Syscall", + "InstructionFetchError", + "LoadStoreError", + "Level1Interrupt", + "Alloca", + "IntegerDivideByZero", + "PCValue", + "Privileged", + "LoadStoreAlignment", + nullptr, + nullptr, + "InstrPDAddrError", + "LoadStorePIFDataError", + "InstrPIFAddrError", + "LoadStorePIFAddrError", + "InstTLBMiss", + "InstTLBMultiHit", + "InstFetchPrivilege", + nullptr, + "InstrFetchProhibited", + nullptr, + nullptr, + nullptr, + "LoadStoreTLBMiss", + "LoadStoreTLBMultihit", + "LoadStorePrivilege", + nullptr, + "LoadProhibited", + "StoreProhibited", + }; + uint32_t cause = s_raw_crash_data.cause; + if (cause < sizeof(REASON) / sizeof(REASON[0]) && REASON[cause] != nullptr) + return REASON[cause]; +#elif CONFIG_IDF_TARGET_ARCH_RISCV + // For SoC-level panics (watchdog, cache error), mcause holds IDF-internal + // interrupt numbers, not standard RISC-V cause codes. The exception type + // field already identifies these, so just return null to use the type name. + if (s_raw_crash_data.pseudo_excause) + return nullptr; + static const char *const REASON[] = { + "Instruction address misaligned", + "Instruction access fault", + "Illegal instruction", + "Breakpoint", + "Load address misaligned", + "Load access fault", + "Store address misaligned", + "Store access fault", + "Environment call from U-mode", + "Environment call from S-mode", + nullptr, + "Environment call from M-mode", + "Instruction page fault", + "Load page fault", + nullptr, + "Store page fault", + }; + uint32_t cause = s_raw_crash_data.cause; + if (cause < sizeof(REASON) / sizeof(REASON[0]) && REASON[cause] != nullptr) + return REASON[cause]; +#endif + return "Unknown"; +} + +// Exception type names matching panic_exception_t enum +static const char *get_exception_type() { + static const char *const TYPES[] = { + "Debug exception", // PANIC_EXCEPTION_DEBUG + "Interrupt wdt", // PANIC_EXCEPTION_IWDT + "Task wdt", // PANIC_EXCEPTION_TWDT + "Abort", // PANIC_EXCEPTION_ABORT + "Fault", // PANIC_EXCEPTION_FAULT + }; + uint8_t exc = s_raw_crash_data.exception; + if (exc < sizeof(TYPES) / sizeof(TYPES[0])) + return TYPES[exc]; + return "Unknown"; +} + +// Intentionally uses separate ESP_LOGE calls per line instead of combining into +// one multi-line log message. This ensures each address appears as its own line +// on the serial console, making it possible to see partial output if the device +// crashes again during boot, and allowing the CLI's process_stacktrace to match +// and decode each address individually. +void crash_handler_log() { + if (!s_crash_data_valid) + return; + + ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***"); + const char *reason = get_exception_reason(); + if (reason != nullptr) { + ESP_LOGE(TAG, " Reason: %s - %s", get_exception_type(), reason); + } else { + ESP_LOGE(TAG, " Reason: %s", get_exception_type()); + } + ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_raw_crash_data.pc); + uint8_t bt_num = 0; + for (uint8_t i = 0; i < s_raw_crash_data.backtrace_count; i++) { + uint32_t addr = s_raw_crash_data.backtrace[i]; +#if CONFIG_IDF_TARGET_ARCH_RISCV + // Register-sourced entries (MEPC/RA) are trusted; only filter stack-scanned ones. + if (i >= s_raw_crash_data.reg_frame_count && !is_return_addr(addr)) + continue; +#endif +#if CONFIG_IDF_TARGET_ARCH_RISCV + const char *source = (i < s_raw_crash_data.reg_frame_count) ? "backtrace" : "stack scan"; +#else + const char *source = "backtrace"; +#endif + ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (%s)", bt_num++, addr, source); + } + // Build addr2line hint with all captured addresses for easy copy-paste + char hint[256]; + int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e firmware.elf 0x%08" PRIX32, s_raw_crash_data.pc); + for (uint8_t i = 0; i < s_raw_crash_data.backtrace_count && pos < (int) sizeof(hint) - 12; i++) { + uint32_t addr = s_raw_crash_data.backtrace[i]; +#if CONFIG_IDF_TARGET_ARCH_RISCV + if (i >= s_raw_crash_data.reg_frame_count && !is_return_addr(addr)) + continue; +#endif + pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, addr); + } + ESP_LOGE(TAG, "%s", hint); +} + +} // namespace esphome::esp32 + +// --- Panic handler wrapper --- +// Intercepts esp_panic_handler() via --wrap linker flag to capture crash data +// into NOINIT memory before the normal panic handler runs. +// +extern "C" { +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +// Names are mandated by the --wrap linker mechanism +extern void __real_esp_panic_handler(panic_info_t *info); + +void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { + // Save the faulting PC and exception info + s_raw_crash_data.pc = (uint32_t) info->addr; + s_raw_crash_data.backtrace_count = 0; + s_raw_crash_data.reg_frame_count = 0; + s_raw_crash_data.exception = (uint8_t) info->exception; + s_raw_crash_data.pseudo_excause = info->pseudo_excause ? 1 : 0; + +#if CONFIG_IDF_TARGET_ARCH_XTENSA + // Xtensa: walk the backtrace using the public API + if (info->frame != nullptr) { + auto *xt_frame = (XtExcFrame *) info->frame; + s_raw_crash_data.cause = xt_frame->exccause; + esp_backtrace_frame_t bt_frame = { + .pc = (uint32_t) xt_frame->pc, + .sp = (uint32_t) xt_frame->a1, + .next_pc = (uint32_t) xt_frame->a0, + .exc_frame = xt_frame, + }; + + uint8_t count = 0; + // First frame PC + uint32_t first_pc = esp_cpu_process_stack_pc(bt_frame.pc); + if (is_code_addr(first_pc)) { + s_raw_crash_data.backtrace[count++] = first_pc; + } + // Walk remaining frames + while (count < MAX_BACKTRACE && bt_frame.next_pc != 0) { + if (!esp_backtrace_get_next_frame(&bt_frame)) { + break; + } + uint32_t pc = esp_cpu_process_stack_pc(bt_frame.pc); + if (is_code_addr(pc)) { + s_raw_crash_data.backtrace[count++] = pc; + } + } + s_raw_crash_data.backtrace_count = count; + } + +#elif CONFIG_IDF_TARGET_ARCH_RISCV + // RISC-V: capture MEPC + RA, then scan stack for code addresses + if (info->frame != nullptr) { + auto *rv_frame = (RvExcFrame *) info->frame; + s_raw_crash_data.cause = rv_frame->mcause; + uint8_t count = 0; + + // Save MEPC (fault PC) and RA (return address) + if (is_code_addr(rv_frame->mepc)) { + s_raw_crash_data.backtrace[count++] = rv_frame->mepc; + } + if (is_code_addr(rv_frame->ra) && rv_frame->ra != rv_frame->mepc) { + s_raw_crash_data.backtrace[count++] = rv_frame->ra; + } + + // Track how many entries came from registers (MEPC/RA) so we can + // skip return-address validation for them at log time. + s_raw_crash_data.reg_frame_count = count; + + // Scan stack for code addresses — captures broadly during panic, + // filtered by is_return_addr() at log time when flash is accessible. + auto *scan_start = (uint32_t *) rv_frame->sp; + for (uint32_t i = 0; i < 64 && count < MAX_BACKTRACE; i++) { + uint32_t val = scan_start[i]; + if (is_code_addr(val) && val != rv_frame->mepc && val != rv_frame->ra) { + s_raw_crash_data.backtrace[count++] = val; + } + } + s_raw_crash_data.backtrace_count = count; + } +#endif + + // Write version and magic last — ensures all data is written before we mark it valid + s_raw_crash_data.version = CRASH_DATA_VERSION; + s_raw_crash_data.magic = CRASH_MAGIC; + + // Call the real panic handler (prints to UART, does core dump, reboots, etc.) + __real_esp_panic_handler(info); +} + +// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +} // extern "C" + +#endif // USE_ESP32_CRASH_HANDLER +#endif // USE_ESP32 diff --git a/esphome/components/esp32/crash_handler.h b/esphome/components/esp32/crash_handler.h new file mode 100644 index 0000000000..97a4d4e116 --- /dev/null +++ b/esphome/components/esp32/crash_handler.h @@ -0,0 +1,18 @@ +#pragma once + +#ifdef USE_ESP32_CRASH_HANDLER + +namespace esphome::esp32 { + +/// Read crash data from NOINIT memory and clear the magic marker. +void crash_handler_read_and_clear(); + +/// Log crash data if a crash was detected on previous boot. +void crash_handler_log(); + +/// Returns true if crash data was found this boot. +bool crash_handler_has_data(); + +} // namespace esphome::esp32 + +#endif // USE_ESP32_CRASH_HANDLER diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index d6ad77ff4f..f5bf782289 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -1,6 +1,7 @@ #ifdef USE_ESP32 #include "logger.h" +#include "esphome/components/esp32/crash_handler.h" #include <esp_log.h> #include <driver/uart.h> @@ -117,6 +118,9 @@ void Logger::pre_setup() { esp_log_set_vprintf(esp_idf_log_vprintf_); ESP_LOGI(TAG, "Log initialized"); +#ifdef USE_ESP32_CRASH_HANDLER + esp32::crash_handler_log(); +#endif } void HOT Logger::write_msg_(const char *msg, uint16_t len) { diff --git a/esphome/core/defines.h b/esphome/core/defines.h index cec77fe2e2..a33f10cb9c 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -195,6 +195,7 @@ // ESP32-specific feature flags #ifdef USE_ESP32 +#define USE_ESP32_CRASH_HANDLER #define USE_MQTT_IDF_ENQUEUE #define USE_ESPHOME_TASK_LOG_BUFFER #define USE_OTA_ROLLBACK diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index 5d4065207f..cb080b2a95 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -340,6 +340,8 @@ STACKTRACE_ESP32_BACKTRACE_RE = re.compile( r"Backtrace:(?:\s*0x[0-9a-fA-F]{8}:0x[0-9a-fA-F]{8})+" ) STACKTRACE_ESP32_BACKTRACE_PC_RE = re.compile(r"4[0-9a-f]{7}") +# ESP32 crash handler (stored backtrace from previous boot) +STACKTRACE_ESP32_CRASH_BT_RE = re.compile(r"BT\d+:\s*0x([0-9a-fA-F]{8})") STACKTRACE_ESP8266_BACKTRACE_PC_RE = re.compile(r"4[0-9a-f]{7}") @@ -371,6 +373,11 @@ def process_stacktrace(config, line, backtrace_state): ) _decode_pc(config, match.group(1)) + # ESP32 crash handler backtrace (from previous boot) + match = re.search(STACKTRACE_ESP32_CRASH_BT_RE, line) + if match is not None: + _decode_pc(config, match.group(1)) + # ESP32 single-line backtrace match = re.match(STACKTRACE_ESP32_BACKTRACE_RE, line) if match is not None: diff --git a/tests/unit_tests/test_platformio_api.py b/tests/unit_tests/test_platformio_api.py index 1686144277..e1b3908c24 100644 --- a/tests/unit_tests/test_platformio_api.py +++ b/tests/unit_tests/test_platformio_api.py @@ -673,6 +673,34 @@ def test_process_stacktrace_bad_alloc( assert state is False +def test_process_stacktrace_esp32_crash_handler( + setup_core: Path, mock_decode_pc: Mock +) -> None: + """Test process_stacktrace handles ESP32 crash handler backtrace lines.""" + config = {"name": "test"} + + # Simulate crash handler log lines as they appear from the API/serial + line_pc = "[E][esp32.crash:078]: PC: 0x400D1234 (fault location)" + state = platformio_api.process_stacktrace(config, line_pc, False) + # PC line is matched by existing STACKTRACE_ESP32_PC_RE + mock_decode_pc.assert_called_with(config, "400D1234") + assert state is False + + mock_decode_pc.reset_mock() + + line_bt0 = "[E][esp32.crash:080]: BT0: 0x400D5678 (backtrace)" + state = platformio_api.process_stacktrace(config, line_bt0, False) + mock_decode_pc.assert_called_once_with(config, "400D5678") + assert state is False + + mock_decode_pc.reset_mock() + + line_bt1 = "[E][esp32.crash:080]: BT1: 0x42005ABC (backtrace)" + state = platformio_api.process_stacktrace(config, line_bt1, False) + mock_decode_pc.assert_called_once_with(config, "42005ABC") + assert state is False + + def test_patch_file_downloader_succeeds_first_try() -> None: """Test patch_file_downloader succeeds on first attempt.""" mock_exception_cls = type("PackageException", (Exception,), {}) From 7cec2d3029ae4d2f873633c060b49ade4f77eadd Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Wed, 11 Mar 2026 23:48:27 -0500 Subject: [PATCH 543/609] [ethernet] ESP32-S3 Ethernet compilation fix (#14717) --- esphome/components/ethernet/ethernet_component.h | 3 +++ esphome/components/ethernet/ethernet_helpers.c | 2 ++ tests/components/ethernet/common-w5500.yaml | 4 ++-- tests/components/ethernet/test.esp32-s3-idf.yaml | 1 + 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 tests/components/ethernet/test.esp32-s3-idf.yaml diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index c464e20b84..f7a0996fb7 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -11,11 +11,14 @@ #include "esp_eth.h" #include "esp_eth_mac.h" +#include "esp_eth_mac_esp.h" #include "esp_netif.h" #include "esp_mac.h" #include "esp_idf_version.h" +#if CONFIG_ETH_USE_ESP32_EMAC extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void); +#endif namespace esphome::ethernet { diff --git a/esphome/components/ethernet/ethernet_helpers.c b/esphome/components/ethernet/ethernet_helpers.c index 96faccad24..963db3ff1c 100644 --- a/esphome/components/ethernet/ethernet_helpers.c +++ b/esphome/components/ethernet/ethernet_helpers.c @@ -3,6 +3,8 @@ // ETH_ESP32_EMAC_DEFAULT_CONFIG() uses out-of-order designated initializers // which are valid in C but not in C++. This wrapper allows C++ code to get // the default config without replicating the macro's contents. +#if CONFIG_ETH_USE_ESP32_EMAC eth_esp32_emac_config_t eth_esp32_emac_default_config(void) { return (eth_esp32_emac_config_t) ETH_ESP32_EMAC_DEFAULT_CONFIG(); } +#endif diff --git a/tests/components/ethernet/common-w5500.yaml b/tests/components/ethernet/common-w5500.yaml index 1f8b8650dd..bf3f6f3f0c 100644 --- a/tests/components/ethernet/common-w5500.yaml +++ b/tests/components/ethernet/common-w5500.yaml @@ -2,10 +2,10 @@ ethernet: type: W5500 clk_pin: 19 mosi_pin: 21 - miso_pin: 23 + miso_pin: 17 cs_pin: 18 interrupt_pin: 36 - reset_pin: 22 + reset_pin: 12 clock_speed: 10Mhz manual_ip: static_ip: 192.168.178.56 diff --git a/tests/components/ethernet/test.esp32-s3-idf.yaml b/tests/components/ethernet/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..36f1b5365f --- /dev/null +++ b/tests/components/ethernet/test.esp32-s3-idf.yaml @@ -0,0 +1 @@ +<<: !include common-w5500.yaml From e8f51fec889ada351d35311c2b3ce1ed7239d35e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 11 Mar 2026 19:23:01 -1000 Subject: [PATCH 544/609] [rp2040] Fix crash handler design flaws (#14716) --- esphome/components/api/api_connection.h | 6 ++++++ esphome/components/logger/logger_rp2040.cpp | 5 +++++ esphome/components/rp2040/__init__.py | 1 + esphome/components/rp2040/core.cpp | 6 +++++- esphome/components/rp2040/crash_handler.cpp | 23 ++++++++++++++++----- esphome/components/rp2040/crash_handler.h | 8 ++++++- esphome/core/defines.h | 1 + 7 files changed, 43 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 60cc3e91b1..68f698d190 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -17,6 +17,9 @@ #ifdef USE_ESP32_CRASH_HANDLER #include "esphome/components/esp32/crash_handler.h" #endif +#ifdef USE_RP2040_CRASH_HANDLER +#include "esphome/components/rp2040/crash_handler.h" +#endif #include "esphome/core/entity_base.h" #include "esphome/core/string_ref.h" @@ -240,6 +243,9 @@ class APIConnection final : public APIServerConnectionBase { App.schedule_dump_config(); #ifdef USE_ESP32_CRASH_HANDLER esp32::crash_handler_log(); +#endif +#ifdef USE_RP2040_CRASH_HANDLER + rp2040::crash_handler_log(); #endif } #ifdef USE_API_HOMEASSISTANT_SERVICES diff --git a/esphome/components/logger/logger_rp2040.cpp b/esphome/components/logger/logger_rp2040.cpp index f76b823a8f..b7225c2a25 100644 --- a/esphome/components/logger/logger_rp2040.cpp +++ b/esphome/components/logger/logger_rp2040.cpp @@ -1,6 +1,9 @@ #ifdef USE_RP2040 #include "logger.h" +#include "esphome/core/defines.h" +#ifdef USE_RP2040_CRASH_HANDLER #include "esphome/components/rp2040/crash_handler.h" +#endif #include "esphome/core/log.h" namespace esphome::logger { @@ -26,7 +29,9 @@ void Logger::pre_setup() { } global_logger = this; ESP_LOGI(TAG, "Log initialized"); +#ifdef USE_RP2040_CRASH_HANDLER rp2040::crash_handler_log(); +#endif } void HOT Logger::write_msg_(const char *msg, uint16_t len) { diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index b15811241c..276187b273 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -212,6 +212,7 @@ async def to_code(config): ) cg.add_define("USE_RP2040_WATCHDOG_TIMEOUT", config[CONF_WATCHDOG_TIMEOUT]) + cg.add_define("USE_RP2040_CRASH_HANDLER") def add_pio_file(component: str, key: str, data: str): diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index 5e5a96c78b..7079cbca15 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -1,8 +1,10 @@ #ifdef USE_RP2040 #include "core.h" -#include "crash_handler.h" #include "esphome/core/defines.h" +#ifdef USE_RP2040_CRASH_HANDLER +#include "crash_handler.h" +#endif #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -25,7 +27,9 @@ void arch_restart() { } void arch_init() { +#ifdef USE_RP2040_CRASH_HANDLER rp2040::crash_handler_read_and_clear(); +#endif #if USE_RP2040_WATCHDOG_TIMEOUT > 0 watchdog_enable(USE_RP2040_WATCHDOG_TIMEOUT, false); #endif diff --git a/esphome/components/rp2040/crash_handler.cpp b/esphome/components/rp2040/crash_handler.cpp index 6ab46da444..1f579c2d18 100644 --- a/esphome/components/rp2040/crash_handler.cpp +++ b/esphome/components/rp2040/crash_handler.cpp @@ -1,5 +1,8 @@ #ifdef USE_RP2040 +#include "esphome/core/defines.h" +#ifdef USE_RP2040_CRASH_HANDLER + #include "crash_handler.h" #include "esphome/core/log.h" @@ -13,13 +16,19 @@ static constexpr uint32_t EF_LR = 5; static constexpr uint32_t EF_PC = 6; -static constexpr uint32_t CRASH_MAGIC = 0xDEADBEEF; +// Version encoded in the magic value: upper 16 bits are sentinel (0xDEAD), +// lower 16 bits are the version number. This avoids using a separate scratch +// register for versioning (we only have 8 total). Future firmware reads the +// sentinel to confirm it's crash data, then the version to know the layout. +static constexpr uint32_t CRASH_MAGIC_SENTINEL = 0xDEAD0000; +static constexpr uint32_t CRASH_DATA_VERSION = 1; +static constexpr uint32_t CRASH_MAGIC_V1 = CRASH_MAGIC_SENTINEL | CRASH_DATA_VERSION; // We only have 8 scratch registers (32 bytes) that survive watchdog reboot. // Use them for the most important data, then scan the stack for code addresses. // // Scratch register layout: -// [0] = magic (CRASH_MAGIC) +// [0] = versioned magic (upper 16 bits = 0xDEAD sentinel, lower 16 bits = version) // [1] = PC (program counter at fault) // [2] = LR (link register from exception frame) // [3] = SP (stack pointer at fault) @@ -57,9 +66,12 @@ static struct { uint8_t backtrace_count; } __attribute__((section(".noinit"))) s_crash_data; +bool crash_handler_has_data() { return s_crash_data.valid; } + void crash_handler_read_and_clear() { s_crash_data.valid = false; - if (watchdog_hw->scratch[0] == CRASH_MAGIC) { + uint32_t magic = watchdog_hw->scratch[0]; + if ((magic & 0xFFFF0000) == CRASH_MAGIC_SENTINEL && (magic & 0xFFFF) == CRASH_DATA_VERSION) { s_crash_data.valid = true; s_crash_data.pc = watchdog_hw->scratch[1]; s_crash_data.lr = watchdog_hw->scratch[2]; @@ -135,7 +147,7 @@ static void __attribute__((used, noreturn)) hard_fault_handler_c(uint32_t *frame // by a stacking error or corrupted SP, frame may be invalid. Write a minimal // crash marker so we at least know a crash occurred. if (!is_valid_sram_ptr(frame)) { - watchdog_hw->scratch[0] = CRASH_MAGIC; + watchdog_hw->scratch[0] = CRASH_MAGIC_V1; watchdog_hw->scratch[1] = 0; // PC unknown watchdog_hw->scratch[2] = 0; // LR unknown watchdog_hw->scratch[3] = reinterpret_cast<uintptr_t>(frame); // Record the bad SP for diagnosis @@ -157,7 +169,7 @@ static void __attribute__((used, noreturn)) hard_fault_handler_c(uint32_t *frame uint32_t pre_fault_sp = reinterpret_cast<uintptr_t>(post_frame); // Write key registers - watchdog_hw->scratch[0] = CRASH_MAGIC; + watchdog_hw->scratch[0] = CRASH_MAGIC_V1; watchdog_hw->scratch[1] = frame[EF_PC]; watchdog_hw->scratch[2] = frame[EF_LR]; watchdog_hw->scratch[3] = pre_fault_sp; @@ -224,4 +236,5 @@ extern "C" void __attribute__((naked, used)) isr_hardfault() { : "i"(hard_fault_handler_c)); } +#endif // USE_RP2040_CRASH_HANDLER #endif // USE_RP2040 diff --git a/esphome/components/rp2040/crash_handler.h b/esphome/components/rp2040/crash_handler.h index f10db47c23..78e8ede08c 100644 --- a/esphome/components/rp2040/crash_handler.h +++ b/esphome/components/rp2040/crash_handler.h @@ -2,7 +2,9 @@ #ifdef USE_RP2040 -#include <cstdint> +#include "esphome/core/defines.h" + +#ifdef USE_RP2040_CRASH_HANDLER namespace esphome::rp2040 { @@ -12,6 +14,10 @@ void crash_handler_read_and_clear(); /// Log crash data if a crash was detected on previous boot. void crash_handler_log(); +/// Returns true if crash data was found this boot. +bool crash_handler_has_data(); + } // namespace esphome::rp2040 +#endif // USE_RP2040_CRASH_HANDLER #endif // USE_RP2040 diff --git a/esphome/core/defines.h b/esphome/core/defines.h index a33f10cb9c..073170aafb 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -338,6 +338,7 @@ #ifdef USE_RP2040 #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 0) #define USE_LOOP_PRIORITY +#define USE_RP2040_CRASH_HANDLER #define USE_HTTP_REQUEST_RESPONSE #define USE_I2C #define USE_LOGGER_USB_CDC From 6002badb3c35b20475a76cef3781f13967c1260e Mon Sep 17 00:00:00 2001 From: Adam DeMuri <adam.demuri@gmail.com> Date: Thu, 12 Mar 2026 02:00:26 -0600 Subject: [PATCH 545/609] [modbus] Fix buffer overflow in modbus (#14719) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/modbus/modbus.cpp | 12 ++--- tests/components/modbus/modbus_test.cpp | 59 +++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 tests/components/modbus/modbus_test.cpp diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 82672217c5..7a61868e6e 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -125,13 +125,17 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { // Byte 0: modbus address (match all) if (at == 0) return true; - uint8_t address = raw[0]; - uint8_t function_code = raw[1]; + // Byte 1: function code + if (at == 1) + return true; // Byte 2: Size (with modbus rtu function code 4/3) // See also https://en.wikipedia.org/wiki/Modbus if (at == 2) return true; + uint8_t address = raw[0]; + uint8_t function_code = raw[1]; + uint8_t data_len = raw[2]; uint8_t data_offset = 3; @@ -146,10 +150,6 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { // chance that this is a complete message ... admittedly there is a small chance is // isn't but that is quite small given the purpose of the CRC in the first place - // Fewer than 2 bytes can't calc CRC - if (at < 2) - return true; - data_len = at - 2; data_offset = 1; diff --git a/tests/components/modbus/modbus_test.cpp b/tests/components/modbus/modbus_test.cpp new file mode 100644 index 0000000000..afe5ced082 --- /dev/null +++ b/tests/components/modbus/modbus_test.cpp @@ -0,0 +1,59 @@ +#include <gtest/gtest.h> +#include "esphome/components/modbus/modbus.h" +#include "esphome/core/helpers.h" + +namespace esphome::modbus { + +// Exposes protected methods for testing. +class TestModbus : public Modbus { + public: + bool test_parse_modbus_byte(uint8_t byte) { return this->parse_modbus_byte_(byte); } + void test_clear_rx_buffer() { this->rx_buffer_.clear(); } + void set_waiting(uint8_t addr) { this->waiting_for_response_ = addr; } +}; + +class MockDevice : public ModbusDevice { + public: + void on_modbus_data(const std::vector<uint8_t> &data) override { this->data_received = true; } + bool data_received{false}; +}; + +TEST(ModbusTest, TwoByteRegressionTest) { + TestModbus modbus; + modbus.set_role(ModbusRole::CLIENT); + // First byte (at=0) + EXPECT_TRUE(modbus.test_parse_modbus_byte(0x01)); + // Second byte (at=1) + // This used to reach raw[2] because it skipped the if(at==2) check, causing a + // buffer overflow. + EXPECT_TRUE(modbus.test_parse_modbus_byte(0x03)); +} + +TEST(ModbusTest, TestValidFrame) { + TestModbus modbus; + modbus.set_role(ModbusRole::CLIENT); + + MockDevice device; + device.set_parent(&modbus); + device.set_address(0x01); + modbus.register_device(&device); + modbus.set_waiting(0x01); + + // Address 1, Function 3, Length 2, Data 0x1234 + uint8_t frame_data[] = {0x01, 0x03, 0x02, 0x12, 0x34}; + uint16_t crc = esphome::crc16(frame_data, sizeof(frame_data)); + + std::vector<uint8_t> frame; + for (uint8_t b : frame_data) + frame.push_back(b); + frame.push_back(crc & 0xFF); + frame.push_back((crc >> 8) & 0xFF); + + for (size_t i = 0; i < frame.size(); i++) { + bool result = modbus.test_parse_modbus_byte(frame[i]); + EXPECT_TRUE(result) << "Failed at byte " << i << " (0x" << std::hex << (int) frame[i] << ")"; + } + EXPECT_TRUE(device.data_received); +} + +} // namespace esphome::modbus From 4b1c4ba5c0561cba93d979aa8fb76da0bafff301 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Thu, 12 Mar 2026 03:16:02 -0500 Subject: [PATCH 546/609] [ledc] Fix high-pressure crash & recovery (#14720) --- esphome/components/ledc/ledc_output.cpp | 53 +++++++++++++++++++++++-- esphome/components/ledc/ledc_output.h | 8 ++-- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index 763de851da..592fc7bd0c 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -5,6 +5,10 @@ #include <driver/ledc.h> #include <cinttypes> +#include <esp_private/periph_ctrl.h> +#if !defined(SOC_LEDC_SUPPORT_FADE_STOP) +#include <hal/ledc_ll.h> +#endif #define CLOCK_FREQUENCY 80e6f @@ -16,10 +20,10 @@ static const uint8_t SETUP_ATTEMPT_COUNT_MAX = 5; -namespace esphome { -namespace ledc { +namespace esphome::ledc { static const char *const TAG = "ledc.output"; +static bool ledc_peripheral_reset_done = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static const int MAX_RES_BITS = LEDC_TIMER_BIT_MAX - 1; #if SOC_LEDC_SUPPORT_HS_MODE @@ -32,6 +36,28 @@ inline ledc_mode_t get_speed_mode(uint8_t channel) { return channel < 8 ? LEDC_H inline ledc_mode_t get_speed_mode(uint8_t) { return LEDC_LOW_SPEED_MODE; } #endif +#if !defined(SOC_LEDC_SUPPORT_FADE_STOP) +// Classic ESP32 (currently the only target without SOC_LEDC_SUPPORT_FADE_STOP) can block in +// ledc_ll_set_duty_start() while duty_start is set. We check the same conf1.duty_start bit here +// to defer updates and avoid entering IDF's unbounded wait loop. +// +// This intentionally depends on the classic ESP32 LEDC register layout used by IDF's own LL HAL. +// If another target without SOC_LEDC_SUPPORT_FADE_STOP is introduced, revisit this helper. +static_assert( +#if defined(CONFIG_IDF_TARGET_ESP32) + true, +#else + false, +#endif + "LEDC duty_start pending check assumes classic ESP32 register layout; " + "re-evaluate for this target"); + +static bool ledc_duty_update_pending(ledc_mode_t speed_mode, ledc_channel_t chan_num) { + auto *hw = LEDC_LL_GET_HW(); + return hw->channel_group[speed_mode].channel[chan_num].conf1.duty_start != 0; +} +#endif + float ledc_max_frequency_for_bit_depth(uint8_t bit_depth) { return static_cast<float>(CLOCK_FREQUENCY) / static_cast<float>(1 << bit_depth); } @@ -105,21 +131,40 @@ void LEDCOutput::write_state(float state) { const uint32_t max_duty = (uint32_t(1) << this->bit_depth_) - 1; const float duty_rounded = roundf(state * max_duty); auto duty = static_cast<uint32_t>(duty_rounded); + if (duty == this->last_duty_) { + return; + } + ESP_LOGV(TAG, "Setting duty: %" PRIu32 " on channel %u", duty, this->channel_); auto speed_mode = get_speed_mode(this->channel_); auto chan_num = static_cast<ledc_channel_t>(this->channel_ % 8); int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_); if (duty == max_duty) { ledc_stop(speed_mode, chan_num, 1); + this->last_duty_ = duty; } else if (duty == 0) { ledc_stop(speed_mode, chan_num, 0); + this->last_duty_ = duty; } else { +#if !defined(SOC_LEDC_SUPPORT_FADE_STOP) + if (ledc_duty_update_pending(speed_mode, chan_num)) { + ESP_LOGV(TAG, "Skipping LEDC duty update on channel %u while previous duty_start is still set", this->channel_); + return; + } +#endif ledc_set_duty_with_hpoint(speed_mode, chan_num, duty, hpoint); ledc_update_duty(speed_mode, chan_num); + this->last_duty_ = duty; } } void LEDCOutput::setup() { + if (!ledc_peripheral_reset_done) { + ESP_LOGV(TAG, "Resetting LEDC peripheral to clear stale state after reboot"); + periph_module_reset(PERIPH_LEDC_MODULE); + ledc_peripheral_reset_done = true; + } + auto speed_mode = get_speed_mode(this->channel_); auto timer_num = static_cast<ledc_timer_t>((this->channel_ % 8) / 2); auto chan_num = static_cast<ledc_channel_t>(this->channel_ % 8); @@ -207,12 +252,12 @@ void LEDCOutput::update_frequency(float frequency) { this->status_clear_error(); // re-apply duty + this->last_duty_ = UINT32_MAX; this->write_state(this->duty_); } uint8_t next_ledc_channel = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace ledc -} // namespace esphome +} // namespace esphome::ledc #endif diff --git a/esphome/components/ledc/ledc_output.h b/esphome/components/ledc/ledc_output.h index b24e3cfdb2..bf5cdb9305 100644 --- a/esphome/components/ledc/ledc_output.h +++ b/esphome/components/ledc/ledc_output.h @@ -4,11 +4,11 @@ #include "esphome/core/hal.h" #include "esphome/core/automation.h" #include "esphome/components/output/float_output.h" +#include <cstdint> #ifdef USE_ESP32 -namespace esphome { -namespace ledc { +namespace esphome::ledc { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern uint8_t next_ledc_channel; @@ -39,6 +39,7 @@ class LEDCOutput : public output::FloatOutput, public Component { float phase_angle_{0.0f}; float frequency_{}; float duty_{0.0f}; + uint32_t last_duty_{UINT32_MAX}; bool initialized_ = false; }; @@ -56,7 +57,6 @@ template<typename... Ts> class SetFrequencyAction : public Action<Ts...> { LEDCOutput *parent_; }; -} // namespace ledc -} // namespace esphome +} // namespace esphome::ledc #endif From df2ddc47ec2baae7da48a6f14831c429c8f292af Mon Sep 17 00:00:00 2001 From: Brian Kaufman <bkaufx@gmail.com> Date: Thu, 12 Mar 2026 02:07:26 -0700 Subject: [PATCH 547/609] [web_server] use DETAIL_ALL in update_all_json_generator (#14711) --- esphome/components/web_server/web_server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 5590e67b82..4083019643 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -2181,7 +2181,7 @@ json::SerializationBuffer<> WebServer::update_state_json_generator(WebServer *we } json::SerializationBuffer<> WebServer::update_all_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE); + return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_ALL); } json::SerializationBuffer<> WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson From 440734dadf0e4c60accb4a5d6b79eaf6a9c9c60d Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Thu, 12 Mar 2026 07:56:01 -0500 Subject: [PATCH 548/609] [audio] Bump microOpus to v0.3.5 (#14727) --- esphome/components/audio/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index d95fcf66d7..b28c2ed3d8 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -214,4 +214,4 @@ async def to_code(config): cg.add_define("USE_AUDIO_MP3_SUPPORT") if data.opus_support: cg.add_define("USE_AUDIO_OPUS_SUPPORT") - add_idf_component(name="esphome/micro-opus", ref="0.3.4") + add_idf_component(name="esphome/micro-opus", ref="0.3.5") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index f7fd3e67bc..df651ae15d 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -4,7 +4,7 @@ dependencies: esphome/esp-audio-libs: version: 2.0.3 esphome/micro-opus: - version: 0.3.4 + version: 0.3.5 espressif/esp-tflite-micro: version: 1.3.3~1 espressif/esp32-camera: From da130c900f82326080c1b790be68c9625912b153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20K=C3=B6nig?= <Matthias.Konitzny@kabelmail.de> Date: Thu, 12 Mar 2026 17:00:08 +0100 Subject: [PATCH 549/609] [mqtt] Fixed permission denied error for client certificates on Windows (#13525) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/mqtt.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/esphome/mqtt.py b/esphome/mqtt.py index cbf78bd3f6..ccacbaea54 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -2,6 +2,7 @@ import contextlib from datetime import datetime import json import logging +import os import ssl import tempfile import time @@ -109,14 +110,18 @@ def prepare( CONF_CLIENT_CERTIFICATE_KEY ): with ( - tempfile.NamedTemporaryFile(mode="w+") as cert_file, - tempfile.NamedTemporaryFile(mode="w+") as key_file, + tempfile.NamedTemporaryFile(mode="w+", delete=False) as cert_file, + tempfile.NamedTemporaryFile(mode="w+", delete=False) as key_file, ): - cert_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE)) - cert_file.flush() - key_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE_KEY)) - key_file.flush() - context.load_cert_chain(cert_file.name, key_file.name) + try: + cert_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE)) + key_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE_KEY)) + cert_file.close() + key_file.close() + context.load_cert_chain(cert_file.name, key_file.name) + finally: + os.unlink(cert_file.name) + os.unlink(key_file.name) client.tls_set_context(context) try: From 3a838d897fac21b120b8d913141dee194cf87c6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:14:49 -1000 Subject: [PATCH 550/609] [socket] Fix use-after-free in LWIP PCB close/abort path (#14706) --- .../components/socket/lwip_raw_tcp_impl.cpp | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index fd1b8a9554..1e03a4935c 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -138,13 +138,46 @@ static const char *const TAG = "socket.lwip"; #define LWIP_LOG(msg, ...) #endif +// Clear arg, recv, and err callbacks, then abort a connected PCB. +// Only valid for full tcp_pcb (not tcp_pcb_listen). +// Must be called before destroying the object that tcp_arg points to — +// tcp_abort() triggers the err callback synchronously, which would +// otherwise call back into a partially-destroyed object. +// tcp_sent/tcp_poll are not cleared because this implementation +// never registers them. +static void pcb_detach_abort(struct tcp_pcb *pcb) { + tcp_arg(pcb, nullptr); + tcp_recv(pcb, nullptr); + tcp_err(pcb, nullptr); + tcp_abort(pcb); +} + +// Clear arg, recv, and err callbacks, then gracefully close a connected PCB. +// Only valid for full tcp_pcb (not tcp_pcb_listen). +// After tcp_close(), the PCB remains alive during the TCP close handshake +// (FIN_WAIT, TIME_WAIT states). Without clearing callbacks first, LWIP +// would call recv/err on a destroyed socket object, corrupting the heap. +// tcp_sent/tcp_poll are not cleared because this implementation +// never registers them. +// Returns ERR_OK on success; on failure the PCB is aborted instead. +static err_t pcb_detach_close(struct tcp_pcb *pcb) { + tcp_arg(pcb, nullptr); + tcp_recv(pcb, nullptr); + tcp_err(pcb, nullptr); + err_t err = tcp_close(pcb); + if (err != ERR_OK) { + tcp_abort(pcb); + } + return err; +} + // ---- LWIPRawCommon methods ---- LWIPRawCommon::~LWIPRawCommon() { LWIP_LOCK(); if (this->pcb_ != nullptr) { LWIP_LOG("tcp_abort(%p)", this->pcb_); - tcp_abort(this->pcb_); + pcb_detach_abort(this->pcb_); this->pcb_ = nullptr; } } @@ -222,15 +255,13 @@ int LWIPRawCommon::close() { return -1; } LWIP_LOG("tcp_close(%p)", this->pcb_); - err_t err = tcp_close(this->pcb_); + err_t err = pcb_detach_close(this->pcb_); + this->pcb_ = nullptr; if (err != ERR_OK) { LWIP_LOG(" -> err %d", err); - tcp_abort(this->pcb_); - this->pcb_ = nullptr; errno = err == ERR_MEM ? ENOMEM : EIO; return -1; } - this->pcb_ = nullptr; return 0; } @@ -673,13 +704,10 @@ ssize_t LWIPRawImpl::writev(const struct iovec *iov, int iovcnt) { LWIPRawListenImpl::~LWIPRawListenImpl() { LWIP_LOCK(); // Abort any queued PCBs that were never accepted by the main loop. - // Clear the error callback first — tcp_abort triggers it, and we don't - // want s_queued_err_fn writing to slots during destruction. for (uint8_t i = 0; i < this->accepted_socket_count_; i++) { auto &entry = this->accepted_pcbs_[i]; if (entry.pcb != nullptr) { - tcp_err(entry.pcb, nullptr); - tcp_abort(entry.pcb); + pcb_detach_abort(entry.pcb); entry.pcb = nullptr; } if (entry.rx_buf != nullptr) { @@ -691,6 +719,10 @@ LWIPRawListenImpl::~LWIPRawListenImpl() { // Listen PCBs must use tcp_close(), not tcp_abort(). // tcp_abandon() asserts pcb->state != LISTEN and would access // fields that don't exist in the smaller tcp_pcb_listen struct. + // Don't use pcb_detach_close() here — tcp_recv()/tcp_err() also access + // fields that only exist in the full tcp_pcb, not tcp_pcb_listen. + // tcp_close() on a listen PCB is synchronous (frees immediately), + // so there are no async callbacks to worry about. // Close here and null pcb_ so the base destructor skips tcp_abort. if (this->pcb_ != nullptr) { tcp_close(this->pcb_); From 1d881ef6f4d4a4ba8575fe5ac1c4bb50c121f767 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:15:04 -1000 Subject: [PATCH 551/609] [socket] Fast path for TCP_NODELAY bypasses lwip_setsockopt overhead (#14693) --- esphome/components/socket/bsd_sockets_impl.h | 11 ++++++++++- esphome/components/socket/lwip_sockets_impl.h | 11 ++++++++++- esphome/core/lwip_fast_select.c | 16 ++++++++++++++++ esphome/core/lwip_fast_select.h | 7 +++++++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/esphome/components/socket/bsd_sockets_impl.h b/esphome/components/socket/bsd_sockets_impl.h index 9ebbe72002..339a699bc9 100644 --- a/esphome/components/socket/bsd_sockets_impl.h +++ b/esphome/components/socket/bsd_sockets_impl.h @@ -14,7 +14,7 @@ #endif #ifdef USE_LWIP_FAST_SELECT -struct lwip_sock; +#include "esphome/core/lwip_fast_select.h" #endif namespace esphome::socket { @@ -56,6 +56,15 @@ class BSDSocketImpl { return ::getsockopt(this->fd_, level, optname, optval, optlen); } int setsockopt(int level, int optname, const void *optval, socklen_t optlen) { +#if defined(USE_LWIP_FAST_SELECT) && defined(CONFIG_LWIP_TCPIP_CORE_LOCKING) + // Fast path for TCP_NODELAY: directly set the pcb flag under the TCPIP core lock, + // bypassing lwip_setsockopt overhead (socket lookups, hook, switch cascade, refcounting). + if (level == IPPROTO_TCP && optname == TCP_NODELAY && optlen == sizeof(int) && optval != nullptr) { + LwIPLock lock; + if (esphome_lwip_set_nodelay(this->cached_sock_, *reinterpret_cast<const int *>(optval) != 0)) + return 0; + } +#endif return ::setsockopt(this->fd_, level, optname, optval, optlen); } int listen(int backlog) { return ::listen(this->fd_, backlog); } diff --git a/esphome/components/socket/lwip_sockets_impl.h b/esphome/components/socket/lwip_sockets_impl.h index c579219863..bfc4da9926 100644 --- a/esphome/components/socket/lwip_sockets_impl.h +++ b/esphome/components/socket/lwip_sockets_impl.h @@ -10,7 +10,7 @@ #include "headers.h" #ifdef USE_LWIP_FAST_SELECT -struct lwip_sock; +#include "esphome/core/lwip_fast_select.h" #endif namespace esphome::socket { @@ -52,6 +52,15 @@ class LwIPSocketImpl { return lwip_getsockopt(this->fd_, level, optname, optval, optlen); } int setsockopt(int level, int optname, const void *optval, socklen_t optlen) { +#if defined(USE_LWIP_FAST_SELECT) && defined(CONFIG_LWIP_TCPIP_CORE_LOCKING) + // Fast path for TCP_NODELAY: directly set the pcb flag under the TCPIP core lock, + // bypassing lwip_setsockopt overhead (socket lookups, hook, switch cascade, refcounting). + if (level == IPPROTO_TCP && optname == TCP_NODELAY && optlen == sizeof(int) && optval != nullptr) { + LwIPLock lock; + if (esphome_lwip_set_nodelay(this->cached_sock_, *reinterpret_cast<const int *>(optval) != 0)) + return 0; + } +#endif return lwip_setsockopt(this->fd_, level, optname, optval, optlen); } int listen(int backlog) { return lwip_listen(this->fd_, backlog); } diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index c578a9aae9..a695fa396b 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -112,6 +112,7 @@ // LwIP headers must come first — they define netconn_callback, struct lwip_sock, etc. #include <lwip/api.h> #include <lwip/priv/sockets_priv.h> +#include <lwip/tcp.h> // FreeRTOS include paths differ: ESP-IDF uses freertos/ prefix, LibreTiny does not #ifdef USE_ESP32 #include <freertos/FreeRTOS.h> @@ -216,6 +217,21 @@ void esphome_lwip_hook_socket(struct lwip_sock *sock) { sock->conn->callback = esphome_socket_event_callback; } +bool esphome_lwip_set_nodelay(struct lwip_sock *sock, bool enable) { + if (sock == NULL || sock->conn == NULL) + return false; + if (NETCONNTYPE_GROUP(sock->conn->type) != NETCONN_TCP) + return false; + if (sock->conn->pcb.tcp == NULL) + return false; + if (enable) { + tcp_nagle_disable(sock->conn->pcb.tcp); + } else { + tcp_nagle_enable(sock->conn->pcb.tcp); + } + return true; +} + // Wake the main loop from another FreeRTOS task. NOT ISR-safe. void esphome_lwip_wake_main_loop(void) { TaskHandle_t task = s_main_loop_task; diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h index 46c6b711cd..50706ba9f6 100644 --- a/esphome/core/lwip_fast_select.h +++ b/esphome/core/lwip_fast_select.h @@ -66,6 +66,13 @@ void esphome_lwip_wake_main_loop(void); /// @param px_higher_priority_task_woken Set to pdTRUE if a context switch is needed. void esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken); +/// Set or clear TCP_NODELAY on a socket's tcp_pcb directly. +/// Must be called with the TCPIP core lock held (LwIPLock in C++). +/// This bypasses lwip_setsockopt() overhead (socket lookups, switch cascade, +/// hooks, refcounting) — just a direct pcb->flags bit set/clear. +/// Returns true if successful, false if sock/conn/pcb is NULL or the socket is not TCP. +bool esphome_lwip_set_nodelay(struct lwip_sock *sock, bool enable); + /// Wake the main loop task from any context (ISR, thread, or main loop). /// ESP32-only: uses xPortInIsrContext() to detect ISR context. /// LibreTiny lacks IRAM_ATTR support needed for ISR-safe paths. From 1b7d0f9c0b6745f3610dc84b38dd7236fc5dced7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:15:21 -1000 Subject: [PATCH 552/609] [esp32_ble_client] Fix disconnect race that causes stuck connections (#14211) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .../esp32_ble_client/ble_client_base.cpp | 43 ++++++++++++++++--- .../esp32_ble_client/ble_client_base.h | 17 +++++++- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 2f17334c77..9d6e079d92 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -27,6 +27,7 @@ static constexpr uint16_t MEDIUM_CONN_TIMEOUT = 800; // 800 * 10ms = 8s static constexpr uint16_t FAST_MIN_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms (BLE minimum) static constexpr uint16_t FAST_MAX_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms static constexpr uint16_t FAST_CONN_TIMEOUT = 1000; // 1000 * 10ms = 10s +static constexpr uint32_t DISCONNECTING_TIMEOUT = 10000; // 10s static const esp_bt_uuid_t NOTIFY_DESC_UUID = { .len = ESP_UUID_LEN_16, .uuid = @@ -62,6 +63,15 @@ void BLEClientBase::loop() { // will enable it again when a connection is needed. else if (this->state() == espbt::ClientState::IDLE) { this->disable_loop(); + } else if (this->state() == espbt::ClientState::DISCONNECTING && + (millis() - this->disconnecting_started_) > DISCONNECTING_TIMEOUT) { + ESP_LOGE(TAG, "[%d] [%s] Timeout waiting for CLOSE_EVT after disconnect, forcing IDLE", this->connection_index_, + this->address_str_); + // release_services() must be called before set_idle_() — if we entered DISCONNECTING + // via unconditional_disconnect() (which doesn't call release_services()), and ESP-IDF + // never delivered CLOSE_EVT/DISCONNECT_EVT, services would leak without this call. + this->release_services(); + this->set_idle_(); } } @@ -101,12 +111,16 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { #endif void BLEClientBase::connect() { - // Prevent duplicate connection attempts + // Prevent duplicate connection attempts or connecting while still disconnecting if (this->state() == espbt::ClientState::CONNECTING || this->state() == espbt::ClientState::CONNECTED || this->state() == espbt::ClientState::ESTABLISHED) { ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_, espbt::client_state_to_string(this->state())); return; + } else if (this->state() == espbt::ClientState::DISCONNECTING) { + ESP_LOGW(TAG, "[%d] [%s] Cannot connect, still waiting for CLOSE_EVT to complete disconnect", + this->connection_index_, this->address_str_); + return; } ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_); this->paired_ = false; @@ -174,7 +188,7 @@ void BLEClientBase::unconditional_disconnect() { this->set_address(0); this->set_state(espbt::ClientState::IDLE); } else { - this->set_state(espbt::ClientState::DISCONNECTING); + this->set_disconnecting_(); } } @@ -220,6 +234,7 @@ void BLEClientBase::log_connection_params_(const char *param_type) { void BLEClientBase::handle_connection_result_(esp_err_t ret) { if (ret) { this->log_gattc_warning_("esp_ble_gattc_open", ret); + // Don't use set_idle_() here — CONNECT_EVT never fired so conn_id_ is still UNSET_CONN_ID. this->set_state(espbt::ClientState::IDLE); } } @@ -311,15 +326,16 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { this->log_gattc_warning_("Connection open", param->open.status); - this->set_state(espbt::ClientState::IDLE); + // Connection was never established so CLOSE_EVT may not follow + this->set_idle_(); break; } if (this->want_disconnect_) { // Disconnect was requested after connecting started, // but before the connection was established. Now that we have // this->conn_id_ set, we can disconnect it. + // Don't reset conn_id_ here — CLOSE_EVT needs it to match and call set_idle_(). this->unconditional_disconnect(); - this->conn_id_ = UNSET_CONN_ID; break; } // MTU negotiation already started in ESP_GATTC_CONNECT_EVT @@ -363,8 +379,22 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_, param->disconnect.reason); } + // For active disconnects (esp_ble_gattc_close), CLOSE_EVT arrives before + // DISCONNECT_EVT. If CLOSE_EVT already transitioned us to IDLE, don't go + // backwards to DISCONNECTING — the connection is already fully cleaned up. + if (this->state() == espbt::ClientState::IDLE) { + this->log_event_("DISCONNECT_EVT after CLOSE_EVT, already IDLE"); + break; + } + // For passive disconnects (remote device disconnected or link lost), + // DISCONNECT_EVT arrives first. Don't transition to IDLE yet — wait for + // CLOSE_EVT to ensure the controller has fully freed resources (L2CAP + // channels, ATT resources, HCI connection handle). Transitioning to IDLE + // here would allow reconnection before cleanup is complete, causing the + // controller to reject the new connection (status=133) or crash with + // ASSERT_PARAM in lld_evt.c. this->release_services(); - this->set_state(espbt::ClientState::IDLE); + this->set_disconnecting_(); break; } @@ -387,8 +417,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ return false; this->log_gattc_lifecycle_event_("CLOSE"); this->release_services(); - this->set_state(espbt::ClientState::IDLE); - this->conn_id_ = UNSET_CONN_ID; + this->set_idle_(); break; } case ESP_GATTC_SEARCH_RES_EVT: { diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index af4f1b3029..4e0b22cc29 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -113,11 +113,14 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { char address_str_[MAC_ADDRESS_PRETTY_BUFFER_SIZE]{}; esp_bd_addr_t remote_bda_; // 6 bytes - // Group 5: 2-byte types + // Group 5: 4-byte types + uint32_t disconnecting_started_{0}; + + // Group 6: 2-byte types uint16_t conn_id_{UNSET_CONN_ID}; uint16_t mtu_{23}; - // Group 6: 1-byte types and small enums + // Group 7: 1-byte types and small enums esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC}; espbt::ConnectionType connection_type_{espbt::ConnectionType::V1}; uint8_t connection_index_; @@ -137,6 +140,16 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void log_gattc_warning_(const char *operation, esp_err_t err); void log_connection_params_(const char *param_type); void handle_connection_result_(esp_err_t ret); + /// Transition to IDLE and reset conn_id — call when the connection is fully dead. + void set_idle_() { + this->set_state(espbt::ClientState::IDLE); + this->conn_id_ = UNSET_CONN_ID; + } + /// Transition to DISCONNECTING and start the safety timeout. + void set_disconnecting_() { + this->disconnecting_started_ = millis(); + this->set_state(espbt::ClientState::DISCONNECTING); + } // Compact error logging helpers to reduce flash usage void log_error_(const char *message); void log_error_(const char *message, int code); From 33475703da772d24e7a6df3e8a15160c5db53f8d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:15:34 -1000 Subject: [PATCH 553/609] [time] Fix settimeofday() failure on ESP8266 (#14707) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/time/real_time_clock.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 566344fa88..4e623942ac 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -88,16 +88,16 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { struct timeval timev { .tv_sec = static_cast<time_t>(epoch), .tv_usec = 0, }; +#ifdef USE_ESP8266 + // ESP8266 settimeofday() requires tz to be nullptr + int ret = settimeofday(&timev, nullptr); +#else struct timezone tz = {0, 0}; int ret = settimeofday(&timev, &tz); - if (ret != 0 && errno == EINVAL) { - // Some ESP8266 frameworks abort when timezone parameter is not NULL - // while ESP32 expects it not to be NULL - ret = settimeofday(&timev, nullptr); - } +#endif if (ret != 0) { - ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); + ESP_LOGW(TAG, "settimeofday() failed with code %d", ret); } #endif auto time = this->now(); From c8cf9b74b104a1965897c16706ee30999d6fe25b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:15:48 -1000 Subject: [PATCH 554/609] [ota][socket] Fix ESP8266/RP2040 OTA timeout by using SO_RCVTIMEO instead of polling (#14675) --- .../components/esphome/ota/ota_esphome.cpp | 46 +++++++++++- esphome/components/socket/headers.h | 2 + .../components/socket/lwip_raw_tcp_impl.cpp | 71 +++++++++++++++++-- esphome/components/socket/lwip_raw_tcp_impl.h | 16 +++-- 4 files changed, 123 insertions(+), 12 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index a1cdf59d2b..d8dbe2dee2 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -18,6 +18,7 @@ #include <cerrno> #include <cstdio> +#include <sys/time.h> namespace esphome { @@ -238,6 +239,31 @@ void ESPHomeOTAComponent::handle_data_() { /// and reboots on success. /// /// Authentication has already been handled in the non-blocking states AUTH_SEND/AUTH_READ. + /// + /// Socket I/O strategy: + /// + /// Before this function, the handshake states use non-blocking I/O: + /// read()/write() return immediately with EWOULDBLOCK if no data + /// loop() retries on next iteration (~16ms), no delay needed + /// + /// This function switches to blocking mode with SO_RCVTIMEO/SO_SNDTIMEO: + /// + /// Path | Wait mechanism | WDT strategy + /// --------------|------------------------|--------------------------- + /// Main read | SO_RCVTIMEO (2s block) | feed_wdt() only, no delay + /// readall_() | SO_RCVTIMEO (2s block) | feed_wdt() + delay(0) + /// writeall_() | SO_SNDTIMEO (2s block) | feed_wdt() + delay(1) + /// + /// readall_() uses delay(0) because SO_RCVTIMEO already waited — just yield. + /// writeall_() uses delay(1) because on raw TCP (ESP8266, RP2040) writes + /// never block (tcp_write returns immediately), so delay(1) prevents spinning. + /// + /// Platform details: + /// BSD sockets (ESP32): setblocking(true) makes read/write block + /// lwip sockets (LT): setblocking(true) makes read/write block + /// Raw TCP (8266, RP2040): setblocking is no-op; SO_RCVTIMEO uses + /// socket_delay()/socket_wake() in read(); + /// write() always returns immediately ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN; bool update_started = false; size_t total = 0; @@ -249,6 +275,14 @@ void ESPHomeOTAComponent::handle_data_() { size_t size_acknowledged = 0; #endif + // Set socket timeouts and blocking mode (see strategy table above) + struct timeval tv; + tv.tv_sec = 2; + tv.tv_usec = 0; + this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + this->client_->setsockopt(SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + this->client_->setblocking(true); + // Acknowledge auth OK - 1 byte this->write_byte_(ota::OTA_RESPONSE_AUTH_OK); @@ -299,7 +333,8 @@ void ESPHomeOTAComponent::handle_data_() { ssize_t read = this->client_->read(buf, requested); if (read == -1) { if (this->would_block_(errno)) { - this->yield_and_feed_watchdog_(); + // read() already waited up to SO_RCVTIMEO for data, just feed WDT + App.feed_wdt(); continue; } ESP_LOGW(TAG, "Read err %d", errno); @@ -401,7 +436,9 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) { } else { at += read; } - this->yield_and_feed_watchdog_(); + // read() already waited via SO_RCVTIMEO, just yield without 1ms stall + App.feed_wdt(); + delay(0); } return true; @@ -422,10 +459,13 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) { ESP_LOGW(TAG, "Write err %zu bytes, errno %d", len, errno); return false; } + // EWOULDBLOCK: on raw TCP writes never block, delay(1) prevents spinning + this->yield_and_feed_watchdog_(); } else { at += written; + // write() may block up to SO_SNDTIMEO on BSD/lwip sockets, feed WDT + App.feed_wdt(); } - this->yield_and_feed_watchdog_(); } return true; } diff --git a/esphome/components/socket/headers.h b/esphome/components/socket/headers.h index 16e4d23d3b..0eece6480f 100644 --- a/esphome/components/socket/headers.h +++ b/esphome/components/socket/headers.h @@ -51,6 +51,8 @@ #define SO_REUSEADDR 0x0004 /* Allow local address reuse */ #define SO_KEEPALIVE 0x0008 /* keep connections alive */ #define SO_BROADCAST 0x0020 /* permit to send and to receive broadcast messages (see IP_SOF_BROADCAST option) */ +#define SO_RCVTIMEO 0x1006 /* receive timeout */ +#define SO_SNDTIMEO 0x1005 /* send timeout */ #define SOL_SOCKET 0xfff /* options for socket level */ diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 1e03a4935c..96328e68c7 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -5,6 +5,7 @@ #include <cerrno> #include <cstring> +#include <sys/time.h> #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -81,7 +82,9 @@ void socket_delay(uint32_t ms) { s_socket_woke = false; return; } - s_socket_woke = false; + // Don't clear s_socket_woke here — if an IRQ fires between the check above + // and the while loop below, the while condition sees it immediately. Clearing + // here would lose that wake and sleep until the timer fires. s_delay_expired = false; // Set a one-shot timer to wake us after the timeout. // add_alarm_in_ms returns >0 on success, 0 if time already passed, <0 on error. @@ -99,6 +102,7 @@ void socket_delay(uint32_t ms) { // Cancel timer if we woke early (socket data arrived before timeout) if (!s_delay_expired) cancel_alarm(alarm); + s_socket_woke = false; // consume the wake for next call } // No IRAM_ATTR equivalent needed: on RP2040, CYW43 async_context runs LWIP @@ -359,6 +363,18 @@ int LWIPRawCommon::getsockopt(int level, int optname, void *optval, socklen_t *o *optlen = 4; return 0; } + if (level == SOL_SOCKET && optname == SO_RCVTIMEO) { + if (*optlen < sizeof(struct timeval)) { + errno = EINVAL; + return -1; + } + uint32_t ms = this->recv_timeout_cs_ * 10; + auto *tv = reinterpret_cast<struct timeval *>(optval); + tv->tv_sec = ms / 1000; + tv->tv_usec = (ms % 1000) * 1000; + *optlen = sizeof(struct timeval); + return 0; + } if (level == IPPROTO_TCP && optname == TCP_NODELAY) { if (*optlen < 4) { errno = EINVAL; @@ -388,6 +404,21 @@ int LWIPRawCommon::setsockopt(int level, int optname, const void *optval, sockle // to prevent warnings return 0; } + if (level == SOL_SOCKET && optname == SO_RCVTIMEO) { + if (optlen < sizeof(struct timeval)) { + errno = EINVAL; + return -1; + } + const auto *tv = reinterpret_cast<const struct timeval *>(optval); + uint32_t ms = tv->tv_sec * 1000 + tv->tv_usec / 1000; + uint32_t cs = (ms + 9) / 10; // round up to nearest centisecond + this->recv_timeout_cs_ = cs > 255 ? 255 : static_cast<uint8_t>(cs); + return 0; + } + if (level == SOL_SOCKET && optname == SO_SNDTIMEO) { + // Raw TCP writes are non-blocking (tcp_write), so send timeout is a no-op. + return 0; + } if (level == IPPROTO_TCP && optname == TCP_NODELAY) { if (optlen != 4) { errno = EINVAL; @@ -518,8 +549,25 @@ err_t LWIPRawImpl::recv_fn(struct pbuf *pb, err_t err) { return ERR_OK; } -ssize_t LWIPRawImpl::read(void *buf, size_t len) { - LWIP_LOCK(); +void LWIPRawImpl::wait_for_data_() { + // Wait for data without holding LWIP_LOCK so recv_fn() can run on RP2040 + // (needs async_context lock). + // + // Loop until data arrives, connection closes, or the full timeout elapses. + // socket_delay() may return early due to other sockets waking the global + // socket_wake() flag, so we re-enter for the remaining time. + uint32_t timeout_ms = this->recv_timeout_cs_ * 10; + uint32_t start = millis(); + while (this->waiting_for_data_()) { + uint32_t elapsed = millis() - start; + if (elapsed >= timeout_ms) + break; + socket_delay(timeout_ms - elapsed); + } +} + +ssize_t LWIPRawImpl::read_locked_(void *buf, size_t len) { + // Caller must hold LWIP_LOCK. Copies available data from rx_buf_ into buf. if (this->pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -578,11 +626,26 @@ ssize_t LWIPRawImpl::read(void *buf, size_t len) { return read; } +ssize_t LWIPRawImpl::read(void *buf, size_t len) { + // See waiting_for_data_() for safety of unlocked reads. + if (this->recv_timeout_cs_ > 0 && this->waiting_for_data_()) { + this->wait_for_data_(); + } + + LWIP_LOCK(); + return this->read_locked_(buf, len); +} + ssize_t LWIPRawImpl::readv(const struct iovec *iov, int iovcnt) { + // See waiting_for_data_() for safety of unlocked reads. + if (this->recv_timeout_cs_ > 0 && this->waiting_for_data_()) { + this->wait_for_data_(); + } + LWIP_LOCK(); // Hold for entire scatter-gather operation ssize_t ret = 0; for (int i = 0; i < iovcnt; i++) { - ssize_t err = this->read(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len); + ssize_t err = this->read_locked_(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len); if (err == -1) { if (ret != 0) { // if we already read some don't return an error diff --git a/esphome/components/socket/lwip_raw_tcp_impl.h b/esphome/components/socket/lwip_raw_tcp_impl.h index 95931afcf3..3c27d71062 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.h +++ b/esphome/components/socket/lwip_raw_tcp_impl.h @@ -57,6 +57,7 @@ class LWIPRawCommon { // instead use it for determining whether to call lwip_output bool nodelay_ = false; sa_family_t family_ = 0; + uint8_t recv_timeout_cs_ = 0; // SO_RCVTIMEO in centiseconds (0 = no timeout, max 2.55s) }; /// Connected socket implementation for LWIP raw TCP. @@ -107,11 +108,8 @@ class LWIPRawImpl : public LWIPRawCommon { errno = ECONNRESET; return -1; } - if (blocking) { - // blocking operation not supported - errno = EINVAL; - return -1; - } + // Raw TCP doesn't use a blocking flag directly. Blocking behavior + // is provided by SO_RCVTIMEO which makes read() wait via socket_delay(). return 0; } int loop() { return 0; } @@ -122,6 +120,14 @@ class LWIPRawImpl : public LWIPRawCommon { static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err); protected: + // True when the socket could receive data but none has arrived yet. + // Safe to call without LWIP_LOCK — only null-checks pointers and reads a bool, + // all atomic on ARM/Xtensa. A stale value is harmless: the caller either does + // an unnecessary wait (stale true) or skips it (stale false), and the + // authoritative recheck happens under LWIP_LOCK afterward. + bool waiting_for_data_() const { return this->rx_buf_ == nullptr && !this->rx_closed_ && this->pcb_ != nullptr; } + void wait_for_data_(); + ssize_t read_locked_(void *buf, size_t len); ssize_t internal_write_(const void *buf, size_t len); int internal_output_(); From 2ba807efe85c5e1216f793b2b1354576d38a49bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:16:08 -1000 Subject: [PATCH 555/609] [adc] Fix PICO_VSYS_PIN compile error on RP2350 boards (#14724) --- esphome/components/adc/adc_sensor_rp2040.cpp | 7 +++++++ tests/components/adc/test.rp2040-pico2-ard.yaml | 11 +++++++++++ tests/components/spi/test.rp2040-pico2-ard.yaml | 6 ++++++ .../build_components_base.rp2040-pico2-ard.yaml | 15 +++++++++++++++ .../common/spi/rp2040-pico2-ard.yaml | 12 ++++++++++++ 5 files changed, 51 insertions(+) create mode 100644 tests/components/adc/test.rp2040-pico2-ard.yaml create mode 100644 tests/components/spi/test.rp2040-pico2-ard.yaml create mode 100644 tests/test_build_components/build_components_base.rp2040-pico2-ard.yaml create mode 100644 tests/test_build_components/common/spi/rp2040-pico2-ard.yaml diff --git a/esphome/components/adc/adc_sensor_rp2040.cpp b/esphome/components/adc/adc_sensor_rp2040.cpp index 8496e0f41e..a79707e234 100644 --- a/esphome/components/adc/adc_sensor_rp2040.cpp +++ b/esphome/components/adc/adc_sensor_rp2040.cpp @@ -8,6 +8,13 @@ #endif // CYW43_USES_VSYS_PIN #include <hardware/adc.h> +// PICO_VSYS_PIN is defined in pico-sdk board headers (e.g. boards/pico2.h), +// but the Arduino framework's config_autogen.h includes a generic board header +// that doesn't define it. Provide the standard value (pin 29) as a fallback. +#ifndef PICO_VSYS_PIN +#define PICO_VSYS_PIN 29 // NOLINT(cppcoreguidelines-macro-usage) +#endif + namespace esphome { namespace adc { diff --git a/tests/components/adc/test.rp2040-pico2-ard.yaml b/tests/components/adc/test.rp2040-pico2-ard.yaml new file mode 100644 index 0000000000..4cc865bb5d --- /dev/null +++ b/tests/components/adc/test.rp2040-pico2-ard.yaml @@ -0,0 +1,11 @@ +sensor: + - id: my_sensor + platform: adc + pin: VCC + name: ADC Test sensor + update_interval: "1:01" + unit_of_measurement: "°C" + icon: "mdi:water-percent" + accuracy_decimals: 5 + setup_priority: -100 + force_update: true diff --git a/tests/components/spi/test.rp2040-pico2-ard.yaml b/tests/components/spi/test.rp2040-pico2-ard.yaml new file mode 100644 index 0000000000..81a8acafd8 --- /dev/null +++ b/tests/components/spi/test.rp2040-pico2-ard.yaml @@ -0,0 +1,6 @@ +substitutions: + clk_pin: GPIO2 + mosi_pin: GPIO3 + miso_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/test_build_components/build_components_base.rp2040-pico2-ard.yaml b/tests/test_build_components/build_components_base.rp2040-pico2-ard.yaml new file mode 100644 index 0000000000..0922a5238e --- /dev/null +++ b/tests/test_build_components/build_components_base.rp2040-pico2-ard.yaml @@ -0,0 +1,15 @@ +esphome: + name: componenttestrp2040pico2ard + friendly_name: $component_name + +rp2040: + board: rpipico2 + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_test_file: $component_test_file diff --git a/tests/test_build_components/common/spi/rp2040-pico2-ard.yaml b/tests/test_build_components/common/spi/rp2040-pico2-ard.yaml new file mode 100644 index 0000000000..205beb6e1b --- /dev/null +++ b/tests/test_build_components/common/spi/rp2040-pico2-ard.yaml @@ -0,0 +1,12 @@ +# Common SPI configuration for RP2040 Pico 2 (RP2350) Arduino tests + +substitutions: + clk_pin: GPIO18 + mosi_pin: GPIO19 + miso_pin: GPIO16 + +spi: + - id: spi_bus + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} From cb4d1d1b5e2f6a354b4b873e444386a4fd4b2924 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:16:23 -1000 Subject: [PATCH 556/609] [api] Fix undefined behavior in noise handshake with empty rx buffer (#14722) --- esphome/components/api/api_frame_helper_noise.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 3e6ecf9dc3..f945253c89 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -258,10 +258,13 @@ APIError APINoiseFrameHelper::state_action_() { // ignore contents, may be used in future for flags // Resize for: existing prologue + 2 size bytes + frame data size_t old_size = this->prologue_.size(); - this->prologue_.resize(old_size + 2 + this->rx_buf_.size()); - this->prologue_[old_size] = (uint8_t) (this->rx_buf_.size() >> 8); - this->prologue_[old_size + 1] = (uint8_t) this->rx_buf_.size(); - std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), this->rx_buf_.size()); + size_t rx_size = this->rx_buf_.size(); + this->prologue_.resize(old_size + 2 + rx_size); + this->prologue_[old_size] = (uint8_t) (rx_size >> 8); + this->prologue_[old_size + 1] = (uint8_t) rx_size; + if (rx_size > 0) { + std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size); + } state_ = State::SERVER_HELLO; } From 23c7e0f8030d5caa5b07a9447293114639e89472 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:16:38 -1000 Subject: [PATCH 557/609] [uart] Allow hardware UART with single pin on RP2040 (#14725) --- .../components/uart/uart_component_rp2040.cpp | 37 +++++++++++++++---- tests/components/uart/test.rp2040-ard.yaml | 3 ++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index 858f1a02dd..6f6f1fb96b 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -105,15 +105,34 @@ void RP2040UartComponent::setup() { } } + // Determine which hardware UART to use. A pin that is not specified + // should not prevent hardware UART selection — one-way UART is valid. + // When both pins are configured, both must be HW-capable and agree on UART number. + // When only one pin is configured (nullptr other), use that pin's HW UART. + // If a pin is configured but not HW-capable (inverted/invalid), fall back to SerialPIO. + int8_t hw_uart = -1; + const bool tx_configured = (this->tx_pin_ != nullptr); + const bool rx_configured = (this->rx_pin_ != nullptr); + + if (tx_configured && rx_configured) { + // Both pins configured — both must map to the same hardware UART + if (tx_hw != -1 && rx_hw != -1 && tx_hw == rx_hw) { + hw_uart = tx_hw; + } + } else if (tx_configured) { + hw_uart = tx_hw; + } else if (rx_configured) { + hw_uart = rx_hw; + } + #ifdef USE_LOGGER - if (tx_hw == rx_hw && logger::global_logger->get_uart() == tx_hw) { - ESP_LOGD(TAG, "Using SerialPIO as UART%d is taken by the logger", tx_hw); - tx_hw = -1; - rx_hw = -1; + if (hw_uart != -1 && logger::global_logger->get_uart() == hw_uart) { + ESP_LOGD(TAG, "Using SerialPIO as UART%d is taken by the logger", hw_uart); + hw_uart = -1; } #endif - if (tx_hw == -1 || rx_hw == -1 || tx_hw != rx_hw) { + if (hw_uart == -1) { ESP_LOGV(TAG, "Using SerialPIO"); pin_size_t tx = this->tx_pin_ == nullptr ? NOPIN : this->tx_pin_->get_pin(); pin_size_t rx = this->rx_pin_ == nullptr ? NOPIN : this->rx_pin_->get_pin(); @@ -127,13 +146,15 @@ void RP2040UartComponent::setup() { } else { ESP_LOGV(TAG, "Using Hardware Serial"); SerialUART *serial; - if (tx_hw == 0) { + if (hw_uart == 0) { serial = &Serial1; } else { serial = &Serial2; } - serial->setTX(this->tx_pin_->get_pin()); - serial->setRX(this->rx_pin_->get_pin()); + if (this->tx_pin_ != nullptr) + serial->setTX(this->tx_pin_->get_pin()); + if (this->rx_pin_ != nullptr) + serial->setRX(this->rx_pin_->get_pin()); serial->setFIFOSize(this->rx_buffer_size_); serial->begin(this->baud_rate_, config); this->serial_ = serial; diff --git a/tests/components/uart/test.rp2040-ard.yaml b/tests/components/uart/test.rp2040-ard.yaml index 5eb2b533ea..1d5f91c6a7 100644 --- a/tests/components/uart/test.rp2040-ard.yaml +++ b/tests/components/uart/test.rp2040-ard.yaml @@ -23,3 +23,6 @@ uart: baud_rate: 115200 debug: debug_prefix: "[UART1] " + - id: uart_rx_only + rx_pin: 17 + baud_rate: 1200 From 14c3e2d9d948837a35aff984646bf55a9a225b33 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:16:53 -1000 Subject: [PATCH 558/609] [api] Fix heap-buffer-overflow in protobuf message dump for StringRef (#14721) --- esphome/components/api/api_pb2_dump.cpp | 2 +- script/api_protobuf/api_protobuf.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 740bf2e47f..5a53f0281f 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -13,7 +13,7 @@ namespace esphome::api { static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) { out.append("'"); if (!ref.empty()) { - out.append(ref.c_str()); + out.append(ref.c_str(), ref.size()); } out.append("'"); } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index b4044c362c..dff6c7690a 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -642,7 +642,7 @@ class StringType(TypeInfo): # For SOURCE_BOTH, check if StringRef is set (sending) or use string (received) return ( f"if (!this->{self.field_name}_ref_.empty()) {{" - f' out.append("\'").append(this->{self.field_name}_ref_.c_str()).append("\'");' + f' out.append("\'").append(this->{self.field_name}_ref_.c_str(), this->{self.field_name}_ref_.size()).append("\'");' f"}} else {{" f' out.append("\'").append(this->{self.field_name}).append("\'");' f"}}" @@ -2705,7 +2705,7 @@ namespace esphome::api { static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) { out.append("'"); if (!ref.empty()) { - out.append(ref.c_str()); + out.append(ref.c_str(), ref.size()); } out.append("'"); } From 390bb0451ffd06cb4e2d88674c9af76bb88c25f0 Mon Sep 17 00:00:00 2001 From: Brian Kaufman <bkaufx@gmail.com> Date: Thu, 12 Mar 2026 16:23:29 -0700 Subject: [PATCH 559/609] [OTA] Stage exact uploaded size for ESP8266 web OTA (gzip fix) (#14741) --- esphome/components/ota/ota_backend_esp8266.cpp | 7 +++++-- esphome/components/ota/ota_backend_esp8266.h | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/ota/ota_backend_esp8266.cpp b/esphome/components/ota/ota_backend_esp8266.cpp index 1f9a77e426..93e6249fb3 100644 --- a/esphome/components/ota/ota_backend_esp8266.cpp +++ b/esphome/components/ota/ota_backend_esp8266.cpp @@ -105,6 +105,7 @@ OTAResponseTypes ESP8266OTABackend::begin(size_t image_size) { this->current_address_ = this->start_address_; this->image_size_ = image_size; + this->bytes_received_ = 0; this->buffer_len_ = 0; this->md5_set_ = false; @@ -140,6 +141,7 @@ OTAResponseTypes ESP8266OTABackend::write(uint8_t *data, size_t len) { size_t to_buffer = std::min(len - written, this->buffer_size_ - this->buffer_len_); memcpy(this->buffer_.get() + this->buffer_len_, data + written, to_buffer); this->buffer_len_ += to_buffer; + this->bytes_received_ += to_buffer; written += to_buffer; // If buffer is full, write to flash @@ -252,8 +254,8 @@ OTAResponseTypes ESP8266OTABackend::end() { } } - // Calculate actual bytes written - size_t actual_size = this->current_address_ - this->start_address_; + // Calculate actual bytes written (exact uploaded size, excluding flash write padding) + size_t actual_size = this->bytes_received_; // Check if any data was written if (actual_size == 0) { @@ -304,6 +306,7 @@ void ESP8266OTABackend::abort() { this->buffer_.reset(); this->buffer_len_ = 0; this->image_size_ = 0; + this->bytes_received_ = 0; esp8266::preferences_prevent_write(false); } diff --git a/esphome/components/ota/ota_backend_esp8266.h b/esphome/components/ota/ota_backend_esp8266.h index 6213289acc..b364e216a3 100644 --- a/esphome/components/ota/ota_backend_esp8266.h +++ b/esphome/components/ota/ota_backend_esp8266.h @@ -48,6 +48,7 @@ class ESP8266OTABackend final { uint32_t start_address_{0}; uint32_t current_address_{0}; size_t image_size_{0}; + size_t bytes_received_{0}; md5::MD5Digest md5_{}; uint8_t expected_md5_[16]; // Fixed-size buffer for 128-bit (16-byte) MD5 digest From 93be53978925e7eb76035a7ca0356282bea7ae21 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 13:28:25 -1000 Subject: [PATCH 560/609] [light] Fix ambiguous set_effect overload for const char* (#14732) --- .../addressable_light/addressable_light_display.h | 2 +- esphome/components/light/light_call.cpp | 2 +- esphome/components/light/light_call.h | 2 ++ tests/components/light/common.yaml | 6 ++++++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/esphome/components/addressable_light/addressable_light_display.h b/esphome/components/addressable_light/addressable_light_display.h index 53f8604b7d..d9b8680547 100644 --- a/esphome/components/addressable_light/addressable_light_display.h +++ b/esphome/components/addressable_light/addressable_light_display.h @@ -33,7 +33,7 @@ class AddressableLightDisplay : public display::DisplayBuffer { // - Save the current effect index. this->last_effect_index_ = light_state_->get_current_effect_index(); // - Disable any current effect. - light_state_->make_call().set_effect(0).perform(); + light_state_->make_call().set_effect(uint32_t{0}).perform(); } } enabled_ = enabled; diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 14cd0e92f6..cd45994f62 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -506,7 +506,7 @@ color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() { LightCall &LightCall::set_effect(const char *effect, size_t len) { if (len == 4 && strncasecmp(effect, "none", 4) == 0) { - this->set_effect(0); + this->set_effect(uint32_t{0}); return *this; } diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index 0926ab6108..0eb1785239 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -130,6 +130,8 @@ class LightCall { LightCall &set_effect(optional<std::string> effect); /// Set the effect of the light by its name. LightCall &set_effect(const std::string &effect) { return this->set_effect(effect.data(), effect.size()); } + /// Set the effect of the light by its name (const char * overload to resolve ambiguity). + LightCall &set_effect(const char *effect) { return this->set_effect(effect, strlen(effect)); } /// Set the effect of the light by its name and length (zero-copy from API). LightCall &set_effect(const char *effect, size_t len); /// Set the effect of the light by its internal index number (only for internal use). diff --git a/tests/components/light/common.yaml b/tests/components/light/common.yaml index e5fab62a79..e1216e7b60 100644 --- a/tests/components/light/common.yaml +++ b/tests/components/light/common.yaml @@ -60,6 +60,12 @@ esphome: } } + # Test set_effect with const char* doesn't cause ambiguous overload (issue #14728) + - lambda: |- + auto call = id(test_monochromatic_light).turn_on(); + call.set_effect("None"); + call.perform(); + - light.toggle: test_binary_light - light.turn_off: test_rgb_light - light.turn_on: From 0b99e8f08d0cca47ccc49e93f0e063b7315cd80b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 14:47:16 -1000 Subject: [PATCH 561/609] [rp2040] Use full flash for sketch in testing mode (#14747) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/rp2040/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 276187b273..71e5f1488c 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -203,7 +203,12 @@ async def to_code(config): cg.add_build_flag(f"-Wl,--wrap={symbol}") cg.add_platformio_option("board_build.core", "earlephilhower") - cg.add_platformio_option("board_build.filesystem_size", "1m") + # In testing mode, use all flash for sketch to allow linking grouped component tests. + # Real RP2040 hardware uses 1MB filesystem + 1MB sketch, but CI tests may combine + # many components that exceed the 1MB sketch partition. + cg.add_platformio_option( + "board_build.filesystem_size", "0m" if CORE.testing_mode else "1m" + ) ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] cg.add_define( From 910784ca841d1a117285df0eb8ba408fe6798b44 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 14:48:06 -1000 Subject: [PATCH 562/609] [debug] Fix missing reset reason for RP2040/RP2350 (#14740) --- esphome/components/debug/debug_rp2040.cpp | 64 +++++++++++++++++++++-- esphome/core/helpers.h | 22 ++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/esphome/components/debug/debug_rp2040.cpp b/esphome/components/debug/debug_rp2040.cpp index c9d41942db..8dc84a2673 100644 --- a/esphome/components/debug/debug_rp2040.cpp +++ b/esphome/components/debug/debug_rp2040.cpp @@ -1,23 +1,81 @@ #include "debug_component.h" #ifdef USE_RP2040 +#include "esphome/core/defines.h" #include "esphome/core/log.h" #include <Arduino.h> +#include <hardware/watchdog.h> +#if defined(PICO_RP2350) +#include <hardware/structs/powman.h> +#else +#include <hardware/structs/vreg_and_chip_reset.h> +#endif +#ifdef USE_RP2040_CRASH_HANDLER +#include "esphome/components/rp2040/crash_handler.h" +#endif namespace esphome { namespace debug { static const char *const TAG = "debug"; -const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; } +const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { + char *buf = buffer.data(); + const size_t size = RESET_REASON_BUFFER_SIZE; + size_t pos = 0; + +#if defined(PICO_RP2350) + uint32_t chip_reset = powman_hw->chip_reset; + if (chip_reset & 0x04000000) // HAD_GLITCH_DETECT + pos = buf_append_str(buf, size, pos, "Power supply glitch|"); + if (chip_reset & 0x00040000) // HAD_RUN_LOW + pos = buf_append_str(buf, size, pos, "RUN pin|"); + if (chip_reset & 0x00020000) // HAD_BOR + pos = buf_append_str(buf, size, pos, "Brown-out|"); + if (chip_reset & 0x00010000) // HAD_POR + pos = buf_append_str(buf, size, pos, "Power-on reset|"); +#else + uint32_t chip_reset = vreg_and_chip_reset_hw->chip_reset; + if (chip_reset & 0x00010000) // HAD_RUN + pos = buf_append_str(buf, size, pos, "RUN pin|"); + if (chip_reset & 0x00000100) // HAD_POR + pos = buf_append_str(buf, size, pos, "Power-on reset|"); +#endif + + if (watchdog_caused_reboot()) { + bool handled = false; +#ifdef USE_RP2040_CRASH_HANDLER + if (rp2040::crash_handler_has_data()) { + pos = buf_append_str(buf, size, pos, "Crash (HardFault)|"); + handled = true; + } +#endif + if (!handled) { + if (watchdog_enable_caused_reboot()) { + pos = buf_append_str(buf, size, pos, "Watchdog timeout|"); + } else { + pos = buf_append_str(buf, size, pos, "Software reset|"); + } + } + } + + // Remove trailing '|' + if (pos > 0 && buf[pos - 1] == '|') { + buf[pos - 1] = '\0'; + } else if (pos == 0) { + return "Unknown"; + } + + return buf; +} const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; } -uint32_t DebugComponent::get_free_heap_() { return rp2040.getFreeHeap(); } +uint32_t DebugComponent::get_free_heap_() { return ::rp2040.getFreeHeap(); } size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos) { constexpr size_t size = DEVICE_INFO_BUFFER_SIZE; char *buf = buffer.data(); - uint32_t cpu_freq = rp2040.f_cpu(); + uint32_t cpu_freq = ::rp2040.f_cpu(); ESP_LOGD(TAG, "CPU Frequency: %" PRIu32, cpu_freq); pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32, cpu_freq); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 70ac1574f0..b2517e2d7a 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -942,6 +942,28 @@ __attribute__((format(printf, 4, 5))) inline size_t buf_append_printf(char *buf, } #endif +/// Safely append a string to buffer without format parsing, returning new position (capped at size). +/// More efficient than buf_append_printf for plain string literals. +/// @param buf Output buffer +/// @param size Total buffer size +/// @param pos Current position in buffer +/// @param str String to append (must not be null) +/// @return New position after appending (capped at size on overflow) +inline size_t buf_append_str(char *buf, size_t size, size_t pos, const char *str) { + if (pos >= size) { + return size; + } + size_t remaining = size - pos - 1; // reserve space for null terminator + size_t len = strlen(str); + if (len > remaining) { + len = remaining; + } + memcpy(buf + pos, str, len); + pos += len; + buf[pos] = '\0'; + return pos; +} + /// Concatenate a name with a separator and suffix using an efficient stack-based approach. /// This avoids multiple heap allocations during string construction. /// Maximum name length supported is 120 characters for friendly names. From c263c2c382292eb33bec5ed560620964b6a25c34 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 14:48:29 -1000 Subject: [PATCH 563/609] [captive_portal] Fix captive portal inaccessible when web_server auth is configured (#14734) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/captive_portal/captive_portal.cpp | 4 ++-- esphome/components/web_server_base/web_server_base.cpp | 4 ++++ esphome/components/web_server_base/web_server_base.h | 8 ++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 5af6ab29a2..183f16c5f8 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -61,7 +61,7 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { // Defer save to main loop thread to avoid NVS operations from HTTP thread this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); }); #endif - request->redirect(ESPHOME_F("/?save")); + request->send(200, ESPHOME_F("text/plain"), ESPHOME_F("Saved. Connecting...")); } void CaptivePortal::setup() { @@ -71,7 +71,7 @@ void CaptivePortal::setup() { void CaptivePortal::start() { this->base_->init(); if (!this->initialized_) { - this->base_->add_handler(this); + this->base_->add_handler_without_auth(this); } network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp index dbbcd10d8d..3e1baf34ba 100644 --- a/esphome/components/web_server_base/web_server_base.cpp +++ b/esphome/components/web_server_base/web_server_base.cpp @@ -11,6 +11,10 @@ void WebServerBase::add_handler(AsyncWebHandler *handler) { handler = new internal::AuthMiddlewareHandler(handler, &credentials_); } #endif + this->add_handler_without_auth(handler); +} + +void WebServerBase::add_handler_without_auth(AsyncWebHandler *handler) { this->handlers_.push_back(handler); if (this->server_ != nullptr) { this->server_->addHandler(handler); diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index 54421c851e..48e13ad71e 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -122,6 +122,14 @@ class WebServerBase { #endif void add_handler(AsyncWebHandler *handler); + /** + * WARNING: Registers a handler that bypasses the USE_WEBSERVER_AUTH middleware. + * + * This should only be used for endpoints that are intentionally unauthenticated + * (for example, captive portal or very limited-status endpoints). For normal + * endpoints that should respect web server authentication, use add_handler(). + */ + void add_handler_without_auth(AsyncWebHandler *handler); void set_port(uint16_t port) { port_ = port; } uint16_t get_port() const { return port_; } From dc5032f72fa5571eebb2381c208f9b4deb33f93d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 14:48:41 -1000 Subject: [PATCH 564/609] [water_heater] Set OPERATION_MODE feature flag when modes are configured (#14748) --- .../template/water_heater/template_water_heater.cpp | 1 + tests/integration/test_water_heater_template.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/template/water_heater/template_water_heater.cpp b/esphome/components/template/water_heater/template_water_heater.cpp index 73081d204b..092df6fdca 100644 --- a/esphome/components/template/water_heater/template_water_heater.cpp +++ b/esphome/components/template/water_heater/template_water_heater.cpp @@ -26,6 +26,7 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() { if (!this->supported_modes_.empty()) { traits.set_supported_modes(this->supported_modes_); + traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_OPERATION_MODE); } traits.set_supports_current_temperature(true); diff --git a/tests/integration/test_water_heater_template.py b/tests/integration/test_water_heater_template.py index 096d4c8461..d63d1d6984 100644 --- a/tests/integration/test_water_heater_template.py +++ b/tests/integration/test_water_heater_template.py @@ -102,7 +102,11 @@ async def test_water_heater_template( f"Expected target temp 60.0, got {initial_state.target_temperature}" ) - # Verify supported features: away mode and on/off (fixture has away + is_on lambdas) + # Verify supported features: operation mode, away mode, and on/off + assert ( + test_water_heater.supported_features + & WaterHeaterFeature.SUPPORTS_OPERATION_MODE + ) != 0, "Expected SUPPORTS_OPERATION_MODE in supported_features" assert ( test_water_heater.supported_features & WaterHeaterFeature.SUPPORTS_AWAY_MODE ) != 0, "Expected SUPPORTS_AWAY_MODE in supported_features" From aacbaab5f800e26c7f614ec641f061dfcda4ce77 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 14:48:55 -1000 Subject: [PATCH 565/609] [wifi] Reject EAP/WPA2 Enterprise config on unsupported platforms (#14746) --- esphome/components/wifi/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 2808d31311..480ccd65c5 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -166,6 +166,7 @@ TTLS_PHASE_2 = { } EAP_AUTH_SCHEMA = cv.All( + cv.only_on([Platform.ESP32, Platform.ESP8266]), cv.Schema( { cv.Optional(CONF_IDENTITY): cv.string_strict, From b0447dc52165abf25b18281bb0e5277e50ba896a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 14:49:07 -1000 Subject: [PATCH 566/609] [light] Fix binary light spamming 'brightness not supported' warning with strobe effect (#14735) --- esphome/components/light/light_call.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index cd45994f62..0b2d391fd6 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -214,7 +214,14 @@ LightColorValues LightCall::validate_() { if (this->has_brightness() && this->brightness_ == 0.0f) { this->state_ = false; this->set_flag_(FLAG_HAS_STATE); - this->brightness_ = 1.0f; + if (color_mode & ColorCapability::BRIGHTNESS) { + // Reset brightness so the light has nonzero brightness when turned back on. + this->brightness_ = 1.0f; + } else { + // Light doesn't support brightness; clear the flag to avoid a spurious + // "brightness not supported" warning during capability validation. + this->clear_flag_(FLAG_HAS_BRIGHTNESS); + } } // Set color brightness to 100% if currently zero and a color is set. From 039efdb02a765f554e33f0d2c897cd69fa9fd581 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 14:53:46 -1000 Subject: [PATCH 567/609] [i2c] Fix RP2040 I2C bus selection based on pin assignment (#14745) --- esphome/components/i2c/__init__.py | 37 ++++++++++++++++++++++ esphome/components/i2c/i2c_bus_arduino.cpp | 10 +++--- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index de3f2be674..1684f479ba 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -93,11 +93,31 @@ def _bus_declare_type(value): raise NotImplementedError +def _rp2040_i2c_controller(pin): + """Return the I2C controller number (0 or 1) for a given RP2040/RP2350 GPIO pin. + + See RP2040 datasheet Table 2 (section 1.4.3, "GPIO Functions"): + https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf + See RP2350 datasheet Table 7 (section 9.4, "Function Select"): + https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf + """ + return (pin // 2) % 2 + + def validate_config(config): if CORE.is_esp32: return cv.require_framework_version( esp_idf=cv.Version(5, 4, 2), esp32_arduino=cv.Version(3, 2, 1) )(config) + if CORE.is_rp2040: + sda_controller = _rp2040_i2c_controller(config[CONF_SDA]) + scl_controller = _rp2040_i2c_controller(config[CONF_SCL]) + if sda_controller != scl_controller: + raise cv.Invalid( + f"SDA pin GPIO{config[CONF_SDA]} is on I2C{sda_controller} but " + f"SCL pin GPIO{config[CONF_SCL]} is on I2C{scl_controller}. " + f"Both pins must be on the same I2C controller." + ) return config @@ -146,6 +166,23 @@ def _final_validate(config): full_config = fv.full_config.get()[CONF_I2C] if CORE.using_zephyr and len(full_config) > 1: raise cv.Invalid("Second i2c is not implemented on Zephyr yet") + if CORE.is_rp2040: + if len(full_config) > 2: + raise cv.Invalid( + "The maximum number of I2C interfaces for RP2040/RP2350 is 2" + ) + if len(full_config) > 1: + controllers = [ + _rp2040_i2c_controller(conf[CONF_SDA]) for conf in full_config + ] + if len(set(controllers)) != len(controllers): + raise cv.Invalid( + "Multiple I2C buses are configured to use the same I2C controller. " + "Each bus must use pins on a different controller. " + "The I2C controller is determined by (gpio / 2) % 2: " + "even pin pairs (0-1, 4-5, 8-9, ...) use I2C0, " + "odd pin pairs (2-3, 6-7, 10-11, ...) use I2C1." + ) if CORE.is_esp32 and get_esp32_variant() in ESP32_I2C_CAPABILITIES: variant = get_esp32_variant() max_num = ESP32_I2C_CAPABILITIES[variant]["NUM"] diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index 5120eb4c00..47a06abe9e 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -20,12 +20,14 @@ void ArduinoI2CBus::setup() { #if defined(USE_ESP8266) wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory) #elif defined(USE_RP2040) - static bool first = true; - if (first) { + // Select Wire instance based on pin assignment, not definition order. + // I2C controller = (gpio / 2) % 2: even pairs (0-1,4-5,...) → I2C0, odd pairs (2-3,6-7,...) → I2C1 + // RP2040 datasheet Table 2 (section 1.4.3): https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf + // RP2350 datasheet Table 7 (section 9.4): https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf + if ((this->sda_pin_ / 2) % 2 == 0) { wire_ = &Wire; - first = false; } else { - wire_ = &Wire1; // NOLINT(cppcoreguidelines-owning-memory) + wire_ = &Wire1; } #endif From 1ab1534028ef2052825e09301f26bb3e02f45d82 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 15:12:22 -1000 Subject: [PATCH 568/609] [mdns] Fix RP2040 mDNS not restarting after WiFi reconnect (#14737) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/mdns/mdns_component.h | 4 +++ esphome/components/mdns/mdns_rp2040.cpp | 32 ++++++++++++++++++---- esphome/components/wifi/__init__.py | 7 ----- esphome/components/wifi/wifi_component.cpp | 14 ---------- esphome/components/wifi/wifi_component.h | 4 --- 5 files changed, 30 insertions(+), 31 deletions(-) diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 13c8ccf288..47cad4bf71 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -129,6 +129,10 @@ class MDNSComponent final : public Component { #endif #ifdef USE_MDNS_STORE_SERVICES StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{}; +#endif +#ifdef USE_RP2040 + bool was_connected_{false}; + bool initialized_{false}; #endif void compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services, char *mac_address_buf); }; diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index 05d991c1fa..c0b22aa84f 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -36,12 +36,32 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV } void MDNSComponent::setup() { - this->setup_buffers_and_register_(register_rp2040); - // Schedule MDNS.update() via set_interval() instead of overriding loop(). - // This removes the component from the per-iteration loop list entirely, - // eliminating virtual dispatch overhead on every main loop cycle. - // See MDNS_UPDATE_INTERVAL_MS comment in mdns_component.h for safety analysis. - this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); }); + // RP2040's LEAmDNS library registers a LwipIntf::stateUpCB() callback to restart + // mDNS when the network interface reconnects. However, stateUpCB() is stubbed out + // in arduino-pico's LwipIntfCB.cpp because the original ESP8266 implementation used + // schedule_function() which doesn't exist in arduino-pico, and the callback can't + // safely run directly since netif status callbacks fire from IRQ context + // (PICO_CYW43_ARCH_THREADSAFE_BACKGROUND) while _restart() allocates UDP sockets. + // + // Workaround: defer MDNS.begin() and service registration until the network is + // connected (has an IP), then call notifyAPChange() on subsequent reconnects to + // restart mDNS probing and announcing — all from main loop context so it's + // thread-safe. + this->set_interval(MDNS_UPDATE_INTERVAL_MS, [this]() { + bool connected = network::is_connected(); + if (connected && !this->was_connected_) { + if (!this->initialized_) { + this->setup_buffers_and_register_(register_rp2040); + this->initialized_ = true; + } else { + MDNS.notifyAPChange(); + } + } + this->was_connected_ = connected; + if (this->initialized_) { + MDNS.update(); + } + }); } void MDNSComponent::on_shutdown() { diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 480ccd65c5..9f73b1cc6f 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -563,13 +563,6 @@ async def to_code(config): cg.add_library("ESP8266WiFi", None) elif CORE.is_rp2040: cg.add_library("WiFi", None) - # RP2040's mDNS library (LEAmDNS) relies on LwipIntf::stateUpCB() to restart - # mDNS when the network interface reconnects. However, this callback is disabled - # in the arduino-pico framework. As a workaround, we block component setup until - # WiFi is connected via can_proceed(), ensuring mDNS.begin() is called with an - # active connection. This define enables the loop priority sorting infrastructure - # used during the setup blocking phase. - cg.add_define("USE_LOOP_PRIORITY") if CORE.is_esp32: if config[CONF_ENABLE_BTM] or config[CONF_ENABLE_RRM]: diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 60764955cc..09f883ed61 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -2109,20 +2109,6 @@ void WiFiComponent::retry_connect() { } } -#ifdef USE_RP2040 -// RP2040's mDNS library (LEAmDNS) relies on LwipIntf::stateUpCB() to restart -// mDNS when the network interface reconnects. However, this callback is disabled -// in the arduino-pico framework. As a workaround, we block component setup until -// WiFi is connected, ensuring mDNS.begin() is called with an active connection. - -bool WiFiComponent::can_proceed() { - if (!this->has_sta() || this->state_ == WIFI_COMPONENT_STATE_DISABLED || this->ap_setup_) { - return true; - } - return this->is_connected_(); -} -#endif - void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } bool WiFiComponent::is_connected_() const { return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index f340b708c9..883cc1344b 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -437,10 +437,6 @@ class WiFiComponent : public Component { void retry_connect(); -#ifdef USE_RP2040 - bool can_proceed() override; -#endif - void set_reboot_timeout(uint32_t reboot_timeout); bool is_connected() const { return this->connected_; } From 45e40223ac3a32fdca7a535f35da73b6d866f29b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 15:37:46 -1000 Subject: [PATCH 569/609] [rp2040] Fix compiler warnings in crash_handler and mdns (#14739) --- esphome/components/mdns/mdns_rp2040.cpp | 5 +++++ esphome/components/rp2040/crash_handler.cpp | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index c0b22aa84f..88f707afd3 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -7,7 +7,12 @@ #include "esphome/core/log.h" #include "mdns_component.h" +// Arduino-Pico's PolledTimeout.h (pulled in by ESP8266mDNS.h) redefines IRAM_ATTR to empty. +// Save and restore our definition around the include to avoid a redefinition warning. +#pragma push_macro("IRAM_ATTR") +#undef IRAM_ATTR #include <ESP8266mDNS.h> +#pragma pop_macro("IRAM_ATTR") namespace esphome::mdns { diff --git a/esphome/components/rp2040/crash_handler.cpp b/esphome/components/rp2040/crash_handler.cpp index 1f579c2d18..f9eb42a0f8 100644 --- a/esphome/components/rp2040/crash_handler.cpp +++ b/esphome/components/rp2040/crash_handler.cpp @@ -57,14 +57,14 @@ static const char *const TAG = "rp2040.crash"; // Placed in .noinit so BSS zero-init cannot race with crash_handler_read_and_clear(). // The valid field is explicitly cleared in crash_handler_read_and_clear() instead. -static struct { +static struct CrashData { bool valid; uint32_t pc; uint32_t lr; uint32_t sp; uint32_t backtrace[MAX_BACKTRACE]; uint8_t backtrace_count; -} __attribute__((section(".noinit"))) s_crash_data; +} s_crash_data __attribute__((section(".noinit"))); bool crash_handler_has_data() { return s_crash_data.valid; } From 18b54f075ee02f1f36da37681968b147554f89ba Mon Sep 17 00:00:00 2001 From: Kjell Braden <afflux@pentabarf.de> Date: Fri, 13 Mar 2026 14:18:42 +0100 Subject: [PATCH 570/609] [runtime_image] fix BMP parsing (#14762) --- .../components/runtime_image/bmp_decoder.h | 4 + .../fixtures/online_image_bmp.yaml | 27 ++++ tests/integration/test_online_image_bmp.py | 119 ++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 tests/integration/fixtures/online_image_bmp.yaml create mode 100644 tests/integration/test_online_image_bmp.py diff --git a/esphome/components/runtime_image/bmp_decoder.h b/esphome/components/runtime_image/bmp_decoder.h index 73e54f5430..a52a561584 100644 --- a/esphome/components/runtime_image/bmp_decoder.h +++ b/esphome/components/runtime_image/bmp_decoder.h @@ -26,6 +26,10 @@ class BmpDecoder : public ImageDecoder { int HOT decode(uint8_t *buffer, size_t size) override; bool is_finished() const override { + if (this->bits_per_pixel_ == 0) { + // header not yet received, so dimensions not yet determined + return false; + } // BMP is finished when we've decoded all pixel data return this->paint_index_ >= static_cast<size_t>(this->width_ * this->height_); } diff --git a/tests/integration/fixtures/online_image_bmp.yaml b/tests/integration/fixtures/online_image_bmp.yaml new file mode 100644 index 0000000000..e36514e9ae --- /dev/null +++ b/tests/integration/fixtures/online_image_bmp.yaml @@ -0,0 +1,27 @@ +esphome: + name: online-image-bmp + +host: + +http_request: + +display: + +online_image: + - url: http://127.0.0.1:HTTP_PORT/foo.bmp + id: myimg + format: BMP + type: RGB + on_download_finished: + logger.log: + format: "download finished. cache hit: %u" + args: [cached] + +api: + actions: + - action: fetch_image + then: + - component.update: myimg + +logger: + level: DEBUG diff --git a/tests/integration/test_online_image_bmp.py b/tests/integration/test_online_image_bmp.py new file mode 100644 index 0000000000..7c32154fdd --- /dev/null +++ b/tests/integration/test_online_image_bmp.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + +# black 8x8 RGB BMP, generated with +# from PIL import Image +# from io import BytesIO +# b = BytesIO() +# img = Image.new("RGB", (8, 8)) +# img.save(b, format="BMP") +# b.getvalue() +BMP_IMAGE = b"BM\xf6\x00\x00\x00\x00\x00\x00\x006\x00\x00\x00(\x00\x00\x00\x08\x00\x00\x00\x08\x00\x00\x00\x01\x00\x18\x00\x00\x00\x00\x00\xc0\x00\x00\x00\xc4\x0e\x00\x00\xc4\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +LEN_BMP_IMAGE = len(BMP_IMAGE) + + +def handle_http(http_request_future): + async def handler(reader, writer): + try: + async with asyncio.timeout(1.0): + data = await reader.readuntil(b"\r\n") + + # ensure our request matches the expectation + expected_request = b"GET /foo.bmp HTTP/1.1\r\n" + assert data[: len(expected_request)] == expected_request + + # consume rest of request + async with asyncio.timeout(1.0): + data = await reader.readuntil(b"\r\n\r\n") + + http_request_future.set_result(True) + + http_response = [ + b"HTTP/1.1 200 OK", + b"Content-Length: %d" % LEN_BMP_IMAGE, + b"Content-Type: text/plain", + b"Connection: close", + b"", + b"", + ] + writer.write(b"\r\n".join(http_response)) + await writer.drain() + + writer.write(BMP_IMAGE) + + await writer.drain() + except Exception as exc: + if not http_request_future.done(): + http_request_future.set_exception(exc) + raise + finally: + writer.close() + + return handler + + +@pytest.mark.asyncio +async def test_online_image_bmp( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Esphome shouldn't block the main loop when a http response is slow""" + loop = asyncio.get_running_loop() + + # Track http request + http_request_future = loop.create_future() + download_finished_future = loop.create_future() + downloaded_bytes_future = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + + if match := re.search(r"Image fully downloaded, (\d+) bytes", line): + downloaded_bytes_future.set_result(int(match.group(1))) + + if "download finished" in line: + download_finished_future.set_result(True) + + server = await asyncio.start_server( + handle_http(http_request_future), "127.0.0.1", 0 + ) + http_server_port = server.sockets[0].getsockname()[1] + + config = yaml_config.replace("HTTP_PORT", str(http_server_port)) + + # Run with log monitoring + async with ( + server, + run_compiled(config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device info + + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "online-image-bmp" + + # List services to find our test service + _, services = await client.list_entities_services() + + # Find test service + request_service = next((s for s in services if s.name == "fetch_image"), None) + + assert request_service is not None, "fetch_image service not found" + + await client.execute_service(request_service, {}) + + async with asyncio.timeout(0.1): + await http_request_future + + async with asyncio.timeout(0.5): + numbytes = await downloaded_bytes_future + assert numbytes == LEN_BMP_IMAGE + await download_finished_future From e9c265914768a3c5fea24b6c2fb28eeb211cfb90 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 13 Mar 2026 03:20:50 -1000 Subject: [PATCH 571/609] [select] Fix -Wmaybe-uninitialized warnings on ESP8266 (#14759) --- esphome/components/select/select_call.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 45fb42c116..83f5052fc8 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -41,7 +41,7 @@ SelectCall &SelectCall::with_index(size_t index) { this->operation_ = SELECT_OP_SET; if (index >= this->parent_->size()) { ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", this->parent_->get_name().c_str(), index); - this->index_ = {}; // Store nullopt for invalid index + this->index_ = nullopt; // Store nullopt for invalid index } else { this->index_ = index; } @@ -52,7 +52,7 @@ optional<size_t> SelectCall::calculate_target_index_(const char *name) { const auto &options = this->parent_->traits.get_options(); if (options.empty()) { ESP_LOGW(TAG, "'%s' - Select has no options", name); - return {}; + return nullopt; } if (this->operation_ == SELECT_OP_FIRST) { @@ -67,7 +67,7 @@ optional<size_t> SelectCall::calculate_target_index_(const char *name) { ESP_LOGD(TAG, "'%s' - Setting", name); if (!this->index_.has_value()) { ESP_LOGW(TAG, "'%s' - No option set", name); - return {}; + return nullopt; } return this->index_; } @@ -96,7 +96,7 @@ optional<size_t> SelectCall::calculate_target_index_(const char *name) { return active_index + 1; } - return {}; // Can't navigate further without cycling + return nullopt; // Can't navigate further without cycling } void SelectCall::perform() { From 49107f217463b8ceb9200fd3bb51aa38d1bb9527 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 13 Mar 2026 07:37:30 -1000 Subject: [PATCH 572/609] [api] Increase log Nagle coalescing on all platforms except ESP8266 (#14752) --- esphome/components/api/api_frame_helper.h | 24 ++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 98de24501e..5e07ad43a9 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -134,12 +134,16 @@ class APIFrameHelper { // // For log messages: Use Nagle to coalesce multiple small log packets into // fewer larger packets, reducing WiFi overhead. However, we limit batching - // to 3 messages to avoid excessive LWIP buffer pressure on memory-constrained - // devices like ESP8266. LWIP's TCP_OVERSIZE option coalesces the data into - // shared pbufs, but holding data too long waiting for Nagle's timer causes - // buffer exhaustion and dropped messages. + // to avoid excessive LWIP buffer pressure on memory-constrained devices. + // LWIP's TCP_OVERSIZE option coalesces the data into shared pbufs, but + // holding data too long waiting for Nagle's timer causes buffer exhaustion + // and dropped messages. // - // Flow: Log 1 (Nagle on) -> Log 2 (Nagle on) -> Log 3 (NODELAY, flush all) + // ESP32 (TCP_SND_BUF=4×MSS+) / RP2040 (8×MSS) / LibreTiny (4×MSS): 4 logs per cycle + // ESP8266 (2×MSS): 3 logs per cycle (tightest buffers) + // + // Flow (ESP32/RP2040/LT): Log 1 (Nagle on) -> Log 2 -> Log 3 -> Log 4 (NODELAY, flush) + // Flow (ESP8266): Log 1 (Nagle on) -> Log 2 -> Log 3 (NODELAY, flush all) // void set_nodelay_for_message(bool is_log_message) { if (!is_log_message) { @@ -150,7 +154,7 @@ class APIFrameHelper { return; } - // Log messages 1-3: state transitions -1 -> 1 -> 2 -> -1 (flush on 3rd) + // Log messages: state transitions -1 -> 1 -> ... -> LOG_NAGLE_COUNT -> -1 (flush) if (this->nodelay_state_ == NODELAY_ON) { this->set_nodelay_raw_(false); this->nodelay_state_ = 1; @@ -255,10 +259,16 @@ class APIFrameHelper { uint8_t tx_buf_tail_{0}; uint8_t tx_buf_count_{0}; // Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled - // (immediate send). Values 1-2 count log messages in the current Nagle batch. + // (immediate send). Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch. // After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset. + // ESP8266 has the tightest TCP send buffer (2×MSS) and needs conservative batching. + // ESP32 (4×MSS+), RP2040 (8×MSS), and LibreTiny (4×MSS) can coalesce more. static constexpr int8_t NODELAY_ON = -1; +#ifdef USE_ESP8266 static constexpr int8_t LOG_NAGLE_COUNT = 2; +#else + static constexpr int8_t LOG_NAGLE_COUNT = 3; +#endif int8_t nodelay_state_{NODELAY_ON}; // Internal helper to set TCP_NODELAY socket option From a064eceb9bf750979c74009b508c8148eca70d81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 13 Mar 2026 07:37:44 -1000 Subject: [PATCH 573/609] [template] Fix misleading 'Text value too long to save' warning (#14753) --- .../components/template/text/template_text.h | 32 ++--- .../fixtures/template_text_save.yaml | 23 +++ tests/integration/test_template_text_save.py | 131 ++++++++++++++++++ 3 files changed, 170 insertions(+), 16 deletions(-) create mode 100644 tests/integration/fixtures/template_text_save.yaml create mode 100644 tests/integration/test_template_text_save.py diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index 7f176db09e..229a61d9b8 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -24,23 +24,23 @@ class TemplateTextSaverBase { template<uint8_t SZ> class TextSaver : public TemplateTextSaverBase { public: bool save(const std::string &value) override { - int diff = value.compare(this->prev_); - if (diff != 0) { - // If string is bigger than the allocation, do not save it. - // We don't need to waste ram setting prev_value either. - int size = value.size(); - if (size <= SZ) { - // Make it into a length prefixed thing - unsigned char temp[SZ + 1]; - memcpy(temp + 1, value.c_str(), size); - // SZ should be pre checked at the schema level, it can't go past the char range. - temp[0] = ((unsigned char) size); - this->pref_.save(&temp); - this->prev_.assign(value); - return true; - } + if (value == this->prev_) { + return true; // No change, nothing to save } - return false; + // If string is bigger than the allocation, do not save it. + // We don't need to waste ram setting prev_value either. + int size = value.size(); + if (size > SZ) { + return false; + } + // Make it into a length prefixed thing + unsigned char temp[SZ + 1]; + memcpy(temp + 1, value.c_str(), size); + // SZ should be pre checked at the schema level, it can't go past the char range. + temp[0] = ((unsigned char) size); + this->pref_.save(&temp); + this->prev_.assign(value); + return true; } // Make the preference object. Fill the provided location with the saved data diff --git a/tests/integration/fixtures/template_text_save.yaml b/tests/integration/fixtures/template_text_save.yaml new file mode 100644 index 0000000000..526561732d --- /dev/null +++ b/tests/integration/fixtures/template_text_save.yaml @@ -0,0 +1,23 @@ +esphome: + name: host-template-text-save-test + +host: + +api: + batch_delay: 0ms + +logger: + +preferences: + flash_write_interval: 0s + +text: + - platform: template + name: "Test Text Restore" + id: test_text_restore + optimistic: true + min_length: 0 + max_length: 10 + mode: text + initial_value: "hello" + restore_value: true diff --git a/tests/integration/test_template_text_save.py b/tests/integration/test_template_text_save.py new file mode 100644 index 0000000000..47c8e3188a --- /dev/null +++ b/tests/integration/test_template_text_save.py @@ -0,0 +1,131 @@ +"""Integration test for template text restore_value persistence. + +Tests that: +1. A template text with restore_value saves its value to preferences +2. The saved value persists across restarts (binary re-run) +3. Setting the same value again does not produce a spurious "too long" warning +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path +import socket +from typing import Any + +from aioesphomeapi import TextInfo, TextState +import pytest + +from .conftest import run_binary_and_wait_for_port, wait_and_connect_api_client +from .state_utils import InitialStateHelper, require_entity +from .types import CompileFunction, ConfigWriter + + +@pytest.mark.asyncio +async def test_template_text_save( + yaml_config: str, + write_yaml_config: ConfigWriter, + compile_esphome: CompileFunction, + reserved_tcp_port: tuple[int, socket.socket], +) -> None: + """Test template text save/restore persistence and duplicate-save behavior.""" + port, port_socket = reserved_tcp_port + + # Clean up any stale preference file from previous runs + prefs_file = ( + Path.home() / ".esphome" / "prefs" / "host-template-text-save-test.prefs" + ) + if prefs_file.exists(): + prefs_file.unlink() + + # Write and compile once + config_path = await write_yaml_config(yaml_config) + binary_path = await compile_esphome(config_path) + + # Release the reserved port so the binary can bind to it + port_socket.close() + + # --- First run: set a value and verify no spurious warnings --- + warning_lines: list[str] = [] + + def capture_warnings(line: str) -> None: + if "too long to save" in line.lower(): + warning_lines.append(line) + + async with ( + run_binary_and_wait_for_port( + binary_path, "127.0.0.1", port, line_callback=capture_warnings + ), + wait_and_connect_api_client(port=port) as client, + ): + device_info = await client.device_info() + assert device_info.name == "host-template-text-save-test" + + entities, _ = await client.list_entities_services() + text_entity = require_entity( + entities, "test_text_restore", TextInfo, "Test Text Restore" + ) + + # Set up state tracking + loop = asyncio.get_running_loop() + state_futures: dict[int, asyncio.Future[Any]] = {} + + def on_state(state: Any) -> None: + if state.key in state_futures and not state_futures[state.key].done(): + state_futures[state.key].set_result(state) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + await initial_state_helper.wait_for_initial_states() + + # Verify initial value from config + initial = initial_state_helper.initial_states[text_entity.key] + assert isinstance(initial, TextState) + assert initial.state == "hello" + + async def wait_for_state(key: int, timeout: float = 2.0) -> Any: + state_futures[key] = loop.create_future() + try: + return await asyncio.wait_for(state_futures[key], timeout) + finally: + state_futures.pop(key, None) + + # Set a new value that fits within max_length + client.text_command(key=text_entity.key, state="world") + state = await wait_for_state(text_entity.key) + assert state.state == "world" + + # Set the same value again - should NOT produce "too long" warning + client.text_command(key=text_entity.key, state="world") + # Give time for the warning to appear (if any) + await asyncio.sleep(0.5) + + # No warnings should have appeared + assert warning_lines == [], ( + f"Unexpected 'too long to save' warning(s): {warning_lines}" + ) + + # --- Second run: verify the value was restored from preferences --- + async with ( + run_binary_and_wait_for_port(binary_path, "127.0.0.1", port), + wait_and_connect_api_client(port=port) as client, + ): + entities, _ = await client.list_entities_services() + text_entity = require_entity( + entities, "test_text_restore", TextInfo, "Test Text Restore" + ) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(lambda s: None)) + await initial_state_helper.wait_for_initial_states() + + # The value should be "world" - restored from preferences + restored = initial_state_helper.initial_states[text_entity.key] + assert isinstance(restored, TextState) + assert restored.state == "world", ( + f"Expected restored value 'world', got '{restored.state}'" + ) + + # Clean up preference file + if prefs_file.exists(): + prefs_file.unlink() From 98d98716203da8d8d7a42f3312bb547ce2c425cb Mon Sep 17 00:00:00 2001 From: leccelecce <24962424+leccelecce@users.noreply.github.com> Date: Sat, 14 Mar 2026 13:15:54 +0000 Subject: [PATCH 574/609] [online_image] Log download duration in milliseconds instead of seconds (#14803) --- esphome/components/online_image/online_image.cpp | 6 +++--- esphome/components/online_image/online_image.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index da866599c9..22bf6a3056 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -129,7 +129,7 @@ void OnlineImage::update() { } ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size); - this->start_time_ = ::time(nullptr); + this->start_time_ = millis(); this->enable_loop(); } @@ -155,8 +155,8 @@ void OnlineImage::loop() { // Finalize decoding this->end_decode(); - ESP_LOGD(TAG, "Image fully downloaded, %zu bytes in %" PRIu32 "s", this->downloader_->get_bytes_read(), - (uint32_t) (::time(nullptr) - this->start_time_)); + ESP_LOGD(TAG, "Image fully downloaded, %zu bytes in %" PRIu32 " ms", this->downloader_->get_bytes_read(), + millis() - this->start_time_); // Save caching headers this->etag_ = this->downloader_->get_response_header(ETAG_HEADER_NAME); diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index c7c80c7c66..12c2564526 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -97,7 +97,7 @@ class OnlineImage : public PollingComponent, */ std::string last_modified_ = ""; - time_t start_time_; + uint32_t start_time_{0}; }; template<typename... Ts> class OnlineImageSetUrlAction : public Action<Ts...> { From 632dbc8fe83a7de72841ad72965a4e484898cc81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 14 Mar 2026 08:12:04 -1000 Subject: [PATCH 575/609] [core] Inline LwIPLock as no-op on platforms without lwIP core locking (#14787) --- esphome/components/esp8266/helpers.cpp | 4 +--- esphome/components/libretiny/helpers.cpp | 4 +--- esphome/components/zephyr/core.cpp | 4 +--- esphome/core/helpers.h | 22 +++++++++++++++------- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/esphome/components/esp8266/helpers.cpp b/esphome/components/esp8266/helpers.cpp index 036594fa17..4a64ae181e 100644 --- a/esphome/components/esp8266/helpers.cpp +++ b/esphome/components/esp8266/helpers.cpp @@ -22,9 +22,7 @@ void Mutex::unlock() {} IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); } IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); } -// ESP8266 doesn't support lwIP core locking, so this is a no-op -LwIPLock::LwIPLock() {} -LwIPLock::~LwIPLock() {} +// ESP8266 LwIPLock is defined inline as a no-op in helpers.h void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) wifi_get_macaddr(STATION_IF, mac); diff --git a/esphome/components/libretiny/helpers.cpp b/esphome/components/libretiny/helpers.cpp index 37ae0fb455..21913e4a16 100644 --- a/esphome/components/libretiny/helpers.cpp +++ b/esphome/components/libretiny/helpers.cpp @@ -26,9 +26,7 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); } IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); } -// LibreTiny doesn't support lwIP core locking, so this is a no-op -LwIPLock::LwIPLock() {} -LwIPLock::~LwIPLock() {} +// LibreTiny LwIPLock is defined inline as a no-op in helpers.h void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) WiFi.macAddress(mac); diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index eee7fb3f4f..1d105a1057 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -76,9 +76,7 @@ void Mutex::unlock() { k_mutex_unlock(static_cast<k_mutex *>(this->handle_)); } IRAM_ATTR InterruptLock::InterruptLock() { state_ = irq_lock(); } IRAM_ATTR InterruptLock::~InterruptLock() { irq_unlock(state_); } -// Zephyr doesn't support lwIP core locking, so this is a no-op -LwIPLock::LwIPLock() {} -LwIPLock::~LwIPLock() {} +// Zephyr LwIPLock is defined inline as a no-op in helpers.h uint32_t random_uint32() { return rand(); } // NOLINT(cert-msc30-c, cert-msc50-cpp) bool random_bytes(uint8_t *data, size_t len) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index b2517e2d7a..dafd899ae4 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1801,19 +1801,27 @@ class InterruptLock { /** Helper class to lock the lwIP TCPIP core when making lwIP API calls from non-TCPIP threads. * - * This is needed on multi-threaded platforms (ESP32) when CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled. - * It ensures thread-safe access to lwIP APIs. + * This is needed on multi-threaded platforms (ESP32) when CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled, + * and on RP2040 when CYW43 WiFi is active (cyw43_arch_lwip_begin/end). * - * @note This follows the same pattern as InterruptLock - platform-specific implementations in helpers.cpp + * On platforms without lwIP core locking (ESP8266, LibreTiny, Zephyr), + * this is a no-op defined inline so the compiler can eliminate all call overhead. */ class LwIPLock { public: - LwIPLock(); - ~LwIPLock(); - - // Delete copy constructor and copy assignment operator to prevent accidental copying LwIPLock(const LwIPLock &) = delete; LwIPLock &operator=(const LwIPLock &) = delete; + +#if defined(USE_ESP32) || defined(USE_RP2040) + // Platforms with potential lwIP core locking — out-of-line implementations in helpers.cpp + LwIPLock(); + ~LwIPLock(); +#else + // No lwIP core locking — inline no-ops (empty bodies instead of = default + // to prevent clang-tidy unused-variable warnings at call sites) + LwIPLock() {} + ~LwIPLock() {} +#endif }; /** Helper class to request `loop()` to be called as fast as possible. From 22ea2764d4c48edfb365150a11be9c557806f9bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 14 Mar 2026 13:17:32 -1000 Subject: [PATCH 576/609] [debug] Fix shared buffer between reset reason and wakeup cause (#14813) --- esphome/components/debug/debug_component.h | 3 ++- esphome/components/debug/debug_esp32.cpp | 9 +++++---- esphome/components/debug/debug_esp8266.cpp | 2 +- esphome/components/debug/debug_host.cpp | 2 +- esphome/components/debug/debug_libretiny.cpp | 2 +- esphome/components/debug/debug_rp2040.cpp | 2 +- esphome/components/debug/debug_zephyr.cpp | 2 +- 7 files changed, 12 insertions(+), 10 deletions(-) diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h index e4f4bb36eb..3da6b800c6 100644 --- a/esphome/components/debug/debug_component.h +++ b/esphome/components/debug/debug_component.h @@ -18,6 +18,7 @@ namespace debug { static constexpr size_t DEVICE_INFO_BUFFER_SIZE = 256; static constexpr size_t RESET_REASON_BUFFER_SIZE = 128; +static constexpr size_t WAKEUP_CAUSE_BUFFER_SIZE = 128; // buf_append_printf is now provided by esphome/core/helpers.h @@ -94,7 +95,7 @@ class DebugComponent : public PollingComponent { #endif // USE_TEXT_SENSOR const char *get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer); - const char *get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer); + const char *get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer); uint32_t get_free_heap_(); size_t get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos); void update_platform_(); diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index 6898621dd0..c9df4fdf21 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -98,7 +98,7 @@ static const char *const WAKEUP_CAUSES[] = { "BT", }; -const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { +const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { const char *wake_reason; unsigned reason = esp_sleep_get_wakeup_cause(); if (reason < sizeof(WAKEUP_CAUSES) / sizeof(WAKEUP_CAUSES[0])) { @@ -196,9 +196,10 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> uint32_t cpu_freq_mhz = arch_get_cpu_freq_hz() / 1000000; pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz); - char reason_buffer[RESET_REASON_BUFFER_SIZE]; - const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer)); - const char *wakeup_cause = get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer)); + char reset_buffer[RESET_REASON_BUFFER_SIZE]; + char wakeup_buffer[WAKEUP_CAUSE_BUFFER_SIZE]; + const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reset_buffer)); + const char *wakeup_cause = get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE>(wakeup_buffer)); uint8_t mac[6]; get_mac_address_raw(mac); diff --git a/esphome/components/debug/debug_esp8266.cpp b/esphome/components/debug/debug_esp8266.cpp index 4df4aaa851..0519ab72fe 100644 --- a/esphome/components/debug/debug_esp8266.cpp +++ b/esphome/components/debug/debug_esp8266.cpp @@ -91,7 +91,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE return buffer.data(); } -const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { +const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { // ESP8266 doesn't have detailed wakeup cause like ESP32 return ""; } diff --git a/esphome/components/debug/debug_host.cpp b/esphome/components/debug/debug_host.cpp index 2fa88f0909..0dfab86e4c 100644 --- a/esphome/components/debug/debug_host.cpp +++ b/esphome/components/debug/debug_host.cpp @@ -7,7 +7,7 @@ namespace debug { const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; } -const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; } +const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { return ""; } uint32_t DebugComponent::get_free_heap_() { return INT_MAX; } diff --git a/esphome/components/debug/debug_libretiny.cpp b/esphome/components/debug/debug_libretiny.cpp index 39269d6f2f..1d458c602a 100644 --- a/esphome/components/debug/debug_libretiny.cpp +++ b/esphome/components/debug/debug_libretiny.cpp @@ -12,7 +12,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE return lt_get_reboot_reason_name(lt_get_reboot_reason()); } -const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; } +const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { return ""; } uint32_t DebugComponent::get_free_heap_() { return lt_heap_get_free(); } diff --git a/esphome/components/debug/debug_rp2040.cpp b/esphome/components/debug/debug_rp2040.cpp index 8dc84a2673..73f08492c8 100644 --- a/esphome/components/debug/debug_rp2040.cpp +++ b/esphome/components/debug/debug_rp2040.cpp @@ -67,7 +67,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE return buf; } -const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; } +const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { return ""; } uint32_t DebugComponent::get_free_heap_() { return ::rp2040.getFreeHeap(); } diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index bd6432e949..bf87b7ae3d 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -53,7 +53,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE return buf; } -const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { +const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { // Zephyr doesn't have detailed wakeup cause like ESP32 return ""; } From deb6b97eea98feea201c73402df493c30a502e7d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 16 Mar 2026 09:25:21 +1300 Subject: [PATCH 577/609] Bump version to 2026.3.0b2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 9f5cd0a2ce..4ec3a24c9f 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.3.0b1 +PROJECT_NUMBER = 2026.3.0b2 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index eb49c9a1d7..2466f2c49c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.3.0b1" +__version__ = "2026.3.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 06d1498c47e78a9635333a109a089c58309f50c2 Mon Sep 17 00:00:00 2001 From: guillempages <guillempages@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:15:20 +0100 Subject: [PATCH 578/609] [runtime_image] Update jpegdec lib version (#14726) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .clang-tidy.hash | 2 +- esphome/components/runtime_image/__init__.py | 2 +- platformio.ini | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index ff25675918..87b4ebb2c6 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -e4b9c4b54e705d3c9400e1cdda8ba0b32634780cfa5f32271832e911bdcafe7e +8e48e836c6fc196d3da000d46eb09db243b87fe33518a74e49c8e009d756074a diff --git a/esphome/components/runtime_image/__init__.py b/esphome/components/runtime_image/__init__.py index 0773a53d91..7c22bfc9d1 100644 --- a/esphome/components/runtime_image/__init__.py +++ b/esphome/components/runtime_image/__init__.py @@ -74,7 +74,7 @@ class JPEGFormat(Format): def actions(self) -> None: cg.add_define("USE_RUNTIME_IMAGE_JPEG") - cg.add_library("JPEGDEC", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2") + cg.add_library("JPEGDEC", "1.8.4", "https://github.com/bitbank2/JPEGDEC#1.8.4") class PNGFormat(Format): diff --git a/platformio.ini b/platformio.ini index deee23d049..3c3d62ef76 100644 --- a/platformio.ini +++ b/platformio.ini @@ -46,11 +46,11 @@ lib_deps_base = lib_deps = ${common.lib_deps_base} - esphome/noise-c@0.1.11 ; api + esphome/noise-c@0.1.11 ; api improv/Improv@1.2.4 ; improv_serial / esp32_improv kikuchan98/pngle@1.1.0 ; online_image ; Using the repository directly, otherwise ESP-IDF can't use the library - https://github.com/bitbank2/JPEGDEC.git#ca1e0f2 ; online_image + https://github.com/bitbank2/JPEGDEC.git#1.8.4 ; online_image ; This dependency is used only in unit tests. ; Must coincide with PLATFORMIO_GOOGLE_TEST_LIB in scripts/cpp_unit_test.py ; See scripts/cpp_unit_test.py and tests/components/README.md From 5d5c2723b2570d06dddab60a1152903df592d5a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 15 Mar 2026 18:14:41 -1000 Subject: [PATCH 579/609] [fastled] Include esp_lcd IDF component for ESP32-S3 compatibility (#14839) --- esphome/components/fastled_base/__init__.py | 5 +++++ tests/components/fastled_clockless/test.esp32-s3-ard.yaml | 1 + 2 files changed, 6 insertions(+) create mode 100644 tests/components/fastled_clockless/test.esp32-s3-ard.yaml diff --git a/esphome/components/fastled_base/__init__.py b/esphome/components/fastled_base/__init__.py index 11e8423258..c944e8a930 100644 --- a/esphome/components/fastled_base/__init__.py +++ b/esphome/components/fastled_base/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_OUTPUT_ID, CONF_RGB_ORDER, ) +from esphome.core import CORE CODEOWNERS = ["@OttoWinter"] fastled_base_ns = cg.esphome_ns.namespace("fastled_base") @@ -41,5 +42,9 @@ async def new_fastled_light(config): cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE])) cg.add_library("fastled/FastLED", "3.9.16") + if CORE.is_esp32: + from esphome.components.esp32 import include_builtin_idf_component + + include_builtin_idf_component("esp_lcd") await light.register_light(var, config) return var diff --git a/tests/components/fastled_clockless/test.esp32-s3-ard.yaml b/tests/components/fastled_clockless/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/fastled_clockless/test.esp32-s3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From d6fba390378b6e8469254c518b3e0e5d1312026b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 15 Mar 2026 18:15:01 -1000 Subject: [PATCH 580/609] [runtime_image] Add esp-dsp dependency for JPEGDEC SIMD on ESP32 (#14840) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/runtime_image/__init__.py | 8 ++++++++ esphome/idf_component.yml | 2 ++ .../online_image/test.esp32-s3-ard.yaml | 19 +++++++++++++++++++ .../online_image/test.esp32-s3-idf.yaml | 19 +++++++++++++++++++ 4 files changed, 48 insertions(+) create mode 100644 tests/components/online_image/test.esp32-s3-ard.yaml create mode 100644 tests/components/online_image/test.esp32-s3-idf.yaml diff --git a/esphome/components/runtime_image/__init__.py b/esphome/components/runtime_image/__init__.py index 7c22bfc9d1..3ae35cc5f1 100644 --- a/esphome/components/runtime_image/__init__.py +++ b/esphome/components/runtime_image/__init__.py @@ -11,6 +11,7 @@ from esphome.components.image import ( ) import esphome.config_validation as cv from esphome.const import CONF_FORMAT, CONF_ID, CONF_RESIZE, CONF_TYPE +from esphome.core import CORE AUTO_LOAD = ["image"] CODEOWNERS = ["@guillempages", "@clydebarrow", "@kahrendt"] @@ -75,6 +76,13 @@ class JPEGFormat(Format): def actions(self) -> None: cg.add_define("USE_RUNTIME_IMAGE_JPEG") cg.add_library("JPEGDEC", "1.8.4", "https://github.com/bitbank2/JPEGDEC#1.8.4") + if CORE.is_esp32: + from esphome.components.esp32 import add_idf_component + + # JPEGDEC uses ESP32-S3 SIMD optimizations (guarded by board-level + # ARDUINO_ESP32S3_DEV define) that require esp-dsp headers. + # On Arduino this overwrites the stub; on IDF it adds the component. + add_idf_component(name="espressif/esp-dsp", ref="1.7.1") class PNGFormat(Format): diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index df651ae15d..381ecdcbf6 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -5,6 +5,8 @@ dependencies: version: 2.0.3 esphome/micro-opus: version: 0.3.5 + espressif/esp-dsp: + version: "1.7.1" espressif/esp-tflite-micro: version: 1.3.3~1 espressif/esp32-camera: diff --git a/tests/components/online_image/test.esp32-s3-ard.yaml b/tests/components/online_image/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..9116fd86e0 --- /dev/null +++ b/tests/components/online_image/test.esp32-s3-ard.yaml @@ -0,0 +1,19 @@ +packages: + spi: !include ../../test_build_components/common/spi/esp32-s3-ard.yaml + +<<: !include common.yaml + +http_request: + +display: + - platform: ili9xxx + spi_id: spi_bus + id: main_lcd + model: ili9342 + cs_pin: 20 + dc_pin: 13 + reset_pin: 21 + invert_colors: true + lambda: |- + it.fill(Color(0, 0, 0)); + it.image(0, 0, id(online_rgba_image)); diff --git a/tests/components/online_image/test.esp32-s3-idf.yaml b/tests/components/online_image/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..f219f71ee2 --- /dev/null +++ b/tests/components/online_image/test.esp32-s3-idf.yaml @@ -0,0 +1,19 @@ +packages: + spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml + +<<: !include common.yaml + +http_request: + +display: + - platform: ili9xxx + spi_id: spi_bus + id: main_lcd + model: ili9342 + cs_pin: 20 + dc_pin: 13 + reset_pin: 21 + invert_colors: true + lambda: |- + it.fill(Color(0, 0, 0)); + it.image(0, 0, id(online_rgba_image)); From ffce637ea547698c034ab6fde3d04c3ee8fc6e41 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 05:43:38 +0000 Subject: [PATCH 581/609] Bump aioesphomeapi from 44.5.1 to 44.5.2 (#14849) 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 e634bcb104..da95dd5a13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.5.1 +aioesphomeapi==44.5.2 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From b8ce907976689a5f78ca4f80d483087d001fc5d8 Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Mon, 16 Mar 2026 08:08:05 +0100 Subject: [PATCH 582/609] [ble_nus] fix uart debug (#14850) --- esphome/components/ble_nus/ble_nus.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index d0d37dbf1c..2f60f81471 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -67,14 +67,14 @@ bool BLENUS::read_array(uint8_t *data, size_t len) { // First, use the peek buffer if available if (this->has_peek_) { +#ifdef USE_UART_DEBUGGER + this->debug_callback_.call(uart::UART_DIRECTION_RX, this->peek_buffer_); +#endif data[0] = this->peek_buffer_; this->has_peek_ = false; data++; if (--len == 0) { // Decrement len first, then check it... -#ifdef USE_UART_DEBUGGER - this->debug_callback_.call(uart::UART_DIRECTION_RX, this->peek_buffer_); -#endif - return true; // No more to read + return true; // No more to read } } From 0c260e483e2700401724706ffd02bc038c65fc93 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:39:26 -0400 Subject: [PATCH 583/609] [gpio][dallas_temp] Fix one_wire read64() and DS18S20 division by zero (#14866) --- esphome/components/dallas_temp/dallas_temp.cpp | 3 +++ esphome/components/gpio/one_wire/gpio_one_wire.cpp | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp index 13f2fa59bd..f119e28e78 100644 --- a/esphome/components/dallas_temp/dallas_temp.cpp +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -136,6 +136,9 @@ bool DallasTemperatureSensor::check_scratch_pad_() { float DallasTemperatureSensor::get_temp_c_() { int16_t temp = (this->scratch_pad_[1] << 8) | this->scratch_pad_[0]; if ((this->address_ & 0xff) == DALLAS_MODEL_DS18S20) { + if (this->scratch_pad_[7] == 0) { + return NAN; + } return (temp >> 1) + (this->scratch_pad_[7] - this->scratch_pad_[6]) / float(this->scratch_pad_[7]) - 0.25; } switch (this->resolution_) { diff --git a/esphome/components/gpio/one_wire/gpio_one_wire.cpp b/esphome/components/gpio/one_wire/gpio_one_wire.cpp index 4191c45de1..4e2a306fc9 100644 --- a/esphome/components/gpio/one_wire/gpio_one_wire.cpp +++ b/esphome/components/gpio/one_wire/gpio_one_wire.cpp @@ -131,7 +131,7 @@ uint8_t IRAM_ATTR GPIOOneWireBus::read8() { uint64_t IRAM_ATTR GPIOOneWireBus::read64() { InterruptLock lock; uint64_t ret = 0; - for (uint8_t i = 0; i < 8; i++) { + for (uint8_t i = 0; i < 64; i++) { ret |= (uint64_t(this->read_bit_()) << i); } return ret; From bb0a5dc8a8c2df1217dd9cab0402b2e9c29e2b7d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:40:24 -0400 Subject: [PATCH 584/609] [lilygo_t5_47] Fix Y coordinate mapping and clamp touch point count (#14865) --- .../lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp index b29e4c2154..ee6c2ee471 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp +++ b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp @@ -42,7 +42,7 @@ void LilygoT547Touchscreen::setup() { this->x_raw_max_ = this->display_->get_native_width(); } if (this->y_raw_max_ == this->y_raw_min_) { - this->x_raw_max_ = this->display_->get_native_height(); + this->y_raw_max_ = this->display_->get_native_height(); } } } @@ -64,6 +64,10 @@ void LilygoT547Touchscreen::update_touches() { } point = buffer[5] & 0xF; + if (point > 2) { + ESP_LOGW(TAG, "Invalid touch point count: %d", point); + point = 2; + } if (point == 1) { err = this->write_register(TOUCH_REGISTER, READ_TOUCH, 1); From f36b0fcb61f78ab2a01cfb03c57f5b1ad75b8fdc Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:42:13 -0400 Subject: [PATCH 585/609] [am43] Fix battery update throttle using wrong type (#14864) --- esphome/components/am43/sensor/am43_sensor.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/am43/sensor/am43_sensor.h b/esphome/components/am43/sensor/am43_sensor.h index 91973d8e33..195b96a19e 100644 --- a/esphome/components/am43/sensor/am43_sensor.h +++ b/esphome/components/am43/sensor/am43_sensor.h @@ -35,7 +35,7 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent uint8_t current_sensor_; // The AM43 often gets into a state where it spams loads of battery update // notifications. Here we will limit to no more than every 10s. - uint8_t last_battery_update_; + uint32_t last_battery_update_; }; } // namespace am43 From 9133582aa0c636bd3e81bc424b46c5248427ccb3 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:44:55 -0400 Subject: [PATCH 586/609] [as3935] Fix ENERGY_MASK dropping bit 4 of lightning energy MMSB (#14861) --- esphome/components/as3935/as3935.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/as3935/as3935.h b/esphome/components/as3935/as3935.h index 5dff1cb0ae..5f46dadfa8 100644 --- a/esphome/components/as3935/as3935.h +++ b/esphome/components/as3935/as3935.h @@ -41,7 +41,7 @@ enum AS3935RegisterMasks { INT_MASK = 0xF0, THRESH_MASK = 0x0F, R_SPIKE_MASK = 0xF0, - ENERGY_MASK = 0xF0, + ENERGY_MASK = 0xE0, CAP_MASK = 0xF0, LIGHT_MASK = 0xCF, DISTURB_MASK = 0xDF, From 0816b27398b0abb63c11d6f7a10e65ef95f6dcec Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:45:16 -0400 Subject: [PATCH 587/609] [core] Support both dot and dash separators in Version.parse (#14858) --- esphome/config_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 1eac53e9b2..32689dab27 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -314,7 +314,7 @@ class Version: @classmethod def parse(cls, value: str) -> Version: - match = re.match(r"^(\d+).(\d+).(\d+)-?(\w*)$", value) + match = re.match(r"^(\d+).(\d+).(\d+)[-.]?(\w*)$", value) if match is None: raise ValueError(f"Not a valid version number {value}") major = int(match[1]) From d6c67d5c357669e547f94a461614976e0d3d54cb Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:45:03 +1300 Subject: [PATCH 588/609] Bump version to 2026.3.0b3 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 4ec3a24c9f..96295b3fc8 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.3.0b2 +PROJECT_NUMBER = 2026.3.0b3 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 2466f2c49c..561a27d228 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.3.0b2" +__version__ = "2026.3.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 8bbfadb59aa39b02156653dee670f6c2990cd506 Mon Sep 17 00:00:00 2001 From: Diorcet Yann <diorcety@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:22:31 +0100 Subject: [PATCH 589/609] [core] Small improvements (#14884) --- esphome/components/bme68x_bsec2/__init__.py | 4 ++-- script/merge_component_configs.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/bme68x_bsec2/__init__.py b/esphome/components/bme68x_bsec2/__init__.py index 4200b2f0b8..5f0afa9c9f 100644 --- a/esphome/components/bme68x_bsec2/__init__.py +++ b/esphome/components/bme68x_bsec2/__init__.py @@ -186,8 +186,8 @@ async def to_code_base(config): cg.add_library("SPI", None) cg.add_library( "BME68x Sensor library", - "1.3.40408", - "https://github.com/boschsensortec/Bosch-BME68x-Library", + None, + "https://github.com/boschsensortec/Bosch-BME68x-Library#v1.3.40408", ) cg.add_library( "BSEC2 Software Library", diff --git a/script/merge_component_configs.py b/script/merge_component_configs.py index 5e98f1fef5..41bbafcd02 100755 --- a/script/merge_component_configs.py +++ b/script/merge_component_configs.py @@ -384,7 +384,7 @@ def merge_component_configs( # Write merged config output_file.parent.mkdir(parents=True, exist_ok=True) yaml_content = yaml_util.dump(merged_config_data) - output_file.write_text(yaml_content) + output_file.write_text(yaml_content, encoding="utf-8") print(f"Successfully merged {len(component_names)} components into {output_file}") From 37f9541f322d3ccee1c232df8b2a9a503165d41a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 08:14:36 -1000 Subject: [PATCH 590/609] [api] Fix ProtoMessage protected destructor compile error on host platform (#14882) --- esphome/components/api/proto.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index d1c955b1fb..44d8f04585 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -442,8 +442,12 @@ class ProtoMessage { virtual const char *message_name() const { return "unknown"; } #endif +#ifndef USE_HOST protected: +#endif // Non-virtual destructor is protected to prevent polymorphic deletion. + // On host platform, made public to allow value-initialization of std::array + // members (e.g. DeviceInfoResponse::devices) without clang errors. ~ProtoMessage() = default; }; From c5d42b05696f0238929447fcbb3db6ba1018ae82 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:18:49 -0400 Subject: [PATCH 591/609] [speaker] Fix media playlist using announcement delay (#14889) --- .../components/speaker/media_player/speaker_media_player.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index 9f168f854d..930373c6fc 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -417,7 +417,7 @@ void SpeakerMediaPlayer::loop() { this->media_playlist_.pop_front(); } // Only delay starting playback if moving on the next playlist item or repeating the current item - timeout_ms = this->announcement_playlist_delay_ms_; + timeout_ms = this->media_playlist_delay_ms_; } if (!this->media_playlist_.empty()) { PlaylistItem playlist_item = this->media_playlist_.front(); From 4122fa5dddaea6b354592dd41e20ad6179223b76 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 09:58:05 -1000 Subject: [PATCH 592/609] [core] Add back deprecated set_internal() for external projects (#14887) --- esphome/core/entity_base.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index cccbafd2c3..723bf54584 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -100,6 +100,14 @@ class EntityBase { // Get whether this Entity should be hidden outside ESPHome bool is_internal() const { return this->flags_.internal; } + // Deprecated: Calling set_internal() at runtime is undefined behavior. Components and clients + // are NOT notified of the change, the flag may have already been read during setup, and there + // is NO guarantee any consumer will observe the new value. Use the 'internal:' YAML key instead. + ESPDEPRECATED("set_internal() is undefined behavior at runtime — components and Home Assistant are NOT " + "notified. Use the 'internal:' YAML key instead. Will be removed in 2027.3.0.", + "2026.3.0") + void set_internal(bool internal) { this->flags_.internal = internal; } + // Check if this object is declared to be disabled by default. // That means that when the device gets added to Home Assistant (or other clients) it should // not be added to the default view by default, and a user action is necessary to manually add it. From 1b70df2c1f8a9a7edbc3b389fe6095ddd7e9f2b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 12:16:44 -1000 Subject: [PATCH 593/609] [espnow] Fix EventPool/LockFreeQueue sizing off-by-one (#14893) --- esphome/components/espnow/espnow_component.cpp | 6 ++++-- esphome/components/espnow/espnow_component.h | 8 ++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index 991803d870..78916891f4 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -87,7 +87,8 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status) // Push the packet to the queue global_esp_now->receive_packet_queue_.push(packet); - // Push always because we're the only producer and the pool ensures we never exceed queue size + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. // Wake main loop immediately to process ESP-NOW send event instead of waiting for select() timeout #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) @@ -109,7 +110,8 @@ void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int // Push the packet to the queue global_esp_now->receive_packet_queue_.push(packet); - // Push always because we're the only producer and the pool ensures we never exceed queue size + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. // Wake main loop immediately to process ESP-NOW receive event instead of waiting for select() timeout #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) diff --git a/esphome/components/espnow/espnow_component.h b/esphome/components/espnow/espnow_component.h index 9941e97227..ee4adc1b4d 100644 --- a/esphome/components/espnow/espnow_component.h +++ b/esphome/components/espnow/espnow_component.h @@ -163,10 +163,14 @@ class ESPNowComponent : public Component { uint8_t own_address_[ESP_NOW_ETH_ALEN]{0}; LockFreeQueue<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE> receive_packet_queue_{}; - EventPool<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE> receive_packet_pool_{}; + // Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring + // buffer that holds N-1 elements. This guarantees allocate() returns nullptr + // before push() can fail, preventing a pool slot leak. + EventPool<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE - 1> receive_packet_pool_{}; LockFreeQueue<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE> send_packet_queue_{}; - EventPool<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE> send_packet_pool_{}; + // Pool sized to queue capacity (SIZE-1) — see receive_packet_pool_ comment. + EventPool<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE - 1> send_packet_pool_{}; ESPNowSendPacket *current_send_packet_{nullptr}; // Currently sending packet, nullptr if none uint8_t wifi_channel_{0}; From 8caa11dcf45aed498be5ec8ef83611be86b9a547 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 12:17:43 -1000 Subject: [PATCH 594/609] [usb_cdc_acm] Fix EventPool/LockFreeQueue sizing off-by-one (#14894) --- .../components/usb_cdc_acm/usb_cdc_acm.cpp | 26 +++++++------------ esphome/components/usb_cdc_acm/usb_cdc_acm.h | 6 ++++- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp b/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp index a4c2e6c4a4..253626f0a3 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp @@ -26,16 +26,13 @@ void USBCDCACMInstance::queue_line_state_event(bool dtr, bool rts) { event->data.line_state.dtr = dtr; event->data.line_state.rts = rts; - if (!this->event_queue_.push(event)) { - ESP_LOGW(TAG, "Event queue full, line state event dropped (itf=%d)", this->itf_); - // Return event to pool since we couldn't queue it - this->event_pool_.release(event); - } else { - // Wake main loop immediately to process event + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. + this->event_queue_.push(event); + #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) - App.wake_loop_threadsafe(); + App.wake_loop_threadsafe(); #endif - } } void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity, @@ -53,16 +50,13 @@ void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_ event->data.line_coding.parity = parity; event->data.line_coding.data_bits = data_bits; - if (!this->event_queue_.push(event)) { - ESP_LOGW(TAG, "Event queue full, line coding event dropped (itf=%d)", this->itf_); - // Return event to pool since we couldn't queue it - this->event_pool_.release(event); - } else { - // Wake main loop immediately to process event + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. + this->event_queue_.push(event); + #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) - App.wake_loop_threadsafe(); + App.wake_loop_threadsafe(); #endif - } } void USBCDCACMInstance::process_events_() { diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.h b/esphome/components/usb_cdc_acm/usb_cdc_acm.h index 624f41cf8c..90c673a89e 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.h +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.h @@ -102,7 +102,11 @@ class USBCDCACMInstance : public uart::UARTComponent, public Parented<USBCDCACMC LineStateCallback line_state_callback_{nullptr}; // Lock-free queue and event pool for cross-task event passing - EventPool<CDCEvent, EVENT_QUEUE_SIZE> event_pool_; + // Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring + // buffer that holds N-1 elements. This guarantees allocate() returns nullptr + // before push() can fail, preventing both a pool slot leak and an SPSC + // violation on the pool's internal free list. + EventPool<CDCEvent, EVENT_QUEUE_SIZE - 1> event_pool_; LockFreeQueue<CDCEvent, EVENT_QUEUE_SIZE> event_queue_; }; From 3bde7ec978f2d691586700ab107cff59a0f014c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 12:17:59 -1000 Subject: [PATCH 595/609] [usb_host] Fix EventPool/LockFreeQueue sizing off-by-one (#14896) --- esphome/components/usb_host/usb_host.h | 5 ++++- esphome/components/usb_host/usb_host_client.cpp | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index 2eec0c9699..dcb76a3a3b 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -144,7 +144,10 @@ class USBClient : public Component { // Lock-free event queue and pool for USB task to main loop communication // Must be public for access from static callbacks LockFreeQueue<UsbEvent, USB_EVENT_QUEUE_SIZE> event_queue; - EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE> event_pool; + // Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring + // buffer that holds N-1 elements. This guarantees allocate() returns nullptr + // before push() can fail, preventing a pool slot leak. + EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE - 1> event_pool; protected: // Process USB events from the queue. Returns true if any work was done. diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 2a460d1a07..18d938344c 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -193,7 +193,8 @@ static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void * return; } - // Push to lock-free queue (always succeeds since pool size == queue size) + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. client->event_queue.push(event); // Re-enable component loop to process the queued event From 6154b673c2b6d25d05ec899731d3f4abd171f033 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 12:18:31 -1000 Subject: [PATCH 596/609] [usb_uart] Fix EventPool/LockFreeQueue sizing off-by-one (#14895) --- esphome/components/usb_uart/usb_uart.cpp | 11 +++++------ esphome/components/usb_uart/usb_uart.h | 8 ++++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 3d35f368fb..7c4358fdbd 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -160,11 +160,9 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) { size_t chunk_len = std::min(len, UsbOutputChunk::MAX_CHUNK_SIZE); memcpy(chunk->data, data, chunk_len); chunk->length = static_cast<uint8_t>(chunk_len); - if (!this->output_queue_.push(chunk)) { - this->output_pool_.release(chunk); - ESP_LOGE(TAG, "Output queue full - lost %zu bytes", len); - break; - } + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. + this->output_queue_.push(chunk); data += chunk_len; len -= chunk_len; } @@ -320,7 +318,8 @@ void USBUartComponent::start_input(USBUartChannel *channel) { chunk->channel = channel; // Push to lock-free queue for main loop processing - // Push always succeeds because pool size == queue size + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. this->usb_data_queue_.push(chunk); // Re-enable component loop to process the queued data diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 16469df7f6..7a06b04f11 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -158,7 +158,10 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon // Larger structures first (8+ bytes) RingBuffer input_buffer_; LockFreeQueue<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT> output_queue_; - EventPool<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT> output_pool_; + // Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring + // buffer that holds N-1 elements. This guarantees allocate() returns nullptr + // before push() can fail, preventing a pool slot leak. + EventPool<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT - 1> output_pool_; std::function<void()> rx_callback_{}; CdcEps cdc_dev_{}; StringRef debug_prefix_{}; @@ -190,7 +193,8 @@ class USBUartComponent : public usb_host::USBClient { // Lock-free data transfer from USB task to main loop static constexpr int USB_DATA_QUEUE_SIZE = 32; LockFreeQueue<UsbDataChunk, USB_DATA_QUEUE_SIZE> usb_data_queue_; - EventPool<UsbDataChunk, USB_DATA_QUEUE_SIZE> chunk_pool_; + // Pool sized to queue capacity (SIZE-1) — see USBUartChannel::output_pool_ comment. + EventPool<UsbDataChunk, USB_DATA_QUEUE_SIZE - 1> chunk_pool_; protected: std::vector<USBUartChannel *> channels_{}; From ccf672d7ee37389dcd85f46f49ca0a5b5471f3d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 12:24:02 -1000 Subject: [PATCH 597/609] [esp32_ble] Fix EventPool/LockFreeQueue sizing off-by-one (#14892) --- esphome/components/esp32_ble/ble.cpp | 3 ++- esphome/components/esp32_ble/ble.h | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index ff9d9bb15a..fee1c546be 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -575,8 +575,9 @@ template<typename... Args> void enqueue_ble_event(Args... args) { load_ble_event(event, args...); // Push the event to the queue + // Push always succeeds: pool is sized to queue capacity (N-1), so if + // allocate() returned non-null, the queue is guaranteed to have room. global_ble->ble_events_.push(event); - // Push always succeeds because we're the only producer and the pool ensures we never exceed queue size } // Explicit template instantiations for the friend function diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 04bec3f785..752ddc9d1f 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -221,7 +221,13 @@ class ESP32BLE : public Component { // Large objects (size depends on template parameters, but typically aligned to 4 bytes) esphome::LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_; - esphome::EventPool<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_event_pool_; + // Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring + // buffer that holds N-1 elements (one slot distinguishes full from empty). + // This guarantees allocate() returns nullptr before push() can fail, which: + // 1. Prevents leaking a pool slot (the Nth allocate succeeds but push fails) + // 2. Avoids needing release() on the producer path after a failed push(), + // preserving the SPSC contract on the pool's internal free list + esphome::EventPool<BLEEvent, MAX_BLE_QUEUE_SIZE - 1> ble_event_pool_; // 4-byte aligned members #ifdef USE_ESP32_BLE_ADVERTISING From 80bd6489cf12c6eadafe02604facb7f841cc5600 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 13:38:41 -1000 Subject: [PATCH 598/609] [esp32_ble_server] Remove vestigial semaphore from BLECharacteristic (#14900) --- .../components/esp32_ble_server/ble_characteristic.cpp | 10 +--------- .../components/esp32_ble_server/ble_characteristic.h | 4 ---- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index 1806354712..aa82b773ba 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -16,13 +16,9 @@ BLECharacteristic::~BLECharacteristic() { for (auto *descriptor : this->descriptors_) { delete descriptor; // NOLINT(cppcoreguidelines-owning-memory) } - vSemaphoreDelete(this->set_value_lock_); } BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) : uuid_(uuid) { - this->set_value_lock_ = xSemaphoreCreateBinary(); - xSemaphoreGive(this->set_value_lock_); - this->properties_ = (esp_gatt_char_prop_t) 0; this->set_broadcast_property((properties & PROPERTY_BROADCAST) != 0); @@ -35,11 +31,7 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); } -void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) { - xSemaphoreTake(this->set_value_lock_, 0L); - this->value_ = std::move(buffer); - xSemaphoreGive(this->set_value_lock_); -} +void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) { this->value_ = std::move(buffer); } void BLECharacteristic::set_value(std::initializer_list<uint8_t> data) { this->set_value(std::vector<uint8_t>(data)); // Delegate to move overload diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index 72897d1dfb..062052cdf8 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -16,8 +16,6 @@ #include <esp_gattc_api.h> #include <esp_gatts_api.h> #include <esp_bt_defs.h> -#include <freertos/FreeRTOS.h> -#include <freertos/semphr.h> namespace esphome { namespace esp32_ble_server { @@ -84,8 +82,6 @@ class BLECharacteristic { uint16_t value_read_offset_{0}; std::vector<uint8_t> value_; - SemaphoreHandle_t set_value_lock_; - std::vector<BLEDescriptor *> descriptors_; struct ClientNotificationEntry { From be2e4a5278a83155fa3128e68c4b9824c311f1cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 13:49:24 -1000 Subject: [PATCH 599/609] [mqtt] Fix data race on inbound event queue (#14891) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../components/mqtt/mqtt_backend_esp32.cpp | 34 ++++++--- esphome/components/mqtt/mqtt_backend_esp32.h | 69 +++++++++++-------- 2 files changed, 66 insertions(+), 37 deletions(-) diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index 5642fd5f7b..ab067c4418 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -82,10 +82,16 @@ bool MQTTBackendESP32::initialize_() { void MQTTBackendESP32::loop() { // process new events // handle only 1 message per loop iteration - if (!mqtt_events_.empty()) { - auto &event = mqtt_events_.front(); - mqtt_event_handler_(event); - mqtt_events_.pop(); + Event *event = this->mqtt_event_queue_.pop(); + if (event != nullptr) { + this->mqtt_event_handler_(*event); + this->mqtt_event_pool_.release(event); + } + + // Log dropped inbound events (check is cheap - single atomic load in common case) + uint16_t inbound_dropped = this->mqtt_event_queue_.get_and_reset_dropped_count(); + if (inbound_dropped > 0) { + ESP_LOGW(TAG, "Dropped %u inbound MQTT events", inbound_dropped); } #if defined(USE_MQTT_IDF_ENQUEUE) @@ -183,10 +189,18 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) { void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { MQTTBackendESP32 *instance = static_cast<MQTTBackendESP32 *>(handler_args); - // queue event to decouple processing + // queue event to decouple processing from ESP-IDF MQTT task to main loop if (instance) { - auto event = *static_cast<esp_mqtt_event_t *>(event_data); - instance->mqtt_events_.emplace(event); + auto *event = instance->mqtt_event_pool_.allocate(); + if (event == nullptr) { + // Pool exhausted, drop event (counted via queue's dropped counter) + instance->mqtt_event_queue_.increment_dropped_count(); + return; + } + event->populate(*static_cast<esp_mqtt_event_t *>(event_data)); + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. + instance->mqtt_event_queue_.push(event); // Wake main loop immediately to process MQTT event instead of waiting for select() timeout #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) @@ -226,14 +240,14 @@ void MQTTBackendESP32::esphome_mqtt_task(void *params) { break; } } - this_mqtt->mqtt_event_pool_.release(elem); + this_mqtt->mqtt_outbound_pool_.release(elem); } } } bool MQTTBackendESP32::enqueue_(MqttQueueTypeT type, const char *topic, int qos, bool retain, const char *payload, size_t len) { - auto *elem = this->mqtt_event_pool_.allocate(); + auto *elem = this->mqtt_outbound_pool_.allocate(); if (!elem) { // Queue is full - increment counter but don't log immediately. @@ -253,7 +267,7 @@ bool MQTTBackendESP32::enqueue_(MqttQueueTypeT type, const char *topic, int qos, // Use the helper to allocate and copy data if (!elem->set_data(topic, payload, len)) { // Allocation failed, return elem to pool - this->mqtt_event_pool_.release(elem); + this->mqtt_outbound_pool_.release(elem); // Increment counter without logging to avoid cascade effect during memory pressure this->mqtt_queue_.increment_dropped_count(); return false; diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index 5c4dc413bd..58d1b29b32 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -5,7 +5,6 @@ #ifdef USE_ESP32 #include <string> -#include <queue> #include <cstring> #include <mqtt_client.h> #include <freertos/FreeRTOS.h> @@ -18,32 +17,39 @@ namespace esphome::mqtt { struct Event { - esp_mqtt_event_id_t event_id; + esp_mqtt_event_id_t event_id{}; std::vector<char> data; - int total_data_len; - int current_data_offset; + int total_data_len{0}; + int current_data_offset{0}; std::string topic; - int msg_id; - bool retain; - int qos; - bool dup; - bool session_present; - esp_mqtt_error_codes_t error_handle; + int msg_id{0}; + bool retain{false}; + int qos{0}; + bool dup{false}; + bool session_present{false}; + esp_mqtt_error_codes_t error_handle{}; - // Construct from esp_mqtt_event_t - // Any pointer values that are unsafe to keep are converted to safe copies - Event(const esp_mqtt_event_t &event) - : event_id(event.event_id), - data(event.data, event.data + event.data_len), - total_data_len(event.total_data_len), - current_data_offset(event.current_data_offset), - topic(event.topic, event.topic_len), - msg_id(event.msg_id), - retain(event.retain), - qos(event.qos), - dup(event.dup), - session_present(event.session_present), - error_handle(*event.error_handle) {} + // Populate from esp_mqtt_event_t + // Copies pointer-based data to owned storage for safe cross-thread transfer + void populate(const esp_mqtt_event_t &event) { + this->event_id = event.event_id; + this->data.assign(event.data, event.data + event.data_len); + this->total_data_len = event.total_data_len; + this->current_data_offset = event.current_data_offset; + this->topic.assign(event.topic, event.topic_len); + this->msg_id = event.msg_id; + this->retain = event.retain; + this->qos = event.qos; + this->dup = event.dup; + this->session_present = event.session_present; + this->error_handle = *event.error_handle; + } + + // Release owned resources for pool reuse (keeps allocated capacity for efficiency) + void release() { + this->data.clear(); + this->topic.clear(); + } }; enum MqttQueueTypeT : uint8_t { @@ -118,7 +124,8 @@ class MQTTBackendESP32 final : public MQTTBackend { static constexpr size_t TASK_STACK_SIZE = 3072; static constexpr size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations static constexpr ssize_t TASK_PRIORITY = 5; - static constexpr uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360 + static constexpr uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360 + static constexpr uint8_t MQTT_EVENT_QUEUE_LENGTH = 32; // Inbound events from broker void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; } void set_client_id(const char *client_id) final { this->client_id_ = client_id; } @@ -251,7 +258,8 @@ class MQTTBackendESP32 final : public MQTTBackend { bool skip_cert_cn_check_{false}; #if defined(USE_MQTT_IDF_ENQUEUE) static void esphome_mqtt_task(void *params); - EventPool<struct QueueElement, MQTT_QUEUE_LENGTH> mqtt_event_pool_; + // Pool sized to queue capacity (SIZE-1) — see mqtt_event_pool_ comment. + EventPool<struct QueueElement, MQTT_QUEUE_LENGTH - 1> mqtt_outbound_pool_; NotifyingLockFreeQueue<struct QueueElement, MQTT_QUEUE_LENGTH> mqtt_queue_; TaskHandle_t task_handle_{nullptr}; bool enqueue_(MqttQueueTypeT type, const char *topic, int qos = 0, bool retain = false, const char *payload = NULL, @@ -266,7 +274,14 @@ class MQTTBackendESP32 final : public MQTTBackend { CallbackManager<on_message_callback_t> on_message_; CallbackManager<on_publish_user_callback_t> on_publish_; std::string cached_topic_; - std::queue<Event> mqtt_events_; + // Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring + // buffer that holds N-1 elements (one slot distinguishes full from empty). + // This guarantees allocate() returns nullptr before push() can fail, which: + // 1. Prevents leaking a pool slot (the Nth allocate succeeds but push fails) + // 2. Avoids needing release() on the producer path after a failed push(), + // preserving the SPSC contract on the pool's internal free list + EventPool<Event, MQTT_EVENT_QUEUE_LENGTH - 1> mqtt_event_pool_; + LockFreeQueue<Event, MQTT_EVENT_QUEUE_LENGTH> mqtt_event_queue_; #if defined(USE_MQTT_IDF_ENQUEUE) uint32_t last_dropped_log_time_{0}; From 0fa96b6e1ea76b46a19d8cc4f847dcea3606452a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 14:19:31 -1000 Subject: [PATCH 600/609] [scheduler] Fix UB in cross-thread counter/vector reads, add atomic fast-path (#14880) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/core/scheduler.cpp | 48 ++++++----- esphome/core/scheduler.h | 159 ++++++++++++++++++++++++++++++++++--- 2 files changed, 177 insertions(+), 30 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 63e1006b03..8c4ff0ddb5 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -211,6 +211,14 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type); } target->push_back(item); + if (target == &this->to_add_) { + this->to_add_count_increment_(); + } +#ifndef ESPHOME_THREAD_SINGLE + else { + this->defer_count_increment_(); + } +#endif } void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, @@ -387,7 +395,7 @@ optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) { // safe when called from the main thread. Other threads must not call this method. // If no items, return empty optional - if (this->cleanup_() == 0) + if (!this->cleanup_()) return {}; SchedulerItem *item = this->items_[0]; @@ -421,7 +429,7 @@ void Scheduler::full_cleanup_removed_items_() { this->items_.erase(this->items_.begin() + write, this->items_.end()); // Rebuild the heap structure since items are no longer in heap order std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); - this->to_remove_ = 0; + this->to_remove_clear_(); } #ifndef ESPHOME_THREAD_SINGLE @@ -502,7 +510,7 @@ void HOT Scheduler::call(uint32_t now) { // If we still have too many cancelled items, do a full cleanup // This only happens if cancelled items are stuck in the middle/bottom of the heap - if (this->to_remove_ >= MAX_LOGICALLY_DELETED_ITEMS) { + if (this->to_remove_count_() >= MAX_LOGICALLY_DELETED_ITEMS) { this->full_cleanup_removed_items_(); } while (!this->items_.empty()) { @@ -529,7 +537,7 @@ void HOT Scheduler::call(uint32_t now) { LockGuard guard{this->lock_}; if (is_item_removed_locked_(item)) { this->recycle_item_main_loop_(this->pop_raw_locked_()); - this->to_remove_--; + this->to_remove_decrement_(); continue; } } @@ -538,7 +546,7 @@ void HOT Scheduler::call(uint32_t now) { if (is_item_removed_(item)) { LockGuard guard{this->lock_}; this->recycle_item_main_loop_(this->pop_raw_locked_()); - this->to_remove_--; + this->to_remove_decrement_(); continue; } #endif @@ -566,7 +574,7 @@ void HOT Scheduler::call(uint32_t now) { if (this->is_item_removed_locked_(executed_item)) { // We were removed/cancelled in the function call, recycle and continue - this->to_remove_--; + this->to_remove_decrement_(); this->recycle_item_main_loop_(executed_item); continue; } @@ -576,6 +584,7 @@ void HOT Scheduler::call(uint32_t now) { // Add new item directly to to_add_ // since we have the lock held this->to_add_.push_back(executed_item); + this->to_add_count_increment_(); } else { // Timeout completed - recycle it this->recycle_item_main_loop_(executed_item); @@ -604,6 +613,10 @@ void HOT Scheduler::call(uint32_t now) { #endif } void HOT Scheduler::process_to_add() { + // Fast path: skip lock acquisition when nothing to add. + // Worst case is a one-loop-iteration delay before newly added items are processed. + if (this->to_add_empty_()) + return; LockGuard guard{this->lock_}; for (auto *&it : this->to_add_) { if (is_item_removed_locked_(it)) { @@ -617,17 +630,14 @@ void HOT Scheduler::process_to_add() { std::push_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); } this->to_add_.clear(); + this->to_add_count_clear_(); } -size_t HOT Scheduler::cleanup_() { - // Fast path: if nothing to remove, just return the current size - // Reading to_remove_ without lock is safe because: - // 1. We only call this from the main thread during call() - // 2. If it's 0, there's definitely nothing to cleanup - // 3. If it becomes non-zero after we check, cleanup will happen on the next loop iteration - // 4. Not all platforms support atomics, so we accept this race in favor of performance - // 5. The worst case is a one-loop-iteration delay in cleanup, which is harmless - if (this->to_remove_ == 0) - return this->items_.size(); +bool HOT Scheduler::cleanup_() { + // Fast path: if nothing to remove, just check if items exist. + // Uses atomic load on platforms with atomics, falls back to always taking the lock otherwise. + // Worst case is a one-loop-iteration delay in cleanup. + if (this->to_remove_empty_()) + return !this->items_.empty(); // We must hold the lock for the entire cleanup operation because: // 1. We're modifying items_ (via pop_raw_locked_) which requires exclusive access @@ -642,10 +652,10 @@ size_t HOT Scheduler::cleanup_() { SchedulerItem *item = this->items_[0]; if (!this->is_item_removed_locked_(item)) break; - this->to_remove_--; + this->to_remove_decrement_(); this->recycle_item_main_loop_(this->pop_raw_locked_()); } - return this->items_.size(); + return !this->items_.empty(); } Scheduler::SchedulerItem *HOT Scheduler::pop_raw_locked_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); @@ -698,7 +708,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type size_t heap_cancelled = this->mark_matching_items_removed_locked_(this->items_, component, name_type, static_name, hash_or_id, type, match_retry); total_cancelled += heap_cancelled; - this->to_remove_ += heap_cancelled; + this->to_remove_add_(heap_cancelled); } // Cancel items in to_add_ diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 0476513bb9..e545055fca 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -284,9 +284,9 @@ class Scheduler { #endif } // Cleanup logically deleted items from the scheduler - // Returns the number of items remaining after cleanup + // Returns true if items remain after cleanup // IMPORTANT: This method should only be called from the main thread (loop task). - size_t cleanup_(); + bool cleanup_(); // Remove and return the front item from the heap as a raw pointer. // Caller takes ownership and must either recycle or delete the item. // IMPORTANT: Caller must hold the scheduler lock before calling this function. @@ -395,15 +395,9 @@ class Scheduler { // erase() on every pop, which would be O(n). The queue is processed once per loop - // any items added during processing are left for the next loop iteration. - // Snapshot the queue end point - only process items that existed at loop start - // Items added during processing (by callbacks or other threads) run next loop - // No lock needed: single consumer (main loop), stale read just means we process less this iteration - size_t defer_queue_end = this->defer_queue_.size(); - // Fast path: nothing to process, avoid lock entirely. - // Safe without lock: single consumer (main loop) reads front_, and a stale size() read - // from a concurrent push can only make us see fewer items — they'll be processed next loop. - if (this->defer_queue_front_ >= defer_queue_end) + // Worst case is a one-loop-iteration delay before newly deferred items are processed. + if (this->defer_empty_()) return; // Merge lock acquisitions: instead of separate locks for move-out and recycle (2N+1 total), @@ -412,6 +406,13 @@ class Scheduler { SchedulerItem *item; this->lock_.lock(); + // Reset counter and snapshot queue end under lock + this->defer_count_clear_(); + size_t defer_queue_end = this->defer_queue_.size(); + if (this->defer_queue_front_ >= defer_queue_end) { + this->lock_.unlock(); + return; + } while (this->defer_queue_front_ < defer_queue_end) { // Take ownership of the item, leaving nullptr in the vector slot. // This is safe because: @@ -527,14 +528,150 @@ class Scheduler { Mutex lock_; std::vector<SchedulerItem *> items_; std::vector<SchedulerItem *> to_add_; + +#ifndef ESPHOME_THREAD_SINGLE + // Fast-path counter for process_to_add() to skip taking the lock when there is + // nothing to add. Uses std::atomic on platforms that support it, plain uint32_t + // otherwise. On non-atomic platforms, callers must hold the scheduler lock when + // mutating this counter. Not needed on single-threaded platforms where we can + // check to_add_.empty() directly. +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + std::atomic<uint32_t> to_add_count_{0}; +#else + uint32_t to_add_count_{0}; +#endif +#endif /* ESPHOME_THREAD_SINGLE */ + + // Fast-path helper for process_to_add() to decide if it can try the lock-free path. + // - On ESPHOME_THREAD_SINGLE: direct container check is safe (no concurrent writers). + // - On ESPHOME_THREAD_MULTI_ATOMICS: performs a lock-free check via to_add_count_. + // - On ESPHOME_THREAD_MULTI_NO_ATOMICS: always returns false to force the caller + // down the locked path; this is NOT a lock-free emptiness check on that platform. + bool to_add_empty_() const { +#ifdef ESPHOME_THREAD_SINGLE + return this->to_add_.empty(); +#elif defined(ESPHOME_THREAD_MULTI_ATOMICS) + return this->to_add_count_.load(std::memory_order_relaxed) == 0; +#else + return false; +#endif + } + + // Increment to_add_count_ (no-op on single-threaded platforms) + void to_add_count_increment_() { +#ifdef ESPHOME_THREAD_SINGLE + // No counter needed — to_add_empty_() checks the vector directly +#elif defined(ESPHOME_THREAD_MULTI_ATOMICS) + this->to_add_count_.fetch_add(1, std::memory_order_relaxed); +#else + this->to_add_count_++; +#endif + } + + // Reset to_add_count_ (no-op on single-threaded platforms) + void to_add_count_clear_() { +#ifdef ESPHOME_THREAD_SINGLE + // No counter needed — to_add_empty_() checks the vector directly +#elif defined(ESPHOME_THREAD_MULTI_ATOMICS) + this->to_add_count_.store(0, std::memory_order_relaxed); +#else + this->to_add_count_ = 0; +#endif + } + #ifndef ESPHOME_THREAD_SINGLE // Single-core platforms don't need the defer queue and save ~32 bytes of RAM // Using std::vector instead of std::deque avoids 512-byte chunked allocations // Index tracking avoids O(n) erase() calls when draining the queue each loop std::vector<SchedulerItem *> defer_queue_; // FIFO queue for defer() calls size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items) -#endif /* ESPHOME_THREAD_SINGLE */ + + // Fast-path counter for process_defer_queue_() to skip lock when nothing to process. +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + std::atomic<uint32_t> defer_count_{0}; +#else + uint32_t defer_count_{0}; +#endif + + bool defer_empty_() const { + // defer_queue_ only exists on multi-threaded platforms, so no ESPHOME_THREAD_SINGLE path + // ESPHOME_THREAD_MULTI_NO_ATOMICS: always take the lock +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + return this->defer_count_.load(std::memory_order_relaxed) == 0; +#else + return false; +#endif + } + + void defer_count_increment_() { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + this->defer_count_.fetch_add(1, std::memory_order_relaxed); +#else + this->defer_count_++; +#endif + } + + void defer_count_clear_() { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + this->defer_count_.store(0, std::memory_order_relaxed); +#else + this->defer_count_ = 0; +#endif + } + +#endif /* ESPHOME_THREAD_SINGLE */ + + // Counter for items marked for removal. Incremented cross-thread in cancel_item_locked_(). + // On ESPHOME_THREAD_MULTI_ATOMICS this is read without a lock in the cleanup_() fast path; + // on ESPHOME_THREAD_MULTI_NO_ATOMICS the fast path is disabled so cleanup_() always takes the lock. +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + std::atomic<uint32_t> to_remove_{0}; +#else uint32_t to_remove_{0}; +#endif + + // Lock-free check if there are items to remove (for fast-path in cleanup_) + bool to_remove_empty_() const { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + return this->to_remove_.load(std::memory_order_relaxed) == 0; +#elif defined(ESPHOME_THREAD_SINGLE) + return this->to_remove_ == 0; +#else + return false; // Always take the lock path +#endif + } + + void to_remove_add_(uint32_t count) { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + this->to_remove_.fetch_add(count, std::memory_order_relaxed); +#else + this->to_remove_ += count; +#endif + } + + void to_remove_decrement_() { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + this->to_remove_.fetch_sub(1, std::memory_order_relaxed); +#else + this->to_remove_--; +#endif + } + + void to_remove_clear_() { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + this->to_remove_.store(0, std::memory_order_relaxed); +#else + this->to_remove_ = 0; +#endif + } + + uint32_t to_remove_count_() const { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + return this->to_remove_.load(std::memory_order_relaxed); +#else + return this->to_remove_; +#endif + } // Memory pool for recycling SchedulerItem objects to reduce heap churn. // Design decisions: From 5cc03d9befb7326fe708e7368778416eddd47253 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:35:21 +1300 Subject: [PATCH 601/609] Bump version to 2026.3.0b4 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 96295b3fc8..ea34106f36 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.3.0b3 +PROJECT_NUMBER = 2026.3.0b4 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 561a27d228..4ba8b1a23f 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.3.0b3" +__version__ = "2026.3.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 6cf32af33f4ec92d0b06939129be8c7cb254cb59 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:57:53 -0400 Subject: [PATCH 602/609] [seeed_mr24hpc1] Fix frame parser length handling bugs (#14863) --- esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp index 263603704a..c9fe3a2e6e 100644 --- a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp @@ -297,19 +297,17 @@ void MR24HPC1Component::r24_split_data_frame_(uint8_t value) { this->sg_recv_data_state_ = FRAME_DATA_LEN_H; break; case FRAME_DATA_LEN_H: - if (value <= 4) { - this->sg_data_len_ = value * 256; + if (value == 0) { this->sg_frame_buf_[4] = value; this->sg_recv_data_state_ = FRAME_DATA_LEN_L; } else { - this->sg_data_len_ = 0; this->sg_recv_data_state_ = FRAME_IDLE; ESP_LOGD(TAG, "FRAME_DATA_LEN_H ERROR value:%x", value); } break; case FRAME_DATA_LEN_L: - this->sg_data_len_ += value; - if (this->sg_data_len_ > 32) { + this->sg_data_len_ = value; + if (this->sg_data_len_ == 0 || this->sg_data_len_ > 32) { ESP_LOGD(TAG, "len=%d, FRAME_DATA_LEN_L ERROR value:%x", this->sg_data_len_, value); this->sg_data_len_ = 0; this->sg_recv_data_state_ = FRAME_IDLE; @@ -320,9 +318,8 @@ void MR24HPC1Component::r24_split_data_frame_(uint8_t value) { } break; case FRAME_DATA_BYTES: - this->sg_data_len_ -= 1; this->sg_frame_buf_[this->sg_frame_len_++] = value; - if (this->sg_data_len_ <= 0) { + if (--this->sg_data_len_ == 0) { this->sg_recv_data_state_ = FRAME_DATA_CRC; } break; From 91e66cfd9d7e3a31ca4a0923463dcc107de321d1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:28:13 -0400 Subject: [PATCH 603/609] [gree] Fix IR checksum for YAA/YAC/YAC1FB9/GENERIC models (#14888) --- esphome/components/gree/gree.cpp | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/esphome/components/gree/gree.cpp b/esphome/components/gree/gree.cpp index 8a9f264932..732ebd9632 100644 --- a/esphome/components/gree/gree.cpp +++ b/esphome/components/gree/gree.cpp @@ -87,19 +87,12 @@ void GreeClimate::transmit_state() { // Calculate the checksum if (this->model_ == GREE_YAN || this->model_ == GREE_YX1FF) { remote_state[7] = ((remote_state[0] << 4) + (remote_state[1] << 4) + 0xC0); - } else if (this->model_ == GREE_YAG) { + } else { remote_state[7] = ((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) + ((remote_state[4] & 0xF0) >> 4) + ((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + 0x0A) & 0x0F) << 4); - } else { - remote_state[7] = - ((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) + - ((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + ((remote_state[7] & 0xF0) >> 4) + 0x0A) & - 0x0F) - << 4) | - (remote_state[7] & 0x0F); } auto transmit = this->transmitter_->transmit(); From 4cb93d4df8c3661158ffb7e96966eccdb08bedad Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:46:26 -0400 Subject: [PATCH 604/609] [sensor][ee895][hdc2010] Fix misc bugs found during component scan (#14890) --- esphome/components/ee895/ee895.cpp | 15 ++--- esphome/components/hdc2010/hdc2010.cpp | 76 +++++++++++--------------- esphome/components/sensor/__init__.py | 10 ++-- 3 files changed, 43 insertions(+), 58 deletions(-) diff --git a/esphome/components/ee895/ee895.cpp b/esphome/components/ee895/ee895.cpp index 22f28be9bc..93e5d4203b 100644 --- a/esphome/components/ee895/ee895.cpp +++ b/esphome/components/ee895/ee895.cpp @@ -24,7 +24,7 @@ void EE895Component::setup() { this->read(serial_number, 20); crc16_check = (serial_number[19] << 8) + serial_number[18]; - if (crc16_check != calc_crc16_(serial_number, 19)) { + if (crc16_check != calc_crc16_(serial_number, 18)) { this->error_code_ = CRC_CHECK_FAILED; this->mark_failed(); return; @@ -84,7 +84,7 @@ void EE895Component::write_command_(uint16_t addr, uint16_t reg_cnt) { address[2] = addr & 0xFF; address[3] = (reg_cnt >> 8) & 0xFF; address[4] = reg_cnt & 0xFF; - crc16 = calc_crc16_(address, 6); + crc16 = calc_crc16_(address, 5); address[5] = crc16 & 0xFF; address[6] = (crc16 >> 8) & 0xFF; this->write(address, 7); @@ -95,7 +95,7 @@ float EE895Component::read_float_() { uint8_t i2c_response[8]; this->read(i2c_response, 8); crc16_check = (i2c_response[7] << 8) + i2c_response[6]; - if (crc16_check != calc_crc16_(i2c_response, 7)) { + if (crc16_check != calc_crc16_(i2c_response, 6)) { this->error_code_ = CRC_CHECK_FAILED; this->status_set_warning(); return 0; @@ -107,12 +107,9 @@ float EE895Component::read_float_() { } uint16_t EE895Component::calc_crc16_(const uint8_t buf[], uint8_t len) { - uint8_t crc_check_buf[22]; - for (int i = 0; i < len; i++) { - crc_check_buf[i + 1] = buf[i]; - } - crc_check_buf[0] = this->address_; - return crc16(crc_check_buf, len); + uint8_t addr = this->address_; + uint16_t crc = crc16(&addr, 1); + return crc16(buf, len, crc); } } // namespace ee895 } // namespace esphome diff --git a/esphome/components/hdc2010/hdc2010.cpp b/esphome/components/hdc2010/hdc2010.cpp index c53fdb3f5b..0334b30eec 100644 --- a/esphome/components/hdc2010/hdc2010.cpp +++ b/esphome/components/hdc2010/hdc2010.cpp @@ -7,50 +7,36 @@ namespace hdc2010 { static const char *const TAG = "hdc2010"; -static const uint8_t HDC2010_ADDRESS = 0x40; // 0b1000000 or 0b1000001 from datasheet -static const uint8_t HDC2010_CMD_CONFIGURATION_MEASUREMENT = 0x8F; -static const uint8_t HDC2010_CMD_START_MEASUREMENT = 0xF9; -static const uint8_t HDC2010_CMD_TEMPERATURE_LOW = 0x00; -static const uint8_t HDC2010_CMD_TEMPERATURE_HIGH = 0x01; -static const uint8_t HDC2010_CMD_HUMIDITY_LOW = 0x02; -static const uint8_t HDC2010_CMD_HUMIDITY_HIGH = 0x03; -static const uint8_t CONFIG = 0x0E; -static const uint8_t MEASUREMENT_CONFIG = 0x0F; +// Register addresses +static constexpr uint8_t REG_TEMPERATURE_LOW = 0x00; +static constexpr uint8_t REG_TEMPERATURE_HIGH = 0x01; +static constexpr uint8_t REG_HUMIDITY_LOW = 0x02; +static constexpr uint8_t REG_HUMIDITY_HIGH = 0x03; +static constexpr uint8_t REG_RESET_DRDY_INT_CONF = 0x0E; +static constexpr uint8_t REG_MEASUREMENT_CONF = 0x0F; + +// REG_MEASUREMENT_CONF (0x0F) bit masks +static constexpr uint8_t MEAS_TRIG = 0x01; // Bit 0: measurement trigger +static constexpr uint8_t MEAS_CONF_MASK = 0x06; // Bits 2:1: measurement mode +static constexpr uint8_t HRES_MASK = 0x30; // Bits 5:4: humidity resolution +static constexpr uint8_t TRES_MASK = 0xC0; // Bits 7:6: temperature resolution + +// REG_RESET_DRDY_INT_CONF (0x0E) bit masks +static constexpr uint8_t AMM_MASK = 0x70; // Bits 6:4: auto measurement mode void HDC2010Component::setup() { ESP_LOGCONFIG(TAG, "Running setup"); - const uint8_t data[2] = { - 0b00000000, // resolution 14bit for both humidity and temperature - 0b00000000 // reserved - }; - - if (!this->write_bytes(HDC2010_CMD_CONFIGURATION_MEASUREMENT, data, 2)) { - ESP_LOGW(TAG, "Initial config instruction error"); - this->status_set_warning(); - return; - } - - // Set measurement mode to temperature and humidity + // Set 14-bit resolution for both sensors and measurement mode to temp + humidity uint8_t config_contents; - this->read_register(MEASUREMENT_CONFIG, &config_contents, 1); - config_contents = (config_contents & 0xF9); // Always set to TEMP_AND_HUMID mode - this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1); + this->read_register(REG_MEASUREMENT_CONF, &config_contents, 1); + config_contents &= ~(TRES_MASK | HRES_MASK | MEAS_CONF_MASK); // 14-bit temp, 14-bit humidity, temp+humidity mode + this->write_bytes(REG_MEASUREMENT_CONF, &config_contents, 1); - // Set rate to manual - this->read_register(CONFIG, &config_contents, 1); - config_contents &= 0x8F; - this->write_bytes(CONFIG, &config_contents, 1); - - // Set temperature resolution to 14bit - this->read_register(CONFIG, &config_contents, 1); - config_contents &= 0x3F; - this->write_bytes(CONFIG, &config_contents, 1); - - // Set humidity resolution to 14bit - this->read_register(CONFIG, &config_contents, 1); - config_contents &= 0xCF; - this->write_bytes(CONFIG, &config_contents, 1); + // Set auto measurement rate to manual (on-demand via MEAS_TRIG) + this->read_register(REG_RESET_DRDY_INT_CONF, &config_contents, 1); + config_contents &= ~AMM_MASK; + this->write_bytes(REG_RESET_DRDY_INT_CONF, &config_contents, 1); } void HDC2010Component::dump_config() { @@ -67,9 +53,9 @@ void HDC2010Component::dump_config() { void HDC2010Component::update() { // Trigger measurement uint8_t config_contents; - this->read_register(CONFIG, &config_contents, 1); - config_contents |= 0x01; - this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1); + this->read_register(REG_MEASUREMENT_CONF, &config_contents, 1); + config_contents |= MEAS_TRIG; + this->write_bytes(REG_MEASUREMENT_CONF, &config_contents, 1); // 1ms delay after triggering the sample set_timeout(1, [this]() { @@ -90,8 +76,8 @@ void HDC2010Component::update() { float HDC2010Component::read_temp() { uint8_t byte[2]; - this->read_register(HDC2010_CMD_TEMPERATURE_LOW, &byte[0], 1); - this->read_register(HDC2010_CMD_TEMPERATURE_HIGH, &byte[1], 1); + this->read_register(REG_TEMPERATURE_LOW, &byte[0], 1); + this->read_register(REG_TEMPERATURE_HIGH, &byte[1], 1); uint16_t temp = encode_uint16(byte[1], byte[0]); return (float) temp * 0.0025177f - 40.0f; @@ -100,8 +86,8 @@ float HDC2010Component::read_temp() { float HDC2010Component::read_humidity() { uint8_t byte[2]; - this->read_register(HDC2010_CMD_HUMIDITY_LOW, &byte[0], 1); - this->read_register(HDC2010_CMD_HUMIDITY_HIGH, &byte[1], 1); + this->read_register(REG_HUMIDITY_LOW, &byte[0], 1); + this->read_register(REG_HUMIDITY_HIGH, &byte[1], 1); uint16_t humidity = encode_uint16(byte[1], byte[0]); return (float) humidity * 0.001525879f; diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 4be6ed1b84..02db381ff4 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -403,10 +403,12 @@ async def filter_out_filter_to_code(config, filter_id): QUANTILE_SCHEMA = cv.All( cv.Schema( { - cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int, - cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int, - cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int, - cv.Optional(CONF_QUANTILE, default=0.9): cv.zero_to_one_float, + cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535), + cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535), + cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535), + cv.Optional(CONF_QUANTILE, default=0.9): cv.float_range( + min=0, min_included=False, max=1 + ), } ), validate_send_first_at, From 98d3dce6727f39dd0a0efd4fdabf0daa9b369140 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:12:13 -0400 Subject: [PATCH 605/609] [voice_assistant][micro_wake_word] Fix null deref and missing error return (#14906) --- esphome/components/micro_wake_word/streaming_model.cpp | 1 + esphome/components/voice_assistant/voice_assistant.cpp | 2 ++ 2 files changed, 3 insertions(+) diff --git a/esphome/components/micro_wake_word/streaming_model.cpp b/esphome/components/micro_wake_word/streaming_model.cpp index 47d2c70e13..0ab6cd3772 100644 --- a/esphome/components/micro_wake_word/streaming_model.cpp +++ b/esphome/components/micro_wake_word/streaming_model.cpp @@ -80,6 +80,7 @@ bool StreamingModel::load_model_() { TfLiteTensor *output = this->interpreter_->output(0); if ((output->dims->size != 2) || (output->dims->data[0] != 1) || (output->dims->data[1] != 1)) { ESP_LOGE(TAG, "Streaming model tensor output dimension is not 1x1."); + return false; } if (output->type != kTfLiteUInt8) { diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 51d52a8af8..15124e422f 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -619,6 +619,8 @@ void VoiceAssistant::start_playback_timeout_() { this->cancel_timeout("speaker-timeout"); this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED); + if (this->api_client_ == nullptr) + return; api::VoiceAssistantAnnounceFinished msg; msg.success = true; this->api_client_->send_message(msg); From fc67551edc0bfc4e5966e11b325c2d5277114df5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:48:43 -0400 Subject: [PATCH 606/609] [tc74][apds9960] Fix signed temperature and FIFO register address (#14907) --- esphome/components/apds9960/apds9960.cpp | 8 ++++---- esphome/components/tc74/tc74.cpp | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/esphome/components/apds9960/apds9960.cpp b/esphome/components/apds9960/apds9960.cpp index 260de82d14..a07175f2c9 100644 --- a/esphome/components/apds9960/apds9960.cpp +++ b/esphome/components/apds9960/apds9960.cpp @@ -251,11 +251,11 @@ void APDS9960::read_gesture_data_() { uint8_t buf[128]; for (uint8_t pos = 0; pos < fifo_level * 4; pos += 32) { - // The ESP's i2c driver has a limited buffer size. - // This way of retrieving the data should be wrong according to the datasheet - // but it seems to work. + // Read in 32-byte chunks due to ESP8266 I2C buffer limit. + // Always read from 0xFC — the FIFO auto-increments through 0xFC-0xFF + // and advances its internal pointer after every 4th byte. uint8_t read = std::min(32, fifo_level * 4 - pos); - APDS9960_WARNING_CHECK(this->read_bytes(0xFC + pos, buf + pos, read), "Reading FIFO buffer failed."); + APDS9960_WARNING_CHECK(this->read_bytes(0xFC, buf + pos, read), "Reading FIFO buffer failed."); } if (millis() - this->gesture_start_ > 500) { diff --git a/esphome/components/tc74/tc74.cpp b/esphome/components/tc74/tc74.cpp index 969ef3671e..cb58e583dc 100644 --- a/esphome/components/tc74/tc74.cpp +++ b/esphome/components/tc74/tc74.cpp @@ -50,8 +50,9 @@ void TC74Component::read_temperature_() { } } - uint8_t temperature_reg; - if (this->read_register(TC74_REGISTER_TEMPERATURE, &temperature_reg, 1) != i2c::ERROR_OK) { + int8_t temperature_reg; + if (this->read_register(TC74_REGISTER_TEMPERATURE, reinterpret_cast<uint8_t *>(&temperature_reg), 1) != + i2c::ERROR_OK) { this->status_set_warning(); return; } From 448402ca2cb49160c66472e3464dafe2209ab47b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 18 Mar 2026 07:46:18 -1000 Subject: [PATCH 607/609] [http_request] Fix data race on update_info_ strings in update task (#14909) --- .../update/http_request_update.cpp | 222 ++++++++++-------- 1 file changed, 121 insertions(+), 101 deletions(-) diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index c40590af95..a15dc61675 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -23,6 +23,12 @@ namespace http_request { static const char *const TAG = "http_request.update"; +// Wraps UpdateInfo + error for the task→main-loop handoff. +struct TaskResult { + update::UpdateInfo info; + const LogString *error_str{nullptr}; +}; + static const size_t MAX_READ_SIZE = 256; static constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0; static constexpr uint32_t INITIAL_CHECK_INTERVAL_MS = 10000; @@ -77,134 +83,148 @@ void HttpRequestUpdate::update() { void HttpRequestUpdate::update_task(void *params) { HttpRequestUpdate *this_update = (HttpRequestUpdate *) params; + // Allocate once — every path below returns via the single defer at the end. + // On failure, error_str is set; on success it is nullptr. + auto *result = new TaskResult(); + auto *info = &result->info; + auto container = this_update->request_parent_->get(this_update->source_url_); if (container == nullptr || container->status_code != HTTP_STATUS_OK) { ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str()); - // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to fetch manifest")); }); - UPDATE_RETURN; + if (container != nullptr) + container->end(); + result->error_str = LOG_STR("Failed to fetch manifest"); + goto defer; // NOLINT(cppcoreguidelines-avoid-goto) } - RAMAllocator<uint8_t> allocator; - uint8_t *data = allocator.allocate(container->content_length); - if (data == nullptr) { - ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length); - // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer( - [this_update]() { this_update->status_set_error(LOG_STR("Failed to allocate memory for manifest")); }); - container->end(); - UPDATE_RETURN; - } - - auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE, - this_update->request_parent_->get_timeout()); - if (read_result.status != HttpReadStatus::OK) { - if (read_result.status == HttpReadStatus::TIMEOUT) { - ESP_LOGE(TAG, "Timeout reading manifest"); - } else { - ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code); + { + RAMAllocator<uint8_t> allocator; + uint8_t *data = allocator.allocate(container->content_length); + if (data == nullptr) { + ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length); + container->end(); + result->error_str = LOG_STR("Failed to allocate memory for manifest"); + goto defer; // NOLINT(cppcoreguidelines-avoid-goto) } - // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to read manifest")); }); - allocator.deallocate(data, container->content_length); - container->end(); - UPDATE_RETURN; - } - size_t read_index = container->get_bytes_read(); - size_t content_length = container->content_length; - container->end(); - container.reset(); // Release ownership of the container's shared_ptr - - bool valid = false; - { // Scope to ensure JsonDocument is destroyed before deallocating buffer - valid = json::parse_json(data, read_index, [this_update](JsonObject root) -> bool { - if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() || - !root[ESPHOME_F("builds")].is<JsonArray>()) { - ESP_LOGE(TAG, "Manifest does not contain required fields"); - return false; + auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE, + this_update->request_parent_->get_timeout()); + if (read_result.status != HttpReadStatus::OK) { + if (read_result.status == HttpReadStatus::TIMEOUT) { + ESP_LOGE(TAG, "Timeout reading manifest"); + } else { + ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code); } - this_update->update_info_.title = root[ESPHOME_F("name")].as<std::string>(); - this_update->update_info_.latest_version = root[ESPHOME_F("version")].as<std::string>(); + allocator.deallocate(data, container->content_length); + container->end(); + result->error_str = LOG_STR("Failed to read manifest"); + goto defer; // NOLINT(cppcoreguidelines-avoid-goto) + } + size_t read_index = container->get_bytes_read(); + size_t content_length = container->content_length; - auto builds_array = root[ESPHOME_F("builds")].as<JsonArray>(); - for (auto build : builds_array) { - if (!build[ESPHOME_F("chipFamily")].is<const char *>()) { + container->end(); + container.reset(); // Release ownership of the container's shared_ptr + + bool valid = false; + { // Scope to ensure JsonDocument is destroyed before deallocating buffer + valid = json::parse_json(data, read_index, [info](JsonObject root) -> bool { + if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() || + !root[ESPHOME_F("builds")].is<JsonArray>()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } - if (build[ESPHOME_F("chipFamily")] == ESPHOME_VARIANT) { - if (!build[ESPHOME_F("ota")].is<JsonObject>()) { + info->title = root[ESPHOME_F("name")].as<std::string>(); + info->latest_version = root[ESPHOME_F("version")].as<std::string>(); + + auto builds_array = root[ESPHOME_F("builds")].as<JsonArray>(); + for (auto build : builds_array) { + if (!build[ESPHOME_F("chipFamily")].is<const char *>()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } - JsonObject ota = build[ESPHOME_F("ota")].as<JsonObject>(); - if (!ota[ESPHOME_F("path")].is<const char *>() || !ota[ESPHOME_F("md5")].is<const char *>()) { - ESP_LOGE(TAG, "Manifest does not contain required fields"); - return false; + if (build[ESPHOME_F("chipFamily")] == ESPHOME_VARIANT) { + if (!build[ESPHOME_F("ota")].is<JsonObject>()) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + JsonObject ota = build[ESPHOME_F("ota")].as<JsonObject>(); + if (!ota[ESPHOME_F("path")].is<const char *>() || !ota[ESPHOME_F("md5")].is<const char *>()) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + info->firmware_url = ota[ESPHOME_F("path")].as<std::string>(); + info->md5 = ota[ESPHOME_F("md5")].as<std::string>(); + + if (ota[ESPHOME_F("summary")].is<const char *>()) + info->summary = ota[ESPHOME_F("summary")].as<std::string>(); + if (ota[ESPHOME_F("release_url")].is<const char *>()) + info->release_url = ota[ESPHOME_F("release_url")].as<std::string>(); + + return true; } - this_update->update_info_.firmware_url = ota[ESPHOME_F("path")].as<std::string>(); - this_update->update_info_.md5 = ota[ESPHOME_F("md5")].as<std::string>(); - - if (ota[ESPHOME_F("summary")].is<const char *>()) - this_update->update_info_.summary = ota[ESPHOME_F("summary")].as<std::string>(); - if (ota[ESPHOME_F("release_url")].is<const char *>()) - this_update->update_info_.release_url = ota[ESPHOME_F("release_url")].as<std::string>(); - - return true; } - } - return false; - }); - } - allocator.deallocate(data, content_length); + return false; + }); + } + allocator.deallocate(data, content_length); - if (!valid) { - ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str()); - // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to parse manifest JSON")); }); - UPDATE_RETURN; - } + if (!valid) { + ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str()); + result->error_str = LOG_STR("Failed to parse manifest JSON"); + goto defer; // NOLINT(cppcoreguidelines-avoid-goto) + } - // Merge source_url_ and this_update->update_info_.firmware_url - if (this_update->update_info_.firmware_url.find("http") == std::string::npos) { - std::string path = this_update->update_info_.firmware_url; - if (path[0] == '/') { - std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8)); - this_update->update_info_.firmware_url = domain + path; - } else { - std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1); - this_update->update_info_.firmware_url = domain + path; + // Merge source_url_ and firmware_url + if (!info->firmware_url.empty() && info->firmware_url.find("http") == std::string::npos) { + std::string path = info->firmware_url; + if (path[0] == '/') { + std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8)); + info->firmware_url = domain + path; + } else { + std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1); + info->firmware_url = domain + path; + } } - } #ifdef ESPHOME_PROJECT_VERSION - this_update->update_info_.current_version = ESPHOME_PROJECT_VERSION; + info->current_version = ESPHOME_PROJECT_VERSION; #else - this_update->update_info_.current_version = ESPHOME_VERSION; + info->current_version = ESPHOME_VERSION; #endif - - bool trigger_update_available = false; - - if (this_update->update_info_.latest_version.empty() || - this_update->update_info_.latest_version == this_update->update_info_.current_version) { - this_update->state_ = update::UPDATE_STATE_NO_UPDATE; - } else { - if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) { - trigger_update_available = true; - } - this_update->state_ = update::UPDATE_STATE_AVAILABLE; } - // Defer to main loop to ensure thread-safe execution of: - // - status_clear_error() performs non-atomic read-modify-write on component_state_ - // - publish_state() triggers API callbacks that write to the shared protobuf buffer - // which can be corrupted if accessed concurrently from task and main loop threads - // - update_available trigger to ensure consistent state when the trigger fires - this_update->defer([this_update, trigger_update_available]() { - this_update->update_info_.has_progress = false; - this_update->update_info_.progress = 0.0f; +defer: + // Release container before vTaskDelete (which doesn't call destructors) + container.reset(); + + // Defer to the main loop so all update_info_ and state_ writes happen on the + // same thread as readers (API, MQTT, web server). This is a single defer for + // both success and error paths to avoid multiple std::function instantiations. + // Lambda captures only 2 pointers (8 bytes) — fits in std::function SBO on supported toolchains. + this_update->defer([this_update, result]() { + if (result->error_str != nullptr) { + this_update->status_set_error(result->error_str); + delete result; + return; + } + + // Determine new state on main loop (avoids extra lambda captures from task) + bool trigger_update_available = false; + update::UpdateState new_state; + if (result->info.latest_version.empty() || result->info.latest_version == result->info.current_version) { + new_state = update::UPDATE_STATE_NO_UPDATE; + } else { + new_state = update::UPDATE_STATE_AVAILABLE; + if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) { + trigger_update_available = true; + } + } + + this_update->update_info_ = std::move(result->info); + this_update->state_ = new_state; + delete result; // Safe: moved-from state is valid for destruction this_update->status_clear_error(); this_update->publish_state(); From af9366fdd4822cb5e8a1a7c571c08d6901993cc3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Mar 2026 08:19:26 +1300 Subject: [PATCH 608/609] Bump version to 2026.3.0b5 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index ea34106f36..53eae48966 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.3.0b4 +PROJECT_NUMBER = 2026.3.0b5 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 4ba8b1a23f..579235ff69 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.3.0b4" +__version__ = "2026.3.0b5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 2c10adba858b409fc5a7f445863fbda6589afc04 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Mar 2026 13:09:03 +1300 Subject: [PATCH 609/609] Bump version to 2026.3.0 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 53eae48966..d8a030536e 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.3.0b5 +PROJECT_NUMBER = 2026.3.0 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 579235ff69..756ad69464 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.3.0b5" +__version__ = "2026.3.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = (